@delegance/claude-autopilot 7.5.0 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/dist/src/cli/help-text.js +1 -1
- package/dist/src/cli/index.js +4 -2
- package/dist/src/cli/scaffold/go.d.ts +27 -0
- package/dist/src/cli/scaffold/go.js +225 -0
- package/dist/src/cli/scaffold/rust.d.ts +38 -0
- package/dist/src/cli/scaffold/rust.js +281 -0
- package/dist/src/cli/scaffold/types.d.ts +7 -4
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +97 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
- v5.6 Phase 7 (docs reconciliation) — pending.
|
|
4
4
|
|
|
5
|
+
## 7.7.0 (2026-05-11)
|
|
6
|
+
|
|
7
|
+
**v7.7.0 — Rust scaffold support.** Minor release. Promotes Rust from
|
|
8
|
+
"detected-but-unsupported" (exit 3 in v7.4–v7.6) to a first-class
|
|
9
|
+
scaffold target, matching the Node + Python + FastAPI + Go shape.
|
|
10
|
+
|
|
11
|
+
**New:** `claude-autopilot scaffold --from-spec <spec.md> --stack rust`
|
|
12
|
+
(or auto-detected when the spec's `## Files` section lists `Cargo.toml`,
|
|
13
|
+
`src/main.rs`, or `src/lib.rs`).
|
|
14
|
+
|
|
15
|
+
**Lib-vs-bin fork.** Rust adds a fork the Go scaffolder doesn't need:
|
|
16
|
+
|
|
17
|
+
- spec lists ONLY `src/lib.rs` (no main.rs) → **library crate**
|
|
18
|
+
(`src/lib.rs` with a public `hello()` + inline `#[cfg(test)] mod tests`,
|
|
19
|
+
Cargo.lock added to `.gitignore`)
|
|
20
|
+
- spec lists `src/main.rs` (with or without lib.rs) → **binary crate**
|
|
21
|
+
(`src/main.rs` with `println!` + `tests/integration_test.rs` smoke
|
|
22
|
+
test, Cargo.lock NOT in `.gitignore`)
|
|
23
|
+
- spec lists BOTH → **mixed mode** (both targets generated; Cargo.lock
|
|
24
|
+
excluded since the binary target wins per Cargo's documented convention)
|
|
25
|
+
- spec hints neither → defaults to binary (matches `cargo init` default)
|
|
26
|
+
|
|
27
|
+
**Cargo.lock heuristic.** Library-only crates omit `Cargo.lock` from the
|
|
28
|
+
commit per Cargo docs; binary + mixed crates commit it. The Rust
|
|
29
|
+
scaffolder's `.gitignore` augmentation reflects this — `target/` is
|
|
30
|
+
always added; `Cargo.lock` is added only when the spec resolves to
|
|
31
|
+
library-only.
|
|
32
|
+
|
|
33
|
+
**Crate name normalization.** Cargo identifiers are `[a-z0-9_]` only and
|
|
34
|
+
must not start with a digit. The scaffolder lowercases `basename(cwd)`,
|
|
35
|
+
replaces any non-allowed char with `_`, collapses underscore runs, and
|
|
36
|
+
prefixes `_` when the result starts with a digit. Examples: `my-pkg-2`
|
|
37
|
+
→ `my_pkg_2`, `2cool` → `_2cool`, `My App` → `my_app`, `foo.bar` →
|
|
38
|
+
`foo_bar`.
|
|
39
|
+
|
|
40
|
+
**Never overwrites.** `Cargo.toml`, `src/main.rs`, `src/lib.rs`, and
|
|
41
|
+
`tests/integration_test.rs` are preserved if they already exist — matches
|
|
42
|
+
the Go + Python scaffolder pattern.
|
|
43
|
+
|
|
44
|
+
**Polyglot detection.** `detectStack()` now includes Rust signals
|
|
45
|
+
(`Cargo.toml` / `src/main.rs` / `src/lib.rs`) in the polyglot count. So
|
|
46
|
+
e.g. `package.json` + `Cargo.toml` together correctly fail-loud with
|
|
47
|
+
`polyglot spec — pass --stack to disambiguate` instead of silently
|
|
48
|
+
picking one.
|
|
49
|
+
|
|
50
|
+
**`--list-stacks`** now shows Rust under Supported and drops it from
|
|
51
|
+
Recognized-but-unsupported. Ruby remains the lone detection-only stack
|
|
52
|
+
(would detect via `Gemfile`).
|
|
53
|
+
|
|
54
|
+
## 7.6.0 (2026-05-10)
|
|
55
|
+
|
|
56
|
+
**v7.6.0 — Go scaffold support.** Minor release. Promotes Go from
|
|
57
|
+
"detected-but-unsupported" (exit 3 in v7.4/v7.5) to a first-class
|
|
58
|
+
scaffold target, matching the Node + Python + FastAPI shape.
|
|
59
|
+
|
|
60
|
+
**New:** `claude-autopilot scaffold --from-spec <spec.md> --stack go`
|
|
61
|
+
(or auto-detected when the spec's `## Files` section lists `go.mod`
|
|
62
|
+
or `main.go`).
|
|
63
|
+
|
|
64
|
+
Generates for a basic spec:
|
|
65
|
+
|
|
66
|
+
- `go.mod` — `module <basename(cwd)>` + `go 1.22`, with an inline
|
|
67
|
+
comment documenting that the module path is the local-scaffold
|
|
68
|
+
default and should be replaced with the full hosted path
|
|
69
|
+
(e.g. `github.com/<user>/<name>`) before publishing.
|
|
70
|
+
- `main.go` — `package main` + Hello world (skipped when the spec
|
|
71
|
+
uses a `cmd/<name>/main.go` layout).
|
|
72
|
+
- `main_test.go` — `TestSmoke` stub for table-driven tests.
|
|
73
|
+
- `.gitignore` — idempotent augmentation: appends `vendor/`,
|
|
74
|
+
`*.exe`, `*.test` if not already present.
|
|
75
|
+
|
|
76
|
+
**Name normalization.** `basename(cwd)` lowercased, whitespace
|
|
77
|
+
collapsed to `-`. Dots + hyphens preserved (valid Go module path
|
|
78
|
+
chars). Path-invalid characters (`/`, `\`, control bytes) reject
|
|
79
|
+
with a clear error.
|
|
80
|
+
|
|
81
|
+
**Never overwrites.** `go.mod`, `main.go`, `main_test.go`, and
|
|
82
|
+
existing `.gitignore` entries are preserved — matches the Python
|
|
83
|
+
scaffolder pattern.
|
|
84
|
+
|
|
85
|
+
**Polyglot detection (codex CRITICAL pass-1).** `detectStack()` now
|
|
86
|
+
scans `## Files` for ALL supported stack signals (Node, Python, Go)
|
|
87
|
+
and exits 3 with `polyglot spec — pass --stack to disambiguate` when
|
|
88
|
+
more than one supported stack is present. Previously the check was
|
|
89
|
+
Node-vs-Python only; v7.6 closes the gap so e.g. `package.json` +
|
|
90
|
+
`go.mod` together correctly fail-loud instead of silently picking one.
|
|
91
|
+
|
|
92
|
+
**`--list-stacks`** now shows Go under Supported and drops it from
|
|
93
|
+
Recognized-but-unsupported. Rust + Ruby remain detection-only; the
|
|
94
|
+
Rust scaffolder is deferred to **v7.7.0** (out of scope here).
|
|
95
|
+
|
|
96
|
+
**Rust deferred.** `Cargo.toml` still exits 3 ("rust detected but
|
|
97
|
+
not supported until v7.7"). Targeted for v7.7.0 alongside the same
|
|
98
|
+
shape (Cargo.toml + `src/main.rs` + a smoke test).
|
|
99
|
+
|
|
5
100
|
## 7.5.0 (2026-05-10)
|
|
6
101
|
|
|
7
102
|
**v7.5.0 — route-sensitivity-tiered membership revocation.** Minor
|
|
@@ -28,7 +28,7 @@ export const HELP_GROUPS = [
|
|
|
28
28
|
verbs: [
|
|
29
29
|
{ verb: 'init', summary: 'Scaffold guardrail.config.yaml + auto-detect migrate stack (writes .autopilot/stack.md)' },
|
|
30
30
|
{ verb: 'setup', summary: 'Auto-detect stack, write config, install pre-push hook' },
|
|
31
|
-
{ verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path> [--stack node|python|fastapi])' },
|
|
31
|
+
{ verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path> [--stack node|python|fastapi|go|rust])' },
|
|
32
32
|
{ verb: 'autopilot', summary: 'Multi-phase orchestrator — run scan → spec → plan → implement under one runId (v6.2.0)' },
|
|
33
33
|
{ verb: 'brainstorm', summary: 'Pipeline entry point (Claude Code skill — see /brainstorm)' },
|
|
34
34
|
{ verb: 'spec', summary: 'Spec-writing pointer (Claude Code skill — see /brainstorm)' },
|
package/dist/src/cli/index.js
CHANGED
|
@@ -934,6 +934,8 @@ switch (subcommand) {
|
|
|
934
934
|
case 'scaffold': {
|
|
935
935
|
// v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
|
|
936
936
|
// v7.4.0 — `--stack <node|python|fastapi>` + `--list-stacks`.
|
|
937
|
+
// v7.6.0 — `--stack go`.
|
|
938
|
+
// v7.7.0 — `--stack rust`.
|
|
937
939
|
if (boolFlag('list-stacks')) {
|
|
938
940
|
const { printStackList } = await import("./scaffold.js");
|
|
939
941
|
printStackList();
|
|
@@ -942,8 +944,8 @@ switch (subcommand) {
|
|
|
942
944
|
const fromSpec = flag('from-spec');
|
|
943
945
|
const dryRun = boolFlag('dry-run');
|
|
944
946
|
const stackArg = flag('stack');
|
|
945
|
-
if (stackArg && !['node', 'python', 'fastapi'].includes(stackArg)) {
|
|
946
|
-
console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi\x1b[0m`);
|
|
947
|
+
if (stackArg && !['node', 'python', 'fastapi', 'go', 'rust'].includes(stackArg)) {
|
|
948
|
+
console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi, go, rust\x1b[0m`);
|
|
947
949
|
console.error(` See: claude-autopilot scaffold --list-stacks`);
|
|
948
950
|
process.exit(3);
|
|
949
951
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ScaffoldResult, ScaffoldRunContext } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a basename into a valid Go module name.
|
|
4
|
+
*
|
|
5
|
+
* - lowercased
|
|
6
|
+
* - whitespace runs collapse to a single `-`
|
|
7
|
+
* - dots + hyphens preserved (Go modules allow them)
|
|
8
|
+
* - empty result falls back to `app`
|
|
9
|
+
*
|
|
10
|
+
* Throws on path-invalid characters (`/`, `\`, NUL, other control chars) —
|
|
11
|
+
* the caller is expected to surface this as a scaffold error.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeGoModuleName(raw: string): string;
|
|
14
|
+
/** Build the go.mod body. Inline comment documents the local-default. */
|
|
15
|
+
export declare function buildGoMod(moduleName: string): string;
|
|
16
|
+
/** Build main.go body — minimal Hello world package. */
|
|
17
|
+
export declare function buildMainGo(): string;
|
|
18
|
+
/** Build main_test.go body — table-test friendly smoke test. */
|
|
19
|
+
export declare function buildMainTestGo(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Augment `.gitignore` with Go-standard ignores. Idempotent: if `vendor/`
|
|
22
|
+
* already appears the second invocation leaves it alone. Creates the file
|
|
23
|
+
* if it doesn't exist.
|
|
24
|
+
*/
|
|
25
|
+
export declare function augmentGitignore(existing: string | null): string;
|
|
26
|
+
export declare function scaffoldGo(ctx: ScaffoldRunContext): Promise<ScaffoldResult>;
|
|
27
|
+
//# sourceMappingURL=go.d.ts.map
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// v7.6.0 — Go scaffolder.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the Python scaffolder shape:
|
|
4
|
+
// - single `scaffoldGo()` exported entrypoint
|
|
5
|
+
// - pure-function helpers (name normalization, builders) for unit tests
|
|
6
|
+
// - never overwrites existing files (matches `· exists` log pattern)
|
|
7
|
+
// - tracks filesCreated / dirsCreated / filesSkippedExisting for return
|
|
8
|
+
//
|
|
9
|
+
// Output for a basic spec (`## Files` listing go.mod + main.go + main_test.go):
|
|
10
|
+
// - go.mod with `module <basename(cwd)>` and `go 1.22`. Inline comment
|
|
11
|
+
// documents the local-scaffold-default per codex NOTE (not a real
|
|
12
|
+
// hosted module path — users override before publishing).
|
|
13
|
+
// - main.go: package main + Hello world (only when not under cmd/<name>/)
|
|
14
|
+
// - main_test.go: smoke test
|
|
15
|
+
// - .gitignore augmentation: appends `vendor/`, `*.exe`, `*.test`
|
|
16
|
+
// idempotently — if already present, leaves them alone.
|
|
17
|
+
//
|
|
18
|
+
// Name normalization: lowercased basename(cwd), whitespace -> `-`. Dots and
|
|
19
|
+
// hyphens preserved (Go module paths permit them). Path-invalid chars
|
|
20
|
+
// (`/`, `\`, control chars) are rejected with a clear error.
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as fsAsync from 'node:fs/promises';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
const PASS = '\x1b[32m✓\x1b[0m';
|
|
25
|
+
const SKIP = '\x1b[2m·\x1b[0m';
|
|
26
|
+
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a basename into a valid Go module name.
|
|
29
|
+
*
|
|
30
|
+
* - lowercased
|
|
31
|
+
* - whitespace runs collapse to a single `-`
|
|
32
|
+
* - dots + hyphens preserved (Go modules allow them)
|
|
33
|
+
* - empty result falls back to `app`
|
|
34
|
+
*
|
|
35
|
+
* Throws on path-invalid characters (`/`, `\`, NUL, other control chars) —
|
|
36
|
+
* the caller is expected to surface this as a scaffold error.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeGoModuleName(raw) {
|
|
39
|
+
// eslint-disable-next-line no-control-regex
|
|
40
|
+
if (/[\/\\\x00-\x1f]/.test(raw)) {
|
|
41
|
+
throw new Error(`invalid Go module name "${raw}" — path/control characters not allowed`);
|
|
42
|
+
}
|
|
43
|
+
const lower = raw.toLowerCase();
|
|
44
|
+
const collapsed = lower.replace(/\s+/g, '-');
|
|
45
|
+
return collapsed.length > 0 ? collapsed : 'app';
|
|
46
|
+
}
|
|
47
|
+
/** Build the go.mod body. Inline comment documents the local-default. */
|
|
48
|
+
export function buildGoMod(moduleName) {
|
|
49
|
+
return `// NOTE: module name is the local-scaffold default (basename of cwd).
|
|
50
|
+
// Replace with your full module path (e.g. github.com/<user>/${moduleName})
|
|
51
|
+
// before publishing or running \`go install\`.
|
|
52
|
+
module ${moduleName}
|
|
53
|
+
|
|
54
|
+
go 1.22
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
/** Build main.go body — minimal Hello world package. */
|
|
58
|
+
export function buildMainGo() {
|
|
59
|
+
return [
|
|
60
|
+
'package main',
|
|
61
|
+
'',
|
|
62
|
+
'import "fmt"',
|
|
63
|
+
'',
|
|
64
|
+
'func main() {',
|
|
65
|
+
'\tfmt.Println("Hello, world!")',
|
|
66
|
+
'}',
|
|
67
|
+
'',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
/** Build main_test.go body — table-test friendly smoke test. */
|
|
71
|
+
export function buildMainTestGo() {
|
|
72
|
+
return [
|
|
73
|
+
'package main',
|
|
74
|
+
'',
|
|
75
|
+
'import "testing"',
|
|
76
|
+
'',
|
|
77
|
+
'func TestSmoke(t *testing.T) {',
|
|
78
|
+
'\t// Smoke test scaffolded by claude-autopilot. Replace with real',
|
|
79
|
+
'\t// table-driven cases for your package under test.',
|
|
80
|
+
'}',
|
|
81
|
+
'',
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
/** Lines to append to .gitignore. Idempotent — only adds missing entries. */
|
|
85
|
+
const GO_GITIGNORE_LINES = ['vendor/', '*.exe', '*.test'];
|
|
86
|
+
/**
|
|
87
|
+
* Augment `.gitignore` with Go-standard ignores. Idempotent: if `vendor/`
|
|
88
|
+
* already appears the second invocation leaves it alone. Creates the file
|
|
89
|
+
* if it doesn't exist.
|
|
90
|
+
*/
|
|
91
|
+
export function augmentGitignore(existing) {
|
|
92
|
+
const lines = existing ? existing.split('\n') : [];
|
|
93
|
+
const present = new Set(lines.map(l => l.trim()));
|
|
94
|
+
const toAdd = GO_GITIGNORE_LINES.filter(l => !present.has(l));
|
|
95
|
+
if (toAdd.length === 0)
|
|
96
|
+
return existing ?? '';
|
|
97
|
+
const prefix = existing && !existing.endsWith('\n') ? existing + '\n' : (existing ?? '');
|
|
98
|
+
return prefix + toAdd.join('\n') + '\n';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True when the spec lists ONLY a `cmd/<name>/main.go` style entrypoint
|
|
102
|
+
* (no top-level main.go). In that case we skip generating the top-level
|
|
103
|
+
* main.go — the spec author intends the cmd/ layout.
|
|
104
|
+
*/
|
|
105
|
+
function specHasCmdMainOnly(paths) {
|
|
106
|
+
const hasTopMain = paths.includes('main.go');
|
|
107
|
+
const hasCmdMain = paths.some(p => /^cmd\/[^/]+\/main\.go$/.test(p));
|
|
108
|
+
return hasCmdMain && !hasTopMain;
|
|
109
|
+
}
|
|
110
|
+
export async function scaffoldGo(ctx) {
|
|
111
|
+
const { cwd, parsed, dryRun } = ctx;
|
|
112
|
+
const moduleName = normalizeGoModuleName(path.basename(cwd));
|
|
113
|
+
const filesCreated = [];
|
|
114
|
+
const filesSkippedExisting = [];
|
|
115
|
+
const dirsCreated = [];
|
|
116
|
+
// Files we generate with content rather than empty placeholders.
|
|
117
|
+
const MANAGED_FILES = new Set(['go.mod', 'main.go', 'main_test.go', '.gitignore']);
|
|
118
|
+
// 1) Create directories implied by spec paths.
|
|
119
|
+
const dirs = new Set();
|
|
120
|
+
for (const p of parsed.paths) {
|
|
121
|
+
const d = path.dirname(p);
|
|
122
|
+
if (d && d !== '.')
|
|
123
|
+
dirs.add(d);
|
|
124
|
+
}
|
|
125
|
+
for (const d of dirs) {
|
|
126
|
+
const abs = path.join(cwd, d);
|
|
127
|
+
if (fs.existsSync(abs))
|
|
128
|
+
continue;
|
|
129
|
+
if (!dryRun)
|
|
130
|
+
await fsAsync.mkdir(abs, { recursive: true });
|
|
131
|
+
dirsCreated.push(d);
|
|
132
|
+
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
133
|
+
}
|
|
134
|
+
// 2) Empty-placeholder pass for spec paths we don't manage.
|
|
135
|
+
for (const p of parsed.paths) {
|
|
136
|
+
if (MANAGED_FILES.has(p))
|
|
137
|
+
continue;
|
|
138
|
+
const abs = path.join(cwd, p);
|
|
139
|
+
if (fs.existsSync(abs)) {
|
|
140
|
+
filesSkippedExisting.push(p);
|
|
141
|
+
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!dryRun) {
|
|
145
|
+
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
146
|
+
await fsAsync.writeFile(abs, '', 'utf8');
|
|
147
|
+
}
|
|
148
|
+
filesCreated.push(p);
|
|
149
|
+
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
150
|
+
}
|
|
151
|
+
// 3) go.mod — never overwrite.
|
|
152
|
+
const goModAbs = path.join(cwd, 'go.mod');
|
|
153
|
+
if (fs.existsSync(goModAbs)) {
|
|
154
|
+
filesSkippedExisting.push('go.mod');
|
|
155
|
+
console.log(` ${SKIP} exists ${DIM('go.mod (preserved)')}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (!dryRun)
|
|
159
|
+
await fsAsync.writeFile(goModAbs, buildGoMod(moduleName), 'utf8');
|
|
160
|
+
filesCreated.push('go.mod');
|
|
161
|
+
console.log(` ${PASS} write ${DIM(`go.mod (module ${moduleName}, go 1.22)`)}`);
|
|
162
|
+
}
|
|
163
|
+
// 4) main.go — only if the spec doesn't push us to a cmd/<name>/ layout.
|
|
164
|
+
const cmdOnly = specHasCmdMainOnly(parsed.paths);
|
|
165
|
+
if (!cmdOnly) {
|
|
166
|
+
const mainAbs = path.join(cwd, 'main.go');
|
|
167
|
+
if (fs.existsSync(mainAbs)) {
|
|
168
|
+
filesSkippedExisting.push('main.go');
|
|
169
|
+
console.log(` ${SKIP} exists ${DIM('main.go (preserved)')}`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
if (!dryRun)
|
|
173
|
+
await fsAsync.writeFile(mainAbs, buildMainGo(), 'utf8');
|
|
174
|
+
filesCreated.push('main.go');
|
|
175
|
+
console.log(` ${PASS} write ${DIM('main.go (package main + Hello)')}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 5) main_test.go — only when we also wrote main.go (same cmd-only guard).
|
|
179
|
+
if (!cmdOnly) {
|
|
180
|
+
const testAbs = path.join(cwd, 'main_test.go');
|
|
181
|
+
if (fs.existsSync(testAbs)) {
|
|
182
|
+
filesSkippedExisting.push('main_test.go');
|
|
183
|
+
console.log(` ${SKIP} exists ${DIM('main_test.go (preserved)')}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (!dryRun)
|
|
187
|
+
await fsAsync.writeFile(testAbs, buildMainTestGo(), 'utf8');
|
|
188
|
+
filesCreated.push('main_test.go');
|
|
189
|
+
console.log(` ${PASS} write ${DIM('main_test.go (TestSmoke stub)')}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// 6) .gitignore — idempotent augmentation.
|
|
193
|
+
const giAbs = path.join(cwd, '.gitignore');
|
|
194
|
+
const existing = fs.existsSync(giAbs) ? await fsAsync.readFile(giAbs, 'utf8') : null;
|
|
195
|
+
const augmented = augmentGitignore(existing);
|
|
196
|
+
if (existing === null) {
|
|
197
|
+
if (!dryRun)
|
|
198
|
+
await fsAsync.writeFile(giAbs, augmented, 'utf8');
|
|
199
|
+
filesCreated.push('.gitignore');
|
|
200
|
+
console.log(` ${PASS} write ${DIM('.gitignore (vendor/, *.exe, *.test)')}`);
|
|
201
|
+
}
|
|
202
|
+
else if (augmented !== existing) {
|
|
203
|
+
if (!dryRun)
|
|
204
|
+
await fsAsync.writeFile(giAbs, augmented, 'utf8');
|
|
205
|
+
// Treat as "augmented" — not in skipped-existing (we modified it) and
|
|
206
|
+
// not in filesCreated (we didn't create a new file). For now we count
|
|
207
|
+
// it as filesCreated since the user sees a write. Tests assert
|
|
208
|
+
// idempotence on disk content, not on this return shape.
|
|
209
|
+
filesCreated.push('.gitignore');
|
|
210
|
+
console.log(` ${PASS} augment ${DIM('.gitignore (added Go ignores)')}`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
filesSkippedExisting.push('.gitignore');
|
|
214
|
+
console.log(` ${SKIP} exists ${DIM('.gitignore (Go entries already present)')}`);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
filesCreated,
|
|
218
|
+
dirsCreated,
|
|
219
|
+
filesSkippedExisting,
|
|
220
|
+
// Node-shape fields — Go scaffolder doesn't touch package.json/tsconfig.
|
|
221
|
+
packageJsonAction: 'skipped-exists',
|
|
222
|
+
tsconfigAction: 'skipped-no-ts',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=go.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ScaffoldResult, ScaffoldRunContext } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a basename into a valid Cargo crate name.
|
|
4
|
+
*
|
|
5
|
+
* Cargo identifiers: `[a-z0-9_]` only, must not start with a digit.
|
|
6
|
+
*
|
|
7
|
+
* - lowercased
|
|
8
|
+
* - any char outside `[a-z0-9_]` collapses to `_`
|
|
9
|
+
* - repeated `_` runs collapse to a single `_`
|
|
10
|
+
* - if the result starts with a digit, prefix `_`
|
|
11
|
+
* - empty result falls back to `app`
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeRustCrateName(raw: string): string;
|
|
14
|
+
/** Build the Cargo.toml body. */
|
|
15
|
+
export declare function buildCargoToml(crateName: string): string;
|
|
16
|
+
/** Build src/main.rs body — minimal Hello world binary. */
|
|
17
|
+
export declare function buildMainRs(): string;
|
|
18
|
+
/** Build src/lib.rs body — public fn + inline tests module. */
|
|
19
|
+
export declare function buildLibRs(): string;
|
|
20
|
+
/** Build tests/integration_test.rs body — smoke test. */
|
|
21
|
+
export declare function buildIntegrationTestRs(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Augment `.gitignore` with Rust-standard ignores. Idempotent.
|
|
24
|
+
*
|
|
25
|
+
* `target/` is always added. `Cargo.lock` is added ONLY when
|
|
26
|
+
* `includeCargoLock` is true (library-only crates — lockfiles aren't
|
|
27
|
+
* committed for libraries per Cargo docs). Binary crates commit
|
|
28
|
+
* Cargo.lock so we leave it out of .gitignore.
|
|
29
|
+
*/
|
|
30
|
+
export declare function augmentGitignore(existing: string | null, includeCargoLock: boolean): string;
|
|
31
|
+
/**
|
|
32
|
+
* Classify the crate kind based on spec `## Files` paths. See the
|
|
33
|
+
* file header comment for the fork rules.
|
|
34
|
+
*/
|
|
35
|
+
export type CrateKind = 'binary' | 'library' | 'mixed';
|
|
36
|
+
export declare function classifyCrateKind(paths: string[]): CrateKind;
|
|
37
|
+
export declare function scaffoldRust(ctx: ScaffoldRunContext): Promise<ScaffoldResult>;
|
|
38
|
+
//# sourceMappingURL=rust.d.ts.map
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// v7.7.0 — Rust scaffolder.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors src/cli/scaffold/go.ts (v7.6.0) shape:
|
|
4
|
+
// - single `scaffoldRust()` exported entrypoint
|
|
5
|
+
// - pure-function helpers (name normalization, builders) for unit tests
|
|
6
|
+
// - never overwrites existing files (matches `· exists` log pattern)
|
|
7
|
+
// - tracks filesCreated / dirsCreated / filesSkippedExisting for return
|
|
8
|
+
//
|
|
9
|
+
// Rust adds a lib-vs-bin fork the Go scaffolder doesn't need:
|
|
10
|
+
//
|
|
11
|
+
// - spec lists ONLY `src/lib.rs` (no main.rs) → library crate
|
|
12
|
+
// Cargo.toml + src/lib.rs (public fn + #[cfg(test)] mod tests)
|
|
13
|
+
// .gitignore augmentation INCLUDES `Cargo.lock` (lockfiles are
|
|
14
|
+
// not committed for libraries — see Cargo docs).
|
|
15
|
+
//
|
|
16
|
+
// - spec lists `src/main.rs` (with or without lib.rs) → binary crate
|
|
17
|
+
// Cargo.toml + src/main.rs (println!) + tests/integration_test.rs
|
|
18
|
+
// .gitignore does NOT include Cargo.lock (binaries commit it).
|
|
19
|
+
//
|
|
20
|
+
// - spec lists BOTH src/main.rs AND src/lib.rs → mixed mode
|
|
21
|
+
// Cargo.toml + main.rs + lib.rs + tests/integration_test.rs
|
|
22
|
+
// .gitignore does NOT include Cargo.lock (binary target wins).
|
|
23
|
+
//
|
|
24
|
+
// - spec lists NEITHER (or no spec hint) → default to bin
|
|
25
|
+
// Same as the `src/main.rs`-listed case. Matches `cargo init`
|
|
26
|
+
// default — `cargo init` produces a binary unless `--lib` is
|
|
27
|
+
// passed.
|
|
28
|
+
//
|
|
29
|
+
// Name normalization: Cargo crate identifiers are `[a-z0-9_]` only, must
|
|
30
|
+
// not start with a digit. Lowercase basename(cwd), replace any non-allowed
|
|
31
|
+
// char with `_`, collapse runs of `_`, prefix `_` if the result starts
|
|
32
|
+
// with a digit.
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
import * as fsAsync from 'node:fs/promises';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
const PASS = '\x1b[32m✓\x1b[0m';
|
|
37
|
+
const SKIP = '\x1b[2m·\x1b[0m';
|
|
38
|
+
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a basename into a valid Cargo crate name.
|
|
41
|
+
*
|
|
42
|
+
* Cargo identifiers: `[a-z0-9_]` only, must not start with a digit.
|
|
43
|
+
*
|
|
44
|
+
* - lowercased
|
|
45
|
+
* - any char outside `[a-z0-9_]` collapses to `_`
|
|
46
|
+
* - repeated `_` runs collapse to a single `_`
|
|
47
|
+
* - if the result starts with a digit, prefix `_`
|
|
48
|
+
* - empty result falls back to `app`
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeRustCrateName(raw) {
|
|
51
|
+
const lower = raw.toLowerCase();
|
|
52
|
+
// Replace any non-[a-z0-9_] character with `_`.
|
|
53
|
+
const sanitized = lower.replace(/[^a-z0-9_]+/g, '_');
|
|
54
|
+
// Collapse repeated `_` runs.
|
|
55
|
+
const collapsed = sanitized.replace(/_+/g, '_');
|
|
56
|
+
if (collapsed.length === 0)
|
|
57
|
+
return 'app';
|
|
58
|
+
// Strip leading/trailing underscores that came from leading/trailing
|
|
59
|
+
// separators, BUT preserve a leading `_` we add for digit-start cases.
|
|
60
|
+
const trimmed = collapsed.replace(/^_+|_+$/g, '');
|
|
61
|
+
const base = trimmed.length > 0 ? trimmed : 'app';
|
|
62
|
+
// Cargo crate names cannot start with a digit.
|
|
63
|
+
return /^[0-9]/.test(base) ? `_${base}` : base;
|
|
64
|
+
}
|
|
65
|
+
/** Build the Cargo.toml body. */
|
|
66
|
+
export function buildCargoToml(crateName) {
|
|
67
|
+
return `[package]
|
|
68
|
+
name = "${crateName}"
|
|
69
|
+
version = "0.1.0"
|
|
70
|
+
edition = "2021"
|
|
71
|
+
|
|
72
|
+
[dependencies]
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
/** Build src/main.rs body — minimal Hello world binary. */
|
|
76
|
+
export function buildMainRs() {
|
|
77
|
+
return `fn main() {
|
|
78
|
+
println!("Hello from {}", env!("CARGO_PKG_NAME"));
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
/** Build src/lib.rs body — public fn + inline tests module. */
|
|
83
|
+
export function buildLibRs() {
|
|
84
|
+
return `//! Library entrypoint — auto-scaffolded by claude-autopilot.
|
|
85
|
+
|
|
86
|
+
pub fn hello() -> &'static str {
|
|
87
|
+
"hello"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
mod tests {
|
|
92
|
+
use super::*;
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn it_works() {
|
|
96
|
+
assert_eq!(hello(), "hello");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
/** Build tests/integration_test.rs body — smoke test. */
|
|
102
|
+
export function buildIntegrationTestRs() {
|
|
103
|
+
return `#[test]
|
|
104
|
+
fn smoke() {
|
|
105
|
+
assert_eq!(2 + 2, 4);
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Augment `.gitignore` with Rust-standard ignores. Idempotent.
|
|
111
|
+
*
|
|
112
|
+
* `target/` is always added. `Cargo.lock` is added ONLY when
|
|
113
|
+
* `includeCargoLock` is true (library-only crates — lockfiles aren't
|
|
114
|
+
* committed for libraries per Cargo docs). Binary crates commit
|
|
115
|
+
* Cargo.lock so we leave it out of .gitignore.
|
|
116
|
+
*/
|
|
117
|
+
export function augmentGitignore(existing, includeCargoLock) {
|
|
118
|
+
const toAddCandidates = includeCargoLock ? ['target/', 'Cargo.lock'] : ['target/'];
|
|
119
|
+
const lines = existing ? existing.split('\n') : [];
|
|
120
|
+
const present = new Set(lines.map(l => l.trim()));
|
|
121
|
+
const toAdd = toAddCandidates.filter(l => !present.has(l));
|
|
122
|
+
if (toAdd.length === 0)
|
|
123
|
+
return existing ?? '';
|
|
124
|
+
const prefix = existing && !existing.endsWith('\n') ? existing + '\n' : (existing ?? '');
|
|
125
|
+
return prefix + toAdd.join('\n') + '\n';
|
|
126
|
+
}
|
|
127
|
+
export function classifyCrateKind(paths) {
|
|
128
|
+
const hasMain = paths.includes('src/main.rs');
|
|
129
|
+
const hasLib = paths.includes('src/lib.rs');
|
|
130
|
+
if (hasMain && hasLib)
|
|
131
|
+
return 'mixed';
|
|
132
|
+
if (hasLib && !hasMain)
|
|
133
|
+
return 'library';
|
|
134
|
+
// Default: binary (covers `src/main.rs` listed AND the "neither listed"
|
|
135
|
+
// case — matches `cargo init` default behavior).
|
|
136
|
+
return 'binary';
|
|
137
|
+
}
|
|
138
|
+
export async function scaffoldRust(ctx) {
|
|
139
|
+
const { cwd, parsed, dryRun } = ctx;
|
|
140
|
+
const crateName = normalizeRustCrateName(path.basename(cwd));
|
|
141
|
+
const kind = classifyCrateKind(parsed.paths);
|
|
142
|
+
// Library-only crates omit Cargo.lock from git. Binary + mixed include it.
|
|
143
|
+
const isLibraryOnly = kind === 'library';
|
|
144
|
+
const filesCreated = [];
|
|
145
|
+
const filesSkippedExisting = [];
|
|
146
|
+
const dirsCreated = [];
|
|
147
|
+
// Files we generate with content rather than empty placeholders.
|
|
148
|
+
const MANAGED_FILES = new Set([
|
|
149
|
+
'Cargo.toml',
|
|
150
|
+
'src/main.rs',
|
|
151
|
+
'src/lib.rs',
|
|
152
|
+
'tests/integration_test.rs',
|
|
153
|
+
'.gitignore',
|
|
154
|
+
]);
|
|
155
|
+
// 1) Create directories implied by spec paths.
|
|
156
|
+
const dirs = new Set();
|
|
157
|
+
for (const p of parsed.paths) {
|
|
158
|
+
const d = path.dirname(p);
|
|
159
|
+
if (d && d !== '.')
|
|
160
|
+
dirs.add(d);
|
|
161
|
+
}
|
|
162
|
+
// Ensure we'll have a src/ dir for the targets we write.
|
|
163
|
+
if (kind !== 'library')
|
|
164
|
+
dirs.add('src');
|
|
165
|
+
if (kind === 'library' || kind === 'mixed')
|
|
166
|
+
dirs.add('src');
|
|
167
|
+
// tests/ dir for binary + mixed integration tests.
|
|
168
|
+
if (kind !== 'library')
|
|
169
|
+
dirs.add('tests');
|
|
170
|
+
for (const d of dirs) {
|
|
171
|
+
const abs = path.join(cwd, d);
|
|
172
|
+
if (fs.existsSync(abs))
|
|
173
|
+
continue;
|
|
174
|
+
if (!dryRun)
|
|
175
|
+
await fsAsync.mkdir(abs, { recursive: true });
|
|
176
|
+
dirsCreated.push(d);
|
|
177
|
+
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
178
|
+
}
|
|
179
|
+
// 2) Empty-placeholder pass for spec paths we don't manage.
|
|
180
|
+
for (const p of parsed.paths) {
|
|
181
|
+
if (MANAGED_FILES.has(p))
|
|
182
|
+
continue;
|
|
183
|
+
const abs = path.join(cwd, p);
|
|
184
|
+
if (fs.existsSync(abs)) {
|
|
185
|
+
filesSkippedExisting.push(p);
|
|
186
|
+
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!dryRun) {
|
|
190
|
+
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
191
|
+
await fsAsync.writeFile(abs, '', 'utf8');
|
|
192
|
+
}
|
|
193
|
+
filesCreated.push(p);
|
|
194
|
+
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
195
|
+
}
|
|
196
|
+
// 3) Cargo.toml — never overwrite.
|
|
197
|
+
const cargoAbs = path.join(cwd, 'Cargo.toml');
|
|
198
|
+
if (fs.existsSync(cargoAbs)) {
|
|
199
|
+
filesSkippedExisting.push('Cargo.toml');
|
|
200
|
+
console.log(` ${SKIP} exists ${DIM('Cargo.toml (preserved)')}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
if (!dryRun)
|
|
204
|
+
await fsAsync.writeFile(cargoAbs, buildCargoToml(crateName), 'utf8');
|
|
205
|
+
filesCreated.push('Cargo.toml');
|
|
206
|
+
console.log(` ${PASS} write ${DIM(`Cargo.toml (name = "${crateName}", edition 2021)`)}`);
|
|
207
|
+
}
|
|
208
|
+
// 4) src/main.rs — binary + mixed modes only.
|
|
209
|
+
if (kind === 'binary' || kind === 'mixed') {
|
|
210
|
+
const mainAbs = path.join(cwd, 'src', 'main.rs');
|
|
211
|
+
if (fs.existsSync(mainAbs)) {
|
|
212
|
+
filesSkippedExisting.push('src/main.rs');
|
|
213
|
+
console.log(` ${SKIP} exists ${DIM('src/main.rs (preserved)')}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
if (!dryRun)
|
|
217
|
+
await fsAsync.writeFile(mainAbs, buildMainRs(), 'utf8');
|
|
218
|
+
filesCreated.push('src/main.rs');
|
|
219
|
+
console.log(` ${PASS} write ${DIM('src/main.rs (println! Hello)')}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// 5) src/lib.rs — library + mixed modes only.
|
|
223
|
+
if (kind === 'library' || kind === 'mixed') {
|
|
224
|
+
const libAbs = path.join(cwd, 'src', 'lib.rs');
|
|
225
|
+
if (fs.existsSync(libAbs)) {
|
|
226
|
+
filesSkippedExisting.push('src/lib.rs');
|
|
227
|
+
console.log(` ${SKIP} exists ${DIM('src/lib.rs (preserved)')}`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
if (!dryRun)
|
|
231
|
+
await fsAsync.writeFile(libAbs, buildLibRs(), 'utf8');
|
|
232
|
+
filesCreated.push('src/lib.rs');
|
|
233
|
+
console.log(` ${PASS} write ${DIM('src/lib.rs (pub fn + #[cfg(test)] mod tests)')}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// 6) tests/integration_test.rs — binary + mixed modes only.
|
|
237
|
+
if (kind === 'binary' || kind === 'mixed') {
|
|
238
|
+
const testAbs = path.join(cwd, 'tests', 'integration_test.rs');
|
|
239
|
+
if (fs.existsSync(testAbs)) {
|
|
240
|
+
filesSkippedExisting.push('tests/integration_test.rs');
|
|
241
|
+
console.log(` ${SKIP} exists ${DIM('tests/integration_test.rs (preserved)')}`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
if (!dryRun)
|
|
245
|
+
await fsAsync.writeFile(testAbs, buildIntegrationTestRs(), 'utf8');
|
|
246
|
+
filesCreated.push('tests/integration_test.rs');
|
|
247
|
+
console.log(` ${PASS} write ${DIM('tests/integration_test.rs (smoke test)')}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// 7) .gitignore — idempotent augmentation. Cargo.lock conditional on
|
|
251
|
+
// library-only crates per Cargo's documented convention.
|
|
252
|
+
const giAbs = path.join(cwd, '.gitignore');
|
|
253
|
+
const existing = fs.existsSync(giAbs) ? await fsAsync.readFile(giAbs, 'utf8') : null;
|
|
254
|
+
const augmented = augmentGitignore(existing, isLibraryOnly);
|
|
255
|
+
const lockNote = isLibraryOnly ? 'target/, Cargo.lock' : 'target/';
|
|
256
|
+
if (existing === null) {
|
|
257
|
+
if (!dryRun)
|
|
258
|
+
await fsAsync.writeFile(giAbs, augmented, 'utf8');
|
|
259
|
+
filesCreated.push('.gitignore');
|
|
260
|
+
console.log(` ${PASS} write ${DIM(`.gitignore (${lockNote})`)}`);
|
|
261
|
+
}
|
|
262
|
+
else if (augmented !== existing) {
|
|
263
|
+
if (!dryRun)
|
|
264
|
+
await fsAsync.writeFile(giAbs, augmented, 'utf8');
|
|
265
|
+
filesCreated.push('.gitignore');
|
|
266
|
+
console.log(` ${PASS} augment ${DIM(`.gitignore (added Rust ignores: ${lockNote})`)}`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
filesSkippedExisting.push('.gitignore');
|
|
270
|
+
console.log(` ${SKIP} exists ${DIM('.gitignore (Rust entries already present)')}`);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
filesCreated,
|
|
274
|
+
dirsCreated,
|
|
275
|
+
filesSkippedExisting,
|
|
276
|
+
// Node-shape fields — Rust scaffolder doesn't touch package.json/tsconfig.
|
|
277
|
+
packageJsonAction: 'skipped-exists',
|
|
278
|
+
tsconfigAction: 'skipped-no-ts',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=rust.js.map
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
/** Supported `--stack` values. v7.
|
|
2
|
-
export type Stack = 'node' | 'python' | 'fastapi';
|
|
1
|
+
/** Supported `--stack` values. v7.6 adds 'go'; v7.7 adds 'rust'. */
|
|
2
|
+
export type Stack = 'node' | 'python' | 'fastapi' | 'go' | 'rust';
|
|
3
3
|
/**
|
|
4
4
|
* Stacks we can DETECT but cannot scaffold yet. Detection still warns +
|
|
5
|
-
* exits 3 so the operator gets a clear "
|
|
5
|
+
* exits 3 so the operator gets a clear "vX.Y" diagnostic instead of a
|
|
6
6
|
* silent fallback to Node, which would generate a wrong-language skeleton.
|
|
7
|
+
*
|
|
8
|
+
* v7.7 promoted Rust from this list. Ruby remains the lone unsupported
|
|
9
|
+
* stack (would detect via `Gemfile`).
|
|
7
10
|
*/
|
|
8
|
-
export type UnsupportedStack = '
|
|
11
|
+
export type UnsupportedStack = 'ruby';
|
|
9
12
|
export interface ParsedFiles {
|
|
10
13
|
/** Raw paths extracted from the `## Files` section bullets. */
|
|
11
14
|
paths: string[];
|
|
@@ -2,7 +2,7 @@ import { buildStarterPackageJson } from './scaffold/node.ts';
|
|
|
2
2
|
import type { ParsedFiles, ScaffoldOptions, ScaffoldResult, Stack, UnsupportedStack } from './scaffold/types.ts';
|
|
3
3
|
export { buildStarterPackageJson };
|
|
4
4
|
export type { ScaffoldOptions, ScaffoldResult, ParsedFiles, Stack };
|
|
5
|
-
/** Valid `--stack` argument values. v7.
|
|
5
|
+
/** Valid `--stack` argument values. v7.6 adds 'go'; v7.7 adds 'rust'. */
|
|
6
6
|
export declare const SUPPORTED_STACKS: readonly Stack[];
|
|
7
7
|
/** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
|
|
8
8
|
export declare const UNSUPPORTED_STACK_FILES: Record<UnsupportedStack, string>;
|
package/dist/src/cli/scaffold.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// `parseSpecFiles`, `buildStarterPackageJson`, plus the
|
|
9
9
|
// `ScaffoldOptions` / `ScaffoldResult` types from here so library
|
|
10
10
|
// consumers don't break.
|
|
11
|
+
// v7.6.0 — Go promoted from unsupported to supported.
|
|
12
|
+
// v7.7.0 — Rust promoted from unsupported to supported (lib/bin fork).
|
|
11
13
|
//
|
|
12
14
|
// Stack detection lives in `detectStack()`. Per the spec ("Stack detection"
|
|
13
15
|
// section), precedence is:
|
|
@@ -15,11 +17,13 @@
|
|
|
15
17
|
// 2. FastAPI (path + 'fastapi' mention) — checked BEFORE generic Python so
|
|
16
18
|
// a FastAPI spec listing pyproject.toml isn't mis-classified
|
|
17
19
|
// 3. Python (pyproject.toml or requirements.txt)
|
|
18
|
-
// 4.
|
|
19
|
-
// 5.
|
|
20
|
-
// 6.
|
|
20
|
+
// 4. Go (go.mod or main.go)
|
|
21
|
+
// 5. Rust (Cargo.toml or src/main.rs or src/lib.rs)
|
|
22
|
+
// 6. Node (package.json)
|
|
23
|
+
// 7. Detected-but-unsupported (Gemfile) -> exit 3
|
|
24
|
+
// 8. Fallback: Node ESM (preserves v7.2.0 default for ambiguous specs)
|
|
21
25
|
//
|
|
22
|
-
// Polyglot guard:
|
|
26
|
+
// Polyglot guard: more than one supported-stack signal listed without --stack
|
|
23
27
|
// -> exit 3 with "polyglot spec — pass --stack to disambiguate".
|
|
24
28
|
//
|
|
25
29
|
// Exit codes:
|
|
@@ -27,24 +31,24 @@
|
|
|
27
31
|
// 1 — spec file missing
|
|
28
32
|
// 2 — spec missing `## Files` section
|
|
29
33
|
// 3 (NEW v7.4.0) — `--stack` value not recognized, detected-but-unsupported
|
|
30
|
-
// stack (
|
|
34
|
+
// stack (Ruby), or polyglot spec without --stack
|
|
31
35
|
import * as fs from 'node:fs';
|
|
32
36
|
import * as fsAsync from 'node:fs/promises';
|
|
33
37
|
import * as path from 'node:path';
|
|
34
38
|
import { scaffoldNode, buildStarterPackageJson } from "./scaffold/node.js";
|
|
35
39
|
import { scaffoldPython } from "./scaffold/python.js";
|
|
40
|
+
import { scaffoldGo } from "./scaffold/go.js";
|
|
41
|
+
import { scaffoldRust } from "./scaffold/rust.js";
|
|
36
42
|
const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
37
43
|
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
38
44
|
// Re-export types + the legacy buildStarterPackageJson so `src/index.ts` and
|
|
39
45
|
// the existing tests/scaffold.test.ts (which imports from this module) keep
|
|
40
46
|
// compiling without changes.
|
|
41
47
|
export { buildStarterPackageJson };
|
|
42
|
-
/** Valid `--stack` argument values. v7.
|
|
43
|
-
export const SUPPORTED_STACKS = ['node', 'python', 'fastapi'];
|
|
48
|
+
/** Valid `--stack` argument values. v7.6 adds 'go'; v7.7 adds 'rust'. */
|
|
49
|
+
export const SUPPORTED_STACKS = ['node', 'python', 'fastapi', 'go', 'rust'];
|
|
44
50
|
/** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
|
|
45
51
|
export const UNSUPPORTED_STACK_FILES = {
|
|
46
|
-
go: 'go.mod',
|
|
47
|
-
rust: 'Cargo.toml',
|
|
48
52
|
ruby: 'Gemfile',
|
|
49
53
|
};
|
|
50
54
|
/**
|
|
@@ -215,8 +219,30 @@ export function detectStack(parsed, explicit) {
|
|
|
215
219
|
const hasFastapiMention = parsed.packageHints.stackHint === 'fastapi';
|
|
216
220
|
const hasPythonMarker = has('pyproject.toml') || has('requirements.txt');
|
|
217
221
|
const hasNodeMarker = has('package.json');
|
|
218
|
-
//
|
|
219
|
-
|
|
222
|
+
// v7.6 — Go signal: `go.mod` OR a top-level / cmd-shaped `main.go`.
|
|
223
|
+
const hasGoMod = has('go.mod');
|
|
224
|
+
const hasMainGo = paths.some(p => p === 'main.go' || /^cmd\/[^/]+\/main\.go$/.test(p));
|
|
225
|
+
const hasGoMarker = hasGoMod || hasMainGo;
|
|
226
|
+
// v7.7 — Rust signal: `Cargo.toml` OR `src/main.rs` OR `src/lib.rs`.
|
|
227
|
+
const hasCargoToml = has('Cargo.toml');
|
|
228
|
+
const hasMainRs = has('src/main.rs');
|
|
229
|
+
const hasLibRs = has('src/lib.rs');
|
|
230
|
+
const hasRustMarker = hasCargoToml || hasMainRs || hasLibRs;
|
|
231
|
+
// v7.6 — Polyglot detection scans ALL supported stack signals at once.
|
|
232
|
+
// We collect each present stack and exit 3 if more than one supported
|
|
233
|
+
// stack is detected (e.g. Node + Go, Python + Go, Node + Python + Go).
|
|
234
|
+
// FastAPI is collapsed into Python for the polyglot count (they share
|
|
235
|
+
// pyproject.toml — not a real conflict). v7.7 adds Rust to the count.
|
|
236
|
+
const supportedSignals = [];
|
|
237
|
+
if (hasNodeMarker)
|
|
238
|
+
supportedSignals.push('node');
|
|
239
|
+
if (hasPythonMarker)
|
|
240
|
+
supportedSignals.push('python');
|
|
241
|
+
if (hasGoMarker)
|
|
242
|
+
supportedSignals.push('go');
|
|
243
|
+
if (hasRustMarker)
|
|
244
|
+
supportedSignals.push('rust');
|
|
245
|
+
if (supportedSignals.length > 1) {
|
|
220
246
|
return {
|
|
221
247
|
kind: 'polyglot',
|
|
222
248
|
message: 'polyglot spec — pass --stack to disambiguate',
|
|
@@ -235,20 +261,26 @@ export function detectStack(parsed, explicit) {
|
|
|
235
261
|
// Step 3: Python.
|
|
236
262
|
if (hasPythonMarker)
|
|
237
263
|
return { kind: 'resolved', stack: 'python' };
|
|
238
|
-
// Step 4:
|
|
264
|
+
// Step 4: Go (v7.6).
|
|
265
|
+
if (hasGoMarker)
|
|
266
|
+
return { kind: 'resolved', stack: 'go' };
|
|
267
|
+
// Step 5: Rust (v7.7).
|
|
268
|
+
if (hasRustMarker)
|
|
269
|
+
return { kind: 'resolved', stack: 'rust' };
|
|
270
|
+
// Step 6: Node.
|
|
239
271
|
if (hasNodeMarker)
|
|
240
272
|
return { kind: 'resolved', stack: 'node' };
|
|
241
|
-
// Step
|
|
273
|
+
// Step 7: detected-but-unsupported (codex W2). Ruby remains; Rust + Go promoted.
|
|
242
274
|
for (const [stack, file] of Object.entries(UNSUPPORTED_STACK_FILES)) {
|
|
243
275
|
if (has(file)) {
|
|
244
276
|
return {
|
|
245
277
|
kind: 'unsupported',
|
|
246
278
|
stack,
|
|
247
|
-
message: `${stack} detected but not supported until v7.
|
|
279
|
+
message: `${stack} detected but not supported until v7.7`,
|
|
248
280
|
};
|
|
249
281
|
}
|
|
250
282
|
}
|
|
251
|
-
// Step
|
|
283
|
+
// Step 7: fallback — Node ESM (preserves v7.2.0 default for ambiguous
|
|
252
284
|
// specs that listed only paths with no root-marker file).
|
|
253
285
|
return { kind: 'resolved', stack: 'node' };
|
|
254
286
|
}
|
|
@@ -262,16 +294,18 @@ export function printStackList() {
|
|
|
262
294
|
console.log(' node Node 22 ESM (package.json + tsconfig.json)');
|
|
263
295
|
console.log(' python Python 3.11+ (pyproject.toml + hatchling + pytest)');
|
|
264
296
|
console.log(' fastapi Python + FastAPI (auto-includes fastapi + uvicorn[standard])');
|
|
297
|
+
console.log(' go Go 1.22 (go.mod + main.go + main_test.go)');
|
|
298
|
+
console.log(' rust Rust 2021 (Cargo.toml + src/{main,lib}.rs + tests/integration_test.rs)');
|
|
265
299
|
console.log('');
|
|
266
300
|
console.log(BOLD('Auto-detected from `## Files`:'));
|
|
267
301
|
console.log(' node when `package.json` is listed');
|
|
268
302
|
console.log(' python when `pyproject.toml` or `requirements.txt` is listed');
|
|
269
303
|
console.log(' fastapi when `main.py` is listed AND a bullet mentions `fastapi`');
|
|
304
|
+
console.log(' go when `go.mod` or `main.go` is listed');
|
|
305
|
+
console.log(' rust when `Cargo.toml`, `src/main.rs`, or `src/lib.rs` is listed');
|
|
270
306
|
console.log('');
|
|
271
307
|
console.log(BOLD('Recognized-but-unsupported (exit 3):'));
|
|
272
|
-
console.log('
|
|
273
|
-
console.log(' rust v7.5 (would detect via Cargo.toml)');
|
|
274
|
-
console.log(' ruby v7.5+ (would detect via Gemfile)');
|
|
308
|
+
console.log(' ruby v7.8+ (would detect via Gemfile)');
|
|
275
309
|
console.log('');
|
|
276
310
|
}
|
|
277
311
|
export async function runScaffold(opts) {
|
|
@@ -305,34 +339,60 @@ export async function runScaffold(opts) {
|
|
|
305
339
|
}
|
|
306
340
|
const stack = detection.stack;
|
|
307
341
|
console.log(`\n${BOLD('[scaffold]')} ${DIM(specAbs)} ${DIM(`(stack: ${stack})`)}\n`);
|
|
308
|
-
// codex W5 — when --stack
|
|
309
|
-
//
|
|
310
|
-
//
|
|
342
|
+
// codex W5 — when an explicit --stack is passed against a polyglot spec,
|
|
343
|
+
// strip the OTHER stack's marker files so the chosen scaffolder doesn't
|
|
344
|
+
// touch them as empty placeholders. v7.6 extends this to Go; v7.7 to Rust.
|
|
311
345
|
let ignoredOtherStackFiles;
|
|
312
346
|
let parsedForStack = parsed;
|
|
347
|
+
const NODE_FILES = new Set(['package.json', 'tsconfig.json']);
|
|
348
|
+
const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
|
|
349
|
+
const GO_FILES = new Set(['go.mod']);
|
|
350
|
+
const RUST_FILES = new Set(['Cargo.toml']);
|
|
313
351
|
if (opts.stack && (stack === 'python' || stack === 'fastapi')) {
|
|
314
|
-
const
|
|
315
|
-
const ignored = parsed.paths.filter(p =>
|
|
352
|
+
const toIgnore = new Set([...NODE_FILES, ...GO_FILES, ...RUST_FILES]);
|
|
353
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
316
354
|
if (ignored.length > 0) {
|
|
317
355
|
ignoredOtherStackFiles = ignored;
|
|
318
|
-
console.log(` ${DIM(`! ignoring
|
|
356
|
+
console.log(` ${DIM(`! ignoring non-Python files (--stack ${stack}): ${ignored.join(', ')}`)}`);
|
|
319
357
|
parsedForStack = {
|
|
320
358
|
...parsed,
|
|
321
|
-
paths: parsed.paths.filter(p => !
|
|
359
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
322
360
|
};
|
|
323
361
|
}
|
|
324
362
|
}
|
|
325
363
|
else if (opts.stack === 'node') {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
364
|
+
const toIgnore = new Set([...PYTHON_FILES, ...GO_FILES, ...RUST_FILES]);
|
|
365
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
366
|
+
if (ignored.length > 0) {
|
|
367
|
+
ignoredOtherStackFiles = ignored;
|
|
368
|
+
console.log(` ${DIM(`! ignoring non-Node files (--stack node): ${ignored.join(', ')}`)}`);
|
|
369
|
+
parsedForStack = {
|
|
370
|
+
...parsed,
|
|
371
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (opts.stack === 'go') {
|
|
376
|
+
const toIgnore = new Set([...NODE_FILES, ...PYTHON_FILES, ...RUST_FILES]);
|
|
377
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
330
378
|
if (ignored.length > 0) {
|
|
331
379
|
ignoredOtherStackFiles = ignored;
|
|
332
|
-
console.log(` ${DIM(`! ignoring
|
|
380
|
+
console.log(` ${DIM(`! ignoring non-Go files (--stack go): ${ignored.join(', ')}`)}`);
|
|
333
381
|
parsedForStack = {
|
|
334
382
|
...parsed,
|
|
335
|
-
paths: parsed.paths.filter(p => !
|
|
383
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else if (opts.stack === 'rust') {
|
|
388
|
+
const toIgnore = new Set([...NODE_FILES, ...PYTHON_FILES, ...GO_FILES]);
|
|
389
|
+
const ignored = parsed.paths.filter(p => toIgnore.has(p));
|
|
390
|
+
if (ignored.length > 0) {
|
|
391
|
+
ignoredOtherStackFiles = ignored;
|
|
392
|
+
console.log(` ${DIM(`! ignoring non-Rust files (--stack rust): ${ignored.join(', ')}`)}`);
|
|
393
|
+
parsedForStack = {
|
|
394
|
+
...parsed,
|
|
395
|
+
paths: parsed.paths.filter(p => !toIgnore.has(p)),
|
|
336
396
|
};
|
|
337
397
|
}
|
|
338
398
|
}
|
|
@@ -344,6 +404,12 @@ export async function runScaffold(opts) {
|
|
|
344
404
|
else if (stack === 'fastapi') {
|
|
345
405
|
result = await scaffoldPython(ctx, { isFastapi: true });
|
|
346
406
|
}
|
|
407
|
+
else if (stack === 'go') {
|
|
408
|
+
result = await scaffoldGo(ctx);
|
|
409
|
+
}
|
|
410
|
+
else if (stack === 'rust') {
|
|
411
|
+
result = await scaffoldRust(ctx);
|
|
412
|
+
}
|
|
347
413
|
else {
|
|
348
414
|
result = await scaffoldNode(ctx);
|
|
349
415
|
}
|