ai-runtime-kit 0.5.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +307 -0
  3. package/bin/cli.js +52 -0
  4. package/package.json +40 -0
  5. package/runtime/BOOTSTRAP.md +230 -0
  6. package/runtime/CAPABILITIES.md +166 -0
  7. package/runtime/INDEX.md +397 -0
  8. package/runtime/PRIORITIES.md +84 -0
  9. package/runtime/RUNTIME_HEALTH.md +87 -0
  10. package/runtime/RUNTIME_MODE.md +109 -0
  11. package/runtime/RUNTIME_TRANSITIONS.md +141 -0
  12. package/runtime/RUNTIME_VERSION.md +17 -0
  13. package/runtime/SAFETY.md +156 -0
  14. package/runtime/adr/0000-template.md +55 -0
  15. package/runtime/agents/executor.md +19 -0
  16. package/runtime/agents/verifier.md +83 -0
  17. package/runtime/hooks/README.md +163 -0
  18. package/runtime/hooks/_template/HOOK.md +87 -0
  19. package/runtime/hooks/pre-executor/runtime-scoped-preflight/HOOK.md +189 -0
  20. package/runtime/memory/architecture/principles.md +107 -0
  21. package/runtime/memory/engineering/principles.md +102 -0
  22. package/runtime/memory/runtime/context-loading.md +107 -0
  23. package/runtime/plans/_template.md +81 -0
  24. package/runtime/prds/_template.md +73 -0
  25. package/runtime/reviews/_template.md +37 -0
  26. package/runtime/rules/README.md +101 -0
  27. package/runtime/rules/_template/RULE.md +75 -0
  28. package/runtime/skills/README.md +96 -0
  29. package/runtime/skills/_template/SKILL.md +61 -0
  30. package/runtime/specs/_template/spec.md +50 -0
  31. package/runtime/specs/_template-bug-fix/spec.md +120 -0
  32. package/runtime/tasks/TASK_STATUS.md +58 -0
  33. package/runtime/tasks/_template.md +73 -0
  34. package/runtime/workflows/branching.md +128 -0
  35. package/runtime/workflows/bug-fix.md +169 -0
  36. package/runtime/workflows/feature-development.md +238 -0
  37. package/src/diff.js +81 -0
  38. package/src/git.js +38 -0
  39. package/src/init.js +166 -0
  40. package/src/prompt.js +17 -0
  41. package/src/snapshot.js +84 -0
  42. package/src/templates.js +96 -0
  43. package/src/upgrade.js +179 -0
  44. package/src/version.js +42 -0
