@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 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)' },
@@ -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.5+ will add 'go', 'rust', 'ruby'. */
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 "v7.5" diagnostic instead of a
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 = 'go' | 'rust' | 'ruby';
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+ adds 'go', 'rust', 'ruby'. */
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>;
@@ -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. Node (package.json)
19
- // 5. Detected-but-unsupported (go.mod / Cargo.toml / Gemfile) -> exit 3
20
- // 6. Fallback: Node ESM (preserves v7.2.0 default for ambiguous specs)
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: package.json AND pyproject.toml together without --stack
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 (Go/Rust/Ruby), or polyglot spec without --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.5+ adds 'go', 'rust', 'ruby'. */
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
- // Polyglot guard (codex W3) Node + Python without --stack.
219
- if (hasNodeMarker && hasPythonMarker) {
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: Node.
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 5: detected-but-unsupported (codex W2).
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.5`,
279
+ message: `${stack} detected but not supported until v7.7`,
248
280
  };
249
281
  }
250
282
  }
251
- // Step 6: fallback — Node ESM (preserves v7.2.0 default for ambiguous
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(' go v7.5 (would detect via go.mod)');
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 <python|fastapi> is explicit and the spec
309
- // ALSO lists Node files, warn + filter them out so the Python
310
- // scaffolder doesn't try to touch them.
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 NODE_FILES = new Set(['package.json', 'tsconfig.json']);
315
- const ignored = parsed.paths.filter(p => NODE_FILES.has(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 Node files (--stack ${stack}): ${ignored.join(', ')}`)}`);
356
+ console.log(` ${DIM(`! ignoring non-Python files (--stack ${stack}): ${ignored.join(', ')}`)}`);
319
357
  parsedForStack = {
320
358
  ...parsed,
321
- paths: parsed.paths.filter(p => !NODE_FILES.has(p)),
359
+ paths: parsed.paths.filter(p => !toIgnore.has(p)),
322
360
  };
323
361
  }
324
362
  }
325
363
  else if (opts.stack === 'node') {
326
- // Symmetric: when --stack node is forced and the spec also lists
327
- // Python markers, drop them so we don't touch them as placeholders.
328
- const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
329
- const ignored = parsed.paths.filter(p => PYTHON_FILES.has(p));
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 Python files (--stack node): ${ignored.join(', ')}`)}`);
380
+ console.log(` ${DIM(`! ignoring non-Go files (--stack go): ${ignored.join(', ')}`)}`);
333
381
  parsedForStack = {
334
382
  ...parsed,
335
- paths: parsed.paths.filter(p => !PYTHON_FILES.has(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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "7.5.0",
3
+ "version": "7.7.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "tag": "next"