specrails-core 4.1.0 → 4.2.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.
Files changed (67) hide show
  1. package/README.md +4 -4
  2. package/VERSION +1 -1
  3. package/bin/specrails-core.mjs +302 -0
  4. package/commands/doctor.md +5 -5
  5. package/commands/enrich.md +9 -9
  6. package/dist/installer/cli.js +167 -0
  7. package/dist/installer/cli.js.map +1 -0
  8. package/dist/installer/commands/doctor.js +144 -0
  9. package/dist/installer/commands/doctor.js.map +1 -0
  10. package/dist/installer/commands/init.js +182 -0
  11. package/dist/installer/commands/init.js.map +1 -0
  12. package/dist/installer/commands/perf-check.js +16 -0
  13. package/dist/installer/commands/perf-check.js.map +1 -0
  14. package/dist/installer/commands/update.js +170 -0
  15. package/dist/installer/commands/update.js.map +1 -0
  16. package/dist/installer/phases/install-config.js +120 -0
  17. package/dist/installer/phases/install-config.js.map +1 -0
  18. package/dist/installer/phases/manifest.js +93 -0
  19. package/dist/installer/phases/manifest.js.map +1 -0
  20. package/dist/installer/phases/prereqs.js +116 -0
  21. package/dist/installer/phases/prereqs.js.map +1 -0
  22. package/dist/installer/phases/provider-detect.js +111 -0
  23. package/dist/installer/phases/provider-detect.js.map +1 -0
  24. package/dist/installer/phases/scaffold.js +373 -0
  25. package/dist/installer/phases/scaffold.js.map +1 -0
  26. package/dist/installer/util/errors.js +79 -0
  27. package/dist/installer/util/errors.js.map +1 -0
  28. package/dist/installer/util/exec.js +151 -0
  29. package/dist/installer/util/exec.js.map +1 -0
  30. package/dist/installer/util/fs.js +153 -0
  31. package/dist/installer/util/fs.js.map +1 -0
  32. package/dist/installer/util/git.js +113 -0
  33. package/dist/installer/util/git.js.map +1 -0
  34. package/dist/installer/util/logger.js +55 -0
  35. package/dist/installer/util/logger.js.map +1 -0
  36. package/dist/installer/util/paths.js +66 -0
  37. package/dist/installer/util/paths.js.map +1 -0
  38. package/dist/installer/util/prompts.js +49 -0
  39. package/dist/installer/util/prompts.js.map +1 -0
  40. package/dist/installer/util/template.js +60 -0
  41. package/dist/installer/util/template.js.map +1 -0
  42. package/docs/deployment.md +2 -1
  43. package/docs/installation.md +6 -3
  44. package/docs/testing/test-matrix-codex.md +19 -11
  45. package/docs/updating.md +24 -49
  46. package/docs/user-docs/faq.md +1 -1
  47. package/docs/windows.md +53 -0
  48. package/{templates/settings/integration-contract.json → integration-contract.json} +2 -2
  49. package/package.json +25 -10
  50. package/pinned-versions.json +4 -0
  51. package/schemas/profile.v1.json +11 -3
  52. package/templates/agents/sr-architect.md +1 -1
  53. package/templates/agents/sr-reviewer.md +1 -1
  54. package/templates/commands/specrails/compat-check.md +3 -3
  55. package/templates/commands/specrails/doctor.md +5 -5
  56. package/templates/commands/specrails/enrich.md +9 -9
  57. package/templates/commands/specrails/reconfig.md +2 -2
  58. package/templates/commands/specrails/refactor-recommender.md +2 -2
  59. package/templates/commands/specrails/vpc-drift.md +1 -1
  60. package/templates/skills/sr-compat-check/SKILL.md +3 -3
  61. package/templates/skills/sr-refactor-recommender/SKILL.md +2 -2
  62. package/bin/doctor.sh +0 -127
  63. package/bin/perf-check.sh +0 -21
  64. package/bin/specrails-core.js +0 -262
  65. package/commands/setup.md +0 -1461
  66. package/install.sh +0 -1231
  67. package/update.sh +0 -870
