Add GSP init and AI usage tooling
This commit is contained in:
93
README.md
93
README.md
@@ -8,7 +8,7 @@ GSP 是一个通用游戏规格协议与配套工具链,用于在人类、AI
|
||||
|
||||
```text
|
||||
GSP/
|
||||
language/ GSP 语言定义与 schema
|
||||
specs/ GSP 版本化协议规范与 schema
|
||||
toolkit/ GSP Toolkit Go CLI 源码
|
||||
examples/ 示例 GSP 工程
|
||||
```
|
||||
@@ -68,9 +68,12 @@ bin/gsp.exe
|
||||
|
||||
| 文档 | 作用 |
|
||||
|---|---|
|
||||
| `language/README.md` | GSP 语言说明。 |
|
||||
| `language/gsp.schema.json` | GSP 核心字段 schema。 |
|
||||
| `language/gsp.manifest.schema.json` | GSP 工程 manifest schema。 |
|
||||
| `specs/README.md` | GSP 规范版本入口。 |
|
||||
| `specs/versions/0.1/README.md` | GSP 0.1 语言说明。 |
|
||||
| `specs/versions/0.1/gsp.schema.json` | GSP 0.1 核心字段 schema。 |
|
||||
| `specs/versions/0.1/gsp.manifest.schema.json` | GSP 0.1 工程 manifest schema。 |
|
||||
| `specs/versions/0.1/commands.md` | GSP 0.1 命令规范。 |
|
||||
| `specs/versions/0.1/ai-usage.md` | GSP 0.1 AI 使用规则。 |
|
||||
| `toolkit/README.md` | GSP Toolkit 命令与实现说明。 |
|
||||
|
||||
## GSP 工程结构
|
||||
@@ -85,3 +88,85 @@ project/
|
||||
```
|
||||
|
||||
Toolkit 默认从工程根目录的 `design/` 目录扫描 `.gsp` 文件。`gsp.manifest` 可声明语言版本、Toolkit 版本、入口 GSP、扫描范围、阶段规则和 type 列表。
|
||||
|
||||
## AI 使用入口
|
||||
|
||||
给当前 GSP 工程生成 AI 友好入口:
|
||||
|
||||
```bash
|
||||
gsp ai-init
|
||||
```
|
||||
|
||||
默认生成:
|
||||
|
||||
```text
|
||||
README.md
|
||||
AI_USAGE.md
|
||||
```
|
||||
|
||||
可选生成代理说明或 skill 入口:
|
||||
|
||||
```bash
|
||||
gsp ai-init --agents
|
||||
gsp ai-init --skill generic
|
||||
gsp ai-init --skill codex
|
||||
gsp ai-init --all
|
||||
```
|
||||
|
||||
## 安装为命令
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\toolkit\scripts\install.ps1
|
||||
gsp version
|
||||
```
|
||||
|
||||
macOS:
|
||||
|
||||
```bash
|
||||
sh ./toolkit/scripts/install.sh
|
||||
gsp version
|
||||
```
|
||||
|
||||
安装后可以在任意目录使用:
|
||||
|
||||
```bash
|
||||
gsp init
|
||||
gsp ai-init
|
||||
gsp version
|
||||
gsp validate
|
||||
gsp index
|
||||
gsp flatten <id>
|
||||
gsp graph <id>
|
||||
gsp stage-check --stage implement
|
||||
```
|
||||
|
||||
## 初始化 GSP 工程
|
||||
|
||||
在当前目录初始化:
|
||||
|
||||
```bash
|
||||
gsp init
|
||||
```
|
||||
|
||||
在指定目录初始化:
|
||||
|
||||
```bash
|
||||
gsp init path/to/project --name project-name --entry project.entry
|
||||
```
|
||||
|
||||
初始化结果:
|
||||
|
||||
```text
|
||||
project/
|
||||
gsp.manifest
|
||||
design/
|
||||
project.entry.gsp
|
||||
```
|
||||
|
||||
已有 `gsp.manifest` 或入口 `.gsp` 时,`gsp init` 默认不会覆盖。需要重建初始化文件时使用:
|
||||
|
||||
```bash
|
||||
gsp init path/to/project --force
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
gspVersion: 0.1
|
||||
toolkitVersion: 0.1
|
||||
toolkitVersion: 0.1.0
|
||||
project: lottery-example
|
||||
|
||||
entry:
|
||||
|
||||
7
specs/README.md
Normal file
7
specs/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# GSP Specs
|
||||
|
||||
GSP specifications are versioned under `versions/`.
|
||||
|
||||
Current version: `0.1`.
|
||||
|
||||
Each toolkit release declares its default GSP version and supported GSP versions. A GSP project declares its protocol version in `gsp.manifest`.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
GSP = Game Specification Protocol。
|
||||
|
||||
GSP 是 AI 游戏制作引擎的底层游戏规格协议模块,用于在人类、AI、工具、运行时、实现模块、测试与验收之间传递游戏设计信息。
|
||||
GSP 是通用游戏规格协议,用于在人类、AI、工具、运行时、实现模块、测试与验收之间传递游戏设计信息。
|
||||
|
||||
README 面向准备使用 GSP 语言的人类和 AI。它说明 GSP 的用途、边界和基本写法。严格字段规范以 `gsp.schema.json` 为准。
|
||||
|
||||
@@ -13,6 +13,8 @@ README 面向准备使用 GSP 语言的人类和 AI。它说明 GSP 的用途、
|
||||
| `README.md` | GSP 语言使用前说明。给人类和 AI 阅读。 |
|
||||
| `gsp.schema.json` | GSP 第一版核心字段规范。使用 JSON Schema 表达,便于 AI、工具、编译器和实现模块识别。 |
|
||||
| `gsp.manifest.schema.json` | GSP 工程 manifest 字段规范。 |
|
||||
| `commands.md` | GSP Toolkit 命令规范。 |
|
||||
| `ai-usage.md` | GSP 项目 AI 使用规则。 |
|
||||
|
||||
## GSP 是什么
|
||||
|
||||
18
specs/versions/0.1/ai-usage.md
Normal file
18
specs/versions/0.1/ai-usage.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# AI Usage 0.1
|
||||
|
||||
- Read `gsp.manifest` first.
|
||||
- Treat `design/` as the default GSP source directory.
|
||||
- `.gsp` files use YAML.
|
||||
- Preserve `id`; do not rename it unless explicitly requested.
|
||||
- `id` is the unique identity of a GSP unit.
|
||||
- Use only fields valid for the declared GSP version.
|
||||
- `with` means related design context.
|
||||
- `refines` means single-source refinement.
|
||||
- Empty `context` means placeholder.
|
||||
- Do not invent missing referenced GSPs silently.
|
||||
- Use `gsp validate` after editing GSP files.
|
||||
- Use `gsp index` to locate GSP units.
|
||||
- Use `gsp trace <id>` to inspect relations.
|
||||
- Use `gsp flatten <id>` before implementation or task splitting.
|
||||
- Use `gsp pack <id>` when a compact AI context is needed.
|
||||
- Use `gsp stage-check --stage <stage>` before stage handoff.
|
||||
106
specs/versions/0.1/commands.md
Normal file
106
specs/versions/0.1/commands.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# GSP Commands 0.1
|
||||
|
||||
## init
|
||||
|
||||
Create a minimal GSP project.
|
||||
|
||||
```bash
|
||||
gsp init [path] [--name project-name] [--entry project.entry] [--force]
|
||||
```
|
||||
|
||||
Generated structure:
|
||||
|
||||
```text
|
||||
project/
|
||||
gsp.manifest
|
||||
design/
|
||||
project.entry.gsp
|
||||
```
|
||||
|
||||
## ai-init
|
||||
|
||||
Create AI-facing usage files for a GSP project.
|
||||
|
||||
```bash
|
||||
gsp ai-init [--root .] [--agents] [--skill generic|codex] [--all] [--force]
|
||||
```
|
||||
|
||||
Default output:
|
||||
|
||||
```text
|
||||
README.md
|
||||
AI_USAGE.md
|
||||
```
|
||||
|
||||
Optional output:
|
||||
|
||||
```text
|
||||
AGENTS.md
|
||||
skills/gsp/SKILL.md
|
||||
.codex/skills/gsp/SKILL.md
|
||||
```
|
||||
|
||||
## version
|
||||
|
||||
Print toolkit and protocol versions.
|
||||
|
||||
```bash
|
||||
gsp version
|
||||
gsp version --json
|
||||
```
|
||||
|
||||
## validate
|
||||
|
||||
Validate GSP files and references.
|
||||
|
||||
```bash
|
||||
gsp validate [--root .] [--out report.json]
|
||||
```
|
||||
|
||||
## index
|
||||
|
||||
Build a stable GSP index.
|
||||
|
||||
```bash
|
||||
gsp index [--root .] [--out index.json]
|
||||
```
|
||||
|
||||
## trace
|
||||
|
||||
Inspect relation chains from one GSP id.
|
||||
|
||||
```bash
|
||||
gsp trace <id> [--root .] [--depth 3] [--out trace.json]
|
||||
```
|
||||
|
||||
## flatten
|
||||
|
||||
Expand one GSP id into a flat context.
|
||||
|
||||
```bash
|
||||
gsp flatten <id> [--root .] [--depth 3] [--include-type a,b] [--exclude-type a,b] [--out flattened.json]
|
||||
```
|
||||
|
||||
## pack
|
||||
|
||||
Create a compact AI context pack.
|
||||
|
||||
```bash
|
||||
gsp pack <id> [--root .] [--depth 3] [--budget 12000] [--out context-pack.json]
|
||||
```
|
||||
|
||||
## graph
|
||||
|
||||
Generate a relation graph.
|
||||
|
||||
```bash
|
||||
gsp graph [id] [--root .] [--depth 3] [--format json|mermaid] [--out graph.json]
|
||||
```
|
||||
|
||||
## stage-check
|
||||
|
||||
Check minimum resolution for a stage.
|
||||
|
||||
```bash
|
||||
gsp stage-check --stage implement [--root .] [--out stage-report.json]
|
||||
```
|
||||
@@ -40,8 +40,8 @@ project/
|
||||
严格字段规范参考:
|
||||
|
||||
```text
|
||||
../language/gsp.schema.json
|
||||
../language/gsp.manifest.schema.json
|
||||
../specs/versions/0.1/gsp.schema.json
|
||||
../specs/versions/0.1/gsp.manifest.schema.json
|
||||
```
|
||||
|
||||
项目级入口配置:
|
||||
@@ -165,7 +165,10 @@ manifest 可用于声明:
|
||||
## 4. 第一版命令
|
||||
|
||||
```bash
|
||||
gsp init
|
||||
gsp validate
|
||||
gsp ai-init
|
||||
gsp version
|
||||
gsp index
|
||||
gsp trace <id>
|
||||
gsp flatten <id>
|
||||
@@ -180,6 +183,7 @@ gsp stage-check --stage implement
|
||||
|
||||
```bash
|
||||
go build -o ../bin/gsp ./cmd/gsp
|
||||
../bin/gsp version
|
||||
../bin/gsp validate --root ../examples/lottery
|
||||
../bin/gsp flatten page.lottery.main --root ../examples/lottery --depth -1 --out ../.gsp/flattened.json
|
||||
../bin/gsp graph page.lottery.main --root ../examples/lottery --format mermaid --out ../.gsp/graph.mmd
|
||||
|
||||
@@ -24,7 +24,18 @@ func run(args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(args) == 1 && (args[0] == "-v" || args[0] == "--version") {
|
||||
printVersion(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "init":
|
||||
return runInit(args[1:])
|
||||
case "ai-init":
|
||||
return runAIInit(args[1:])
|
||||
case "version":
|
||||
return runVersion(args[1:])
|
||||
case "validate":
|
||||
return runValidate(args[1:])
|
||||
case "index":
|
||||
@@ -51,6 +62,9 @@ func printHelp() {
|
||||
fmt.Print(`GSP Toolkit
|
||||
|
||||
Usage:
|
||||
gsp init [path] [--name project-name] [--entry project.entry] [--force]
|
||||
gsp ai-init [--root .] [--agents] [--skill generic|codex] [--all] [--force]
|
||||
gsp version [--json]
|
||||
gsp validate [--root .] [--out report.json]
|
||||
gsp index [--root .] [--out index.json]
|
||||
gsp trace <id> [--root .] [--depth 3] [--out trace.json]
|
||||
@@ -61,6 +75,88 @@ Usage:
|
||||
`)
|
||||
}
|
||||
|
||||
func runAIInit(args []string) error {
|
||||
fs := flag.NewFlagSet("ai-init", flag.ContinueOnError)
|
||||
root := commonRoot(fs)
|
||||
agents := fs.Bool("agents", false, "generate AGENTS.md")
|
||||
skill := fs.String("skill", "", "generate a skill adapter: generic or codex")
|
||||
all := fs.Bool("all", false, "generate README.md, AI_USAGE.md, AGENTS.md, and generic skill")
|
||||
force := fs.Bool("force", false, "overwrite generated AI files when they already exist")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("ai-init does not accept positional arguments; use --root")
|
||||
}
|
||||
result, err := gsp.InitAIUsage(*root, gsp.AIInitOptions{
|
||||
Agents: *agents,
|
||||
Skill: *skill,
|
||||
All: *all,
|
||||
Force: *force,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSON("", result)
|
||||
}
|
||||
|
||||
func runInit(args []string) error {
|
||||
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||
name := fs.String("name", "", "project name")
|
||||
entry := fs.String("entry", "project.entry", "entry GSP id")
|
||||
force := fs.Bool("force", false, "overwrite generated init files when they already exist")
|
||||
if err := fs.Parse(normalizeFlagArgs(args)); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() > 1 {
|
||||
return fmt.Errorf("init accepts zero or one path")
|
||||
}
|
||||
root := "."
|
||||
if fs.NArg() == 1 {
|
||||
root = fs.Arg(0)
|
||||
}
|
||||
result, err := gsp.InitProject(root, gsp.InitOptions{
|
||||
Name: *name,
|
||||
Entry: *entry,
|
||||
Force: *force,
|
||||
GSPVersion: gsp.DefaultGSPVersion,
|
||||
ToolkitVersion: gsp.ToolkitVersion,
|
||||
DefaultOutputFolder: ".gsp",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSON("", result)
|
||||
}
|
||||
|
||||
func runVersion(args []string) error {
|
||||
fs := flag.NewFlagSet("version", flag.ContinueOnError)
|
||||
asJSON := fs.Bool("json", false, "print version as JSON")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
printVersion(*asJSON)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printVersion(asJSON bool) {
|
||||
value := struct {
|
||||
ToolkitVersion string `json:"toolkitVersion"`
|
||||
DefaultGSPVersion string `json:"defaultGspVersion"`
|
||||
SupportedGSPVersion []string `json:"supportedGspVersions"`
|
||||
}{
|
||||
ToolkitVersion: gsp.ToolkitVersion,
|
||||
DefaultGSPVersion: gsp.DefaultGSPVersion,
|
||||
SupportedGSPVersion: gsp.SupportedGSPVersions,
|
||||
}
|
||||
if asJSON {
|
||||
data, _ := json.MarshalIndent(value, "", " ")
|
||||
fmt.Println(string(data))
|
||||
return
|
||||
}
|
||||
fmt.Printf("GSP Toolkit %s\nDefault GSP %s\nSupported GSP %s\n", gsp.ToolkitVersion, gsp.DefaultGSPVersion, strings.Join(gsp.SupportedGSPVersions, ", "))
|
||||
}
|
||||
|
||||
func commonRoot(fs *flag.FlagSet) *string {
|
||||
return fs.String("root", ".", "project root to scan")
|
||||
}
|
||||
@@ -236,6 +332,8 @@ func normalizeFlagArgs(args []string) []string {
|
||||
"-budget": true, "--budget": true,
|
||||
"-format": true, "--format": true,
|
||||
"-stage": true, "--stage": true,
|
||||
"-name": true, "--name": true,
|
||||
"-entry": true, "--entry": true,
|
||||
}
|
||||
var flags []string
|
||||
var positionals []string
|
||||
|
||||
154
toolkit/internal/gsp/ai_init.go
Normal file
154
toolkit/internal/gsp/ai_init.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package gsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AIInitOptions struct {
|
||||
Agents bool
|
||||
Skill string
|
||||
All bool
|
||||
Force bool
|
||||
}
|
||||
|
||||
func InitAIUsage(root string, options AIInitOptions) (InitResult, error) {
|
||||
project, err := LoadProject(root)
|
||||
if err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
if project.Manifest == nil {
|
||||
return InitResult{}, fmt.Errorf("gsp.manifest not found; run gsp init first")
|
||||
}
|
||||
|
||||
result := InitResult{Root: filepath.ToSlash(project.Root)}
|
||||
projectName := project.Manifest.Project
|
||||
if projectName == "" {
|
||||
projectName = filepath.Base(project.Root)
|
||||
}
|
||||
entry := "project.entry"
|
||||
if len(project.Manifest.Entry) > 0 && project.Manifest.Entry[0] != "" {
|
||||
entry = project.Manifest.Entry[0]
|
||||
}
|
||||
scan := "design"
|
||||
if len(project.Manifest.Scan) > 0 && project.Manifest.Scan[0] != "" {
|
||||
scan = project.Manifest.Scan[0]
|
||||
}
|
||||
|
||||
files := []initFile{
|
||||
{
|
||||
Path: "README.md",
|
||||
Data: projectReadme(projectName, scan, entry),
|
||||
},
|
||||
{
|
||||
Path: "AI_USAGE.md",
|
||||
Data: aiUsage(project.Manifest.GSPVersion, scan),
|
||||
},
|
||||
}
|
||||
|
||||
if options.All || options.Agents {
|
||||
files = append(files, initFile{
|
||||
Path: "AGENTS.md",
|
||||
Data: agentsUsage(),
|
||||
})
|
||||
}
|
||||
|
||||
skill := strings.TrimSpace(options.Skill)
|
||||
if options.All && skill == "" {
|
||||
skill = "generic"
|
||||
}
|
||||
if skill != "" {
|
||||
path, data, err := skillUsage(skill)
|
||||
if err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
files = append(files, initFile{Path: path, Data: data})
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
path := filepath.Join(project.Root, filepath.FromSlash(file.Path))
|
||||
if err := writeInitFile(project.Root, path, []byte(file.Data), options.Force, &result); err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type initFile struct {
|
||||
Path string
|
||||
Data string
|
||||
}
|
||||
|
||||
func projectReadme(projectName, scan, entry string) string {
|
||||
return fmt.Sprintf(`# %s
|
||||
|
||||
This is a GSP project.
|
||||
|
||||
- Manifest: `+"`gsp.manifest`"+`
|
||||
- GSP source: `+"`%s/`"+`
|
||||
- Entry GSP: `+"`%s`"+`
|
||||
- AI rules: `+"`AI_USAGE.md`"+`
|
||||
|
||||
`, projectName, scan, entry)
|
||||
}
|
||||
|
||||
func aiUsage(gspVersion, scan string) string {
|
||||
if gspVersion == "" {
|
||||
gspVersion = DefaultGSPVersion
|
||||
}
|
||||
return fmt.Sprintf(`# AI Usage
|
||||
|
||||
- Read `+"`gsp.manifest`"+` first.
|
||||
- Use GSP version `+"`%s`"+`.
|
||||
- Treat `+"`%s/`"+` as the default GSP source directory.
|
||||
- `+"`.gsp`"+` files use YAML.
|
||||
- Preserve `+"`id`"+`; do not rename it unless explicitly requested.
|
||||
- `+"`id`"+` is the unique identity of a GSP unit.
|
||||
- Use only fields valid for the declared GSP version.
|
||||
- `+"`with`"+` means related design context.
|
||||
- `+"`refines`"+` means single-source refinement.
|
||||
- Empty `+"`context`"+` means placeholder.
|
||||
- Do not invent missing referenced GSPs silently.
|
||||
- Use `+"`gsp validate`"+` after editing GSP files.
|
||||
- Use `+"`gsp index`"+` to locate GSP units.
|
||||
- Use `+"`gsp trace <id>`"+` to inspect relations.
|
||||
- Use `+"`gsp flatten <id>`"+` before implementation or task splitting.
|
||||
- Use `+"`gsp pack <id>`"+` when a compact AI context is needed.
|
||||
- Use `+"`gsp stage-check --stage <stage>`"+` before stage handoff.
|
||||
|
||||
`, gspVersion, scan)
|
||||
}
|
||||
|
||||
func agentsUsage() string {
|
||||
return `# Agent Instructions
|
||||
|
||||
Read ` + "`AI_USAGE.md`" + ` before working on this GSP project.
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
func skillUsage(kind string) (string, string, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(kind)) {
|
||||
case "generic":
|
||||
return "skills/gsp/SKILL.md", `# GSP
|
||||
|
||||
Use this skill when working in a GSP project.
|
||||
|
||||
Read ` + "`gsp.manifest`" + `, then ` + "`AI_USAGE.md`" + `.
|
||||
Use ` + "`gsp validate`" + `, ` + "`gsp index`" + `, and ` + "`gsp flatten <id>`" + ` when handling GSP files.
|
||||
|
||||
`, nil
|
||||
case "codex":
|
||||
return ".codex/skills/gsp/SKILL.md", `# GSP
|
||||
|
||||
Use this skill when working in a GSP project.
|
||||
|
||||
Read ` + "`gsp.manifest`" + `, then ` + "`AI_USAGE.md`" + `.
|
||||
Use GSP Toolkit commands before editing, handoff, implementation, or review.
|
||||
|
||||
`, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported skill %q; use generic or codex", kind)
|
||||
}
|
||||
}
|
||||
179
toolkit/internal/gsp/init.go
Normal file
179
toolkit/internal/gsp/init.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package gsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InitOptions struct {
|
||||
Name string
|
||||
Entry string
|
||||
Force bool
|
||||
GSPVersion string
|
||||
ToolkitVersion string
|
||||
DefaultOutputFolder string
|
||||
}
|
||||
|
||||
type InitResult struct {
|
||||
Root string `json:"root"`
|
||||
Created []string `json:"created,omitempty"`
|
||||
Updated []string `json:"updated,omitempty"`
|
||||
}
|
||||
|
||||
func InitProject(root string, options InitOptions) (InitResult, error) {
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
if err := os.MkdirAll(absRoot, 0755); err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(options.Name)
|
||||
if name == "" {
|
||||
name = filepath.Base(absRoot)
|
||||
}
|
||||
entry := strings.TrimSpace(options.Entry)
|
||||
if entry == "" {
|
||||
entry = "project.entry"
|
||||
}
|
||||
gspVersion := strings.TrimSpace(options.GSPVersion)
|
||||
if gspVersion == "" {
|
||||
gspVersion = DefaultGSPVersion
|
||||
}
|
||||
toolkitVersion := strings.TrimSpace(options.ToolkitVersion)
|
||||
if toolkitVersion == "" {
|
||||
toolkitVersion = ToolkitVersion
|
||||
}
|
||||
output := strings.TrimSpace(options.DefaultOutputFolder)
|
||||
if output == "" {
|
||||
output = ".gsp"
|
||||
}
|
||||
|
||||
result := InitResult{Root: filepath.ToSlash(absRoot)}
|
||||
designDir := filepath.Join(absRoot, "design")
|
||||
if err := os.MkdirAll(designDir, 0755); err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
result.Created = append(result.Created, relPath(absRoot, designDir)+"/")
|
||||
|
||||
manifest := fmt.Sprintf(`gspVersion: %s
|
||||
toolkitVersion: %s
|
||||
project: %s
|
||||
entry:
|
||||
- %s
|
||||
scan:
|
||||
- design
|
||||
stageRules:
|
||||
design:
|
||||
minResolution: L0
|
||||
integrate:
|
||||
minResolution: L2
|
||||
implement:
|
||||
minResolution: L3
|
||||
bind:
|
||||
minResolution: L4
|
||||
release:
|
||||
minResolution: L5
|
||||
types:
|
||||
- concept
|
||||
- style
|
||||
- feedback
|
||||
- interaction
|
||||
- mechanic
|
||||
- page
|
||||
output: %s
|
||||
`, gspVersion, toolkitVersion, yamlScalar(name), yamlScalar(entry), yamlScalar(output))
|
||||
|
||||
entryFile := filepath.Join(designDir, safeFileName(entry)+".gsp")
|
||||
entryContent := fmt.Sprintf(`id: %s
|
||||
type: concept
|
||||
resolution: L0
|
||||
context: Project entry GSP.
|
||||
`, yamlScalar(entry))
|
||||
|
||||
if err := writeInitFile(absRoot, filepath.Join(absRoot, "gsp.manifest"), []byte(manifest), options.Force, &result); err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
if err := writeInitFile(absRoot, entryFile, []byte(entryContent), options.Force, &result); err != nil {
|
||||
return InitResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeInitFile(root, path string, data []byte, force bool, result *InitResult) error {
|
||||
existed := false
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
existed = true
|
||||
if !force {
|
||||
return fmt.Errorf("%s already exists; use --force to overwrite generated init files", relPath(root, path))
|
||||
}
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
rel := relPath(root, path)
|
||||
if existed {
|
||||
result.Updated = append(result.Updated, rel)
|
||||
return nil
|
||||
}
|
||||
result.Created = append(result.Created, rel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func safeFileName(value string) string {
|
||||
var builder strings.Builder
|
||||
for _, r := range value {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
builder.WriteRune(r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
builder.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
builder.WriteRune(r)
|
||||
case r == '.', r == '-', r == '_':
|
||||
builder.WriteRune(r)
|
||||
default:
|
||||
builder.WriteRune('_')
|
||||
}
|
||||
}
|
||||
name := strings.Trim(builder.String(), "._-")
|
||||
if name == "" {
|
||||
return "project.entry"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func yamlScalar(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return `""`
|
||||
}
|
||||
if isPlainYAMLScalar(value) {
|
||||
return value
|
||||
}
|
||||
escaped := strings.ReplaceAll(value, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
|
||||
return `"` + escaped + `"`
|
||||
}
|
||||
|
||||
func isPlainYAMLScalar(value string) bool {
|
||||
for _, r := range value {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '.', r == '-', r == '_':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -19,6 +19,13 @@ func (p *Project) Validate(stage string) Report {
|
||||
report.Notices = append(report.Notices, issue)
|
||||
}
|
||||
}
|
||||
if p.Manifest != nil {
|
||||
if p.Manifest.GSPVersion == "" {
|
||||
report.addWarning("missing_gsp_version", "", p.Manifest.File, "manifest has no gspVersion")
|
||||
} else if !SupportsGSPVersion(p.Manifest.GSPVersion) {
|
||||
report.addError("unsupported_gsp_version", "", p.Manifest.File, fmt.Sprintf("GSP version %q is not supported by this toolkit", p.Manifest.GSPVersion))
|
||||
}
|
||||
}
|
||||
for _, unit := range p.Units {
|
||||
if unit.ID == "" {
|
||||
report.addError("missing_id", "", unit.File, "GSP requires id")
|
||||
|
||||
@@ -105,6 +105,125 @@ resolution: L2
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUnsupportedGSPVersion(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
design := filepath.Join(root, "design")
|
||||
if err := os.MkdirAll(design, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTestFile(t, root, "gsp.manifest", `gspVersion: 9.9
|
||||
scan:
|
||||
- design
|
||||
`)
|
||||
writeTestFile(t, design, "entry.gsp", `id: project.entry
|
||||
`)
|
||||
project, err := LoadProject(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report := project.Validate("")
|
||||
if report.OK {
|
||||
t.Fatal("expected unsupported version failure")
|
||||
}
|
||||
if report.Errors[0].Code != "unsupported_gsp_version" {
|
||||
t.Fatalf("expected unsupported_gsp_version, got %+v", report.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitProjectCreatesManifestAndEntry(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "sample-project")
|
||||
result, err := InitProject(root, InitOptions{
|
||||
Name: "sample",
|
||||
Entry: "page.sample.main",
|
||||
GSPVersion: "0.1",
|
||||
ToolkitVersion: "0.1.0",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Created) == 0 {
|
||||
t.Fatal("expected created paths")
|
||||
}
|
||||
|
||||
project, err := LoadProject(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if project.Manifest == nil {
|
||||
t.Fatal("expected manifest")
|
||||
}
|
||||
if project.Manifest.Entry[0] != "page.sample.main" {
|
||||
t.Fatalf("manifest entry = %v", project.Manifest.Entry)
|
||||
}
|
||||
if _, ok := project.ByID["page.sample.main"]; !ok {
|
||||
t.Fatal("expected initialized entry GSP")
|
||||
}
|
||||
report := project.Validate("")
|
||||
if !report.OK {
|
||||
t.Fatalf("expected valid initialized project, got %+v", report.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitProjectRefusesOverwrite(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if _, err := InitProject(root, InitOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := InitProject(root, InitOptions{}); err == nil {
|
||||
t.Fatal("expected overwrite refusal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAIUsageCreatesCoreFiles(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if _, err := InitProject(root, InitOptions{
|
||||
Name: "sample",
|
||||
Entry: "page.sample.main",
|
||||
GSPVersion: "0.1",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := InitAIUsage(root, AIInitOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !sameStrings(result.Created, []string{"README.md", "AI_USAGE.md"}) {
|
||||
t.Fatalf("created = %v", result.Created)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "README.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "AI_USAGE.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAIUsageOptionalAdapters(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if _, err := InitProject(root, InitOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := InitAIUsage(root, AIInitOptions{
|
||||
Agents: true,
|
||||
Skill: "generic",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "AGENTS.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "skills", "gsp", "SKILL.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAIUsageRequiresManifest(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if _, err := InitAIUsage(root, AIInitOptions{}); err == nil {
|
||||
t.Fatal("expected missing manifest error")
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestFile(t *testing.T, root, name, content string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(root, name)
|
||||
|
||||
18
toolkit/internal/gsp/version.go
Normal file
18
toolkit/internal/gsp/version.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package gsp
|
||||
|
||||
const ToolkitVersion = "0.1.0"
|
||||
const DefaultGSPVersion = "0.1"
|
||||
|
||||
var SupportedGSPVersions = []string{DefaultGSPVersion}
|
||||
|
||||
func SupportsGSPVersion(version string) bool {
|
||||
if version == "" {
|
||||
return true
|
||||
}
|
||||
for _, supported := range SupportedGSPVersions {
|
||||
if version == supported {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
38
toolkit/scripts/install.ps1
Normal file
38
toolkit/scripts/install.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
param(
|
||||
[string]$InstallDir = "$HOME\.gsp\bin"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ToolkitDir = Resolve-Path -LiteralPath (Join-Path $ScriptDir "..")
|
||||
$TargetDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InstallDir)
|
||||
$TargetExe = Join-Path $TargetDir "gsp.exe"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null
|
||||
|
||||
Push-Location $ToolkitDir
|
||||
try {
|
||||
go build -o $TargetExe .\cmd\gsp
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
$parts = @()
|
||||
if ($userPath) {
|
||||
$parts = $userPath -split ";" | Where-Object { $_ -ne "" }
|
||||
}
|
||||
|
||||
if ($parts -notcontains $TargetDir) {
|
||||
$newPath = (($parts + $TargetDir) -join ";")
|
||||
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
||||
Write-Host "Installed gsp to $TargetExe"
|
||||
Write-Host "Added $TargetDir to the user PATH. Open a new terminal before running gsp."
|
||||
}
|
||||
else {
|
||||
Write-Host "Installed gsp to $TargetExe"
|
||||
}
|
||||
|
||||
& $TargetExe version
|
||||
27
toolkit/scripts/install.sh
Normal file
27
toolkit/scripts/install.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
TOOLKIT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
INSTALL_DIR=${GSP_INSTALL_DIR:-"$HOME/.gsp/bin"}
|
||||
TARGET="$INSTALL_DIR/gsp"
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
(
|
||||
cd "$TOOLKIT_DIR"
|
||||
go build -o "$TARGET" ./cmd/gsp
|
||||
)
|
||||
|
||||
chmod +x "$TARGET"
|
||||
|
||||
echo "Installed gsp to $TARGET"
|
||||
case ":$PATH:" in
|
||||
*":$INSTALL_DIR:"*) ;;
|
||||
*)
|
||||
echo "Add this to your shell profile if gsp is not found:"
|
||||
echo "export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||
;;
|
||||
esac
|
||||
|
||||
"$TARGET" version
|
||||
Reference in New Issue
Block a user