@@ -0,0 +1,238 @@
1
+ # Feature Development Workflow
2
+
3
+ ## Purpose
4
+
5
+ Use this workflow to ship one small feature safely with AI assistance.
6
+
7
+ > If the change is corrective (a bug fix), use
8
+ > `.ai/runtime/workflows/bug-fix.md` instead. That workflow is a
9
+ > strict superset of this one with three additional required spec
10
+ > sections (Root Cause, Reproduction, Regression Test) and a
11
+ > regression-test-first executor order.
12
+
13
+ ## Roles
14
+
15
+ - ChatGPT: define, design, review, summarize
16
+ - Claude Code: implement, refactor, verify
17
+
18
+ ## Spec Lifecycle
19
+
20
+ Specs should use one of:
21
+
22
+ ```txt
23
+ DRAFT
24
+ APPROVED
25
+ REJECTED
26
+ SUPERSEDED
27
+ ```
28
+
29
+ Rules:
30
+
31
+ - New specs start as DRAFT.
32
+ - Approved specs may generate plans/tasks.
33
+ - Rejected specs must not generate executable tasks.
34
+ - Superseded specs should reference the replacement spec.
35
+
36
+ ## Workflow
37
+
38
+ ### 0. Define PRD (optional — product-driven features)
39
+
40
+ For features driven by product intent (new capabilities, UX
41
+ changes, user-visible behavior shifts), draft a PRD first under:
42
+
43
+ ```txt
44
+ .ai/project/prds/YYYY-MM-DD-<slug>/prd.md
45
+ ```
46
+
47
+ Use `.ai/runtime/prds/_template.md` as the starting point. The
48
+ PRD answers **what & why** — Problem, Target Users, Success
49
+ Metrics, User Stories, Out of Scope, Open Questions,
50
+ Stakeholders. Engineering details (architecture, data flow, code
51
+ contracts) belong in the downstream spec, not the PRD.
52
+
53
+ Skip this step when:
54
+
55
+ - the change is corrective (use `bug-fix.md` instead — bug fixes
56
+ don't need PRDs),
57
+ - the change is engineering-only (refactor, dependency bump, test
58
+ coverage, governance maintenance),
59
+ - the scope is small enough that the spec alone communicates
60
+ intent.
61
+
62
+ PRD lifecycle mirrors specs: `DRAFT → APPROVED → REJECTED →
63
+ SUPERSEDED`. An APPROVED PRD authorizes spec drafting. The
64
+ downstream spec MUST reference its PRD by path in §1 Goal so
65
+ review can verify the spec satisfies the PRD.
66
+
67
+ ### 1. Define Spec
68
+
69
+ Create a feature spec under:
70
+
71
+ ```txt
72
+ .ai/project/specs/YYYY-MM-DD-feature-name/spec.md
73
+ ```
74
+
75
+ If the spec is downstream of a PRD (Step 0), §1 Goal must cite
76
+ the PRD path so reviewers can check that the spec covers the
77
+ PRD's requirements without quietly expanding scope.
78
+
79
+ Spec must include:
80
+
81
+ - Goal
82
+ - Scope
83
+ - Requirements
84
+ - Acceptance Criteria
85
+ - Test Checklist
86
+ - Verification Commands
87
+ - Rollback Plan
88
+
89
+ ### 2. Execute with Claude Code
90
+
91
+ Claude Code must read:
92
+
93
+ - `.ai/runtime/agents/executor.md`
94
+ - `.ai/project/memory/core/tech-stack.md`
95
+ - `.ai/runtime/workflows/feature-development.md`
96
+ - `.ai/runtime/memory/engineering/principles.md`
97
+ - related `.ai/project/contracts/**` files if the feature touches public APIs
98
+ - relevant skill files from **both** `.ai/runtime/skills/**`
99
+ (kit-shipped, framework-generic) **and** `.ai/project/skills/**`
100
+ (project-shipped, project-specific). Project-side files take
101
+ precedence on path collision.
102
+ - feature spec
103
+
104
+ If the spec itself authors or modifies a kit-shipped skill under
105
+ `.ai/runtime/skills/**`, follow `.ai/runtime/skills/README.md` and
106
+ treat the spec as runtime-scoped governance per
107
+ `.ai/runtime/SAFETY.md` § Runtime Tree Protection. Authoring a
108
+ project-side skill at `.ai/project/skills/**` is not governance-
109
+ protected (project owns its tree); it still follows the structural
110
+ convention from `.ai/runtime/skills/README.md`.
111
+
112
+ If the spec authors or modifies a kit-shipped rule under
113
+ `.ai/runtime/rules/**`, follow `.ai/runtime/rules/README.md` and
114
+ treat the spec as runtime-scoped governance per `.ai/runtime/SAFETY.md`
115
+ § Runtime Tree Protection. Authoring a project-side rule at
116
+ `.ai/project/rules/**` is not governance-protected.
117
+
118
+ When the executor touches files in a rule's scope (e.g. a `.ts`
119
+ file when a TypeScript rule exists), it must load the relevant
120
+ rules from **both** `.ai/runtime/rules/<language>/*.md` (kit) and
121
+ `.ai/project/rules/<language>/*.md` (project) before writing code.
122
+ Project-side rules take precedence on path collision.
123
+
124
+ If the spec authors or modifies a kit-shipped hook under
125
+ `.ai/runtime/hooks/**`, follow `.ai/runtime/hooks/README.md` and
126
+ treat the spec as runtime-scoped governance per `.ai/runtime/SAFETY.md`
127
+ § Runtime Tree Protection. Authoring a project-side hook at
128
+ `.ai/project/hooks/**` is not governance-protected.
129
+
130
+ At each agent-pipeline transition (Architect → Planner → Executor
131
+ → Verifier → Reviewer), the active agent must load hooks attached
132
+ to its phase from **both** `.ai/runtime/hooks/<phase>-<agent>/*/HOOK.md`
133
+ (kit) and `.ai/project/hooks/<phase>-<agent>/*/HOOK.md` (project)
134
+ whose `appliesWhen` matches the current spec, diff, or runtime
135
+ mode. Project-side hooks take precedence on path collision. GATE
136
+ and MUTATION hooks block the transition until their action
137
+ completes; ADVISORY hooks log into the review file.
138
+
139
+ Claude Code must return:
140
+
141
+ - Changed files
142
+ - Implementation summary
143
+ - Test result
144
+ - Build result
145
+ - Unresolved risks
146
+
147
+ ### 3. Verify
148
+
149
+ Verification should include:
150
+
151
+ - `.ai/runtime/agents/verifier.md`
152
+ - related `.ai/project/contracts/**`
153
+ - existing tests
154
+ - build integrity
155
+ - backward compatibility
156
+
157
+ Verification results should be saved under `.ai/project/verifications/` when:
158
+
159
+ - verification fails
160
+ - a contract violation is detected
161
+ - a breaking change is proposed
162
+ - the feature is a complex refactor
163
+ - explicit audit trail is required
164
+
165
+ For simple successful features, verification results may be recorded in the review file instead.
166
+
167
+ At minimum, run:
168
+
169
+ ```bash
170
+ npm run verify
171
+ ```
172
+
173
+ Local verification must use `npm run verify` as the canonical verification command.
174
+ If the project has lint/typecheck commands, run them too.
175
+
176
+ ### 4. Review
177
+
178
+ Create review file under:
179
+
180
+ ```txt
181
+ .ai/project/reviews/YYYY-MM-DD-feature-name-review.md
182
+ ```
183
+
184
+ Review must include:
185
+
186
+ - Summary
187
+ - Blocking issues
188
+ - Non-blocking issues
189
+ - Suggested fixes
190
+ - Verdict
191
+
192
+ ### 5. Fix
193
+
194
+ If review finds issues, Claude Code must:
195
+
196
+ - Read the review
197
+ - Fix blocking issues
198
+ - Re-run verification
199
+ - Report final result
200
+
201
+ ### 6. Commit
202
+
203
+ Use conventional commits:
204
+
205
+ ```txt
206
+ feat: ...
207
+ fix: ...
208
+ test: ...
209
+ refactor: ...
210
+ docs: ...
211
+ ```
212
+
213
+ ## Definition of Done
214
+
215
+ A feature is done only when:
216
+
217
+ - Spec exists
218
+ - Code is implemented
219
+ - Tests pass
220
+ - Build passes
221
+ - Review exists
222
+ - Changes are committed
223
+
224
+ ## Task Status Lifecycle
225
+
226
+ Tasks should follow:
227
+
228
+ ```txt
229
+ TODO → IN_PROGRESS → IN_REVIEW → DONE
230
+ ```
231
+
232
+ If blocked:
233
+
234
+ ```txt
235
+ TODO or IN_PROGRESS → BLOCKED
236
+ ```
237
+
238
+ Task status must be updated when execution state changes.
package/src/diff.js ADDED
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ const { listFilesUnder, KIT_RUNTIME_DIR } = require('./snapshot');
8
+
9
+ function computeRuntimeDiff(projectRuntimeDir) {
10
+ const kitFiles = new Set(listFilesUnder(KIT_RUNTIME_DIR));
11
+ const projectFiles = new Set(listFilesUnder(projectRuntimeDir).filter((f) => f !== 'KIT_VERSION'));
12
+
13
+ const added = [];
14
+ const removed = [];
15
+ const replaced = [];
16
+ const unchanged = [];
17
+
18
+ for (const f of kitFiles) {
19
+ if (!projectFiles.has(f)) {
20
+ added.push(f);
21
+ } else {
22
+ const a = fs.readFileSync(path.join(KIT_RUNTIME_DIR, f));
23
+ const b = fs.readFileSync(path.join(projectRuntimeDir, f));
24
+ if (a.equals(b)) unchanged.push(f);
25
+ else replaced.push(f);
26
+ }
27
+ }
28
+ for (const f of projectFiles) {
29
+ if (!kitFiles.has(f)) removed.push(f);
30
+ }
31
+
32
+ added.sort();
33
+ removed.sort();
34
+ replaced.sort();
35
+ unchanged.sort();
36
+
37
+ return { added, removed, replaced, unchanged };
38
+ }
39
+
40
+ function printDiffSummary(diff) {
41
+ const { added, removed, replaced, unchanged } = diff;
42
+ console.log('Upgrade preview:');
43
+ console.log(` ${added.length} file(s) to ADD`);
44
+ console.log(` ${replaced.length} file(s) to REPLACE`);
45
+ console.log(` ${removed.length} file(s) to DELETE`);
46
+ console.log(` ${unchanged.length} file(s) UNCHANGED`);
47
+ console.log('');
48
+ if (added.length) {
49
+ console.log('ADD:');
50
+ for (const f of added) console.log(` + ${f}`);
51
+ console.log('');
52
+ }
53
+ if (replaced.length) {
54
+ console.log('REPLACE:');
55
+ for (const f of replaced) console.log(` ~ ${f}`);
56
+ console.log('');
57
+ }
58
+ if (removed.length) {
59
+ console.log('DELETE:');
60
+ for (const f of removed) console.log(` - ${f}`);
61
+ console.log('');
62
+ }
63
+ }
64
+
65
+ function printPerFileDiff(diff, projectRuntimeDir, out = process.stdout) {
66
+ const writeLine = (line) => out.write(`${line}\n`);
67
+ for (const f of diff.replaced) {
68
+ const a = path.join(projectRuntimeDir, f);
69
+ const b = path.join(KIT_RUNTIME_DIR, f);
70
+ writeLine(`--- a/.ai/runtime/${f}`);
71
+ writeLine(`+++ b/.ai/runtime/${f} (kit)`);
72
+ const r = spawnSync('diff', ['-u', a, b], { encoding: 'utf8' });
73
+ if (r.stdout) {
74
+ const body = r.stdout.split('\n').slice(2).join('\n');
75
+ out.write(body);
76
+ }
77
+ writeLine('');
78
+ }
79
+ }
80
+
81
+ module.exports = { computeRuntimeDiff, printDiffSummary, printPerFileDiff };
package/src/git.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('node:child_process');
4
+
5
+ function gitStatusPorcelain(targetPath, cwd) {
6
+ const r = spawnSync('git', ['status', '--porcelain', '--', targetPath], {
7
+ cwd,
8
+ encoding: 'utf8',
9
+ });
10
+ if (r.status === 128) {
11
+ return { ok: false, error: 'not a git repository' };
12
+ }
13
+ if (r.status !== 0) {
14
+ return { ok: false, error: r.stderr.trim() || `git exited ${r.status}` };
15
+ }
16
+ const lines = r.stdout.split('\n').filter(Boolean);
17
+ return { ok: true, lines };
18
+ }
19
+
20
+ function isGitRepo(cwd) {
21
+ const r = spawnSync('git', ['rev-parse', '--git-dir'], { cwd, encoding: 'utf8' });
22
+ return r.status === 0;
23
+ }
24
+
25
+ // `git check-ignore` returns 0 if path is ignored, 1 if not, 128 if not a
26
+ // git repo. Null on any non-deterministic result so callers stay silent
27
+ // rather than misreporting.
28
+ function isPathGitignored(targetPath, cwd) {
29
+ const r = spawnSync('git', ['check-ignore', '-q', '--', targetPath], {
30
+ cwd,
31
+ encoding: 'utf8',
32
+ });
33
+ if (r.status === 0) return true;
34
+ if (r.status === 1) return false;
35
+ return null;
36
+ }
37
+
38
+ module.exports = { gitStatusPorcelain, isGitRepo, isPathGitignored };
package/src/init.js ADDED
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { parseArgs } = require('node:util');
6
+
7
+ const { copyKitRuntimeTo, dirHasFiles, removeDir } = require('./snapshot');
8
+ const { writeProjectKitVersion, KIT_VERSION } = require('./version');
9
+ const { projectStateMd, projectTaskStatusMd, agentEntryClaudeMd } = require('./templates');
10
+ const { isPathGitignored } = require('./git');
11
+
12
+ const PROJECT_SKELETON_DIRS = [
13
+ 'prds',
14
+ 'specs',
15
+ 'plans',
16
+ 'tasks',
17
+ 'reviews',
18
+ 'verifications',
19
+ 'adr',
20
+ 'contracts',
21
+ 'memory',
22
+ 'rules',
23
+ 'skills',
24
+ 'hooks',
25
+ ];
26
+
27
+ function run(argv) {
28
+ let parsed;
29
+ try {
30
+ parsed = parseArgs({
31
+ args: argv,
32
+ options: {
33
+ cwd: { type: 'string' },
34
+ migrate: { type: 'boolean', default: false },
35
+ 'no-agent-entry': { type: 'boolean', default: false },
36
+ help: { type: 'boolean', short: 'h', default: false },
37
+ },
38
+ strict: true,
39
+ allowPositionals: false,
40
+ });
41
+ } catch (e) {
42
+ console.error(`init: ${e.message}`);
43
+ console.error('Try: ai-runtime-kit init --help');
44
+ process.exit(1);
45
+ }
46
+
47
+ if (parsed.values.help) {
48
+ printHelp();
49
+ return;
50
+ }
51
+
52
+ const cwd = path.resolve(parsed.values.cwd ?? process.cwd());
53
+ const aiDir = path.join(cwd, '.ai');
54
+ const runtimeDir = path.join(aiDir, 'runtime');
55
+ const projectDir = path.join(aiDir, 'project');
56
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
57
+
58
+ // Detect "real" presence — empty-only directory tree counts as
59
+ // absent so `init --migrate` post `git rm` doesn't have to be
60
+ // chased with a manual `rm -rf` (kit v0.3.0 fix).
61
+ const runtimeHasFiles = dirHasFiles(runtimeDir);
62
+ const projectHasFiles = dirHasFiles(projectDir);
63
+ const runtimeEmptyButPresent = fs.existsSync(runtimeDir) && !runtimeHasFiles;
64
+ const projectEmptyButPresent = fs.existsSync(projectDir) && !projectHasFiles;
65
+ const claudeMdExists = fs.existsSync(claudeMdPath);
66
+ const writeAgentEntry = !parsed.values['no-agent-entry'];
67
+
68
+ if (!parsed.values.migrate) {
69
+ if (runtimeHasFiles || projectHasFiles) {
70
+ console.error('init: .ai/runtime/ or .ai/project/ already exists.');
71
+ console.error('If you intended to bootstrap an existing-.ai/project/ repo, pass --migrate.');
72
+ console.error('To upgrade an installed runtime, use: ai-runtime-kit upgrade');
73
+ process.exit(1);
74
+ }
75
+ if (writeAgentEntry && claudeMdExists) {
76
+ console.error('init: CLAUDE.md already exists at the project root.');
77
+ console.error('Pass --no-agent-entry to skip CLAUDE.md generation, or --migrate to keep the existing file.');
78
+ process.exit(1);
79
+ }
80
+ } else {
81
+ if (runtimeHasFiles) {
82
+ console.error('init --migrate: .ai/runtime/ already has content; refusing to overwrite.');
83
+ console.error('Move it aside or use: ai-runtime-kit upgrade');
84
+ process.exit(1);
85
+ }
86
+ // In --migrate mode .ai/project/ MAY already exist with content;
87
+ // we only lay down .ai/runtime/ and skip any pre-existing
88
+ // project skeleton files.
89
+ }
90
+
91
+ // Empty parent dirs (e.g. left by `git rm -r .ai/runtime/`) are
92
+ // removed here so copyKitRuntimeTo can re-create the tree cleanly.
93
+ if (runtimeEmptyButPresent) {
94
+ removeDir(runtimeDir);
95
+ }
96
+ // For --migrate (or fresh init), only remove an empty .ai/project/
97
+ // if it has no content — otherwise it stays untouched.
98
+ if (!parsed.values.migrate && projectEmptyButPresent) {
99
+ removeDir(projectDir);
100
+ }
101
+
102
+ fs.mkdirSync(aiDir, { recursive: true });
103
+ copyKitRuntimeTo(runtimeDir);
104
+ writeProjectKitVersion(cwd, KIT_VERSION);
105
+
106
+ if (!projectHasFiles) {
107
+ fs.mkdirSync(projectDir, { recursive: true });
108
+ for (const d of PROJECT_SKELETON_DIRS) {
109
+ fs.mkdirSync(path.join(projectDir, d), { recursive: true });
110
+ }
111
+ fs.writeFileSync(path.join(projectDir, 'STATE.md'), projectStateMd());
112
+ fs.writeFileSync(path.join(projectDir, 'tasks', 'TASK_STATUS.md'), projectTaskStatusMd());
113
+ }
114
+
115
+ let agentEntryWritten = false;
116
+ if (writeAgentEntry && !claudeMdExists) {
117
+ fs.writeFileSync(claudeMdPath, agentEntryClaudeMd());
118
+ agentEntryWritten = true;
119
+ }
120
+
121
+ console.log(`ai-runtime-kit ${KIT_VERSION}: initialized .ai/ at ${cwd}`);
122
+ console.log(' - .ai/runtime/ (kit-managed; do not hand-edit)');
123
+ if (!projectHasFiles) {
124
+ console.log(' - .ai/project/ (project-owned; STATE.md and tasks/TASK_STATUS.md scaffolded)');
125
+ } else {
126
+ console.log(' - .ai/project/ (pre-existing; not modified)');
127
+ }
128
+ console.log(` - .ai/runtime/KIT_VERSION = ${KIT_VERSION}`);
129
+ if (agentEntryWritten) {
130
+ console.log(' - CLAUDE.md (agent entry; project-owned, never touched by upgrade)');
131
+ } else if (claudeMdExists) {
132
+ console.log(' - CLAUDE.md (pre-existing; not modified)');
133
+ } else {
134
+ console.log(' - CLAUDE.md (skipped via --no-agent-entry)');
135
+ }
136
+
137
+ if (isPathGitignored('.ai/runtime', cwd) === true) {
138
+ console.log('');
139
+ console.log('Note: .ai/runtime/ is gitignored. Clones of this repo will not');
140
+ console.log(' contain the runtime tree; they will need to run');
141
+ console.log(' `ai-runtime-kit init` or `upgrade` to regenerate it.');
142
+ }
143
+ }
144
+
145
+ function printHelp() {
146
+ console.log(`ai-runtime-kit init [options]
147
+
148
+ Lay down .ai/runtime/, a .ai/project/ skeleton (if absent), and a
149
+ project-root CLAUDE.md (if absent) in the current directory.
150
+
151
+ Options:
152
+ --cwd <dir> Target directory (default: process.cwd())
153
+ --migrate Allow pre-existing .ai/project/ and/or
154
+ CLAUDE.md (for bootstrapping an existing repo
155
+ into kit-consumer mode). Still refuses if
156
+ .ai/runtime/ already exists.
157
+ --no-agent-entry Skip CLAUDE.md generation entirely.
158
+ -h, --help Show this help.
159
+
160
+ Refuses (without --migrate) if .ai/runtime/, .ai/project/, or
161
+ CLAUDE.md already exists. To upgrade an installed runtime, use the
162
+ upgrade command instead. CLAUDE.md is project-owned and never
163
+ modified by upgrade.`);
164
+ }
165
+
166
+ module.exports = { run, printHelp };
package/src/prompt.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const readline = require('node:readline/promises');
4
+
5
+ async function confirm(question, { defaultYes = false } = {}) {
6
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
+ try {
8
+ const suffix = defaultYes ? '(Y/n)' : '(y/N)';
9
+ const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
10
+ if (!answer) return defaultYes;
11
+ return answer === 'y' || answer === 'yes';
12
+ } finally {
13
+ rl.close();
14
+ }
15
+ }
16
+
17
+ module.exports = { confirm };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const KIT_RUNTIME_DIR = path.resolve(__dirname, '..', 'runtime');
7
+
8
+ function copyKitRuntimeTo(targetRuntimeDir) {
9
+ if (fs.existsSync(targetRuntimeDir)) {
10
+ throw new Error(`copyKitRuntimeTo: target already exists: ${targetRuntimeDir}`);
11
+ }
12
+ fs.cpSync(KIT_RUNTIME_DIR, targetRuntimeDir, { recursive: true });
13
+ }
14
+
15
+ function listKitRuntimeFiles() {
16
+ const out = [];
17
+ walk(KIT_RUNTIME_DIR, '');
18
+ function walk(absDir, relDir) {
19
+ const entries = fs.readdirSync(absDir, { withFileTypes: true });
20
+ for (const entry of entries) {
21
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
22
+ if (entry.isDirectory()) {
23
+ walk(path.join(absDir, entry.name), rel);
24
+ } else {
25
+ out.push(rel);
26
+ }
27
+ }
28
+ }
29
+ return out.sort();
30
+ }
31
+
32
+ function listFilesUnder(absDir) {
33
+ if (!fs.existsSync(absDir)) return [];
34
+ const out = [];
35
+ walk(absDir, '');
36
+ function walk(dir, relDir) {
37
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
38
+ for (const entry of entries) {
39
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
40
+ if (entry.isDirectory()) {
41
+ walk(path.join(dir, entry.name), rel);
42
+ } else {
43
+ out.push(rel);
44
+ }
45
+ }
46
+ }
47
+ return out.sort();
48
+ }
49
+
50
+ function removeDir(absDir) {
51
+ if (fs.existsSync(absDir)) {
52
+ fs.rmSync(absDir, { recursive: true, force: true });
53
+ }
54
+ }
55
+
56
+ // True iff absDir exists AND contains at least one regular file
57
+ // anywhere in its subtree. Empty parent directories (e.g. left over
58
+ // from `git rm -r` which doesn't remove the parent dirs) return
59
+ // false — they're not real content.
60
+ function dirHasFiles(absDir) {
61
+ if (!fs.existsSync(absDir)) return false;
62
+ const stack = [absDir];
63
+ while (stack.length) {
64
+ const current = stack.pop();
65
+ const entries = fs.readdirSync(current, { withFileTypes: true });
66
+ for (const entry of entries) {
67
+ if (entry.isDirectory()) {
68
+ stack.push(path.join(current, entry.name));
69
+ } else {
70
+ return true;
71
+ }
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+
77
+ module.exports = {
78
+ KIT_RUNTIME_DIR,
79
+ copyKitRuntimeTo,
80
+ listKitRuntimeFiles,
81
+ listFilesUnder,
82
+ removeDir,
83
+ dirHasFiles,
84
+ };