specrails-core 5.2.3 → 5.3.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/README.md +3 -0
- package/bin/specrails-core.mjs +5 -0
- package/dist/agent-runtime/cli-executor.d.ts +34 -0
- package/dist/agent-runtime/cli-executor.js +335 -0
- package/dist/agent-runtime/cli-executor.js.map +1 -0
- package/dist/agent-runtime/cli-process.d.ts +32 -0
- package/dist/agent-runtime/cli-process.js +159 -0
- package/dist/agent-runtime/cli-process.js.map +1 -0
- package/dist/agent-runtime/cli.d.ts +38 -0
- package/dist/agent-runtime/cli.js +168 -0
- package/dist/agent-runtime/cli.js.map +1 -0
- package/dist/agent-runtime/config.d.ts +7 -0
- package/dist/agent-runtime/config.js +162 -0
- package/dist/agent-runtime/config.js.map +1 -0
- package/dist/agent-runtime/core-host.d.ts +33 -0
- package/dist/agent-runtime/core-host.js +97 -0
- package/dist/agent-runtime/core-host.js.map +1 -0
- package/dist/agent-runtime/durable-store.d.ts +22 -0
- package/dist/agent-runtime/durable-store.js +205 -0
- package/dist/agent-runtime/durable-store.js.map +1 -0
- package/dist/agent-runtime/executor-types.d.ts +92 -0
- package/dist/agent-runtime/executor-types.js +29 -0
- package/dist/agent-runtime/executor-types.js.map +1 -0
- package/dist/agent-runtime/executors.d.ts +22 -0
- package/dist/agent-runtime/executors.js +44 -0
- package/dist/agent-runtime/executors.js.map +1 -0
- package/dist/agent-runtime/gemini-policy.d.ts +8 -0
- package/dist/agent-runtime/gemini-policy.js +36 -0
- package/dist/agent-runtime/gemini-policy.js.map +1 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +40 -0
- package/dist/agent-runtime/graph/artifacts.js +179 -0
- package/dist/agent-runtime/graph/artifacts.js.map +1 -0
- package/dist/agent-runtime/graph/nodes.d.ts +36 -0
- package/dist/agent-runtime/graph/nodes.js +274 -0
- package/dist/agent-runtime/graph/nodes.js.map +1 -0
- package/dist/agent-runtime/graph/review-policy.d.ts +18 -0
- package/dist/agent-runtime/graph/review-policy.js +32 -0
- package/dist/agent-runtime/graph/review-policy.js.map +1 -0
- package/dist/agent-runtime/graph/roles.d.ts +44 -0
- package/dist/agent-runtime/graph/roles.js +126 -0
- package/dist/agent-runtime/graph/roles.js.map +1 -0
- package/dist/agent-runtime/graph/state.d.ts +75 -0
- package/dist/agent-runtime/graph/state.js +23 -0
- package/dist/agent-runtime/graph/state.js.map +1 -0
- package/dist/agent-runtime/graph-checkpointer.d.ts +61 -0
- package/dist/agent-runtime/graph-checkpointer.js +137 -0
- package/dist/agent-runtime/graph-checkpointer.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +14 -0
- package/dist/agent-runtime/index.js +15 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/kimi-acp.d.ts +7 -0
- package/dist/agent-runtime/kimi-acp.js +179 -0
- package/dist/agent-runtime/kimi-acp.js.map +1 -0
- package/dist/agent-runtime/openai-executor.d.ts +18 -0
- package/dist/agent-runtime/openai-executor.js +174 -0
- package/dist/agent-runtime/openai-executor.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +40 -0
- package/dist/agent-runtime/prompts.js +291 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agent-runtime/workflow-types.d.ts +210 -0
- package/dist/agent-runtime/workflow-types.js +2 -0
- package/dist/agent-runtime/workflow-types.js.map +1 -0
- package/dist/agent-runtime/workflow.d.ts +17 -0
- package/dist/agent-runtime/workflow.js +609 -0
- package/dist/agent-runtime/workflow.js.map +1 -0
- package/dist/agent-runtime/workspace-tools.d.ts +25 -0
- package/dist/agent-runtime/workspace-tools.js +120 -0
- package/dist/agent-runtime/workspace-tools.js.map +1 -0
- package/dist/installer/cli.d.ts +35 -0
- package/dist/installer/cli.js +3 -0
- package/dist/installer/cli.js.map +1 -1
- package/dist/installer/commands/doctor.d.ts +27 -0
- package/dist/installer/commands/framework.d.ts +85 -0
- package/dist/installer/commands/init.d.ts +147 -0
- package/dist/installer/commands/update.d.ts +56 -0
- package/dist/installer/commands/v5-migration.d.ts +32 -0
- package/dist/installer/phases/framework-lifecycle.d.ts +53 -0
- package/dist/installer/phases/install-config.d.ts +64 -0
- package/dist/installer/phases/manifest.d.ts +45 -0
- package/dist/installer/phases/prereqs.d.ts +51 -0
- package/dist/installer/phases/provider-detect.d.ts +89 -0
- package/dist/installer/phases/scaffold.d.ts +211 -0
- package/dist/installer/runtime/kimi.d.ts +84 -0
- package/dist/installer/runtime/pipeline-state.d.ts +210 -0
- package/dist/installer/runtime/pipeline-state.js +46 -22
- package/dist/installer/runtime/pipeline-state.js.map +1 -1
- package/dist/installer/util/errors.d.ts +46 -0
- package/dist/installer/util/exec.d.ts +41 -0
- package/dist/installer/util/fs.d.ts +153 -0
- package/dist/installer/util/git.d.ts +44 -0
- package/dist/installer/util/install-transaction.d.ts +29 -0
- package/dist/installer/util/logger.d.ts +31 -0
- package/dist/installer/util/paths.d.ts +34 -0
- package/dist/installer/util/prompts.d.ts +23 -0
- package/dist/installer/util/registry.d.ts +174 -0
- package/dist/installer/util/template.d.ts +23 -0
- package/docs/README.md +1 -0
- package/docs/agent-runtime.md +268 -0
- package/integration-contract.json +79 -7
- package/package.json +16 -1
- package/schemas/agent-runtime.schema.json +55 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy for the installer. Each subclass carries an
|
|
3
|
+
* `exitCode` which the CLI dispatcher translates to the process exit
|
|
4
|
+
* code so callers (shells, CI, specrails-desktop wizard) can distinguish
|
|
5
|
+
* failure modes without string-parsing stdout.
|
|
6
|
+
*
|
|
7
|
+
* Exit-code ranges:
|
|
8
|
+
* 0 — success
|
|
9
|
+
* 1 — generic runtime error
|
|
10
|
+
* 10-19 — prerequisite failures (missing tools, auth, etc.)
|
|
11
|
+
* 20-29 — filesystem / I/O errors
|
|
12
|
+
* 30-39 — git errors
|
|
13
|
+
* 40-49 — provider detection / resolution errors
|
|
14
|
+
* 50-59 — child-process errors (spawned command failed)
|
|
15
|
+
* 60 — user aborted a prompt (Ctrl+C or non-TTY with no default)
|
|
16
|
+
*/
|
|
17
|
+
export declare class InstallerError extends Error {
|
|
18
|
+
readonly exitCode: number;
|
|
19
|
+
constructor(message: string, exitCode?: number);
|
|
20
|
+
}
|
|
21
|
+
export declare class PrerequisiteError extends InstallerError {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
export declare class FilesystemError extends InstallerError {
|
|
25
|
+
readonly path: string | undefined;
|
|
26
|
+
constructor(message: string, path?: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class GitError extends InstallerError {
|
|
29
|
+
readonly command: string | undefined;
|
|
30
|
+
constructor(message: string, command?: string);
|
|
31
|
+
}
|
|
32
|
+
export declare class ProviderError extends InstallerError {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
export declare class ExecError extends InstallerError {
|
|
36
|
+
readonly command: string;
|
|
37
|
+
readonly code: number | null;
|
|
38
|
+
readonly stdout: string;
|
|
39
|
+
readonly stderr: string;
|
|
40
|
+
constructor(command: string, code: number | null, stdout: string, stderr: string);
|
|
41
|
+
}
|
|
42
|
+
export declare class PromptAbortError extends InstallerError {
|
|
43
|
+
constructor(message?: string);
|
|
44
|
+
}
|
|
45
|
+
/** Runtime check used by the CLI to map a caught unknown to a typed error. */
|
|
46
|
+
export declare function isInstallerError(err: unknown): err is InstallerError;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface RunOptions {
|
|
2
|
+
/** Working directory for the child. Defaults to `process.cwd()`. */
|
|
3
|
+
cwd?: string;
|
|
4
|
+
/** Environment variables, merged on top of `process.env`. */
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
/** Stream stdio to the parent (default). When `false`, stdout/stderr are captured and returned. */
|
|
7
|
+
inherit?: boolean;
|
|
8
|
+
/** Hard timeout in milliseconds. Child is SIGKILL'd on timeout. Defaults to none. */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RunResult {
|
|
12
|
+
code: number;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Cross-platform spawn wrapper.
|
|
18
|
+
*
|
|
19
|
+
* - On Windows sets `shell: true` so `.cmd` / `.bat` shims (claude.cmd,
|
|
20
|
+
* npm.cmd, gh.cmd) are executable. Required by Node.js since
|
|
21
|
+
* CVE-2024-27980; without it, spawning a `.cmd` throws.
|
|
22
|
+
* - On POSIX sets `shell: false` to keep argv boundaries clean and
|
|
23
|
+
* avoid shell-injection surface on user-provided args.
|
|
24
|
+
* - Streams stdio by default so long commands show progress. Set
|
|
25
|
+
* `inherit: false` to capture stdout/stderr into the returned
|
|
26
|
+
* {@link RunResult}.
|
|
27
|
+
* - Throws {@link ExecError} on non-zero exit (captured stdout/stderr
|
|
28
|
+
* attached) unless the caller passed `inherit: true` (stream mode).
|
|
29
|
+
*/
|
|
30
|
+
export declare function runCommand(cmd: string, args: string[], opts?: RunOptions): Promise<RunResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Runs a command and returns whether it exited zero. Never throws.
|
|
33
|
+
* Useful for feature-detection style probes (`commandExists`,
|
|
34
|
+
* `git --version`).
|
|
35
|
+
*/
|
|
36
|
+
export declare function tryRunCommand(cmd: string, args: string[], opts?: RunOptions): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Cross-platform check for whether a command is on PATH. Uses `where`
|
|
39
|
+
* on Windows and `which` on POSIX.
|
|
40
|
+
*/
|
|
41
|
+
export declare function commandExists(cmd: string): Promise<boolean>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
/**
|
|
3
|
+
* `mkdir -p` equivalent. Idempotent — no-op when `dir` already exists.
|
|
4
|
+
* Wraps filesystem errors as {@link FilesystemError} so callers get
|
|
5
|
+
* a typed exit code via the CLI dispatcher.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mkdirp(dir: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* True when the path exists at all (file, directory, symlink, …).
|
|
10
|
+
*/
|
|
11
|
+
export declare function pathExists(p: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Writes `contents` to `filePath` after ensuring the parent directory
|
|
14
|
+
* exists and normalising line endings to LF. Normalisation rule:
|
|
15
|
+
* every `\r\n` → `\n`; lone `\r` left alone (very uncommon in our
|
|
16
|
+
* template inputs and more invasive to rewrite). Callers relying on
|
|
17
|
+
* strict LF-only output should pre-clean their strings.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeFileLf(filePath: string, contents: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Reads a UTF-8 text file. Wraps failures as {@link FilesystemError}.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readTextFile(filePath: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Reads a file and returns its raw bytes. Used by the manifest hasher
|
|
26
|
+
* to avoid UTF-8 re-encoding round trips.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readBytes(filePath: string): Buffer;
|
|
29
|
+
/**
|
|
30
|
+
* Copies a single file, ensuring the destination directory exists.
|
|
31
|
+
*/
|
|
32
|
+
export declare function copyFile(src: string, dest: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Recursively copies a directory. Overwrites existing files by default.
|
|
35
|
+
* Honours a `filter(src, relPath)` predicate — returning false skips
|
|
36
|
+
* the entry (and, if it's a directory, its subtree).
|
|
37
|
+
*/
|
|
38
|
+
export declare function copyDir(srcDir: string, destDir: string, options?: {
|
|
39
|
+
filter?: (src: string, relPath: string) => boolean;
|
|
40
|
+
}): void;
|
|
41
|
+
/**
|
|
42
|
+
* Lists the immediate entries (files + directories) of `dir`. Returns
|
|
43
|
+
* absolute paths. Empty array when `dir` does not exist.
|
|
44
|
+
*/
|
|
45
|
+
export declare function listDir(dir: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* True when the target path exists and resolves to a regular file.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isFile(p: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* True when the target path exists and resolves to a directory.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isDir(p: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* True when `p` itself is a symbolic link (does NOT follow the link). Used by
|
|
56
|
+
* the bundled-framework assembly to detect (and re-create) the per-workspace
|
|
57
|
+
* provider subtree links. Returns false for a real dir/file or a missing path.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isSymlink(p: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a path through symlinks to its real on-disk location, falling back to
|
|
62
|
+
* the resolved-but-unreal path when the target does not exist yet (mirrors
|
|
63
|
+
* registry.ts's `realpathSafe`).
|
|
64
|
+
*/
|
|
65
|
+
export declare function realpathSafe(p: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Remove a file, directory, or symlink at `p` if present. For a symlink the
|
|
68
|
+
* link itself is removed (the target is never followed/deleted). Best-effort:
|
|
69
|
+
* a missing path is a no-op. Used before (re-)creating a workspace link.
|
|
70
|
+
*/
|
|
71
|
+
export declare function removePath(p: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Ensure `<linkPath>` resolves to `<target>` via a symbolic link, with the same
|
|
74
|
+
* junction→symlink→copy fallback dance specrails-desktop uses for the project
|
|
75
|
+
* link (`workspace-manager.ts ensureProjectLink`).
|
|
76
|
+
*
|
|
77
|
+
* 1. POSIX: a plain `symlinkSync(target, linkPath, type)`.
|
|
78
|
+
* 2. Windows: try a junction (dirs) / file symlink first, then a plain symlink.
|
|
79
|
+
* 3. Both failed (e.g. unprivileged Windows): COPY the target subtree verbatim.
|
|
80
|
+
*
|
|
81
|
+
* When `preferCopy` is true the symlink/junction attempts are SKIPPED entirely
|
|
82
|
+
* and the target is copied directly — used by the in-repo standalone install so
|
|
83
|
+
* the repo receives real, committable files instead of symlinks pointing into
|
|
84
|
+
* `$HOME/.specrails/framework`. (An existing correct symlink at `linkPath` is
|
|
85
|
+
* still honoured idempotently before the copy so a re-run of a relocated install
|
|
86
|
+
* does not thrash.)
|
|
87
|
+
*
|
|
88
|
+
* Returns the mechanism used so the caller can record it (copy-fallback loses the
|
|
89
|
+
* O(1) `current`-swap update path; the caller may warn). Idempotent: when the
|
|
90
|
+
* link already points at `target` it is left untouched and `'symlink'` returned.
|
|
91
|
+
*/
|
|
92
|
+
export declare function symlinkOrCopy(target: string, linkPath: string, preferCopy?: boolean): 'symlink' | 'junction' | 'copy';
|
|
93
|
+
/**
|
|
94
|
+
* Atomically repoint `<linkPath>` at `<target>`: create a sibling temp link,
|
|
95
|
+
* then `renameSync` over the destination (rename of a symlink is atomic on
|
|
96
|
+
* POSIX; on Windows the temp+rename pattern still avoids a torn intermediate
|
|
97
|
+
* state). Used for the `framework/current` indirection swap.
|
|
98
|
+
*/
|
|
99
|
+
export declare function atomicSymlinkSwap(target: string, linkPath: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* One link found while snapshotting a protected surface.
|
|
102
|
+
*
|
|
103
|
+
* `rel` is the POSIX path of the link RELATIVE to the snapshot root, and is the
|
|
104
|
+
* empty string when the surface itself is a link.
|
|
105
|
+
*/
|
|
106
|
+
export interface LinkRecord {
|
|
107
|
+
rel: string;
|
|
108
|
+
target: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Copy `src` to `dest` for a rollback snapshot WITHOUT ever creating a link.
|
|
112
|
+
*
|
|
113
|
+
* `fs.cpSync(..., { dereference: false, verbatimSymlinks: true })` looks like the
|
|
114
|
+
* obvious primitive, but it does not copy a link — it RECREATES it with
|
|
115
|
+
* `symlinkSync(target, dest)` and never passes a `type`. Node then autodetects
|
|
116
|
+
* `'file'`/`'dir'`, never `'junction'`, and a `'file'`/`'dir'` symlink on Windows
|
|
117
|
+
* requires SeCreateSymbolicLinkPrivilege. On an ordinary account that raises
|
|
118
|
+
* EPERM, so merely BACKING UP a surface that contains a link (every
|
|
119
|
+
* `<frameworkDir>/current`, every assembled workspace provider dir) failed before
|
|
120
|
+
* the install had done anything at all.
|
|
121
|
+
*
|
|
122
|
+
* Links are therefore RECORDED, not reproduced: the returned records carry each
|
|
123
|
+
* link's target, and {@link restoreTree} puts them back through
|
|
124
|
+
* {@link symlinkOrCopy}, which already knows the junction-first, copy-fallback
|
|
125
|
+
* dance. Taking a snapshot needs no filesystem privilege on any platform.
|
|
126
|
+
*
|
|
127
|
+
* Inode types that are neither link, directory nor file are skipped — the same
|
|
128
|
+
* stance {@link copyDir} takes. Walking in JS also keeps these copies away from
|
|
129
|
+
* Node 22's native Unicode directory-copy defect on Windows (nodejs/node#61878),
|
|
130
|
+
* which the `cpSync` call this replaced had to work around explicitly.
|
|
131
|
+
*/
|
|
132
|
+
export declare function snapshotTree(src: string, dest: string): LinkRecord[];
|
|
133
|
+
/**
|
|
134
|
+
* Put a snapshot taken by {@link snapshotTree} back at `target`.
|
|
135
|
+
*
|
|
136
|
+
* `skip(rel)` receives the same POSIX-relative paths the records use ('' for the
|
|
137
|
+
* root) and suppresses restoring that entry — the reserved-path rule, so a file
|
|
138
|
+
* the user created or edited while the install was running is never overwritten
|
|
139
|
+
* by a stale backup copy.
|
|
140
|
+
*/
|
|
141
|
+
export declare function restoreTree(saved: string, target: string, links: LinkRecord[], options?: {
|
|
142
|
+
skip?: (rel: string) => boolean;
|
|
143
|
+
}): void;
|
|
144
|
+
/**
|
|
145
|
+
* Recreate a recorded link at `linkPath`. Delegates to {@link symlinkOrCopy} so
|
|
146
|
+
* Windows gets a junction (no privilege required) and an unprivileged machine
|
|
147
|
+
* with no link mechanism at all still ends up with the right CONTENTS.
|
|
148
|
+
*
|
|
149
|
+
* A target that no longer exists cannot be copied, so the pointer is recreated
|
|
150
|
+
* verbatim where the platform allows a dangling link rather than failing the
|
|
151
|
+
* whole rollback.
|
|
152
|
+
*/
|
|
153
|
+
export declare function restoreLink(target: string, linkPath: string): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level git wrapper. Every method shells out to the user's `git`
|
|
3
|
+
* binary (already a prerequisite — the installer refuses to run
|
|
4
|
+
* without a git repo). Failures surface as {@link GitError} so the
|
|
5
|
+
* CLI dispatcher translates them to exit code 30.
|
|
6
|
+
*
|
|
7
|
+
* We intentionally keep the surface minimal (the six operations used
|
|
8
|
+
* by the installer) rather than wrapping `simple-git` — that package
|
|
9
|
+
* is ~100 KB and wraps the same shell-out we do here.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isGitRepo(cwd: string): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the absolute path of the repository root containing `cwd`.
|
|
14
|
+
* Throws {@link GitError} if `cwd` is not inside a git repo.
|
|
15
|
+
*/
|
|
16
|
+
export declare function repoRoot(cwd: string): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Initialises a new git repository at `cwd` with an initial commit on
|
|
19
|
+
* the default branch name. Idempotent — no-op when `cwd` is already
|
|
20
|
+
* a repo.
|
|
21
|
+
*/
|
|
22
|
+
export declare function initRepo(cwd: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Porcelain working-tree status. Returns the raw short-format output
|
|
25
|
+
* (empty string when the tree is clean).
|
|
26
|
+
*/
|
|
27
|
+
export declare function status(cwd: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Stages the given pathspecs. Empty array stages nothing and returns
|
|
30
|
+
* without invoking git.
|
|
31
|
+
*/
|
|
32
|
+
export declare function add(cwd: string, pathspecs: string[]): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a commit with the given message using the committer identity
|
|
35
|
+
* currently configured on the repo / globally. Does NOT attempt to
|
|
36
|
+
* install an identity — that is a prerequisite check.
|
|
37
|
+
*/
|
|
38
|
+
export declare function commit(cwd: string, message: string, opts?: {
|
|
39
|
+
allowEmpty?: boolean;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* True when git is installed and on PATH.
|
|
43
|
+
*/
|
|
44
|
+
export declare function gitInstalled(): Promise<boolean>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const frameworkLifecycleLockPath: (frameworkDir: string) => string;
|
|
2
|
+
export declare function withFrameworkLifecycleLock<T>(frameworkDir: string, apply: () => T | Promise<T>): Promise<T>;
|
|
3
|
+
/**
|
|
4
|
+
* A surface the transaction protects.
|
|
5
|
+
*
|
|
6
|
+
* `snapshotContents: false` marks a surface whose CONTENTS must not be copied:
|
|
7
|
+
* the versioned framework store, which `installFramework` already rebuilds from
|
|
8
|
+
* a content-hash stamp and stages through a temp directory. Rollback still
|
|
9
|
+
* removes such a surface when the install CREATED it, and leaves a pre-existing
|
|
10
|
+
* one exactly as it stands — copying it bought no safety and cost a full copy of
|
|
11
|
+
* the framework store on every install.
|
|
12
|
+
*/
|
|
13
|
+
export type ProtectedSurface = string | {
|
|
14
|
+
path: string;
|
|
15
|
+
snapshotContents: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** Snapshot only installer-owned surfaces; never follow links into a shared framework.
|
|
18
|
+
* Backups survive a failed rollback and their location is reported for recovery.
|
|
19
|
+
*
|
|
20
|
+
* Snapshotting requires NO filesystem privilege: links are recorded by target
|
|
21
|
+
* rather than recreated (see `snapshotTree`), so a protected surface that is —
|
|
22
|
+
* or contains — a Windows junction no longer fails the install on an account
|
|
23
|
+
* without SeCreateSymbolicLinkPrivilege.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withInstallRollback<T>(paths: ProtectedSurface[], apply: () => Promise<T>): Promise<T>;
|
|
26
|
+
export declare function compareCoreVersions(left: string, right: string): number | null;
|
|
27
|
+
/** Refuse an implicit downgrade. Explicit swap-current is the rollback interface. */
|
|
28
|
+
export declare function assertNoCoreDowngrade(current: string | null, target: string): void;
|
|
29
|
+
export declare function currentFrameworkVersion(frameworkDir: string): string | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coloured output helpers for the installer. The emoji prefixes match
|
|
3
|
+
* what the retired bash scripts printed (`✓ ⚠ ✗ →`) so existing user
|
|
4
|
+
* bug reports and screenshots still read the same.
|
|
5
|
+
*
|
|
6
|
+
* The logger writes to stdout by default; callers may override via
|
|
7
|
+
* {@link setLoggerStreams} (useful in tests that capture output).
|
|
8
|
+
*/
|
|
9
|
+
interface Streams {
|
|
10
|
+
out: NodeJS.WritableStream;
|
|
11
|
+
err: NodeJS.WritableStream;
|
|
12
|
+
}
|
|
13
|
+
export declare function setLoggerStreams(next: Partial<Streams>): void;
|
|
14
|
+
/** Restore logger to process.stdout/stderr. */
|
|
15
|
+
export declare function resetLoggerStreams(): void;
|
|
16
|
+
/** Section heading — bold, no prefix, leading blank line. */
|
|
17
|
+
export declare function step(title: string): void;
|
|
18
|
+
/** Success line. Prefix: ` ✓ ` in green. */
|
|
19
|
+
export declare function ok(msg: string): void;
|
|
20
|
+
/** Warning line. Prefix: ` ⚠ ` in yellow. */
|
|
21
|
+
export declare function warn(msg: string): void;
|
|
22
|
+
/** Failure line, routed to stderr. Prefix: ` ✗ ` in red. */
|
|
23
|
+
export declare function fail(msg: string): void;
|
|
24
|
+
/** Info line. Prefix: ` → ` in blue. */
|
|
25
|
+
export declare function info(msg: string): void;
|
|
26
|
+
/** Print a fatal error and its hint, routed to stderr. */
|
|
27
|
+
export declare function fatal(message: string, hint?: string): void;
|
|
28
|
+
/** Primitive write for cases where the caller owns formatting. */
|
|
29
|
+
export declare function rawOut(text: string): void;
|
|
30
|
+
export declare function rawErr(text: string): void;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths the installer MUST NOT create, modify, or delete. These hold
|
|
3
|
+
* user / team state that survives re-runs (profile JSON authored by
|
|
4
|
+
* specrails-desktop, custom agents authored by the user). Breaking this
|
|
5
|
+
* contract silently destroys user work.
|
|
6
|
+
*
|
|
7
|
+
* Audited by vitest spec `reserved-paths.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const RESERVED_PATHS: readonly [".specrails/profiles/", ".claude/agents/custom-", ".kimi-code/skills/custom-", ".kimi-code/skills/rails/custom-"];
|
|
10
|
+
/**
|
|
11
|
+
* Returns true when a repo-relative path falls inside a reserved
|
|
12
|
+
* region. Accepts both POSIX (/) and Windows (\) separators; the
|
|
13
|
+
* check normalises internally so callers can pass a value straight
|
|
14
|
+
* from `path.relative()` without worrying about host platform.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isReservedPath(relPath: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Joins segments and forces POSIX separators. Use when a generated
|
|
19
|
+
* string is destined for a cross-platform artefact (JSON manifest,
|
|
20
|
+
* YAML config) rather than the local filesystem.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toPosix(...segments: string[]): string;
|
|
23
|
+
/**
|
|
24
|
+
* Joins segments using the host platform's separator. Use when the
|
|
25
|
+
* result goes directly to `fs.*` / `child_process.*` calls.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toNative(...segments: string[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Repository-relative representation of an absolute path. Returns a
|
|
30
|
+
* POSIX-style path so callers can store it in manifests without OS
|
|
31
|
+
* contamination. If `absPath` is outside `repoRoot`, returns the
|
|
32
|
+
* original absolute path unchanged (POSIX-normalised).
|
|
33
|
+
*/
|
|
34
|
+
export declare function repoRelative(repoRoot: string, absPath: string): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TextOptions {
|
|
2
|
+
message: string;
|
|
3
|
+
/** Value used when stdin is non-TTY. Absence here in non-TTY mode throws. */
|
|
4
|
+
default?: string;
|
|
5
|
+
/** Returns error message when invalid, or `true` / undefined when valid. */
|
|
6
|
+
validate?: (value: string) => string | true | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function text(opts: TextOptions): Promise<string>;
|
|
9
|
+
export interface ConfirmOptions {
|
|
10
|
+
message: string;
|
|
11
|
+
default?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function confirmYesNo(opts: ConfirmOptions): Promise<boolean>;
|
|
14
|
+
export interface SelectOptions<T> {
|
|
15
|
+
message: string;
|
|
16
|
+
choices: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
value: T;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
default?: T;
|
|
22
|
+
}
|
|
23
|
+
export declare function chooseOne<T>(opts: SelectOptions<T>): Promise<T>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared artifact registry — the contract that lets specrails-core place,
|
|
3
|
+
* and specrails-desktop read, a repo's relocated artifacts OUTSIDE the repo,
|
|
4
|
+
* under `$HOME/.specrails/projects/<slug>/workspace`.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth: `$HOME/.specrails/registry.json`, an inspectable,
|
|
7
|
+
* schema-versioned JSON file keyed by the **canonical realpath of the repo**.
|
|
8
|
+
* specrails-desktop is the primary writer (a projection of its `desktop.sqlite`);
|
|
9
|
+
* specrails-core reads it and, when run standalone, allocates its own entry.
|
|
10
|
+
*
|
|
11
|
+
* Design contract: `docs/internals/global-artifacts-alignment-contract.md`
|
|
12
|
+
* (in the specrails-desktop repo). The slug algorithm, canonical-key rule,
|
|
13
|
+
* atomic-write rule and lock protocol here MUST stay byte-identical to the
|
|
14
|
+
* desktop-side implementation — the correctness of the cross-tool contract
|
|
15
|
+
* depends on both tools resolving the same repo to the same paths.
|
|
16
|
+
*
|
|
17
|
+
* Everything in this module is synchronous to fit the installer's sync flow.
|
|
18
|
+
*/
|
|
19
|
+
/** Registry schema version. A reader that sees a higher value MUST treat all
|
|
20
|
+
* entries as absent (legacy fallback), never mis-parse. */
|
|
21
|
+
export declare const REGISTRY_SCHEMA_VERSION = 1;
|
|
22
|
+
/** Who owns/allocated an entry. Encodes the single-writer-at-a-time rule. */
|
|
23
|
+
export type RegistrySource = 'desktop' | 'core-standalone';
|
|
24
|
+
/** One repo's relocated-artifact locations. All paths are absolute and in the
|
|
25
|
+
* host platform's native separator; consumers treat them as opaque. */
|
|
26
|
+
export interface ProjectEntry {
|
|
27
|
+
/** The canonical realpath of the repo (mirror of the map key). */
|
|
28
|
+
repoPath: string;
|
|
29
|
+
/** Shared slug — MUST equal desktop.sqlite `projects.slug` for the same repo. */
|
|
30
|
+
slug: string;
|
|
31
|
+
/** `$HOME/.specrails/projects/<slug>/workspace` — root of all relocated artifacts. */
|
|
32
|
+
workspaceDir: string;
|
|
33
|
+
/** The dir core treats as its `.specrails`/install root instead of repoRoot (= workspaceDir). */
|
|
34
|
+
artifactRoot: string;
|
|
35
|
+
/** Always the repo (= repoPath); carries the `openspec/**` + worktree carve-outs. */
|
|
36
|
+
codeRoot: string;
|
|
37
|
+
/** Runtime-state base (agent-memory, pipeline-state, …). Injected as SPECRAILS_STATE_DIR. */
|
|
38
|
+
stateDir: string;
|
|
39
|
+
/** Absolute path to the relocated local-tickets.json. Injected as SPECRAILS_TICKETS_PATH. */
|
|
40
|
+
ticketsPath: string;
|
|
41
|
+
/** Absolute path to backlog-config.json (Jira read-only switch). Injected as SPECRAILS_BACKLOG_CONFIG_PATH. */
|
|
42
|
+
backlogConfigPath: string;
|
|
43
|
+
/** Relocated `.specrails/profiles/`. Injected as SPECRAILS_PROFILES_DIR. */
|
|
44
|
+
profilesDir: string;
|
|
45
|
+
/** Relocated `.specrails/plugins/` (desktop-only; present for inspectability). */
|
|
46
|
+
pluginsStateDir: string;
|
|
47
|
+
/** Relocated `.specrails/file-summaries/` (desktop-only). */
|
|
48
|
+
fileSummariesDir: string;
|
|
49
|
+
/** Installed providers; mirror of desktop.sqlite `projects.providers`. */
|
|
50
|
+
providers: string[];
|
|
51
|
+
/** providers[0]; mirror of desktop.sqlite `projects.provider`. */
|
|
52
|
+
primaryProvider: string;
|
|
53
|
+
/** The `specrails-version` pin (name/format frozen; only location moved). */
|
|
54
|
+
coreVersion?: string;
|
|
55
|
+
createdAt?: string;
|
|
56
|
+
lastInstallAt?: string;
|
|
57
|
+
/** Single-owner-at-a-time marker; governs reconciliation. */
|
|
58
|
+
source: RegistrySource;
|
|
59
|
+
/** desktop.sqlite projects.id when source='desktop', for robust repo-move re-link. */
|
|
60
|
+
desktopProjectId?: string;
|
|
61
|
+
}
|
|
62
|
+
/** On-disk shape of `registry.json`. */
|
|
63
|
+
export interface RegistryFile {
|
|
64
|
+
schemaVersion: number;
|
|
65
|
+
generator?: string;
|
|
66
|
+
updatedAt?: string;
|
|
67
|
+
/** Map: canonical-repo-key -> ProjectEntry. */
|
|
68
|
+
projects: Record<string, ProjectEntry>;
|
|
69
|
+
}
|
|
70
|
+
/** The flattened result both tools consume. */
|
|
71
|
+
export interface Resolution {
|
|
72
|
+
/** Normalized map key. */
|
|
73
|
+
key: string;
|
|
74
|
+
repoPath: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
workspaceDir: string;
|
|
77
|
+
artifactRoot: string;
|
|
78
|
+
codeRoot: string;
|
|
79
|
+
stateDir: string;
|
|
80
|
+
ticketsPath: string;
|
|
81
|
+
backlogConfigPath: string;
|
|
82
|
+
profilesDir: string;
|
|
83
|
+
pluginsStateDir: string;
|
|
84
|
+
fileSummariesDir: string;
|
|
85
|
+
providers: string[];
|
|
86
|
+
primaryProvider: string;
|
|
87
|
+
source: RegistrySource | 'legacy';
|
|
88
|
+
/** True when there is no registry entry and we fell back to the in-repo layout. */
|
|
89
|
+
isLegacy: boolean;
|
|
90
|
+
}
|
|
91
|
+
export interface ResolveOptions {
|
|
92
|
+
/** When true, allocate + persist an entry if none exists. Readers pass false. */
|
|
93
|
+
allocate?: boolean;
|
|
94
|
+
/** Who is allocating (only consulted when allocate=true). Default 'core-standalone'. */
|
|
95
|
+
allocator?: RegistrySource;
|
|
96
|
+
/** Override `$HOME` (tests). Default `os.homedir()`. */
|
|
97
|
+
home?: string;
|
|
98
|
+
/** Providers to record on allocation. Default ['claude']. */
|
|
99
|
+
providers?: string[];
|
|
100
|
+
coreVersion?: string;
|
|
101
|
+
desktopProjectId?: string;
|
|
102
|
+
/** Override timestamp (tests). */
|
|
103
|
+
now?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Slug derivation — byte-identical to specrails-desktop's `slugify`
|
|
107
|
+
* (`server/desktop-router.ts`). Do not "improve" it; parity is the contract.
|
|
108
|
+
*/
|
|
109
|
+
export declare function slugify(name: string): string;
|
|
110
|
+
/** `$HOME` for the registry, overridable for tests. */
|
|
111
|
+
export declare function resolveHome(home?: string): string;
|
|
112
|
+
/** Absolute path to `registry.json`. */
|
|
113
|
+
export declare function registryPath(home?: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* Absolute path to the versioned framework store
|
|
116
|
+
* (`<home>/.specrails/framework`). The bundled-framework split materializes the
|
|
117
|
+
* provider-invariant subtree to `<frameworkDir>/<version>/<providerDir>/` once
|
|
118
|
+
* and every workspace symlinks `<frameworkDir>/current/<providerDir>/...`. Shares
|
|
119
|
+
* the SAME home as the registry so framework, registry, and workspaces co-locate
|
|
120
|
+
* (and tests pinning `SPECRAILS_REGISTRY_HOME` redirect all three together).
|
|
121
|
+
*/
|
|
122
|
+
export declare function frameworkRoot(home?: string): string;
|
|
123
|
+
/** Absolute path to the advisory lock file. */
|
|
124
|
+
export declare function lockPath(home?: string): string;
|
|
125
|
+
/** `fs.realpathSync` that falls back to the resolved-but-unreal path on error
|
|
126
|
+
* (the path may not exist yet, or be on a volume that rejects realpath). */
|
|
127
|
+
export declare function realpathSafe(abs: string): string;
|
|
128
|
+
/** Case-fold the key on case-insensitive platforms (macOS, Windows) so two
|
|
129
|
+
* spellings of the same path map to one entry. The stored `repoPath` keeps
|
|
130
|
+
* its canonical case; only the index key is folded. */
|
|
131
|
+
export declare function normalizeKey(canon: string): string;
|
|
132
|
+
/** Canonical repo path: resolve to absolute, then realpath (collapses symlinks). */
|
|
133
|
+
export declare function canonicalizeRepoPath(repoPathInput: string): string;
|
|
134
|
+
/** The per-project sub-path layout under a workspace dir. Single source of the
|
|
135
|
+
* layout so writer and (allocation) reader never disagree. */
|
|
136
|
+
export declare function workspaceLayout(home: string, slug: string, canon: string, primaryProvider?: string): Omit<ProjectEntry, 'providers' | 'primaryProvider' | 'coreVersion' | 'createdAt' | 'lastInstallAt' | 'source' | 'desktopProjectId'>;
|
|
137
|
+
/** The in-repo layout, used when there is no registry entry (legacy fallback). */
|
|
138
|
+
export declare function legacyResolution(canon: string): Resolution;
|
|
139
|
+
/** Flatten a stored entry into a Resolution. */
|
|
140
|
+
export declare function entryToResolution(key: string, entry: ProjectEntry): Resolution;
|
|
141
|
+
/**
|
|
142
|
+
* Total, fail-open read. A missing file, a parse error, or a `schemaVersion`
|
|
143
|
+
* greater than we understand all yield an empty registry, so a caller treats
|
|
144
|
+
* it as "no entry" and (if allocating) writes a fresh, understood entry rather
|
|
145
|
+
* than crashing. Biases toward availability over strict consistency — correct
|
|
146
|
+
* for a local, inspectable file.
|
|
147
|
+
*/
|
|
148
|
+
export declare function readRegistryOrEmpty(home?: string): RegistryFile;
|
|
149
|
+
/** Write `data` to `filePath` atomically: temp file in the same dir, fsync,
|
|
150
|
+
* rename. A reader (even without the lock) only ever sees a complete old or
|
|
151
|
+
* new file — the lock serialises writers, the rename protects readers. */
|
|
152
|
+
export declare function atomicWrite(filePath: string, data: string): void;
|
|
153
|
+
/**
|
|
154
|
+
* Run `fn` while holding an advisory lock over the registry. Mutual exclusion
|
|
155
|
+
* is an exclusive-create lock file (`open(..., 'wx')`) with bounded spin-retry
|
|
156
|
+
* and stale-lock breaking (a lock whose mtime exceeds the TTL is reclaimed —
|
|
157
|
+
* covers a crashed writer). Read-only callers never take the lock.
|
|
158
|
+
*/
|
|
159
|
+
export declare function withFileLock<T>(home: string | undefined, fn: () => T): T;
|
|
160
|
+
/** Deterministic slug allocation: basename-derived, `-N` dedup suffix. Both
|
|
161
|
+
* tools implement this identically so they only ever diverge on the suffix,
|
|
162
|
+
* which read-before-allocate resolves. */
|
|
163
|
+
export declare function allocateSlug(canon: string, existing: ReadonlySet<string>): string;
|
|
164
|
+
/**
|
|
165
|
+
* The shared resolver both tools run. Given a repo path, returns where that
|
|
166
|
+
* repo's artifacts live. With `allocate:true`, creates + persists an entry when
|
|
167
|
+
* none exists (under the lock, with a re-read double-check); readers pass
|
|
168
|
+
* `allocate:false` and fall back to the in-repo legacy layout when absent.
|
|
169
|
+
*/
|
|
170
|
+
export declare function resolveArtifacts(repoPathInput: string, opts?: ResolveOptions): Resolution;
|
|
171
|
+
/** Commit version metadata only after successful materialization and assembly.
|
|
172
|
+
* Desktop-owned entries remain the host's responsibility; never claim ownership.
|
|
173
|
+
*/
|
|
174
|
+
export declare function recordSuccessfulInstall(repoPath: string, opts: Pick<ResolveOptions, 'home' | 'providers' | 'coreVersion' | 'now'>): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export type TemplateContext = Record<string, string | boolean | number | null | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Renders `template` by evaluating its directives against `context`.
|
|
19
|
+
* Missing interpolation variables render as the empty string rather
|
|
20
|
+
* than throwing — the bash scripts behaved the same way (`${X:-}` in
|
|
21
|
+
* a heredoc). Unknown flags in an `{{#if}}` are treated as falsy.
|
|
22
|
+
*/
|
|
23
|
+
export declare function render(template: string, context: TemplateContext): string;
|
package/docs/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Welcome to the SpecRails docs. This guide will take you from zero to a fully aut
|
|
|
18
18
|
| [Plugin Architecture](plugin-architecture.md) | Plugin vs scaffold, what lives where, how updates work |
|
|
19
19
|
| [Agents](agents.md) | Every agent explained — role, when it runs, why it exists |
|
|
20
20
|
| [Workflows & Commands](workflows.md) | How to use `/specrails:implement`, `/specrails:get-backlog-specs`, and more |
|
|
21
|
+
| [Programmatic Agent Runtime](agent-runtime.md) | LangGraph phases, Claude/Codex/Gemini/Kimi and local providers, CLI/API configuration, recovery and host delivery |
|
|
21
22
|
| [Customization](customization.md) | Adapt agents, rules, personas, and conventions to your project |
|
|
22
23
|
| [Updating](updating.md) | Keep SpecRails up to date without losing your customizations |
|
|
23
24
|
|