@delegance/claude-autopilot 7.6.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,55 @@
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
+
5
54
  ## 7.6.0 (2026-05-10)
6
55
 
7
56
  **v7.6.0 — Go scaffold support.** Minor release. Promotes Go from
@@ -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|go])' },
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)' },
@@ -935,6 +935,7 @@ switch (subcommand) {
935
935
  // v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
936
936
  // v7.4.0 — `--stack <node|python|fastapi>` + `--list-stacks`.
937
937
  // v7.6.0 — `--stack go`.
938
+ // v7.7.0 — `--stack rust`.
938
939
  if (boolFlag('list-stacks')) {
939
940
  const { printStackList } = await import("./scaffold.js");
940
941
  printStackList();
@@ -943,8 +944,8 @@ switch (subcommand) {
943
944
  const fromSpec = flag('from-spec');
944
945
  const dryRun = boolFlag('dry-run');
945
946
  const stackArg = flag('stack');
946
- if (stackArg && !['node', 'python', 'fastapi', 'go'].includes(stackArg)) {
947
- console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi, go\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`);
948
949
  console.error(` See: claude-autopilot scaffold --list-stacks`);
949
950
  process.exit(3);
950
951
  }
@@ -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.6 adds 'go'; v7.7+ will add 'rust'. */
2
- export type Stack = 'node' | 'python' | 'fastapi' | 'go';
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.7" 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 = '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.6 adds 'go'; v7.7+ will add 'rust'. */
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";
36
40
  import { scaffoldGo } from "./scaffold/go.js";
41
+ import { scaffoldRust } from "./scaffold/rust.js";
37
42
  const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
38
43
  const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
39
44
  // Re-export types + the legacy buildStarterPackageJson so `src/index.ts` and
40
45
  // the existing tests/scaffold.test.ts (which imports from this module) keep
41
46
  // compiling without changes.
42
47
  export { buildStarterPackageJson };
43
- /** Valid `--stack` argument values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
44
- export const SUPPORTED_STACKS = ['node', 'python', 'fastapi', 'go'];
48
+ /** Valid `--stack` argument values. v7.6 adds 'go'; v7.7 adds 'rust'. */
49
+ export const SUPPORTED_STACKS = ['node', 'python', 'fastapi', 'go', 'rust'];
45
50
  /** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
46
51
  export const UNSUPPORTED_STACK_FILES = {
47
- rust: 'Cargo.toml',
48
52
  ruby: 'Gemfile',
49
53
  };
50
54
  /**
@@ -219,11 +223,16 @@ export function detectStack(parsed, explicit) {
219
223
  const hasGoMod = has('go.mod');
220
224
  const hasMainGo = paths.some(p => p === 'main.go' || /^cmd\/[^/]+\/main\.go$/.test(p));
221
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;
222
231
  // v7.6 — Polyglot detection scans ALL supported stack signals at once.
223
232
  // We collect each present stack and exit 3 if more than one supported
224
233
  // stack is detected (e.g. Node + Go, Python + Go, Node + Python + Go).
225
234
  // FastAPI is collapsed into Python for the polyglot count (they share
226
- // pyproject.toml — not a real conflict).
235
+ // pyproject.toml — not a real conflict). v7.7 adds Rust to the count.
227
236
  const supportedSignals = [];
228
237
  if (hasNodeMarker)
229
238
  supportedSignals.push('node');
@@ -231,6 +240,8 @@ export function detectStack(parsed, explicit) {
231
240
  supportedSignals.push('python');
232
241
  if (hasGoMarker)
233
242
  supportedSignals.push('go');
243
+ if (hasRustMarker)
244
+ supportedSignals.push('rust');
234
245
  if (supportedSignals.length > 1) {
235
246
  return {
236
247
  kind: 'polyglot',
@@ -253,10 +264,13 @@ export function detectStack(parsed, explicit) {
253
264
  // Step 4: Go (v7.6).
254
265
  if (hasGoMarker)
255
266
  return { kind: 'resolved', stack: 'go' };
256
- // Step 5: Node.
267
+ // Step 5: Rust (v7.7).
268
+ if (hasRustMarker)
269
+ return { kind: 'resolved', stack: 'rust' };
270
+ // Step 6: Node.
257
271
  if (hasNodeMarker)
258
272
  return { kind: 'resolved', stack: 'node' };
259
- // Step 6: detected-but-unsupported (codex W2). Rust + Ruby still here.
273
+ // Step 7: detected-but-unsupported (codex W2). Ruby remains; Rust + Go promoted.
260
274
  for (const [stack, file] of Object.entries(UNSUPPORTED_STACK_FILES)) {
261
275
  if (has(file)) {
262
276
  return {
@@ -281,16 +295,17 @@ export function printStackList() {
281
295
  console.log(' python Python 3.11+ (pyproject.toml + hatchling + pytest)');
282
296
  console.log(' fastapi Python + FastAPI (auto-includes fastapi + uvicorn[standard])');
283
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)');
284
299
  console.log('');
285
300
  console.log(BOLD('Auto-detected from `## Files`:'));
286
301
  console.log(' node when `package.json` is listed');
287
302
  console.log(' python when `pyproject.toml` or `requirements.txt` is listed');
288
303
  console.log(' fastapi when `main.py` is listed AND a bullet mentions `fastapi`');
289
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');
290
306
  console.log('');
291
307
  console.log(BOLD('Recognized-but-unsupported (exit 3):'));
292
- console.log(' rust v7.7 (would detect via Cargo.toml)');
293
- console.log(' ruby v7.7+ (would detect via Gemfile)');
308
+ console.log(' ruby v7.8+ (would detect via Gemfile)');
294
309
  console.log('');
295
310
  }
296
311
  export async function runScaffold(opts) {
@@ -326,14 +341,15 @@ export async function runScaffold(opts) {
326
341
  console.log(`\n${BOLD('[scaffold]')} ${DIM(specAbs)} ${DIM(`(stack: ${stack})`)}\n`);
327
342
  // codex W5 — when an explicit --stack is passed against a polyglot spec,
328
343
  // strip the OTHER stack's marker files so the chosen scaffolder doesn't
329
- // touch them as empty placeholders. v7.6 extends this to Go.
344
+ // touch them as empty placeholders. v7.6 extends this to Go; v7.7 to Rust.
330
345
  let ignoredOtherStackFiles;
331
346
  let parsedForStack = parsed;
332
347
  const NODE_FILES = new Set(['package.json', 'tsconfig.json']);
333
348
  const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
334
349
  const GO_FILES = new Set(['go.mod']);
350
+ const RUST_FILES = new Set(['Cargo.toml']);
335
351
  if (opts.stack && (stack === 'python' || stack === 'fastapi')) {
336
- const toIgnore = new Set([...NODE_FILES, ...GO_FILES]);
352
+ const toIgnore = new Set([...NODE_FILES, ...GO_FILES, ...RUST_FILES]);
337
353
  const ignored = parsed.paths.filter(p => toIgnore.has(p));
338
354
  if (ignored.length > 0) {
339
355
  ignoredOtherStackFiles = ignored;
@@ -345,7 +361,7 @@ export async function runScaffold(opts) {
345
361
  }
346
362
  }
347
363
  else if (opts.stack === 'node') {
348
- const toIgnore = new Set([...PYTHON_FILES, ...GO_FILES]);
364
+ const toIgnore = new Set([...PYTHON_FILES, ...GO_FILES, ...RUST_FILES]);
349
365
  const ignored = parsed.paths.filter(p => toIgnore.has(p));
350
366
  if (ignored.length > 0) {
351
367
  ignoredOtherStackFiles = ignored;
@@ -357,7 +373,7 @@ export async function runScaffold(opts) {
357
373
  }
358
374
  }
359
375
  else if (opts.stack === 'go') {
360
- const toIgnore = new Set([...NODE_FILES, ...PYTHON_FILES]);
376
+ const toIgnore = new Set([...NODE_FILES, ...PYTHON_FILES, ...RUST_FILES]);
361
377
  const ignored = parsed.paths.filter(p => toIgnore.has(p));
362
378
  if (ignored.length > 0) {
363
379
  ignoredOtherStackFiles = ignored;
@@ -368,6 +384,18 @@ export async function runScaffold(opts) {
368
384
  };
369
385
  }
370
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)),
396
+ };
397
+ }
398
+ }
371
399
  const ctx = { cwd, parsed: parsedForStack, dryRun: !!opts.dryRun };
372
400
  let result;
373
401
  if (stack === 'python') {
@@ -379,6 +407,9 @@ export async function runScaffold(opts) {
379
407
  else if (stack === 'go') {
380
408
  result = await scaffoldGo(ctx);
381
409
  }
410
+ else if (stack === 'rust') {
411
+ result = await scaffoldRust(ctx);
412
+ }
382
413
  else {
383
414
  result = await scaffoldNode(ctx);
384
415
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "tag": "next"