@@ -0,0 +1,153 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { FilesystemError } from './errors.js';
4
+ /**
5
+ * `mkdir -p` equivalent. Idempotent — no-op when `dir` already exists.
6
+ * Wraps filesystem errors as {@link FilesystemError} so callers get
7
+ * a typed exit code via the CLI dispatcher.
8
+ */
9
+ export function mkdirp(dir) {
10
+ try {
11
+ mkdirSync(dir, { recursive: true });
12
+ }
13
+ catch (err) {
14
+ throw new FilesystemError(`failed to create directory: ${err.message}`, dir);
15
+ }
16
+ }
17
+ /**
18
+ * True when the path exists at all (file, directory, symlink, …).
19
+ */
20
+ export function pathExists(p) {
21
+ return existsSync(p);
22
+ }
23
+ /**
24
+ * Writes `contents` to `filePath` after ensuring the parent directory
25
+ * exists and normalising line endings to LF. Normalisation rule:
26
+ * every `\r\n` → `\n`; lone `\r` left alone (very uncommon in our
27
+ * template inputs and more invasive to rewrite). Callers relying on
28
+ * strict LF-only output should pre-clean their strings.
29
+ */
30
+ export function writeFileLf(filePath, contents) {
31
+ const parent = path.dirname(filePath);
32
+ mkdirp(parent);
33
+ const lf = contents.replace(/\r\n/g, '\n');
34
+ try {
35
+ writeFileSync(filePath, lf, { encoding: 'utf8' });
36
+ }
37
+ catch (err) {
38
+ throw new FilesystemError(`failed to write file: ${err.message}`, filePath);
39
+ }
40
+ }
41
+ /**
42
+ * Reads a UTF-8 text file. Wraps failures as {@link FilesystemError}.
43
+ */
44
+ export function readTextFile(filePath) {
45
+ try {
46
+ return readFileSync(filePath, 'utf8');
47
+ }
48
+ catch (err) {
49
+ throw new FilesystemError(`failed to read file: ${err.message}`, filePath);
50
+ }
51
+ }
52
+ /**
53
+ * Reads a file and returns its raw bytes. Used by the manifest hasher
54
+ * to avoid UTF-8 re-encoding round trips.
55
+ */
56
+ export function readBytes(filePath) {
57
+ try {
58
+ return readFileSync(filePath);
59
+ }
60
+ catch (err) {
61
+ throw new FilesystemError(`failed to read file: ${err.message}`, filePath);
62
+ }
63
+ }
64
+ /**
65
+ * Copies a single file, ensuring the destination directory exists.
66
+ */
67
+ export function copyFile(src, dest) {
68
+ mkdirp(path.dirname(dest));
69
+ try {
70
+ cpSync(src, dest);
71
+ }
72
+ catch (err) {
73
+ throw new FilesystemError(`failed to copy ${src} → ${dest}: ${err.message}`, dest);
74
+ }
75
+ }
76
+ /**
77
+ * Recursively copies a directory. Overwrites existing files by default.
78
+ * Honours a `filter(src, relPath)` predicate — returning false skips
79
+ * the entry (and, if it's a directory, its subtree).
80
+ */
81
+ export function copyDir(srcDir, destDir, options = {}) {
82
+ if (!pathExists(srcDir)) {
83
+ throw new FilesystemError(`source directory does not exist`, srcDir);
84
+ }
85
+ mkdirp(destDir);
86
+ const walk = (currentSrc, currentDest, relPrefix) => {
87
+ let entries;
88
+ try {
89
+ entries = readdirSync(currentSrc);
90
+ }
91
+ catch (err) {
92
+ throw new FilesystemError(`failed to read directory: ${err.message}`, currentSrc);
93
+ }
94
+ for (const name of entries) {
95
+ const absSrc = path.join(currentSrc, name);
96
+ const absDest = path.join(currentDest, name);
97
+ const relPath = relPrefix === '' ? name : path.join(relPrefix, name);
98
+ if (options.filter && !options.filter(absSrc, relPath))
99
+ continue;
100
+ const st = statSync(absSrc);
101
+ if (st.isDirectory()) {
102
+ mkdirp(absDest);
103
+ walk(absSrc, absDest, relPath);
104
+ }
105
+ else if (st.isFile()) {
106
+ copyFile(absSrc, absDest);
107
+ }
108
+ // Other inode types (symlinks, sockets, …) intentionally skipped.
109
+ }
110
+ };
111
+ walk(srcDir, destDir, '');
112
+ }
113
+ /**
114
+ * Lists the immediate entries (files + directories) of `dir`. Returns
115
+ * absolute paths. Empty array when `dir` does not exist.
116
+ */
117
+ export function listDir(dir) {
118
+ if (!pathExists(dir))
119
+ return [];
120
+ try {
121
+ return readdirSync(dir).map((name) => path.join(dir, name));
122
+ }
123
+ catch (err) {
124
+ throw new FilesystemError(`failed to list directory: ${err.message}`, dir);
125
+ }
126
+ }
127
+ /**
128
+ * True when the target path exists and resolves to a regular file.
129
+ */
130
+ export function isFile(p) {
131
+ if (!pathExists(p))
132
+ return false;
133
+ try {
134
+ return statSync(p).isFile();
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * True when the target path exists and resolves to a directory.
142
+ */
143
+ export function isDir(p) {
144
+ if (!pathExists(p))
145
+ return false;
146
+ try {
147
+ return statSync(p).isDirectory();
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../../src/installer/util/fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import { GitError } from './errors.js';
2
+ import { runCommand } from './exec.js';
3
+ /**
4
+ * Low-level git wrapper. Every method shells out to the user's `git`
5
+ * binary (already a prerequisite — the installer refuses to run
6
+ * without a git repo). Failures surface as {@link GitError} so the
7
+ * CLI dispatcher translates them to exit code 30.
8
+ *
9
+ * We intentionally keep the surface minimal (the six operations used
10
+ * by the installer) rather than wrapping `simple-git` — that package
11
+ * is ~100 KB and wraps the same shell-out we do here.
12
+ */
13
+ export async function isGitRepo(cwd) {
14
+ try {
15
+ await runCommand('git', ['rev-parse', '--is-inside-work-tree'], {
16
+ cwd,
17
+ inherit: false,
18
+ });
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ /**
26
+ * Returns the absolute path of the repository root containing `cwd`.
27
+ * Throws {@link GitError} if `cwd` is not inside a git repo.
28
+ */
29
+ export async function repoRoot(cwd) {
30
+ try {
31
+ const { stdout } = await runCommand('git', ['rev-parse', '--show-toplevel'], {
32
+ cwd,
33
+ inherit: false,
34
+ });
35
+ return stdout.trim();
36
+ }
37
+ catch (err) {
38
+ throw new GitError(`not a git repository (or parent directories): ${cwd}`, 'git rev-parse --show-toplevel');
39
+ }
40
+ }
41
+ /**
42
+ * Initialises a new git repository at `cwd` with an initial commit on
43
+ * the default branch name. Idempotent — no-op when `cwd` is already
44
+ * a repo.
45
+ */
46
+ export async function initRepo(cwd) {
47
+ if (await isGitRepo(cwd))
48
+ return;
49
+ try {
50
+ await runCommand('git', ['init', '--initial-branch=main'], {
51
+ cwd,
52
+ inherit: false,
53
+ });
54
+ }
55
+ catch (err) {
56
+ // Older git versions (< 2.28) lack --initial-branch. Retry without.
57
+ await runCommand('git', ['init'], { cwd, inherit: false });
58
+ }
59
+ }
60
+ /**
61
+ * Porcelain working-tree status. Returns the raw short-format output
62
+ * (empty string when the tree is clean).
63
+ */
64
+ export async function status(cwd) {
65
+ const { stdout } = await runCommand('git', ['status', '--porcelain'], {
66
+ cwd,
67
+ inherit: false,
68
+ });
69
+ return stdout;
70
+ }
71
+ /**
72
+ * Stages the given pathspecs. Empty array stages nothing and returns
73
+ * without invoking git.
74
+ */
75
+ export async function add(cwd, pathspecs) {
76
+ if (pathspecs.length === 0)
77
+ return;
78
+ try {
79
+ await runCommand('git', ['add', '--', ...pathspecs], { cwd, inherit: false });
80
+ }
81
+ catch (err) {
82
+ throw new GitError(`failed to stage paths: ${pathspecs.join(', ')}`, `git add -- ${pathspecs.join(' ')}`);
83
+ }
84
+ }
85
+ /**
86
+ * Creates a commit with the given message using the committer identity
87
+ * currently configured on the repo / globally. Does NOT attempt to
88
+ * install an identity — that is a prerequisite check.
89
+ */
90
+ export async function commit(cwd, message, opts = {}) {
91
+ const args = ['commit', '-m', message];
92
+ if (opts.allowEmpty)
93
+ args.push('--allow-empty');
94
+ try {
95
+ await runCommand('git', args, { cwd, inherit: false });
96
+ }
97
+ catch (err) {
98
+ throw new GitError(`git commit failed: ${err.message}`, `git ${args.join(' ')}`);
99
+ }
100
+ }
101
+ /**
102
+ * True when git is installed and on PATH.
103
+ */
104
+ export async function gitInstalled() {
105
+ try {
106
+ await runCommand('git', ['--version'], { inherit: false });
107
+ return true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../../src/installer/util/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import pc from 'picocolors';
2
+ let streams = {
3
+ out: process.stdout,
4
+ err: process.stderr,
5
+ };
6
+ export function setLoggerStreams(next) {
7
+ streams = { ...streams, ...next };
8
+ }
9
+ /** Restore logger to process.stdout/stderr. */
10
+ export function resetLoggerStreams() {
11
+ streams = { out: process.stdout, err: process.stderr };
12
+ }
13
+ function writeOut(line) {
14
+ streams.out.write(line + '\n');
15
+ }
16
+ function writeErr(line) {
17
+ streams.err.write(line + '\n');
18
+ }
19
+ /** Section heading — bold, no prefix, leading blank line. */
20
+ export function step(title) {
21
+ writeOut('');
22
+ writeOut(pc.bold(title));
23
+ }
24
+ /** Success line. Prefix: ` ✓ ` in green. */
25
+ export function ok(msg) {
26
+ writeOut(` ${pc.green('✓')} ${msg}`);
27
+ }
28
+ /** Warning line. Prefix: ` ⚠ ` in yellow. */
29
+ export function warn(msg) {
30
+ writeOut(` ${pc.yellow('⚠')} ${msg}`);
31
+ }
32
+ /** Failure line, routed to stderr. Prefix: ` ✗ ` in red. */
33
+ export function fail(msg) {
34
+ writeErr(` ${pc.red('✗')} ${msg}`);
35
+ }
36
+ /** Info line. Prefix: ` → ` in blue. */
37
+ export function info(msg) {
38
+ writeOut(` ${pc.blue('→')} ${msg}`);
39
+ }
40
+ /** Print a fatal error and its hint, routed to stderr. */
41
+ export function fatal(message, hint) {
42
+ writeErr('');
43
+ writeErr(pc.red(pc.bold(`✗ ${message}`)));
44
+ if (hint) {
45
+ writeErr(pc.dim(` ${hint}`));
46
+ }
47
+ }
48
+ /** Primitive write for cases where the caller owns formatting. */
49
+ export function rawOut(text) {
50
+ streams.out.write(text);
51
+ }
52
+ export function rawErr(text) {
53
+ streams.err.write(text);
54
+ }
55
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../../src/installer/util/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAA;AAgB3B,IAAI,OAAO,GAAY;IACrB,GAAG,EAAE,OAAO,CAAC,MAAM;IACnB,GAAG,EAAE,OAAO,CAAC,MAAM;CACpB,CAAA;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAsB;IACrD,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,EAAE,CAAA;AACnC,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,kBAAkB;IAChC,OAAO,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,CAAA;AACxD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;AAChC,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;AAChC,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,IAAI,CAAC,KAAa;IAChC,QAAQ,CAAC,EAAE,CAAC,CAAA;IACZ,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;AAC1B,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,EAAE,CAAC,GAAW;IAC5B,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;AACvC,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,IAAI,CAAC,GAAW;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;AACxC,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,IAAI,CAAC,GAAW;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;AACrC,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,IAAI,CAAC,GAAW;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;AACtC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,KAAK,CAAC,OAAe,EAAE,IAAa;IAClD,QAAQ,CAAC,EAAE,CAAC,CAAA;IACZ,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAA;IAC/B,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC"}
@@ -0,0 +1,66 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Paths the installer MUST NOT create, modify, or delete. These hold
4
+ * user / team state that survives re-runs (profile JSON authored by
5
+ * specrails-hub, custom agents authored by the user). Breaking this
6
+ * contract silently destroys user work.
7
+ *
8
+ * Audited by vitest spec `reserved-paths.test.ts`.
9
+ */
10
+ export const RESERVED_PATHS = [
11
+ /**
12
+ * .specrails/profiles/** — project + hub-authored profile JSON.
13
+ * specrails-hub writes profile files here and expects them preserved
14
+ * across specrails-core updates.
15
+ */
16
+ '.specrails/profiles/',
17
+ /**
18
+ * .claude/agents/custom-*.md — user-authored custom agents, kept
19
+ * distinct from the bundled sr-* agents so updates never overwrite
20
+ * them.
21
+ */
22
+ '.claude/agents/custom-',
23
+ ];
24
+ /**
25
+ * Returns true when a repo-relative path falls inside a reserved
26
+ * region. Accepts both POSIX (/) and Windows (\) separators; the
27
+ * check normalises internally so callers can pass a value straight
28
+ * from `path.relative()` without worrying about host platform.
29
+ */
30
+ export function isReservedPath(relPath) {
31
+ const normalised = relPath.replace(/\\/g, '/');
32
+ for (const prefix of RESERVED_PATHS) {
33
+ if (normalised.startsWith(prefix))
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * Joins segments and forces POSIX separators. Use when a generated
40
+ * string is destined for a cross-platform artefact (JSON manifest,
41
+ * YAML config) rather than the local filesystem.
42
+ */
43
+ export function toPosix(...segments) {
44
+ return path.posix.join(...segments.map((s) => s.replace(/\\/g, '/')));
45
+ }
46
+ /**
47
+ * Joins segments using the host platform's separator. Use when the
48
+ * result goes directly to `fs.*` / `child_process.*` calls.
49
+ */
50
+ export function toNative(...segments) {
51
+ return path.join(...segments);
52
+ }
53
+ /**
54
+ * Repository-relative representation of an absolute path. Returns a
55
+ * POSIX-style path so callers can store it in manifests without OS
56
+ * contamination. If `absPath` is outside `repoRoot`, returns the
57
+ * original absolute path unchanged (POSIX-normalised).
58
+ */
59
+ export function repoRelative(repoRoot, absPath) {
60
+ const rel = path.relative(repoRoot, absPath);
61
+ if (rel.startsWith('..')) {
62
+ return absPath.replace(/\\/g, '/');
63
+ }
64
+ return rel.replace(/\\/g, '/');
65
+ }
66
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/installer/util/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B;;;;OAIG;IACH,sBAAsB;IAEtB;;;;OAIG;IACH,wBAAwB;CAChB,CAAA;AAEV;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC9C,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;QACpC,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAA;IAChD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,GAAG,QAAkB;IAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;AACvE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAG,QAAkB;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,OAAe;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC5C,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACpC,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;AAChC,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { confirm, input, select } from '@inquirer/prompts';
2
+ import { PromptAbortError } from './errors.js';
3
+ /**
4
+ * Thin wrappers over `@inquirer/prompts` that add:
5
+ * - Non-TTY detection → throws {@link PromptAbortError} unless the
6
+ * caller provided a default, matching the old bash behaviour of
7
+ * "use the default and don't ask when piped".
8
+ * - Consistent typing (`string`, `boolean`, `T`) across the call
9
+ * sites so command handlers don't need to think about `undefined`.
10
+ */
11
+ function isInteractive() {
12
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
13
+ }
14
+ export async function text(opts) {
15
+ if (!isInteractive()) {
16
+ if (opts.default === undefined) {
17
+ throw new PromptAbortError(`cannot prompt for "${opts.message}" in a non-interactive environment`);
18
+ }
19
+ return opts.default;
20
+ }
21
+ return input({
22
+ message: opts.message,
23
+ default: opts.default,
24
+ validate: opts.validate,
25
+ });
26
+ }
27
+ export async function confirmYesNo(opts) {
28
+ if (!isInteractive()) {
29
+ if (opts.default === undefined) {
30
+ throw new PromptAbortError(`cannot confirm "${opts.message}" in a non-interactive environment`);
31
+ }
32
+ return opts.default;
33
+ }
34
+ return confirm({ message: opts.message, default: opts.default });
35
+ }
36
+ export async function chooseOne(opts) {
37
+ if (!isInteractive()) {
38
+ if (opts.default === undefined) {
39
+ throw new PromptAbortError(`cannot present choices for "${opts.message}" in a non-interactive environment`);
40
+ }
41
+ return opts.default;
42
+ }
43
+ return select({
44
+ message: opts.message,
45
+ choices: opts.choices,
46
+ default: opts.default,
47
+ });
48
+ }
49
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/installer/util/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C;;;;;;;GAOG;AAEH,SAAS,aAAa;IACpB,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC7D,CAAC;AAUD,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAiB;IAC1C,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,gBAAgB,CACxB,sBAAsB,IAAI,CAAC,OAAO,oCAAoC,CACvE,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAA;IACrB,CAAC;IACD,OAAO,KAAK,CAAC;QACX,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,QAAQ,EAAE,IAAI,CAAC,QAAsD;KACtE,CAAC,CAAA;AACJ,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAoB;IACrD,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,gBAAgB,CACxB,mBAAmB,IAAI,CAAC,OAAO,oCAAoC,CACpE,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAA;IACrB,CAAC;IACD,OAAO,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;AAClE,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,IAAsB;IACvD,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,gBAAgB,CACxB,+BAA+B,IAAI,CAAC,OAAO,oCAAoC,CAChF,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAA;IACrB,CAAC;IACD,OAAO,MAAM,CAAI;QACf,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Small in-house template renderer. Replaces the bash heredocs the
3
+ * retired install.sh / update.sh used to emit manifests and config
4
+ * files.
5
+ *
6
+ * Syntax (intentionally minimal):
7
+ * ${VAR_NAME} — value interpolation
8
+ * {{#if FLAG}}block{{/if}} — conditional block, rendered when
9
+ * FLAG is truthy in the context
10
+ * {{#ifnot FLAG}}block{{/ifnot}} — conditional block when FLAG is falsy
11
+ *
12
+ * Nested blocks are NOT supported. If a template outgrows this
13
+ * capability we switch to mustache — but every heredoc we've audited
14
+ * fits inside these three forms.
15
+ */
16
+ /**
17
+ * Renders `template` by evaluating its directives against `context`.
18
+ * Missing interpolation variables render as the empty string rather
19
+ * than throwing — the bash scripts behaved the same way (`${X:-}` in
20
+ * a heredoc). Unknown flags in an `{{#if}}` are treated as falsy.
21
+ */
22
+ export function render(template, context) {
23
+ let out = template;
24
+ // 1. Handle conditionals first so their variables don't get
25
+ // interpolated if the block is skipped.
26
+ out = renderConditionals(out, context);
27
+ // 2. Interpolate variables.
28
+ out = renderInterpolations(out, context);
29
+ return out;
30
+ }
31
+ function renderConditionals(input, context) {
32
+ const ifRegex = /{{#if\s+([A-Z0-9_]+)\s*}}([\s\S]*?){{\/if}}/g;
33
+ const ifnotRegex = /{{#ifnot\s+([A-Z0-9_]+)\s*}}([\s\S]*?){{\/ifnot}}/g;
34
+ let out = input;
35
+ out = out.replace(ifRegex, (_match, name, body) => {
36
+ return truthy(context[name]) ? body : '';
37
+ });
38
+ out = out.replace(ifnotRegex, (_match, name, body) => {
39
+ return truthy(context[name]) ? '' : body;
40
+ });
41
+ return out;
42
+ }
43
+ function renderInterpolations(input, context) {
44
+ return input.replace(/\$\{([A-Z0-9_]+)\}/g, (_match, name) => {
45
+ const value = context[name];
46
+ if (value === null || value === undefined || value === false)
47
+ return '';
48
+ return String(value);
49
+ });
50
+ }
51
+ function truthy(value) {
52
+ if (value === null || value === undefined)
53
+ return false;
54
+ if (value === false)
55
+ return false;
56
+ if (value === '' || value === 0)
57
+ return false;
58
+ return true;
59
+ }
60
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/installer/util/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,QAAgB,EAAE,OAAwB;IAC/D,IAAI,GAAG,GAAG,QAAQ,CAAA;IAClB,4DAA4D;IAC5D,2CAA2C;IAC3C,GAAG,GAAG,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IACtC,4BAA4B;IAC5B,GAAG,GAAG,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IACxC,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa,EAAE,OAAwB;IACjE,MAAM,OAAO,GAAG,8CAA8C,CAAA;IAC9D,MAAM,UAAU,GAAG,oDAAoD,CAAA;IAEvE,IAAI,GAAG,GAAG,KAAK,CAAA;IACf,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,IAAY,EAAE,EAAE;QAChE,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1C,CAAC,CAAC,CAAA;IACF,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,IAAY,EAAE,EAAE;QACnE,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1C,CAAC,CAAC,CAAA;IACF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa,EAAE,OAAwB;IACnE,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE;QACnE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK;YAAE,OAAO,EAAE,CAAA;QACvE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IACjC,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC7C,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -55,7 +55,8 @@ Clone the repository for full control and the ability to customize agents.
55
55
  git clone https://github.com/fjpulidop/specrails-core
56
56
  cd specrails-core
57
57
  npm install
58
- ./install.sh --root-dir <your-project>
58
+ npm run build
59
+ node bin/specrails-core.mjs init --root-dir <your-project>
59
60
  ```
60
61
 
61
62
  ### Updating
@@ -59,7 +59,7 @@ The plugin bundles the logic layer — agents, skills, commands, hooks, and refe
59
59
 
60
60
  | Tool | Why | Install |
61
61
  |------|-----|---------|
62
- | **Node.js 18+** | Required for the installer | [nodejs.org](https://nodejs.org/) or via [nvm](https://github.com/nvm-sh/nvm) |
62
+ | **Node.js 20+** | Required for the installer (macOS, Linux, Windows) | [nodejs.org](https://nodejs.org/) or via [nvm](https://github.com/nvm-sh/nvm) |
63
63
  | **Git** | SpecRails operates on git repositories | [git-scm.com](https://git-scm.com/) |
64
64
  | **Claude Code** or **Codex CLI** | The AI CLI that runs the agents | See [codex-vs-claude-code.md](user-docs/codex-vs-claude-code.md) |
65
65
 
@@ -75,7 +75,10 @@ No cloning required. Downloads the latest version and runs the installer automat
75
75
 
76
76
  ```bash
77
77
  git clone https://github.com/fjpulidop/specrails-core.git
78
- ./specrails-core/install.sh --root-dir <your-project>
78
+ cd specrails-core
79
+ npm install
80
+ npm run build
81
+ node bin/specrails-core.mjs init --root-dir <your-project>
79
82
  ```
80
83
 
81
84
  > **Important:** Always run the installer from the **target repository** — the project where you want SpecRails installed.
@@ -349,7 +352,7 @@ You're running the installer from inside the SpecRails repo. Run it from your ta
349
352
 
350
353
  ```bash
351
354
  cd /path/to/your-project
352
- bash /path/to/specrails/install.sh
355
+ npx specrails-core@latest init
353
356
  ```
354
357
 
355
358
  ### Existing `.claude/` directory detected
@@ -100,12 +100,22 @@ Epic: [SPEA-505](/SPEA/issues/SPEA-505) — Codex Compatibility Approach B
100
100
 
101
101
  ## Test Files
102
102
 
103
+ The shell-based test suite was retired in v4.2.0 when the installer
104
+ moved to native Node. Coverage is now provided by vitest specs
105
+ co-located with the installer source:
106
+
103
107
  | File | Suite | Covers |
104
108
  |------|-------|--------|
105
- | `tests/test-install.sh` | Install | Existing install flow (Claude Code only) |
106
- | `tests/test-update.sh` | Update | Update flow (existing) |
107
- | `tests/test-cli.sh` | CLI | Argument validation, injection safety |
108
- | `tests/test-codex-compat.sh` | Codex compat | Provider detection, dual-output structure, edge cases |
109
+ | `src/installer/commands/init.test.ts` | Install | init flow end-to-end (Claude + Codex) |
110
+ | `src/installer/commands/update.test.ts` | Update | update flow + reserved paths + --only |
111
+ | `src/installer/commands/doctor.test.ts` | Doctor | health checks against fixture repos |
112
+ | `src/installer/cli.test.ts` | CLI | Argument parsing, dispatch, exit codes |
113
+ | `src/installer/phases/scaffold.test.ts` | Scaffold | template placement, VPC exclusion, agent-teams gate |
114
+ | `src/installer/phases/manifest.test.ts` | Manifest | sha256 stability, sorted output, exclusions |
115
+ | `src/installer/phases/install-config.test.ts` | Config validation | YAML round-trip + validation errors |
116
+ | `src/installer/phases/provider-detect.test.ts` | Provider | claude vs codex resolution + Codex coming-soon error |
117
+ | `src/installer/phases/prereqs.test.ts` | Prereqs | OSS detection + provider auth |
118
+ | `src/installer/__tests__/reserved-paths.test.ts` | Reserved paths | profile + custom-* survival across init/update |
109
119
 
110
120
  ---
111
121
 
@@ -113,10 +123,8 @@ Epic: [SPEA-505](/SPEA/issues/SPEA-505) — Codex Compatibility Approach B
113
123
 
114
124
  Before SPEA-505 can be merged and released:
115
125
 
116
- 1. `tests/test-codex-compat.sh` — all tests green
117
- 2. `tests/test-install.sh` all existing tests still green (regression)
118
- 3. `tests/test-update.sh` all existing tests still green (regression)
119
- 4. `tests/test-cli.sh` all existing tests still green (regression)
120
- 5. No broken placeholders: `grep -r '{{[A-Z_]*}}' .claude/agents/ .codex/agents/ 2>/dev/null` returns empty
121
- 6. Skills valid: every `SKILL.md` in `.claude/skills/` has required frontmatter
122
- 7. Agent TOML valid: every `.toml` in `.codex/agents/` is parseable TOML
126
+ 1. `npm run test` — full vitest suite green (currently 168 specs).
127
+ 2. CI matrix `[ubuntu, macos, windows] × [node 20, 22]` all green.
128
+ 3. No broken placeholders: `grep -r '{{[A-Z_]*}}' .claude/agents/ .codex/agents/ 2>/dev/null` returns empty.
129
+ 4. Skills valid: every `SKILL.md` in `.claude/skills/` has required frontmatter.
130
+ 5. Agent TOML valid: every `.toml` in `.codex/agents/` is parseable TOML.