@stackbilt/cli 0.9.2 → 0.11.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/LICENSE +17 -1
- package/README.md +57 -0
- package/dist/__tests__/auth-wiring.test.d.ts +2 -0
- package/dist/__tests__/auth-wiring.test.d.ts.map +1 -0
- package/dist/__tests__/auth-wiring.test.js +158 -0
- package/dist/__tests__/auth-wiring.test.js.map +1 -0
- package/dist/__tests__/bootstrap.test.d.ts +2 -0
- package/dist/__tests__/bootstrap.test.d.ts.map +1 -0
- package/dist/__tests__/bootstrap.test.js +134 -0
- package/dist/__tests__/bootstrap.test.js.map +1 -0
- package/dist/__tests__/credentials.test.d.ts +2 -0
- package/dist/__tests__/credentials.test.d.ts.map +1 -0
- package/dist/__tests__/credentials.test.js +145 -0
- package/dist/__tests__/credentials.test.js.map +1 -0
- package/dist/__tests__/integration/precommit-hook.test.js +4 -4
- package/dist/__tests__/login.test.d.ts +2 -0
- package/dist/__tests__/login.test.d.ts.map +1 -0
- package/dist/__tests__/login.test.js +43 -0
- package/dist/__tests__/login.test.js.map +1 -0
- package/dist/__tests__/named-scaffolds.test.d.ts +2 -0
- package/dist/__tests__/named-scaffolds.test.d.ts.map +1 -0
- package/dist/__tests__/named-scaffolds.test.js +58 -0
- package/dist/__tests__/named-scaffolds.test.js.map +1 -0
- package/dist/__tests__/score.test.d.ts +2 -0
- package/dist/__tests__/score.test.d.ts.map +1 -0
- package/dist/__tests__/score.test.js +234 -0
- package/dist/__tests__/score.test.js.map +1 -0
- package/dist/commands/adf-named-scaffolds.d.ts +45 -0
- package/dist/commands/adf-named-scaffolds.d.ts.map +1 -0
- package/dist/commands/adf-named-scaffolds.js +114 -0
- package/dist/commands/adf-named-scaffolds.js.map +1 -0
- package/dist/commands/adf-tidy.d.ts.map +1 -1
- package/dist/commands/adf-tidy.js +6 -3
- package/dist/commands/adf-tidy.js.map +1 -1
- package/dist/commands/adf.d.ts +1 -0
- package/dist/commands/adf.d.ts.map +1 -1
- package/dist/commands/adf.js +39 -15
- package/dist/commands/adf.js.map +1 -1
- package/dist/commands/architect.d.ts.map +1 -1
- package/dist/commands/architect.js +7 -3
- package/dist/commands/architect.js.map +1 -1
- package/dist/commands/audit.js +24 -7
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/blast.d.ts +13 -0
- package/dist/commands/blast.d.ts.map +1 -0
- package/dist/commands/blast.js +226 -0
- package/dist/commands/blast.js.map +1 -0
- package/dist/commands/bootstrap.d.ts.map +1 -1
- package/dist/commands/bootstrap.js +300 -101
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +9 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +127 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +4 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +27 -6
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +8 -5
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/score.d.ts +9 -0
- package/dist/commands/score.d.ts.map +1 -0
- package/dist/commands/score.js +1273 -0
- package/dist/commands/score.js.map +1 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +44 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/setup.js +2 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/surface.d.ts +15 -0
- package/dist/commands/surface.d.ts.map +1 -0
- package/dist/commands/surface.js +112 -0
- package/dist/commands/surface.js.map +1 -0
- package/dist/commands/validate-ontology.d.ts +14 -0
- package/dist/commands/validate-ontology.d.ts.map +1 -0
- package/dist/commands/validate-ontology.js +328 -0
- package/dist/commands/validate-ontology.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +44 -0
- package/dist/commands/validate.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +22 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +35 -1
- package/dist/credentials.js.map +1 -1
- package/dist/http-client.d.ts +13 -4
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +3 -2
- package/dist/http-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/types/scaffold-contract-types.d.ts +90 -0
- package/dist/types/scaffold-contract-types.d.ts.map +1 -0
- package/dist/types/scaffold-contract-types.js +22 -0
- package/dist/types/scaffold-contract-types.js.map +1 -0
- package/package.json +12 -9
|
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
exports.bootstrapCommand = bootstrapCommand;
|
|
43
43
|
const fs = __importStar(require("node:fs"));
|
|
44
44
|
const path = __importStar(require("node:path"));
|
|
45
|
+
const readline = __importStar(require("node:readline"));
|
|
45
46
|
const crypto = __importStar(require("node:crypto"));
|
|
46
47
|
const node_child_process_1 = require("node:child_process");
|
|
47
48
|
const index_1 = require("../index");
|
|
@@ -62,7 +63,9 @@ async function bootstrapCommand(options, args) {
|
|
|
62
63
|
const presetFlag = (0, flags_1.getFlag)(args, '--preset');
|
|
63
64
|
const skipInstall = args.includes('--skip-install');
|
|
64
65
|
const skipDoctor = args.includes('--skip-doctor');
|
|
65
|
-
const force =
|
|
66
|
+
const force = args.includes('--force');
|
|
67
|
+
const nonInteractive = options.yes;
|
|
68
|
+
const setupOverwrite = options.yes || force;
|
|
66
69
|
if (ciTarget && ciTarget !== 'github') {
|
|
67
70
|
throw new index_1.CLIError(`Unsupported CI target: ${ciTarget}. Supported: github`);
|
|
68
71
|
}
|
|
@@ -80,14 +83,13 @@ async function bootstrapCommand(options, args) {
|
|
|
80
83
|
// ========================================================================
|
|
81
84
|
const detectResult = runDetectPhase(options, presetFlag);
|
|
82
85
|
result.steps.push(detectResult.step);
|
|
83
|
-
|
|
84
|
-
warnings++;
|
|
86
|
+
warnings += detectResult.step.warnings.length;
|
|
85
87
|
const selectedPreset = detectResult.selectedPreset;
|
|
86
88
|
const detection = detectResult.detection;
|
|
87
89
|
const contexts = detectResult.contexts;
|
|
88
90
|
const packageManager = detectResult.packageManager;
|
|
89
91
|
if (options.format === 'text') {
|
|
90
|
-
console.log('[1/
|
|
92
|
+
console.log('[1/7] Detecting stack...');
|
|
91
93
|
console.log(` Stack: ${selectedPreset} (${detection.confidence} confidence)`);
|
|
92
94
|
console.log(` Monorepo: ${detection.monorepo ? 'yes' : 'no'}${detection.monorepo && detection.signals.hasPnpm ? ' (pnpm workspace)' : ''}`);
|
|
93
95
|
if (detection.warnings.length > 0) {
|
|
@@ -100,12 +102,11 @@ async function bootstrapCommand(options, args) {
|
|
|
100
102
|
// ========================================================================
|
|
101
103
|
// Phase 2: Setup
|
|
102
104
|
// ========================================================================
|
|
103
|
-
const setupResult = runSetupPhase(options, selectedPreset, detection, contexts, ciTarget, packageManager,
|
|
105
|
+
const setupResult = runSetupPhase(options, selectedPreset, detection, contexts, ciTarget, packageManager, setupOverwrite);
|
|
104
106
|
result.steps.push(setupResult.step);
|
|
105
|
-
|
|
106
|
-
warnings++;
|
|
107
|
+
warnings += setupResult.step.warnings.length;
|
|
107
108
|
if (options.format === 'text') {
|
|
108
|
-
console.log('[2/
|
|
109
|
+
console.log('[2/7] Setting up governance...');
|
|
109
110
|
for (const f of (setupResult.step.details.created || [])) {
|
|
110
111
|
console.log(` Created ${f}`);
|
|
111
112
|
}
|
|
@@ -119,27 +120,51 @@ async function bootstrapCommand(options, args) {
|
|
|
119
120
|
// ========================================================================
|
|
120
121
|
const adfResult = runAdfInitPhase(options, force, selectedPreset);
|
|
121
122
|
result.steps.push(adfResult.step);
|
|
122
|
-
|
|
123
|
-
warnings++;
|
|
123
|
+
warnings += adfResult.step.warnings.length;
|
|
124
124
|
if (options.format === 'text') {
|
|
125
|
-
console.log('[3/
|
|
125
|
+
console.log('[3/7] Initializing ADF context...');
|
|
126
126
|
for (const f of (adfResult.step.details.files || [])) {
|
|
127
127
|
console.log(` Created ${f}`);
|
|
128
128
|
}
|
|
129
129
|
for (const f of (adfResult.step.details.pointers || [])) {
|
|
130
130
|
console.log(` Generated ${f}`);
|
|
131
131
|
}
|
|
132
|
+
const backedUp = adfResult.step.details.backedUp;
|
|
133
|
+
if (backedUp && backedUp > 0) {
|
|
134
|
+
console.log(` Backed up ${backedUp} files to .ai/.backup/`);
|
|
135
|
+
}
|
|
136
|
+
for (const warning of adfResult.step.warnings) {
|
|
137
|
+
console.log(` Warning: ${warning}`);
|
|
138
|
+
}
|
|
132
139
|
console.log('');
|
|
133
140
|
}
|
|
141
|
+
// Orphan registration: auto-register in --yes mode, prompt interactively otherwise
|
|
142
|
+
const orphans = adfResult.step.details.orphans || [];
|
|
143
|
+
if (orphans.length > 0) {
|
|
144
|
+
let shouldRegister = false;
|
|
145
|
+
if (nonInteractive) {
|
|
146
|
+
shouldRegister = true;
|
|
147
|
+
}
|
|
148
|
+
else if (options.format === 'text') {
|
|
149
|
+
shouldRegister = await promptYesNo(' Register these modules now? (y/N) ');
|
|
150
|
+
}
|
|
151
|
+
if (shouldRegister) {
|
|
152
|
+
registerOrphansInManifest(path.join('.ai', 'manifest.adf'), orphans);
|
|
153
|
+
(0, adf_migrate_1.updateModuleIndex)('CLAUDE.md', '.ai');
|
|
154
|
+
if (options.format === 'text') {
|
|
155
|
+
console.log(` Registered ${orphans.length} module(s) as ON_DEMAND in manifest.adf`);
|
|
156
|
+
console.log('');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
134
160
|
// ========================================================================
|
|
135
161
|
// Phase 4: Migrate Agent Configs
|
|
136
162
|
// ========================================================================
|
|
137
|
-
const migrateResult = runMigratePhase(options,
|
|
163
|
+
const migrateResult = runMigratePhase(options, nonInteractive);
|
|
138
164
|
result.steps.push(migrateResult.step);
|
|
139
|
-
|
|
140
|
-
warnings++;
|
|
165
|
+
warnings += migrateResult.step.warnings.length;
|
|
141
166
|
if (options.format === 'text') {
|
|
142
|
-
console.log('[4/
|
|
167
|
+
console.log('[4/7] Migrating agent configs...');
|
|
143
168
|
if (migrateResult.step.status === 'skip') {
|
|
144
169
|
console.log(' Skipped (no migratable files)');
|
|
145
170
|
}
|
|
@@ -159,10 +184,9 @@ async function bootstrapCommand(options, args) {
|
|
|
159
184
|
// ========================================================================
|
|
160
185
|
const installResult = runInstallPhase(options, skipInstall);
|
|
161
186
|
result.steps.push(installResult.step);
|
|
162
|
-
|
|
163
|
-
warnings++;
|
|
187
|
+
warnings += installResult.step.warnings.length;
|
|
164
188
|
if (options.format === 'text') {
|
|
165
|
-
console.log('[5/
|
|
189
|
+
console.log('[5/7] Installing dependencies...');
|
|
166
190
|
if (skipInstall) {
|
|
167
191
|
console.log(' Skipped (--skip-install)');
|
|
168
192
|
}
|
|
@@ -185,14 +209,31 @@ async function bootstrapCommand(options, args) {
|
|
|
185
209
|
console.log('');
|
|
186
210
|
}
|
|
187
211
|
// ========================================================================
|
|
188
|
-
// Phase 6:
|
|
212
|
+
// Phase 6: Populate (#89)
|
|
213
|
+
// ========================================================================
|
|
214
|
+
const populateResult = await runPopulatePhase(options);
|
|
215
|
+
result.steps.push(populateResult.step);
|
|
216
|
+
warnings += populateResult.step.warnings.length;
|
|
217
|
+
if (options.format === 'text') {
|
|
218
|
+
console.log('[6/7] Auto-populating ADF modules...');
|
|
219
|
+
const populated = populateResult.step.details.populated;
|
|
220
|
+
const skipped = populateResult.step.details.skipped;
|
|
221
|
+
if (populated > 0) {
|
|
222
|
+
console.log(` Populated ${populated} module(s), skipped ${skipped} (already customized)`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log(' No scaffold content to replace');
|
|
226
|
+
}
|
|
227
|
+
console.log('');
|
|
228
|
+
}
|
|
229
|
+
// ========================================================================
|
|
230
|
+
// Phase 7: Doctor
|
|
189
231
|
// ========================================================================
|
|
190
232
|
const doctorResult = runDoctorPhase(options, skipDoctor);
|
|
191
233
|
result.steps.push(doctorResult.step);
|
|
192
|
-
|
|
193
|
-
warnings++;
|
|
234
|
+
warnings += doctorResult.step.warnings.length;
|
|
194
235
|
if (options.format === 'text') {
|
|
195
|
-
console.log('[
|
|
236
|
+
console.log('[7/7] Running health check...');
|
|
196
237
|
if (skipDoctor) {
|
|
197
238
|
console.log(' Skipped (--skip-doctor)');
|
|
198
239
|
}
|
|
@@ -212,25 +253,80 @@ async function bootstrapCommand(options, args) {
|
|
|
212
253
|
result.status = failCount === 0 ? 'success' : failCount < result.steps.length ? 'partial' : 'failure';
|
|
213
254
|
// Build next steps
|
|
214
255
|
result.nextSteps.push({
|
|
215
|
-
cmd: '
|
|
256
|
+
cmd: 'charter serve # start MCP server for Claude Code / Cursor integration',
|
|
216
257
|
required: false,
|
|
217
|
-
reason: '
|
|
258
|
+
reason: 'Enable real-time governance via MCP (add to .claude/settings.json)',
|
|
218
259
|
});
|
|
219
260
|
result.nextSteps.push({
|
|
220
|
-
cmd: 'charter
|
|
261
|
+
cmd: 'Review .charter/patterns/ and customize for your stack',
|
|
221
262
|
required: false,
|
|
222
|
-
reason: '
|
|
263
|
+
reason: 'Customize blessed stack patterns',
|
|
223
264
|
});
|
|
224
265
|
result.nextSteps.push({
|
|
225
266
|
cmd: 'git add .charter .ai CLAUDE.md .cursorrules agents.md && git commit -m "chore: bootstrap charter governance"',
|
|
226
267
|
required: false,
|
|
227
268
|
reason: 'Commit governance baseline',
|
|
228
269
|
});
|
|
270
|
+
// ========================================================================
|
|
271
|
+
// Governance Gaps — surface what's configured but not enforced
|
|
272
|
+
// ========================================================================
|
|
273
|
+
const gaps = [];
|
|
274
|
+
// Check: trailers enabled but no commit-msg hook
|
|
275
|
+
if (fs.existsSync('.charter/config.json')) {
|
|
276
|
+
try {
|
|
277
|
+
const cfg = JSON.parse(fs.readFileSync('.charter/config.json', 'utf-8'));
|
|
278
|
+
if (cfg.git?.requireTrailers) {
|
|
279
|
+
const hookPath = path.resolve('.githooks/commit-msg');
|
|
280
|
+
const gitHookPath = path.resolve('.git/hooks/commit-msg');
|
|
281
|
+
if (!fs.existsSync(hookPath) && !fs.existsSync(gitHookPath)) {
|
|
282
|
+
gaps.push({
|
|
283
|
+
gap: 'requireTrailers enabled but no commit-msg hook installed',
|
|
284
|
+
fix: 'charter hook install --commit-msg',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (cfg.drift?.enabled && !ciTarget) {
|
|
289
|
+
const workflowPath = path.resolve('.github/workflows/charter.yml');
|
|
290
|
+
if (!fs.existsSync(workflowPath)) {
|
|
291
|
+
gaps.push({
|
|
292
|
+
gap: 'drift detection enabled but no CI workflow',
|
|
293
|
+
fix: 'charter bootstrap --ci github (or add manually)',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch { /* config not parseable — doctor already caught it */ }
|
|
299
|
+
}
|
|
300
|
+
// Check: no SECURITY.md
|
|
301
|
+
if (!fs.existsSync('SECURITY.md')) {
|
|
302
|
+
gaps.push({
|
|
303
|
+
gap: 'no SECURITY.md for responsible disclosure',
|
|
304
|
+
fix: 'add SECURITY.md with reporting contact and supported versions',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// Check: no pre-commit hook for ADF evidence
|
|
308
|
+
const preCommitHook = path.resolve('.githooks/pre-commit');
|
|
309
|
+
const gitPreCommit = path.resolve('.git/hooks/pre-commit');
|
|
310
|
+
if (!fs.existsSync(preCommitHook) && !fs.existsSync(gitPreCommit)) {
|
|
311
|
+
gaps.push({
|
|
312
|
+
gap: 'no pre-commit hook for ADF evidence gate',
|
|
313
|
+
fix: 'charter hook install --pre-commit',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
229
316
|
if (options.format === 'json') {
|
|
317
|
+
result.governanceGaps = gaps;
|
|
230
318
|
console.log(JSON.stringify(result, null, 2));
|
|
231
319
|
}
|
|
232
320
|
else {
|
|
233
321
|
console.log(`Bootstrap complete. ${warnings} warning${warnings === 1 ? '' : 's'}.`);
|
|
322
|
+
if (gaps.length > 0) {
|
|
323
|
+
console.log('');
|
|
324
|
+
console.log('Governance gaps (configured but not enforced):');
|
|
325
|
+
for (const { gap, fix } of gaps) {
|
|
326
|
+
console.log(` ⚠ ${gap}`);
|
|
327
|
+
console.log(` → ${fix}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
234
330
|
console.log('');
|
|
235
331
|
console.log('Next steps:');
|
|
236
332
|
result.nextSteps.forEach((step, i) => {
|
|
@@ -384,74 +480,109 @@ function runSetupPhase(options, selectedPreset, detection, contexts, ciTarget, p
|
|
|
384
480
|
// ============================================================================
|
|
385
481
|
// Phase 3: ADF Init
|
|
386
482
|
// ============================================================================
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
483
|
+
function getAdfScaffolds(preset) {
|
|
484
|
+
const scaffolds = [
|
|
485
|
+
{ name: 'manifest.adf', content: (0, adf_2.manifestForPreset)(preset) },
|
|
486
|
+
{ name: 'core.adf', content: adf_2.CORE_SCAFFOLD },
|
|
487
|
+
{ name: 'state.adf', content: adf_2.STATE_SCAFFOLD },
|
|
488
|
+
];
|
|
393
489
|
if (preset === 'docs') {
|
|
394
|
-
|
|
395
|
-
fs.writeFileSync(path.join(aiDir, 'decisions.adf'), adf_2.DECISIONS_SCAFFOLD);
|
|
396
|
-
fs.writeFileSync(path.join(aiDir, 'planning.adf'), adf_2.PLANNING_SCAFFOLD);
|
|
397
|
-
files.push('.ai/content.adf', '.ai/decisions.adf', '.ai/planning.adf');
|
|
490
|
+
scaffolds.push({ name: 'content.adf', content: adf_2.CONTENT_SCAFFOLD }, { name: 'decisions.adf', content: adf_2.DECISIONS_SCAFFOLD }, { name: 'planning.adf', content: adf_2.PLANNING_SCAFFOLD });
|
|
398
491
|
}
|
|
399
492
|
else if (preset === 'frontend') {
|
|
400
|
-
|
|
401
|
-
files.push('.ai/frontend.adf');
|
|
493
|
+
scaffolds.push({ name: 'frontend.adf', content: adf_2.FRONTEND_SCAFFOLD });
|
|
402
494
|
}
|
|
403
495
|
else if (preset === 'backend' || preset === 'worker') {
|
|
404
|
-
|
|
405
|
-
files.push('.ai/backend.adf');
|
|
496
|
+
scaffolds.push({ name: 'backend.adf', content: adf_2.BACKEND_SCAFFOLD });
|
|
406
497
|
}
|
|
407
498
|
else {
|
|
408
|
-
|
|
409
|
-
fs.writeFileSync(path.join(aiDir, 'frontend.adf'), adf_2.FRONTEND_SCAFFOLD);
|
|
410
|
-
fs.writeFileSync(path.join(aiDir, 'backend.adf'), adf_2.BACKEND_SCAFFOLD);
|
|
411
|
-
files.push('.ai/frontend.adf', '.ai/backend.adf');
|
|
499
|
+
scaffolds.push({ name: 'frontend.adf', content: adf_2.FRONTEND_SCAFFOLD }, { name: 'backend.adf', content: adf_2.BACKEND_SCAFFOLD });
|
|
412
500
|
}
|
|
413
|
-
return
|
|
501
|
+
return scaffolds;
|
|
502
|
+
}
|
|
503
|
+
function buildAdfLockContent(aiDir) {
|
|
504
|
+
const lockData = {};
|
|
505
|
+
for (const mod of ['core.adf', 'state.adf']) {
|
|
506
|
+
const modPath = path.join(aiDir, mod);
|
|
507
|
+
if (!fs.existsSync(modPath))
|
|
508
|
+
continue;
|
|
509
|
+
lockData[mod] = hashContent(fs.readFileSync(modPath, 'utf-8'));
|
|
510
|
+
}
|
|
511
|
+
return JSON.stringify(lockData, null, 2) + '\n';
|
|
512
|
+
}
|
|
513
|
+
function writeAdfScaffolds(aiDir, force, preset) {
|
|
514
|
+
fs.mkdirSync(aiDir, { recursive: true });
|
|
515
|
+
const files = [];
|
|
516
|
+
const warnings = [];
|
|
517
|
+
let backedUp = 0;
|
|
518
|
+
let backupDir;
|
|
519
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
520
|
+
for (const scaffold of getAdfScaffolds(preset)) {
|
|
521
|
+
const targetPath = path.join(aiDir, scaffold.name);
|
|
522
|
+
const label = `.ai/${scaffold.name}`;
|
|
523
|
+
if (!fs.existsSync(targetPath)) {
|
|
524
|
+
fs.writeFileSync(targetPath, scaffold.content);
|
|
525
|
+
files.push(label);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
529
|
+
if (existing.trim() === scaffold.content.trim()) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const byteCount = Buffer.byteLength(existing, 'utf-8');
|
|
533
|
+
if (!force) {
|
|
534
|
+
warnings.push(`${label} has custom content (${byteCount} bytes); skipping scaffold overwrite`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
backupDir ||= path.join(aiDir, '.backup');
|
|
538
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
539
|
+
const backupName = `${scaffold.name}.${timestamp}`;
|
|
540
|
+
fs.copyFileSync(targetPath, path.join(backupDir, backupName));
|
|
541
|
+
warnings.push(`Backed up ${label} (${byteCount} bytes) → .ai/.backup/${backupName}`);
|
|
542
|
+
backedUp++;
|
|
543
|
+
fs.writeFileSync(targetPath, scaffold.content);
|
|
544
|
+
files.push(label);
|
|
545
|
+
}
|
|
546
|
+
const lockPath = path.join(aiDir, '.adf.lock');
|
|
547
|
+
const lockContent = buildAdfLockContent(aiDir);
|
|
548
|
+
if (!fs.existsSync(lockPath) || fs.readFileSync(lockPath, 'utf-8') !== lockContent) {
|
|
549
|
+
fs.writeFileSync(lockPath, lockContent);
|
|
550
|
+
files.push('.ai/.adf.lock');
|
|
551
|
+
}
|
|
552
|
+
return { files, warnings, backedUp };
|
|
414
553
|
}
|
|
415
554
|
function runAdfInitPhase(options, force, preset) {
|
|
416
555
|
const warnings = [];
|
|
417
556
|
const files = [];
|
|
418
557
|
const pointers = [];
|
|
558
|
+
const detectedOrphans = [];
|
|
419
559
|
try {
|
|
420
560
|
const aiDir = '.ai';
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
561
|
+
const scaffoldResult = writeAdfScaffolds(aiDir, force, preset);
|
|
562
|
+
files.push(...scaffoldResult.files);
|
|
563
|
+
warnings.push(...scaffoldResult.warnings);
|
|
564
|
+
// Detect orphaned ADF modules not registered in manifest (#65)
|
|
565
|
+
const manifestPath2 = path.join(aiDir, 'manifest.adf');
|
|
566
|
+
if (fs.existsSync(manifestPath2)) {
|
|
567
|
+
try {
|
|
568
|
+
const manifestContent = fs.readFileSync(manifestPath2, 'utf-8');
|
|
569
|
+
const allAdfFiles = fs.readdirSync(aiDir).filter(f => f.endsWith('.adf') && f !== 'manifest.adf');
|
|
570
|
+
const doc = (0, adf_3.parseAdf)(manifestContent);
|
|
571
|
+
const manifest = (0, adf_3.parseManifest)(doc);
|
|
572
|
+
const registeredModules = new Set([
|
|
573
|
+
...manifest.defaultLoad,
|
|
574
|
+
...manifest.onDemand.map(m => m.path),
|
|
575
|
+
]);
|
|
576
|
+
const orphans = allAdfFiles.filter(f => !registeredModules.has(f));
|
|
577
|
+
if (orphans.length > 0) {
|
|
578
|
+
detectedOrphans.push(...orphans);
|
|
579
|
+
warnings.push(`Found ${orphans.length} unregistered .adf module(s): ${orphans.join(', ')}`);
|
|
580
|
+
warnings.push('Run `charter adf register` to add them to the manifest.');
|
|
581
|
+
}
|
|
433
582
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
else if (hasCustomContent && !force) {
|
|
438
|
-
// Custom ADF content exists — don't overwrite, suggest migrate
|
|
439
|
-
warnings.push('.ai/ contains custom ADF content; skipping scaffold overwrite');
|
|
440
|
-
warnings.push("Run 'charter adf migrate' to consolidate agent configs into ADF");
|
|
441
|
-
}
|
|
442
|
-
else if (force) {
|
|
443
|
-
// Force overwrite (preset-aware on-demand modules)
|
|
444
|
-
files.push(...writeAdfScaffolds(aiDir, preset));
|
|
445
|
-
const lockData = {};
|
|
446
|
-
for (const mod of ['core.adf', 'state.adf']) {
|
|
447
|
-
const content = fs.readFileSync(path.join(aiDir, mod), 'utf-8');
|
|
448
|
-
lockData[mod] = hashContent(content);
|
|
583
|
+
catch {
|
|
584
|
+
// Non-critical — manifest parse failure shouldn't block bootstrap
|
|
449
585
|
}
|
|
450
|
-
fs.writeFileSync(path.join(aiDir, '.adf.lock'), JSON.stringify(lockData, null, 2) + '\n');
|
|
451
|
-
files.push('.ai/.adf.lock');
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
warnings.push('.ai/ already exists; skipping scaffold (use --yes to overwrite)');
|
|
455
586
|
}
|
|
456
587
|
// Generate pointer files (CLAUDE.md uses hybrid template with module index)
|
|
457
588
|
const pointerFiles = [
|
|
@@ -468,16 +599,16 @@ function runAdfInitPhase(options, force, preset) {
|
|
|
468
599
|
fs.writeFileSync(pointerPath, pf.content);
|
|
469
600
|
pointers.push(pf.label);
|
|
470
601
|
}
|
|
471
|
-
else if (exists && !isAlreadyThinPointer(pointerPath)) {
|
|
472
|
-
// File has custom content — don't overwrite, suggest migrate
|
|
473
|
-
warnings.push(`${pf.name} has custom content; skipping pointer (use 'charter adf migrate' first)`);
|
|
474
|
-
}
|
|
475
602
|
else if (force) {
|
|
476
603
|
fs.writeFileSync(pointerPath, pf.content);
|
|
477
604
|
pointers.push(pf.label);
|
|
478
605
|
}
|
|
606
|
+
else if (!isAlreadyThinPointer(pointerPath)) {
|
|
607
|
+
// File has custom content — don't overwrite, suggest migrate
|
|
608
|
+
warnings.push(`${pf.name} has custom content; skipping pointer (run 'charter adf migrate' first or use --force to overwrite)`);
|
|
609
|
+
}
|
|
479
610
|
else {
|
|
480
|
-
warnings.push(`${pf.name} already exists; skipping (use --
|
|
611
|
+
warnings.push(`${pf.name} already exists; skipping (use --force to overwrite)`);
|
|
481
612
|
}
|
|
482
613
|
}
|
|
483
614
|
// Populate module index in CLAUDE.md from manifest
|
|
@@ -486,7 +617,7 @@ function runAdfInitPhase(options, force, preset) {
|
|
|
486
617
|
step: {
|
|
487
618
|
name: 'adf-init',
|
|
488
619
|
status: 'pass',
|
|
489
|
-
details: { files, pointers },
|
|
620
|
+
details: { files, pointers, backedUp: scaffoldResult.backedUp, orphans: detectedOrphans },
|
|
490
621
|
warnings,
|
|
491
622
|
},
|
|
492
623
|
};
|
|
@@ -498,7 +629,7 @@ function runAdfInitPhase(options, force, preset) {
|
|
|
498
629
|
step: {
|
|
499
630
|
name: 'adf-init',
|
|
500
631
|
status: 'fail',
|
|
501
|
-
details: { files, pointers, error: msg },
|
|
632
|
+
details: { files, pointers, orphans: detectedOrphans, error: msg },
|
|
502
633
|
warnings,
|
|
503
634
|
},
|
|
504
635
|
};
|
|
@@ -643,7 +774,48 @@ function detectPackageManagerFromLockfiles() {
|
|
|
643
774
|
return 'npm';
|
|
644
775
|
}
|
|
645
776
|
// ============================================================================
|
|
646
|
-
// Phase 6:
|
|
777
|
+
// Phase 6: Populate (#89)
|
|
778
|
+
// ============================================================================
|
|
779
|
+
async function runPopulatePhase(options) {
|
|
780
|
+
const warnings = [];
|
|
781
|
+
const aiDir = '.ai';
|
|
782
|
+
if (!fs.existsSync(path.join(aiDir, 'manifest.adf'))) {
|
|
783
|
+
return {
|
|
784
|
+
step: {
|
|
785
|
+
name: 'populate',
|
|
786
|
+
status: 'skip',
|
|
787
|
+
details: { populated: 0, skipped: 0, reason: 'no manifest.adf' },
|
|
788
|
+
warnings,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const { adfPopulateCommand } = require('./adf-populate');
|
|
794
|
+
const code = await adfPopulateCommand(options, ['--force']);
|
|
795
|
+
return {
|
|
796
|
+
step: {
|
|
797
|
+
name: 'populate',
|
|
798
|
+
status: code === 0 ? 'pass' : 'fail',
|
|
799
|
+
details: { populated: code === 0 ? 1 : 0, skipped: 0 },
|
|
800
|
+
warnings,
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
806
|
+
warnings.push(`Populate failed (non-fatal): ${msg}`);
|
|
807
|
+
return {
|
|
808
|
+
step: {
|
|
809
|
+
name: 'populate',
|
|
810
|
+
status: 'pass', // non-fatal — bootstrap shouldn't fail on populate
|
|
811
|
+
details: { populated: 0, skipped: 0, error: msg },
|
|
812
|
+
warnings,
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// ============================================================================
|
|
818
|
+
// Phase 7: Doctor
|
|
647
819
|
// ============================================================================
|
|
648
820
|
function runDoctorPhase(options, skipDoctor) {
|
|
649
821
|
const warnings = [];
|
|
@@ -754,32 +926,59 @@ function hashContent(content) {
|
|
|
754
926
|
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
755
927
|
}
|
|
756
928
|
/**
|
|
757
|
-
* Check if
|
|
929
|
+
* Check if a file is already a thin ADF pointer.
|
|
758
930
|
*/
|
|
759
|
-
function
|
|
760
|
-
const coreAdfPath = path.join(aiDir, 'core.adf');
|
|
761
|
-
if (!fs.existsSync(coreAdfPath))
|
|
762
|
-
return false;
|
|
931
|
+
function isAlreadyThinPointer(filePath) {
|
|
763
932
|
try {
|
|
764
|
-
const content = fs.readFileSync(
|
|
765
|
-
|
|
766
|
-
// A custom file will have different content than the CORE_SCAFFOLD
|
|
767
|
-
return content.trim() !== adf_2.CORE_SCAFFOLD.trim();
|
|
933
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
934
|
+
return adf_1.POINTER_MARKERS.some(marker => content.includes(marker));
|
|
768
935
|
}
|
|
769
936
|
catch {
|
|
770
937
|
return false;
|
|
771
938
|
}
|
|
772
939
|
}
|
|
773
940
|
/**
|
|
774
|
-
*
|
|
941
|
+
* Prompt user for a yes/no answer via readline.
|
|
775
942
|
*/
|
|
776
|
-
function
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
943
|
+
function promptYesNo(question) {
|
|
944
|
+
const rl = readline.createInterface({
|
|
945
|
+
input: process.stdin,
|
|
946
|
+
output: process.stdout,
|
|
947
|
+
});
|
|
948
|
+
return new Promise((resolve) => {
|
|
949
|
+
rl.question(question, (answer) => {
|
|
950
|
+
rl.close();
|
|
951
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Append orphaned modules to the ON_DEMAND section of manifest.adf.
|
|
957
|
+
*/
|
|
958
|
+
function registerOrphansInManifest(manifestPath, orphans) {
|
|
959
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
960
|
+
const lines = content.split('\n');
|
|
961
|
+
// Find ON_DEMAND section
|
|
962
|
+
const onDemandIdx = lines.findIndex(l => l.includes('ON_DEMAND:'));
|
|
963
|
+
if (onDemandIdx === -1) {
|
|
964
|
+
// No ON_DEMAND section — append one
|
|
965
|
+
const newEntries = orphans.map(name => {
|
|
966
|
+
const stem = name.replace('.adf', '');
|
|
967
|
+
return ` - ${name} (Triggers on: ${stem})`;
|
|
968
|
+
});
|
|
969
|
+
fs.writeFileSync(manifestPath, content.trimEnd() + '\n\n📂 ON_DEMAND:\n' + newEntries.join('\n') + '\n');
|
|
970
|
+
return;
|
|
780
971
|
}
|
|
781
|
-
|
|
782
|
-
|
|
972
|
+
// Find end of ON_DEMAND entries
|
|
973
|
+
let insertIdx = onDemandIdx + 1;
|
|
974
|
+
while (insertIdx < lines.length && lines[insertIdx].match(/^\s+-\s/)) {
|
|
975
|
+
insertIdx++;
|
|
783
976
|
}
|
|
977
|
+
const newEntries = orphans.map(name => {
|
|
978
|
+
const stem = name.replace('.adf', '');
|
|
979
|
+
return ` - ${name} (Triggers on: ${stem})`;
|
|
980
|
+
});
|
|
981
|
+
lines.splice(insertIdx, 0, ...newEntries);
|
|
982
|
+
fs.writeFileSync(manifestPath, lines.join('\n'));
|
|
784
983
|
}
|
|
785
984
|
//# sourceMappingURL=bootstrap.js.map
|