@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 +49 -0
- package/dist/src/cli/help-text.js +1 -1
- package/dist/src/cli/index.js +3 -2
- 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 +48 -17
- package/package.json +1 -1
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)' },
|
package/dist/src/cli/index.js
CHANGED
|
@@ -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
|
|
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 "
|
|
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.6 adds 'go'; v7.7
|
|
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";
|
|
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
|
|
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:
|
|
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
|
|
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('
|
|
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
|
}
|