@girardmedia/bootspring 2.0.21 → 2.0.22

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 (147) hide show
  1. package/cli/preseed/index.js +16 -0
  2. package/cli/preseed/interactive.js +143 -0
  3. package/cli/preseed/templates.js +227 -0
  4. package/cli/seed/builders/ai-context-builder.js +85 -0
  5. package/cli/seed/builders/index.js +13 -0
  6. package/cli/seed/builders/seed-builder.js +272 -0
  7. package/cli/seed/extractors/content-extractors.js +383 -0
  8. package/cli/seed/extractors/index.js +47 -0
  9. package/cli/seed/extractors/metadata-extractors.js +167 -0
  10. package/cli/seed/extractors/section-extractor.js +54 -0
  11. package/cli/seed/extractors/stack-extractors.js +228 -0
  12. package/cli/seed/index.js +18 -0
  13. package/cli/seed/utils/folder-structure.js +84 -0
  14. package/cli/seed/utils/index.js +11 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.js +3220 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/context-McpJQa_2.d.ts +5710 -0
  19. package/dist/core/index.d.ts +635 -0
  20. package/dist/core/index.js +2593 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/index-QqbeEiDm.d.ts +857 -0
  23. package/dist/index-UiYCgwiH.d.ts +174 -0
  24. package/dist/index.d.ts +453 -0
  25. package/dist/index.js +44228 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/index.d.ts +1 -0
  28. package/dist/mcp/index.js +41173 -0
  29. package/dist/mcp/index.js.map +1 -0
  30. package/generators/index.ts +82 -0
  31. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  32. package/intelligence/orchestrator/config/index.js +20 -0
  33. package/intelligence/orchestrator/config/phases.js +111 -0
  34. package/intelligence/orchestrator/config/remediation.js +150 -0
  35. package/intelligence/orchestrator/config/workflows.js +168 -0
  36. package/intelligence/orchestrator/core/index.js +16 -0
  37. package/intelligence/orchestrator/core/state-manager.js +88 -0
  38. package/intelligence/orchestrator/core/telemetry.js +24 -0
  39. package/intelligence/orchestrator/index.js +17 -0
  40. package/mcp/contracts/mcp-contract.v1.json +1 -1
  41. package/package.json +16 -3
  42. package/src/cli/agent.ts +703 -0
  43. package/src/cli/analyze.ts +640 -0
  44. package/src/cli/audit.ts +707 -0
  45. package/src/cli/auth.ts +930 -0
  46. package/src/cli/billing.ts +364 -0
  47. package/src/cli/build.ts +1089 -0
  48. package/src/cli/business.ts +508 -0
  49. package/src/cli/checkpoint-utils.ts +236 -0
  50. package/src/cli/checkpoint.ts +757 -0
  51. package/src/cli/cloud-sync.ts +534 -0
  52. package/src/cli/content.ts +273 -0
  53. package/src/cli/context.ts +667 -0
  54. package/src/cli/dashboard.ts +133 -0
  55. package/src/cli/deploy.ts +704 -0
  56. package/src/cli/doctor.ts +480 -0
  57. package/src/cli/fundraise.ts +494 -0
  58. package/src/cli/generate.ts +346 -0
  59. package/src/cli/github-cmd.ts +566 -0
  60. package/src/cli/health.ts +599 -0
  61. package/src/cli/index.ts +113 -0
  62. package/src/cli/init.ts +838 -0
  63. package/src/cli/legal.ts +495 -0
  64. package/src/cli/log.ts +316 -0
  65. package/src/cli/loop.ts +1660 -0
  66. package/src/cli/manager.ts +878 -0
  67. package/src/cli/mcp.ts +275 -0
  68. package/src/cli/memory.ts +346 -0
  69. package/src/cli/metrics.ts +590 -0
  70. package/src/cli/monitor.ts +960 -0
  71. package/src/cli/mvp.ts +662 -0
  72. package/src/cli/onboard.ts +663 -0
  73. package/src/cli/orchestrator.ts +622 -0
  74. package/src/cli/plugin.ts +483 -0
  75. package/src/cli/prd.ts +671 -0
  76. package/src/cli/preseed-start.ts +1633 -0
  77. package/src/cli/preseed.ts +2434 -0
  78. package/src/cli/project.ts +526 -0
  79. package/src/cli/quality.ts +885 -0
  80. package/src/cli/security.ts +1079 -0
  81. package/src/cli/seed.ts +1224 -0
  82. package/src/cli/skill.ts +537 -0
  83. package/src/cli/suggest.ts +1225 -0
  84. package/src/cli/switch.ts +518 -0
  85. package/src/cli/task.ts +780 -0
  86. package/src/cli/telemetry.ts +172 -0
  87. package/src/cli/todo.ts +627 -0
  88. package/src/cli/types.ts +15 -0
  89. package/src/cli/update.ts +334 -0
  90. package/src/cli/visualize.ts +609 -0
  91. package/src/cli/watch.ts +895 -0
  92. package/src/cli/workspace.ts +709 -0
  93. package/src/core/action-recorder.ts +673 -0
  94. package/src/core/analyze-workflow.ts +1453 -0
  95. package/src/core/api-client.ts +1120 -0
  96. package/src/core/audit-workflow.ts +1681 -0
  97. package/src/core/auth.ts +471 -0
  98. package/src/core/build-orchestrator.ts +509 -0
  99. package/src/core/build-state.ts +621 -0
  100. package/src/core/checkpoint-engine.ts +482 -0
  101. package/src/core/config.ts +1285 -0
  102. package/src/core/context-loader.ts +694 -0
  103. package/src/core/context.ts +410 -0
  104. package/src/core/deploy-workflow.ts +1085 -0
  105. package/src/core/entitlements.ts +322 -0
  106. package/src/core/github-sync.ts +720 -0
  107. package/src/core/index.ts +981 -0
  108. package/src/core/ingest.ts +1186 -0
  109. package/src/core/metrics-engine.ts +886 -0
  110. package/src/core/mvp.ts +847 -0
  111. package/src/core/onboard-workflow.ts +1293 -0
  112. package/src/core/policies.ts +81 -0
  113. package/src/core/preseed-workflow.ts +1163 -0
  114. package/src/core/preseed.ts +1826 -0
  115. package/src/core/project-context.ts +380 -0
  116. package/src/core/project-state.ts +699 -0
  117. package/src/core/r2-sync.ts +691 -0
  118. package/src/core/scaffold.ts +1715 -0
  119. package/src/core/session.ts +286 -0
  120. package/src/core/task-extractor.ts +799 -0
  121. package/src/core/telemetry.ts +371 -0
  122. package/src/core/tier-enforcement.ts +737 -0
  123. package/src/core/utils.ts +437 -0
  124. package/src/index.ts +29 -0
  125. package/src/intelligence/agent-collab.ts +2376 -0
  126. package/src/intelligence/auto-suggest.ts +713 -0
  127. package/src/intelligence/content-gen.ts +1351 -0
  128. package/src/intelligence/cross-project.ts +1692 -0
  129. package/src/intelligence/git-memory.ts +529 -0
  130. package/src/intelligence/index.ts +318 -0
  131. package/src/intelligence/orchestrator.ts +534 -0
  132. package/src/intelligence/prd.ts +466 -0
  133. package/src/intelligence/recommendations.ts +982 -0
  134. package/src/intelligence/workflow-composer.ts +1472 -0
  135. package/src/mcp/capabilities.ts +233 -0
  136. package/src/mcp/index.ts +37 -0
  137. package/src/mcp/registry.ts +1268 -0
  138. package/src/mcp/response-formatter.ts +797 -0
  139. package/src/mcp/server.ts +240 -0
  140. package/src/types/agent.ts +69 -0
  141. package/src/types/config.ts +86 -0
  142. package/src/types/context.ts +77 -0
  143. package/src/types/index.ts +53 -0
  144. package/src/types/mcp.ts +91 -0
  145. package/src/types/skills.ts +47 -0
  146. package/src/types/workflow.ts +155 -0
  147. package/generators/index.js +0 -18
@@ -0,0 +1,2434 @@
1
+ /**
2
+ * Bootspring Preseed Command
3
+ * Generate foundational documents from minimal input
4
+ *
5
+ * Commands:
6
+ * start Smart entry point - detects context & guides you
7
+ * setup Create context input folders for early docs
8
+ * init [--preset] Initialize preseed with interactive Q&A
9
+ * generate Generate/regenerate all documents
10
+ * sync Sync documents with codebase changes
11
+ * status Show preseed status
12
+ * update <path> Update a specific config value
13
+ * export Export preseed config
14
+ * pull Download documents from dashboard
15
+ * push Upload documents to dashboard
16
+ * merge Merge source docs from context folders
17
+ * wizard Run full guided wizard
18
+ * workflow start Begin guided approval workflow
19
+ * workflow resume Continue workflow from last point
20
+ * workflow status Show workflow progress
21
+ * workflow reset Reset workflow state
22
+ * doc <type> approve Approve a document
23
+ * doc <type> reject Reject a document with feedback
24
+ * doc <type> edit Open document in external editor
25
+ * doc <type> review Submit document for review
26
+ *
27
+ * @package bootspring
28
+ * @command preseed
29
+ */
30
+
31
+ import * as path from 'path';
32
+ import * as fs from 'fs';
33
+ import type { ParsedArgs } from './types';
34
+ import type { Interface as ReadlineInterface } from 'readline';
35
+
36
+ // Module interfaces
37
+ interface Utils {
38
+ parseArgs: (args: string[]) => ParsedArgs;
39
+ COLORS: {
40
+ cyan: string;
41
+ bold: string;
42
+ reset: string;
43
+ dim: string;
44
+ green: string;
45
+ yellow: string;
46
+ red: string;
47
+ blue: string;
48
+ };
49
+ createSpinner: (text: string) => Spinner;
50
+ print: {
51
+ dim: (msg: string) => void;
52
+ warning: (msg: string) => void;
53
+ warn: (msg: string) => void;
54
+ error: (msg: string) => void;
55
+ success: (msg: string) => void;
56
+ info: (msg: string) => void;
57
+ debug: (msg: string) => void;
58
+ };
59
+ formatRelativeTime: (date: Date) => string;
60
+ }
61
+
62
+ interface Spinner {
63
+ start: () => Spinner;
64
+ succeed: (text?: string) => Spinner;
65
+ fail: (text?: string) => Spinner;
66
+ warn: (text?: string) => Spinner;
67
+ stop: () => Spinner;
68
+ text: string;
69
+ }
70
+
71
+ interface Config {
72
+ findProjectRoot: () => string;
73
+ loadConfig: (root: string) => ProjectConfig | null;
74
+ }
75
+
76
+ interface ProjectConfig {
77
+ project?: {
78
+ id?: string | undefined;
79
+ name?: string | undefined;
80
+ } | undefined;
81
+ }
82
+
83
+ interface PreseedEngineModule {
84
+ PreseedEngine: new (root: string, options?: { preset?: string }) => PreseedEngine;
85
+ PRESETS: Record<string, string[]>;
86
+ DOCUMENT_TYPES: Record<string, { name: string }>;
87
+ }
88
+
89
+ interface PreseedEngine {
90
+ outputDir: string;
91
+ config: PreseedConfig | null;
92
+ setupContextFolders: () => string[];
93
+ hasContextDocuments: () => boolean;
94
+ ingestContext: () => { summary: { totalFiles: number; byCategory: Record<string, number> } };
95
+ initialize: (input: WizardInput) => Promise<void>;
96
+ generateAll: () => Promise<GeneratedDoc[]>;
97
+ loadConfig: () => boolean;
98
+ getStatus: () => PreseedStatus;
99
+ generateDocument: (type: string) => string;
100
+ sync: () => Promise<{ synced: number; documents: { title: string }[] }>;
101
+ updateConfig: (path: string, value: string) => void;
102
+ }
103
+
104
+ interface PreseedConfig {
105
+ identity?: {
106
+ name?: string | undefined;
107
+ } | undefined;
108
+ [key: string]: unknown;
109
+ }
110
+
111
+ interface WizardInput {
112
+ name?: string | undefined;
113
+ tagline?: string | undefined;
114
+ description?: string | undefined;
115
+ category?: string | undefined;
116
+ problem?: string | undefined;
117
+ painPoints?: string[] | undefined;
118
+ whyNow?: string | undefined;
119
+ solution?: string | undefined;
120
+ uniqueValue?: string | undefined;
121
+ keyFeatures?: string[] | undefined;
122
+ primaryAudience?: string | undefined;
123
+ segments?: string[] | undefined;
124
+ personas?: Persona[] | undefined;
125
+ tam?: string | undefined;
126
+ sam?: string | undefined;
127
+ som?: string | undefined;
128
+ marketGrowth?: string | undefined;
129
+ trends?: string[] | undefined;
130
+ directCompetitors?: { name: string; strengths: string; weaknesses: string }[] | undefined;
131
+ positioning?: string | undefined;
132
+ differentiation?: string[] | undefined;
133
+ businessModel?: string | undefined;
134
+ revenueStreams?: string[] | undefined;
135
+ pricing?: { model: string; tiers: PricingTier[] } | undefined;
136
+ productVision?: string | undefined;
137
+ mvpFeatures?: string[] | undefined;
138
+ futureFeatures?: string[] | undefined;
139
+ phases?: Phase[] | undefined;
140
+ milestones?: string[] | undefined;
141
+ }
142
+
143
+ interface Persona {
144
+ name: string;
145
+ role: string;
146
+ goals: string[];
147
+ painPoints: string[];
148
+ }
149
+
150
+ interface PricingTier {
151
+ name: string;
152
+ price: string;
153
+ features: string[];
154
+ }
155
+
156
+ interface Phase {
157
+ name: string;
158
+ duration: string;
159
+ goals: string[];
160
+ }
161
+
162
+ interface GeneratedDoc {
163
+ title: string;
164
+ type: string;
165
+ }
166
+
167
+ interface PreseedStatus {
168
+ initialized: boolean;
169
+ initMode?: string | undefined;
170
+ projectName?: string | undefined;
171
+ preset?: string | undefined;
172
+ lastSync?: string | undefined;
173
+ outputDir?: string | undefined;
174
+ documents: StatusDocument[];
175
+ }
176
+
177
+ interface StatusDocument {
178
+ title: string;
179
+ exists: boolean;
180
+ required?: boolean | undefined;
181
+ modified?: string | undefined;
182
+ }
183
+
184
+ interface PreseedWorkflowEngineModule {
185
+ PreseedWorkflowEngine: new (root: string) => PreseedWorkflowEngine;
186
+ DOCUMENT_STATUS: Record<string, string>;
187
+ }
188
+
189
+ interface PreseedWorkflowEngine {
190
+ state: WorkflowState;
191
+ approvedDir: string;
192
+ hasWorkflow: () => boolean;
193
+ loadState: () => void;
194
+ getResumePoint: () => ResumePoint | null;
195
+ initializeWorkflow: () => void;
196
+ resetWorkflow: () => void;
197
+ getProgress: () => WorkflowProgress | null;
198
+ importDocument: (type: string, path: string) => { path: string };
199
+ createDraft: (type: string, content: string, source: string) => { path: string; version: number };
200
+ readDraft: (type: string) => string | null;
201
+ calculateQualityScore: (type: string, content: string) => QualityScore;
202
+ openInEditor: (type: string) => Promise<{ changed: boolean }>;
203
+ approveDocument: (type: string) => ApproveResult;
204
+ rejectDocument: (type: string, feedback: string) => void;
205
+ submitForReview: (type: string) => QualityScore;
206
+ overridePhaseGate: (phase: string, justification: string) => void;
207
+ }
208
+
209
+ interface WorkflowState {
210
+ currentPhase: string;
211
+ documents: Record<string, DocumentState>;
212
+ }
213
+
214
+ interface DocumentState {
215
+ status: string;
216
+ version: number;
217
+ feedback: { message: string }[];
218
+ }
219
+
220
+ interface ResumePoint {
221
+ phaseName: string;
222
+ documentName: string;
223
+ documentStatus: string;
224
+ document: string;
225
+ lastUpdated: string;
226
+ }
227
+
228
+ interface WorkflowProgress {
229
+ overall: { percentage: number; approved: number; total: number };
230
+ phases: PhaseProgress[];
231
+ currentPhase: string;
232
+ currentDocument?: string | undefined;
233
+ isComplete: boolean;
234
+ }
235
+
236
+ interface PhaseProgress {
237
+ id: string;
238
+ name: string;
239
+ status: string;
240
+ dependenciesMet: boolean;
241
+ progress: { approved: number; total: number };
242
+ documents: ProgressDocument[];
243
+ }
244
+
245
+ interface ProgressDocument {
246
+ type: string;
247
+ name: string;
248
+ status: string;
249
+ qualityScore: number | null;
250
+ }
251
+
252
+ interface QualityScore {
253
+ score: number;
254
+ completeness: number;
255
+ clarity: number;
256
+ breakdown: {
257
+ completeness?: { checks?: QualityCheck[] } | undefined;
258
+ clarity?: { checks?: QualityCheck[] } | undefined;
259
+ };
260
+ }
261
+
262
+ interface QualityCheck {
263
+ label: string;
264
+ passed: boolean;
265
+ }
266
+
267
+ interface ApproveResult {
268
+ workflowComplete?: boolean | undefined;
269
+ next?: { document: string } | undefined;
270
+ }
271
+
272
+ interface ProjectState {
273
+ PROJECT_TYPES: { DEVELOPMENT: string };
274
+ setProjectType: (root: string, type: string, meta: Record<string, unknown>) => void;
275
+ }
276
+
277
+ interface CheckpointEngine {
278
+ syncCheckpoints: (root: string, options: { verbose: boolean }) => void;
279
+ }
280
+
281
+ interface Auth {
282
+ isAuthenticated: () => boolean;
283
+ }
284
+
285
+ interface Session {
286
+ getEffectiveProject: () => { id?: string; name?: string } | null;
287
+ }
288
+
289
+ interface ApiClient {
290
+ listProjects: () => Promise<{ projects?: { id: string; name: string; slug?: string }[] }>;
291
+ listPreseedDocuments: (projectId: string) => Promise<{ documents?: { name: string }[] }>;
292
+ getPreseedDocument: (projectId: string, name: string) => Promise<{ content: string }>;
293
+ directRequest: (method: string, path: string, body: Record<string, unknown>) => Promise<void>;
294
+ }
295
+
296
+ interface TierEnforcement {
297
+ requirePreseedAccess: (command: string) => void;
298
+ }
299
+
300
+ interface PreseedStartModule {
301
+ start: (args: ParsedArgs) => Promise<void>;
302
+ }
303
+
304
+ interface InteractiveModule {
305
+ createInterface: () => ReadlineInterface;
306
+ askText: (rl: ReadlineInterface, prompt: string, defaultValue?: string) => Promise<string>;
307
+ askList: (rl: ReadlineInterface, prompt: string, hint: string) => Promise<string[]>;
308
+ askChoice: (rl: ReadlineInterface, prompt: string, options: ChoiceOption[]) => Promise<string>;
309
+ displaySection: (step: number, total: number, title: string, description: string) => void;
310
+ }
311
+
312
+ interface ChoiceOption {
313
+ label: string;
314
+ value: string;
315
+ description?: string | undefined;
316
+ }
317
+
318
+ interface TemplatesModule {
319
+ generateDocumentTemplate: (type: string, name: string) => string;
320
+ }
321
+
322
+ interface TierError extends Error {
323
+ code?: string | undefined;
324
+ upgradePrompt?: string | undefined;
325
+ }
326
+
327
+ // Lazy-loaded modules
328
+ let _utils: Utils | null = null;
329
+ let _config: Config | null = null;
330
+ let _preseedEngineModule: PreseedEngineModule | null = null;
331
+ let _preseedWorkflowModule: PreseedWorkflowEngineModule | null = null;
332
+ let _projectState: ProjectState | null = null;
333
+ let _checkpointEngine: CheckpointEngine | null = null;
334
+ let _apiClient: ApiClient | null = null;
335
+ let _auth: Auth | null = null;
336
+ let _session: Session | null = null;
337
+ let _tierEnforcement: TierEnforcement | null = null;
338
+ let _preseedStart: PreseedStartModule | null = null;
339
+ let _interactive: InteractiveModule | null = null;
340
+ let _templates: TemplatesModule | null = null;
341
+ let _yaml: { stringify: (obj: unknown) => string } | null = null;
342
+
343
+ function getUtils(): Utils {
344
+ if (!_utils) {
345
+ _utils = require('../core/utils') as Utils;
346
+ }
347
+ return _utils;
348
+ }
349
+
350
+ function getConfig(): Config {
351
+ if (!_config) {
352
+ _config = require('../core/config') as Config;
353
+ }
354
+ return _config;
355
+ }
356
+
357
+ function getPreseedEngineModule(): PreseedEngineModule {
358
+ if (!_preseedEngineModule) {
359
+ _preseedEngineModule = require('../core/preseed') as PreseedEngineModule;
360
+ }
361
+ return _preseedEngineModule;
362
+ }
363
+
364
+ function getPreseedWorkflowModule(): PreseedWorkflowEngineModule {
365
+ if (!_preseedWorkflowModule) {
366
+ _preseedWorkflowModule = require('../core/preseed-workflow') as PreseedWorkflowEngineModule;
367
+ }
368
+ return _preseedWorkflowModule;
369
+ }
370
+
371
+ function getProjectState(): ProjectState {
372
+ if (!_projectState) {
373
+ _projectState = require('../core/project-state') as ProjectState;
374
+ }
375
+ return _projectState;
376
+ }
377
+
378
+ function getCheckpointEngine(): CheckpointEngine {
379
+ if (!_checkpointEngine) {
380
+ _checkpointEngine = require('../core/checkpoint-engine') as CheckpointEngine;
381
+ }
382
+ return _checkpointEngine;
383
+ }
384
+
385
+ function getApiClient(): ApiClient {
386
+ if (!_apiClient) {
387
+ _apiClient = require('../core/api-client') as ApiClient;
388
+ }
389
+ return _apiClient;
390
+ }
391
+
392
+ function getAuth(): Auth {
393
+ if (!_auth) {
394
+ _auth = require('../core/auth') as Auth;
395
+ }
396
+ return _auth;
397
+ }
398
+
399
+ function getSession(): Session {
400
+ if (!_session) {
401
+ _session = require('../core/session') as Session;
402
+ }
403
+ return _session;
404
+ }
405
+
406
+ function getTierEnforcement(): TierEnforcement {
407
+ if (!_tierEnforcement) {
408
+ _tierEnforcement = require('../core/tier-enforcement') as TierEnforcement;
409
+ }
410
+ return _tierEnforcement;
411
+ }
412
+
413
+ function getPreseedStart(): PreseedStartModule {
414
+ if (!_preseedStart) {
415
+ _preseedStart = require('./preseed-start') as PreseedStartModule;
416
+ }
417
+ return _preseedStart;
418
+ }
419
+
420
+ function getInteractive(): InteractiveModule {
421
+ if (!_interactive) {
422
+ _interactive = require('./preseed/interactive') as InteractiveModule;
423
+ }
424
+ return _interactive;
425
+ }
426
+
427
+ function getTemplates(): TemplatesModule {
428
+ if (!_templates) {
429
+ _templates = require('./preseed/templates') as TemplatesModule;
430
+ }
431
+ return _templates;
432
+ }
433
+
434
+ function getYaml(): { stringify: (obj: unknown) => string } {
435
+ if (!_yaml) {
436
+ _yaml = require('yaml') as { stringify: (obj: unknown) => string };
437
+ }
438
+ return _yaml;
439
+ }
440
+
441
+ /**
442
+ * Run the preseed wizard
443
+ */
444
+ async function runWizard(rl: ReadlineInterface, preset: string = 'startup'): Promise<WizardInput> {
445
+ const utils = getUtils();
446
+ const { PRESETS } = getPreseedEngineModule();
447
+ const { askText, askList, askChoice, displaySection } = getInteractive();
448
+
449
+ const input: WizardInput = {};
450
+
451
+ const presetDocs = PRESETS[preset];
452
+ if (!presetDocs) {
453
+ throw new Error(`Invalid preset: ${preset}`);
454
+ }
455
+
456
+ console.log(`
457
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Wizard${utils.COLORS.reset}
458
+ ${utils.COLORS.dim}Let's build your foundational documents from scratch${utils.COLORS.reset}
459
+ ${utils.COLORS.dim}Preset: ${preset} (${presetDocs.length} documents)${utils.COLORS.reset}
460
+ `);
461
+
462
+ const totalSteps = 8;
463
+
464
+ // Step 1: Identity
465
+ displaySection(1, totalSteps, 'Project Identity', 'Basic information about your project');
466
+
467
+ input.name = await askText(rl, 'Project name');
468
+ input.tagline = await askText(rl, 'One-line tagline');
469
+ input.description = await askText(rl, 'Brief description (2-3 sentences)');
470
+ input.category = await askChoice(rl, 'What type of product is this?', [
471
+ { label: 'SaaS', value: 'saas', description: 'Software as a Service' },
472
+ { label: 'Marketplace', value: 'marketplace', description: 'Two-sided marketplace' },
473
+ { label: 'E-commerce', value: 'ecommerce', description: 'Online store' },
474
+ { label: 'Mobile App', value: 'mobile', description: 'Mobile application' },
475
+ { label: 'Developer Tool', value: 'devtool', description: 'Tools for developers' },
476
+ { label: 'Consumer App', value: 'consumer', description: 'B2C application' },
477
+ { label: 'Enterprise', value: 'enterprise', description: 'Enterprise software' },
478
+ { label: 'Other', value: 'other', description: 'Something else' }
479
+ ]);
480
+
481
+ // Step 2: Problem
482
+ displaySection(2, totalSteps, 'The Problem', 'What problem are you solving?');
483
+
484
+ input.problem = await askText(rl, 'Describe the problem you\'re solving');
485
+ input.painPoints = await askList(rl, 'What are the top pain points?', 'Enter 3-5 pain points, comma-separated');
486
+ input.whyNow = await askText(rl, 'Why is now the right time to solve this?');
487
+
488
+ // Step 3: Solution
489
+ displaySection(3, totalSteps, 'Your Solution', 'How do you solve the problem?');
490
+
491
+ input.solution = await askText(rl, 'Describe your solution in one paragraph');
492
+ input.uniqueValue = await askText(rl, 'What makes your solution unique?');
493
+ input.keyFeatures = await askList(rl, 'What are the key features?', 'Enter 3-6 features, comma-separated');
494
+
495
+ // Step 4: Target Audience
496
+ displaySection(4, totalSteps, 'Target Audience', 'Who are you building this for?');
497
+
498
+ input.primaryAudience = await askText(rl, 'Describe your primary audience');
499
+ input.segments = await askList(rl, 'What market segments are you targeting?', 'Enter 2-4 segments, comma-separated');
500
+
501
+ console.log(`\n${utils.COLORS.bold}Let's create a user persona${utils.COLORS.reset}`);
502
+ const personaName = await askText(rl, 'Persona name (e.g., "Tech-Savvy Startup Founder")');
503
+ const personaRole = await askText(rl, 'Their role/job title');
504
+ const personaGoals = await askList(rl, 'What are their goals?', 'Enter 2-3 goals, comma-separated');
505
+ const personaPains = await askList(rl, 'What are their pain points?', 'Enter 2-3 pain points, comma-separated');
506
+
507
+ input.personas = [{
508
+ name: personaName,
509
+ role: personaRole,
510
+ goals: personaGoals,
511
+ painPoints: personaPains
512
+ }];
513
+
514
+ // Step 5: Market (if in preset)
515
+ if (presetDocs.includes('market')) {
516
+ displaySection(5, totalSteps, 'Market Analysis', 'Understanding the market opportunity');
517
+
518
+ input.tam = await askText(rl, 'Total Addressable Market (TAM)', '$1B+');
519
+ input.sam = await askText(rl, 'Serviceable Addressable Market (SAM)', '$100M+');
520
+ input.som = await askText(rl, 'Serviceable Obtainable Market (SOM) in 3 years', '$10M+');
521
+ input.marketGrowth = await askText(rl, 'Market growth rate or trend');
522
+ input.trends = await askList(rl, 'Key market trends', 'Enter 2-4 trends, comma-separated');
523
+ }
524
+
525
+ // Step 6: Competition
526
+ if (presetDocs.includes('competitors')) {
527
+ displaySection(6, totalSteps, 'Competition', 'Understanding the competitive landscape');
528
+
529
+ const competitors = await askList(rl, 'Who are your direct competitors?', 'Enter company names, comma-separated');
530
+ input.directCompetitors = competitors.map(name => ({ name, strengths: 'TBD', weaknesses: 'TBD' }));
531
+
532
+ input.positioning = await askText(rl, 'How do you position against competitors?');
533
+ input.differentiation = await askList(rl, 'Key differentiators', 'Enter 2-4 differentiators, comma-separated');
534
+ }
535
+
536
+ // Step 7: Business Model
537
+ displaySection(7, totalSteps, 'Business Model', 'How will you make money?');
538
+
539
+ input.businessModel = await askChoice(rl, 'What is your business model?', [
540
+ { label: 'Subscription', value: 'subscription', description: 'Recurring revenue' },
541
+ { label: 'Usage-based', value: 'usage', description: 'Pay per use' },
542
+ { label: 'Freemium', value: 'freemium', description: 'Free tier + paid upgrades' },
543
+ { label: 'Marketplace', value: 'marketplace', description: 'Transaction fees' },
544
+ { label: 'One-time', value: 'one-time', description: 'One-time purchase' },
545
+ { label: 'Enterprise', value: 'enterprise', description: 'Custom contracts' }
546
+ ]);
547
+
548
+ input.revenueStreams = await askList(rl, 'Revenue streams', 'Enter revenue sources, comma-separated');
549
+
550
+ console.log(`\n${utils.COLORS.bold}Pricing Tiers${utils.COLORS.reset}`);
551
+ const tiers: PricingTier[] = [];
552
+
553
+ const tier1Name = await askText(rl, 'Tier 1 name', 'Free');
554
+ const tier1Price = await askText(rl, 'Tier 1 price', '$0');
555
+ tiers.push({ name: tier1Name, price: tier1Price, features: ['Basic features'] });
556
+
557
+ const tier2Name = await askText(rl, 'Tier 2 name', 'Pro');
558
+ const tier2Price = await askText(rl, 'Tier 2 price', '$29/mo');
559
+ tiers.push({ name: tier2Name, price: tier2Price, features: ['Pro features'] });
560
+
561
+ const tier3Name = await askText(rl, 'Tier 3 name', 'Enterprise');
562
+ const tier3Price = await askText(rl, 'Tier 3 price', 'Custom');
563
+ tiers.push({ name: tier3Name, price: tier3Price, features: ['All features', 'Custom support'] });
564
+
565
+ input.pricing = { model: input.businessModel || 'subscription', tiers };
566
+
567
+ // Step 8: MVP Features
568
+ displaySection(8, totalSteps, 'MVP & Roadmap', 'What will you build first?');
569
+
570
+ input.productVision = await askText(rl, 'What is your product vision?');
571
+ input.mvpFeatures = await askList(rl, 'MVP features (must-have for launch)', 'Enter 5-8 features, comma-separated');
572
+ input.futureFeatures = await askList(rl, 'Future features (post-MVP)', 'Enter 3-5 features, comma-separated');
573
+
574
+ // Roadmap phases
575
+ console.log(`\n${utils.COLORS.bold}Development Phases${utils.COLORS.reset}`);
576
+ input.phases = [
577
+ {
578
+ name: 'MVP',
579
+ duration: await askText(rl, 'MVP duration', '4-6 weeks'),
580
+ goals: ['Launch core features', 'Validate with early users']
581
+ },
582
+ {
583
+ name: 'Growth',
584
+ duration: await askText(rl, 'Growth phase duration', '2-3 months'),
585
+ goals: ['Scale user base', 'Add key features']
586
+ },
587
+ {
588
+ name: 'Scale',
589
+ duration: await askText(rl, 'Scale phase duration', '3-6 months'),
590
+ goals: ['Enterprise features', 'Market expansion']
591
+ }
592
+ ];
593
+
594
+ input.milestones = await askList(rl, 'Key milestones', 'Enter milestone names, comma-separated');
595
+
596
+ return input;
597
+ }
598
+
599
+ /**
600
+ * Setup context input folders
601
+ */
602
+ async function preseedSetup(_args: ParsedArgs): Promise<void> {
603
+ const utils = getUtils();
604
+ const config = getConfig();
605
+ const { PreseedEngine } = getPreseedEngineModule();
606
+
607
+ const projectRoot = config.findProjectRoot();
608
+
609
+ console.log(`
610
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Setup${utils.COLORS.reset}
611
+ ${utils.COLORS.dim}Create context input folders for early documentation${utils.COLORS.reset}
612
+ `);
613
+
614
+ const engine = new PreseedEngine(projectRoot);
615
+ const spinner = utils.createSpinner('Creating context folders...').start();
616
+
617
+ const created = engine.setupContextFolders();
618
+ spinner.succeed(`Created ${created.length} folders`);
619
+
620
+ console.log(`
621
+ ${utils.COLORS.green}${utils.COLORS.bold}Setup complete!${utils.COLORS.reset}
622
+
623
+ ${utils.COLORS.bold}Context folder structure:${utils.COLORS.reset}
624
+ .bootspring/preseed/context/
625
+
626
+ │ ${utils.COLORS.yellow}Universal${utils.COLORS.reset}
627
+ ├── ${utils.COLORS.green}drop/${utils.COLORS.reset} ${utils.COLORS.dim}# ${utils.COLORS.bold}Drop anything here${utils.COLORS.reset}${utils.COLORS.dim} - AI sorts it out${utils.COLORS.reset}
628
+
629
+ │ ${utils.COLORS.yellow}General${utils.COLORS.reset}
630
+ ├── ideas/ ${utils.COLORS.dim}# Rough ideas, brainstorms, notes${utils.COLORS.reset}
631
+ ├── research/ ${utils.COLORS.dim}# Market research, industry reports${utils.COLORS.reset}
632
+
633
+ │ ${utils.COLORS.yellow}Document-specific (maps to output)${utils.COLORS.reset}
634
+ ├── vision/ ${utils.COLORS.dim}# → VISION.md${utils.COLORS.reset}
635
+ ├── audience/ ${utils.COLORS.dim}# → AUDIENCE.md${utils.COLORS.reset}
636
+ ├── market/ ${utils.COLORS.dim}# → MARKET.md${utils.COLORS.reset}
637
+ ├── competitors/ ${utils.COLORS.dim}# → COMPETITORS.md${utils.COLORS.reset}
638
+ ├── business/ ${utils.COLORS.dim}# → BUSINESS_MODEL.md${utils.COLORS.reset}
639
+ ├── prd/ ${utils.COLORS.dim}# → PRD.md${utils.COLORS.reset}
640
+ ├── technical/ ${utils.COLORS.dim}# → TECHNICAL_SPEC.md${utils.COLORS.reset}
641
+ └── roadmap/ ${utils.COLORS.dim}# → ROADMAP.md${utils.COLORS.reset}
642
+
643
+ ${utils.COLORS.bold}Next steps:${utils.COLORS.reset}
644
+ 1. Drop files in the matching folder (e.g., vision/ for VISION.md sources)
645
+ 2. Or use ${utils.COLORS.green}drop/${utils.COLORS.reset} if unsure - AI will sort it out
646
+ 3. Run ${utils.COLORS.cyan}bootspring preseed start${utils.COLORS.reset}
647
+
648
+ ${utils.COLORS.dim}Tip: Each folder maps 1:1 to an output document for easy organization!${utils.COLORS.reset}
649
+ `);
650
+ }
651
+
652
+ /**
653
+ * Initialize preseed
654
+ */
655
+ async function preseedInit(args: ParsedArgs): Promise<void> {
656
+ const utils = getUtils();
657
+ const config = getConfig();
658
+ const projectState = getProjectState();
659
+ const checkpointEngine = getCheckpointEngine();
660
+ const { PreseedEngine, PRESETS, DOCUMENT_TYPES } = getPreseedEngineModule();
661
+ const { createInterface } = getInteractive();
662
+
663
+ const projectRoot = config.findProjectRoot();
664
+ const preset = (args.preset || args.p || 'startup') as string;
665
+ const quick = args.quick || args.q;
666
+
667
+ console.log(`
668
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Init${utils.COLORS.reset}
669
+ ${utils.COLORS.dim}Generate foundational documents from minimal input${utils.COLORS.reset}
670
+ `);
671
+
672
+ // Validate preset
673
+ if (!PRESETS[preset]) {
674
+ utils.print.error(`Invalid preset: ${preset}`);
675
+ console.log(`\nAvailable presets: ${Object.keys(PRESETS).join(', ')}`);
676
+ return;
677
+ }
678
+
679
+ const engine = new PreseedEngine(projectRoot, { preset });
680
+
681
+ // Auto-create folder structure
682
+ const setupSpinner = utils.createSpinner('Setting up folder structure...').start();
683
+ engine.setupContextFolders();
684
+ setupSpinner.succeed('Folder structure ready');
685
+
686
+ // Check for context documents
687
+ if (engine.hasContextDocuments()) {
688
+ const spinner = utils.createSpinner('Analyzing context documents...').start();
689
+ const context = engine.ingestContext();
690
+ spinner.succeed(`Found ${context.summary.totalFiles} context files`);
691
+
692
+ console.log(`\n${utils.COLORS.bold}Context Found:${utils.COLORS.reset}`);
693
+ for (const [category, count] of Object.entries(context.summary.byCategory)) {
694
+ if (count > 0) {
695
+ console.log(` ${utils.COLORS.green}✓${utils.COLORS.reset} ${category}: ${count} file(s)`);
696
+ }
697
+ }
698
+ console.log(`${utils.COLORS.dim}Your context will be used to enhance document generation.${utils.COLORS.reset}\n`);
699
+ }
700
+
701
+ console.log(`${utils.COLORS.bold}Preset:${utils.COLORS.reset} ${preset}`);
702
+ console.log(`${utils.COLORS.bold}Documents:${utils.COLORS.reset} ${PRESETS[preset].length}`);
703
+ console.log(`${utils.COLORS.dim}${PRESETS[preset].join(', ')}${utils.COLORS.reset}\n`);
704
+
705
+ const rl = createInterface();
706
+
707
+ try {
708
+ let input: WizardInput;
709
+
710
+ if (quick) {
711
+ // Quick mode - just essential info
712
+ const { askText, askList, askChoice } = getInteractive();
713
+ console.log(`${utils.COLORS.yellow}Quick mode - minimal questions${utils.COLORS.reset}\n`);
714
+
715
+ input = {
716
+ name: await askText(rl, 'Project name'),
717
+ tagline: await askText(rl, 'Tagline'),
718
+ problem: await askText(rl, 'Problem you\'re solving'),
719
+ solution: await askText(rl, 'Your solution'),
720
+ primaryAudience: await askText(rl, 'Target audience'),
721
+ businessModel: await askChoice(rl, 'Business model?', [
722
+ { label: 'Subscription', value: 'subscription' },
723
+ { label: 'Freemium', value: 'freemium' },
724
+ { label: 'Usage-based', value: 'usage' },
725
+ { label: 'One-time', value: 'one-time' }
726
+ ]),
727
+ keyFeatures: await askList(rl, 'Key features', 'Comma-separated')
728
+ };
729
+ } else {
730
+ // Full wizard
731
+ input = await runWizard(rl, preset);
732
+ }
733
+
734
+ rl.close();
735
+
736
+ // Initialize engine
737
+ const initEngine = new PreseedEngine(projectRoot, { preset });
738
+ const spinner = utils.createSpinner('Initializing preseed...').start();
739
+
740
+ await initEngine.initialize(input);
741
+ spinner.succeed('Preseed initialized');
742
+
743
+ // Generate documents
744
+ const genSpinner = utils.createSpinner('Generating documents...').start();
745
+ const results = await initEngine.generateAll();
746
+ genSpinner.succeed(`Generated ${results.length} documents`);
747
+
748
+ // Show results
749
+ console.log(`
750
+ ${utils.COLORS.green}${utils.COLORS.bold}Preseed complete!${utils.COLORS.reset}
751
+
752
+ ${utils.COLORS.bold}Generated Documents:${utils.COLORS.reset}`);
753
+
754
+ for (const doc of results) {
755
+ const docTypeInfo = DOCUMENT_TYPES[doc.type];
756
+ const docName = docTypeInfo ? docTypeInfo.name : doc.type;
757
+ console.log(` ${utils.COLORS.green}+${utils.COLORS.reset} ${doc.title}: ${utils.COLORS.cyan}.bootspring/preseed/${docName}${utils.COLORS.reset}`);
758
+ }
759
+
760
+ // Auto-tag project as development type
761
+ try {
762
+ projectState.setProjectType(projectRoot, projectState.PROJECT_TYPES.DEVELOPMENT, {
763
+ autoTagged: true,
764
+ taggedBy: 'preseed'
765
+ });
766
+ checkpointEngine.syncCheckpoints(projectRoot, { verbose: false });
767
+ console.log(`\n ${utils.COLORS.cyan}●${utils.COLORS.reset} Project tagged as ${utils.COLORS.cyan}development${utils.COLORS.reset} type`);
768
+ } catch (err) {
769
+ utils.print.debug(`Auto-tagging failed: ${(err as Error).message}`);
770
+ }
771
+
772
+ console.log(`
773
+ ${utils.COLORS.bold}Next Steps:${utils.COLORS.reset}
774
+ 1. Review generated documents in ${utils.COLORS.cyan}.bootspring/preseed/${utils.COLORS.reset}
775
+ 2. Edit ${utils.COLORS.cyan}.bootspring/preseed/PRESEED_CONFIG.json${utils.COLORS.reset} to refine
776
+ 3. Run ${utils.COLORS.cyan}bootspring preseed sync${utils.COLORS.reset} to regenerate
777
+ 4. Run ${utils.COLORS.cyan}bootspring seed init${utils.COLORS.reset} to scaffold project
778
+
779
+ ${utils.COLORS.dim}Tip: These are living documents. Update the config and sync to keep them fresh.${utils.COLORS.reset}
780
+ `);
781
+ } catch (error) {
782
+ rl.close();
783
+ utils.print.error(`Preseed init failed: ${(error as Error).message}`);
784
+ if (process.env.DEBUG) {
785
+ console.error((error as Error).stack);
786
+ }
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Generate/regenerate documents
792
+ */
793
+ async function preseedGenerate(args: ParsedArgs): Promise<void> {
794
+ const utils = getUtils();
795
+ const config = getConfig();
796
+ const { PreseedEngine, DOCUMENT_TYPES } = getPreseedEngineModule();
797
+
798
+ const projectRoot = config.findProjectRoot();
799
+ const preset = (args.preset || 'startup') as string;
800
+ const doc = (args.doc || args.d) as string | undefined;
801
+
802
+ console.log(`
803
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Generate${utils.COLORS.reset}
804
+ ${utils.COLORS.dim}Regenerate preseed documents${utils.COLORS.reset}
805
+ `);
806
+
807
+ const engine = new PreseedEngine(projectRoot, { preset });
808
+
809
+ if (!engine.loadConfig()) {
810
+ utils.print.error('No preseed configuration found');
811
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed init${utils.COLORS.reset} first`);
812
+ return;
813
+ }
814
+
815
+ const spinner = utils.createSpinner('Generating documents...').start();
816
+
817
+ try {
818
+ if (doc) {
819
+ // Generate single document
820
+ if (!DOCUMENT_TYPES[doc]) {
821
+ spinner.fail(`Unknown document type: ${doc}`);
822
+ console.log(`\nAvailable types: ${Object.keys(DOCUMENT_TYPES).join(', ')}`);
823
+ return;
824
+ }
825
+
826
+ const content = engine.generateDocument(doc);
827
+ const filePath = path.join(engine.outputDir, DOCUMENT_TYPES[doc].name);
828
+ fs.writeFileSync(filePath, content);
829
+ spinner.succeed(`Generated ${DOCUMENT_TYPES[doc].name}`);
830
+ } else {
831
+ // Generate all documents
832
+ const results = await engine.generateAll();
833
+ spinner.succeed(`Generated ${results.length} documents`);
834
+
835
+ console.log(`\n${utils.COLORS.bold}Documents:${utils.COLORS.reset}`);
836
+ for (const docResult of results) {
837
+ console.log(` ${utils.COLORS.green}+${utils.COLORS.reset} ${docResult.title}`);
838
+ }
839
+ }
840
+ } catch (error) {
841
+ spinner.fail(`Generation failed: ${(error as Error).message}`);
842
+ }
843
+ }
844
+
845
+ /**
846
+ * Sync documents with codebase
847
+ */
848
+ async function preseedSync(args: ParsedArgs): Promise<void> {
849
+ const utils = getUtils();
850
+ const config = getConfig();
851
+ const { PreseedEngine } = getPreseedEngineModule();
852
+
853
+ const projectRoot = config.findProjectRoot();
854
+ const preset = (args.preset || 'startup') as string;
855
+
856
+ console.log(`
857
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Sync${utils.COLORS.reset}
858
+ ${utils.COLORS.dim}Sync documents with latest configuration${utils.COLORS.reset}
859
+ `);
860
+
861
+ const engine = new PreseedEngine(projectRoot, { preset });
862
+
863
+ if (!engine.loadConfig()) {
864
+ utils.print.error('No preseed configuration found');
865
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed init${utils.COLORS.reset} first`);
866
+ return;
867
+ }
868
+
869
+ const spinner = utils.createSpinner('Syncing documents...').start();
870
+
871
+ try {
872
+ const result = await engine.sync();
873
+ spinner.succeed(`Synced ${result.synced} documents`);
874
+
875
+ console.log(`
876
+ ${utils.COLORS.bold}Synced Documents:${utils.COLORS.reset}`);
877
+ for (const doc of result.documents) {
878
+ console.log(` ${utils.COLORS.green}✓${utils.COLORS.reset} ${doc.title}`);
879
+ }
880
+ } catch (error) {
881
+ spinner.fail(`Sync failed: ${(error as Error).message}`);
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Show preseed status
887
+ */
888
+ function preseedStatus(args: ParsedArgs): void {
889
+ const utils = getUtils();
890
+ const config = getConfig();
891
+ const { PreseedEngine } = getPreseedEngineModule();
892
+
893
+ const projectRoot = config.findProjectRoot();
894
+ const preset = (args.preset || 'startup') as string;
895
+
896
+ console.log(`
897
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Status${utils.COLORS.reset}
898
+ `);
899
+
900
+ const engine = new PreseedEngine(projectRoot, { preset });
901
+ const status = engine.getStatus();
902
+
903
+ if (!status.initialized) {
904
+ console.log(`${utils.COLORS.yellow}○${utils.COLORS.reset} Not initialized`);
905
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed init${utils.COLORS.reset} to get started`);
906
+ return;
907
+ }
908
+
909
+ // Show status based on init mode
910
+ if (status.initMode === 'merged') {
911
+ console.log(`${utils.COLORS.cyan}⊕${utils.COLORS.reset} ${status.projectName}`);
912
+ console.log(`${utils.COLORS.dim}Mode: Merged documents (no config file)${utils.COLORS.reset}`);
913
+ } else {
914
+ console.log(`${utils.COLORS.green}✓${utils.COLORS.reset} ${status.projectName}`);
915
+ console.log(`${utils.COLORS.dim}Preset: ${status.preset}${utils.COLORS.reset}`);
916
+ if (status.lastSync) {
917
+ console.log(`${utils.COLORS.dim}Last sync: ${status.lastSync}${utils.COLORS.reset}`);
918
+ }
919
+ }
920
+ console.log(`${utils.COLORS.dim}Output: ${status.outputDir}${utils.COLORS.reset}`);
921
+
922
+ console.log(`\n${utils.COLORS.bold}Documents:${utils.COLORS.reset}`);
923
+
924
+ for (const doc of status.documents) {
925
+ const icon = doc.exists
926
+ ? `${utils.COLORS.green}✓${utils.COLORS.reset}`
927
+ : doc.required
928
+ ? `${utils.COLORS.red}✗${utils.COLORS.reset}`
929
+ : `${utils.COLORS.yellow}○${utils.COLORS.reset}`;
930
+
931
+ const modified = doc.modified
932
+ ? ` ${utils.COLORS.dim}(${doc.modified.split('T')[0]})${utils.COLORS.reset}`
933
+ : '';
934
+
935
+ console.log(` ${icon} ${doc.title}${modified}`);
936
+ }
937
+
938
+ const existing = status.documents.filter(d => d.exists).length;
939
+ const total = status.documents.length;
940
+ console.log(`\n${utils.COLORS.bold}Progress:${utils.COLORS.reset} ${existing}/${total} documents`);
941
+
942
+ // Show next steps for merged mode
943
+ if (status.initMode === 'merged' && existing > 0) {
944
+ console.log(`\n${utils.COLORS.bold}Next steps:${utils.COLORS.reset}`);
945
+ console.log(` ${utils.COLORS.cyan}bootspring seed synthesize${utils.COLORS.reset} # Generate SEED.md`);
946
+ console.log(` ${utils.COLORS.cyan}bootspring seed scaffold${utils.COLORS.reset} # Generate project`);
947
+ }
948
+ }
949
+
950
+ /**
951
+ * Update config value
952
+ */
953
+ async function preseedUpdate(args: ParsedArgs): Promise<void> {
954
+ const utils = getUtils();
955
+ const config = getConfig();
956
+ const { PreseedEngine } = getPreseedEngineModule();
957
+
958
+ const projectRoot = config.findProjectRoot();
959
+ const configPath = (args._ as string[])[0];
960
+ const value = (args._ as string[])[1];
961
+
962
+ if (!configPath) {
963
+ utils.print.error('Please specify a config path');
964
+ console.log(`\nUsage: ${utils.COLORS.cyan}bootspring preseed update <path> <value>${utils.COLORS.reset}`);
965
+ console.log('\nExamples:');
966
+ console.log(' bootspring preseed update identity.name "My App"');
967
+ console.log(' bootspring preseed update problem.statement "Description..."');
968
+ return;
969
+ }
970
+
971
+ const engine = new PreseedEngine(projectRoot);
972
+
973
+ if (!engine.loadConfig()) {
974
+ utils.print.error('No preseed configuration found');
975
+ return;
976
+ }
977
+
978
+ if (value) {
979
+ engine.updateConfig(configPath, value);
980
+ utils.print.success(`Updated ${configPath}`);
981
+
982
+ // Ask if user wants to regenerate
983
+ console.log(`\n${utils.COLORS.dim}Run ${utils.COLORS.cyan}bootspring preseed sync${utils.COLORS.reset}${utils.COLORS.dim} to regenerate documents${utils.COLORS.reset}`);
984
+ } else {
985
+ // Show current value
986
+ const parts = configPath.split('.');
987
+ let current: unknown = engine.config;
988
+ for (const part of parts) {
989
+ current = (current as Record<string, unknown>)?.[part];
990
+ }
991
+
992
+ console.log(`\n${utils.COLORS.bold}${configPath}:${utils.COLORS.reset}`);
993
+ if (typeof current === 'object') {
994
+ console.log(JSON.stringify(current, null, 2));
995
+ } else {
996
+ console.log(current || '(not set)');
997
+ }
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Export preseed config
1003
+ */
1004
+ function preseedExport(args: ParsedArgs): void {
1005
+ const utils = getUtils();
1006
+ const config = getConfig();
1007
+ const yaml = getYaml();
1008
+ const { PreseedEngine } = getPreseedEngineModule();
1009
+
1010
+ const projectRoot = config.findProjectRoot();
1011
+ const format = (args.format || args.f || 'json') as string;
1012
+ const output = (args.output || args.o) as string | undefined;
1013
+
1014
+ const engine = new PreseedEngine(projectRoot);
1015
+
1016
+ if (!engine.loadConfig()) {
1017
+ utils.print.error('No preseed configuration found');
1018
+ return;
1019
+ }
1020
+
1021
+ let exported: string;
1022
+ if (format === 'yaml' || format === 'yml') {
1023
+ exported = yaml.stringify(engine.config);
1024
+ } else {
1025
+ exported = JSON.stringify(engine.config, null, 2);
1026
+ }
1027
+
1028
+ if (output) {
1029
+ const outputPath = path.resolve(projectRoot, output);
1030
+ fs.writeFileSync(outputPath, exported);
1031
+ utils.print.success(`Exported to ${outputPath}`);
1032
+ } else {
1033
+ console.log(exported);
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Pull preseed documents from dashboard
1039
+ */
1040
+ async function preseedPull(args: ParsedArgs): Promise<void> {
1041
+ const utils = getUtils();
1042
+ const config = getConfig();
1043
+ const auth = getAuth();
1044
+ const session = getSession();
1045
+ const apiClient = getApiClient();
1046
+ const { createInterface, askChoice } = getInteractive();
1047
+
1048
+ const projectRoot = config.findProjectRoot();
1049
+ const projectId = (args.project || args.p) as string | undefined;
1050
+ const docName = (args.doc || args.d) as string | undefined;
1051
+ const force = args.force || args.f;
1052
+
1053
+ console.log(`
1054
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Pull${utils.COLORS.reset}
1055
+ ${utils.COLORS.dim}Sync preseed documents from dashboard${utils.COLORS.reset}
1056
+ `);
1057
+
1058
+ // Check authentication
1059
+ if (!auth.isAuthenticated()) {
1060
+ utils.print.error('Authentication required');
1061
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset} first`);
1062
+ return;
1063
+ }
1064
+
1065
+ // Get project ID
1066
+ let targetProjectId = projectId;
1067
+
1068
+ if (!targetProjectId) {
1069
+ // Try to get from session or local config
1070
+ const currentProject = session.getEffectiveProject();
1071
+ if (currentProject?.id) {
1072
+ targetProjectId = currentProject.id;
1073
+ console.log(`${utils.COLORS.dim}Using project: ${currentProject.name || targetProjectId}${utils.COLORS.reset}\n`);
1074
+ } else {
1075
+ // Try to find project by name from local config
1076
+ const localConfig = config.loadConfig(projectRoot);
1077
+ if (localConfig?.project?.id) {
1078
+ targetProjectId = localConfig.project.id;
1079
+ } else if (localConfig?.project?.name) {
1080
+ // Look up project by name
1081
+ const spinner = utils.createSpinner('Finding project...').start();
1082
+ try {
1083
+ const projects = await apiClient.listProjects();
1084
+ const match = projects.projects?.find(p =>
1085
+ p.name.toLowerCase() === (localConfig.project?.name || '').toLowerCase() ||
1086
+ p.slug === (localConfig.project?.name || '').toLowerCase()
1087
+ );
1088
+ if (match) {
1089
+ targetProjectId = match.id;
1090
+ spinner.succeed(`Found project: ${match.name}`);
1091
+ } else {
1092
+ spinner.fail('Project not found');
1093
+ console.log(`\nSpecify project with ${utils.COLORS.cyan}--project=<id>${utils.COLORS.reset}`);
1094
+ return;
1095
+ }
1096
+ } catch (error) {
1097
+ spinner.fail(`Failed to find project: ${(error as Error).message}`);
1098
+ return;
1099
+ }
1100
+ }
1101
+ }
1102
+ }
1103
+
1104
+ if (!targetProjectId) {
1105
+ utils.print.error('No project specified');
1106
+ console.log(`
1107
+ ${utils.COLORS.bold}Options:${utils.COLORS.reset}
1108
+ 1. Specify project: ${utils.COLORS.cyan}bootspring preseed pull --project=<id>${utils.COLORS.reset}
1109
+ 2. Set current project: ${utils.COLORS.cyan}bootspring project use <id>${utils.COLORS.reset}
1110
+ 3. Run from a linked project directory
1111
+ `);
1112
+ return;
1113
+ }
1114
+
1115
+ // Set up output directory
1116
+ const outputDir = path.join(projectRoot, '.bootspring', 'preseed');
1117
+ if (!fs.existsSync(outputDir)) {
1118
+ fs.mkdirSync(outputDir, { recursive: true });
1119
+ }
1120
+
1121
+ // Pull single document or all
1122
+ if (docName) {
1123
+ // Pull single document
1124
+ const spinner = utils.createSpinner(`Fetching ${docName}...`).start();
1125
+
1126
+ try {
1127
+ const result = await apiClient.getPreseedDocument(targetProjectId, docName);
1128
+ const filename = docName.endsWith('.md') ? docName : `${docName}.md`;
1129
+ const filePath = path.join(outputDir, filename);
1130
+
1131
+ // Check for existing file
1132
+ if (fs.existsSync(filePath) && !force) {
1133
+ const existingContent = fs.readFileSync(filePath, 'utf-8');
1134
+ if (existingContent === result.content) {
1135
+ spinner.succeed(`${docName} is already up to date`);
1136
+ return;
1137
+ }
1138
+
1139
+ spinner.stop();
1140
+ const rl = createInterface();
1141
+ const overwrite = await askChoice(rl, `${filename} exists locally. Overwrite?`, [
1142
+ { label: 'Yes, overwrite', value: 'yes' },
1143
+ { label: 'No, keep local', value: 'no' },
1144
+ { label: 'Show diff', value: 'diff' }
1145
+ ]);
1146
+ rl.close();
1147
+
1148
+ if (overwrite === 'no') {
1149
+ console.log(`${utils.COLORS.dim}Skipped ${filename}${utils.COLORS.reset}`);
1150
+ return;
1151
+ }
1152
+
1153
+ if (overwrite === 'diff') {
1154
+ console.log(`\n${utils.COLORS.bold}Remote content:${utils.COLORS.reset}`);
1155
+ console.log(`${utils.COLORS.dim}${'─'.repeat(60)}${utils.COLORS.reset}`);
1156
+ console.log(result.content.slice(0, 500) + (result.content.length > 500 ? '...' : ''));
1157
+ console.log(`${utils.COLORS.dim}${'─'.repeat(60)}${utils.COLORS.reset}`);
1158
+ return;
1159
+ }
1160
+
1161
+ spinner.start();
1162
+ spinner.text = 'Writing file...';
1163
+ }
1164
+
1165
+ fs.writeFileSync(filePath, result.content, 'utf-8');
1166
+ spinner.succeed(`Saved ${filename}`);
1167
+ } catch (error) {
1168
+ spinner.fail(`Failed to fetch ${docName}: ${(error as Error).message}`);
1169
+ }
1170
+ } else {
1171
+ // Pull all documents
1172
+ const spinner = utils.createSpinner('Fetching preseed documents...').start();
1173
+
1174
+ try {
1175
+ // First, list available documents
1176
+ const listResult = await apiClient.listPreseedDocuments(targetProjectId);
1177
+
1178
+ if (!listResult.documents || listResult.documents.length === 0) {
1179
+ spinner.warn('No preseed documents found in dashboard');
1180
+ console.log(`
1181
+ ${utils.COLORS.dim}Generate documents in the dashboard first:${utils.COLORS.reset}
1182
+ ${utils.COLORS.cyan}https://bootspring.com/dashboard/projects/${targetProjectId}/preseed/studio${utils.COLORS.reset}
1183
+ `);
1184
+ return;
1185
+ }
1186
+
1187
+ spinner.text = `Found ${listResult.documents.length} documents. Downloading...`;
1188
+
1189
+ // Download each document
1190
+ let downloaded = 0;
1191
+ let skipped = 0;
1192
+ const errors: { doc: string; error: string }[] = [];
1193
+
1194
+ for (const doc of listResult.documents) {
1195
+ try {
1196
+ const result = await apiClient.getPreseedDocument(targetProjectId, doc.name);
1197
+ const filename = doc.name.endsWith('.md') ? doc.name : `${doc.name}.md`;
1198
+ const filePath = path.join(outputDir, filename);
1199
+
1200
+ // Check if file exists and is different
1201
+ if (fs.existsSync(filePath) && !force) {
1202
+ const existingContent = fs.readFileSync(filePath, 'utf-8');
1203
+ if (existingContent === result.content) {
1204
+ skipped++;
1205
+ continue;
1206
+ }
1207
+ }
1208
+
1209
+ fs.writeFileSync(filePath, result.content, 'utf-8');
1210
+ downloaded++;
1211
+ } catch (err) {
1212
+ errors.push({ doc: doc.name, error: (err as Error).message });
1213
+ }
1214
+ }
1215
+
1216
+ // Also try to get PRESEED_CONFIG.json
1217
+ try {
1218
+ const configResult = await apiClient.getPreseedDocument(targetProjectId, 'PRESEED_CONFIG.json');
1219
+ const configPath = path.join(outputDir, 'PRESEED_CONFIG.json');
1220
+ fs.writeFileSync(configPath, configResult.content, 'utf-8');
1221
+ downloaded++;
1222
+ } catch {
1223
+ // Config may not exist, that's ok
1224
+ }
1225
+
1226
+ if (downloaded > 0 || skipped > 0) {
1227
+ spinner.succeed(`Downloaded ${downloaded} documents${skipped > 0 ? `, ${skipped} already up to date` : ''}`);
1228
+ } else {
1229
+ spinner.warn('No documents to download');
1230
+ }
1231
+
1232
+ if (errors.length > 0) {
1233
+ console.log(`\n${utils.COLORS.yellow}Some documents failed:${utils.COLORS.reset}`);
1234
+ for (const err of errors) {
1235
+ console.log(` ${utils.COLORS.red}✗${utils.COLORS.reset} ${err.doc}: ${err.error}`);
1236
+ }
1237
+ }
1238
+
1239
+ // Show summary
1240
+ console.log(`
1241
+ ${utils.COLORS.bold}Documents saved to:${utils.COLORS.reset} ${utils.COLORS.cyan}${outputDir}${utils.COLORS.reset}
1242
+
1243
+ ${utils.COLORS.bold}Next steps:${utils.COLORS.reset}
1244
+ ${utils.COLORS.dim}•${utils.COLORS.reset} Review documents in ${utils.COLORS.cyan}.bootspring/preseed/${utils.COLORS.reset}
1245
+ ${utils.COLORS.dim}•${utils.COLORS.reset} Run ${utils.COLORS.cyan}bootspring preseed status${utils.COLORS.reset} to see document status
1246
+ ${utils.COLORS.dim}•${utils.COLORS.reset} Run ${utils.COLORS.cyan}bootspring seed init${utils.COLORS.reset} to scaffold project
1247
+ `);
1248
+ } catch (error) {
1249
+ const err = error as { message: string; status?: number };
1250
+ spinner.fail(`Failed to fetch documents: ${err.message}`);
1251
+
1252
+ if (err.status === 404) {
1253
+ console.log(`
1254
+ ${utils.COLORS.dim}Project not found or no access. Check:${utils.COLORS.reset}
1255
+ ${utils.COLORS.dim}•${utils.COLORS.reset} Project ID is correct
1256
+ ${utils.COLORS.dim}•${utils.COLORS.reset} You have access to this project
1257
+ ${utils.COLORS.dim}•${utils.COLORS.reset} Run ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset} to refresh auth
1258
+ `);
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ /**
1265
+ * Push preseed documents to dashboard
1266
+ */
1267
+ async function preseedPush(args: ParsedArgs): Promise<void> {
1268
+ const utils = getUtils();
1269
+ const config = getConfig();
1270
+ const auth = getAuth();
1271
+ const session = getSession();
1272
+ const apiClient = getApiClient();
1273
+
1274
+ const projectRoot = config.findProjectRoot();
1275
+ const projectId = (args.project || args.p) as string | undefined;
1276
+ const docName = (args.doc || args.d) as string | undefined;
1277
+
1278
+ console.log(`
1279
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Push${utils.COLORS.reset}
1280
+ ${utils.COLORS.dim}Upload preseed documents to dashboard${utils.COLORS.reset}
1281
+ `);
1282
+
1283
+ // Check authentication
1284
+ if (!auth.isAuthenticated()) {
1285
+ utils.print.error('Authentication required');
1286
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring auth login${utils.COLORS.reset} first`);
1287
+ return;
1288
+ }
1289
+
1290
+ // Get project ID (same logic as pull)
1291
+ let targetProjectId = projectId;
1292
+
1293
+ if (!targetProjectId) {
1294
+ const currentProject = session.getEffectiveProject();
1295
+ if (currentProject?.id) {
1296
+ targetProjectId = currentProject.id;
1297
+ console.log(`${utils.COLORS.dim}Using project: ${currentProject.name || targetProjectId}${utils.COLORS.reset}\n`);
1298
+ } else {
1299
+ const localConfig = config.loadConfig(projectRoot);
1300
+ if (localConfig?.project?.id) {
1301
+ targetProjectId = localConfig.project.id;
1302
+ }
1303
+ }
1304
+ }
1305
+
1306
+ if (!targetProjectId) {
1307
+ utils.print.error('No project specified');
1308
+ console.log(`\nSpecify project with ${utils.COLORS.cyan}--project=<id>${utils.COLORS.reset}`);
1309
+ return;
1310
+ }
1311
+
1312
+ const preseedDir = path.join(projectRoot, '.bootspring', 'preseed');
1313
+ if (!fs.existsSync(preseedDir)) {
1314
+ utils.print.error('No local preseed documents found');
1315
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed init${utils.COLORS.reset} first`);
1316
+ return;
1317
+ }
1318
+
1319
+ const spinner = utils.createSpinner('Uploading preseed documents...').start();
1320
+
1321
+ try {
1322
+ // Get list of local documents
1323
+ const files = fs.readdirSync(preseedDir).filter(f =>
1324
+ f.endsWith('.md') || f === 'PRESEED_CONFIG.json'
1325
+ );
1326
+
1327
+ if (files.length === 0) {
1328
+ spinner.warn('No documents to upload');
1329
+ return;
1330
+ }
1331
+
1332
+ let uploaded = 0;
1333
+ const errors: { file: string; error: string }[] = [];
1334
+
1335
+ for (const file of files) {
1336
+ if (docName && !file.startsWith(docName)) continue;
1337
+
1338
+ const filePath = path.join(preseedDir, file);
1339
+ const content = fs.readFileSync(filePath, 'utf-8');
1340
+ const name = file.replace('.md', '');
1341
+
1342
+ try {
1343
+ // Use direct request to PUT documents
1344
+ await apiClient.directRequest('PUT', `/projects/${targetProjectId}/preseed/documents`, {
1345
+ name,
1346
+ content
1347
+ });
1348
+ uploaded++;
1349
+ } catch (err) {
1350
+ errors.push({ file, error: (err as Error).message });
1351
+ }
1352
+ }
1353
+
1354
+ if (uploaded > 0) {
1355
+ spinner.succeed(`Uploaded ${uploaded} documents`);
1356
+ } else {
1357
+ spinner.warn('No documents uploaded');
1358
+ }
1359
+
1360
+ if (errors.length > 0) {
1361
+ console.log(`\n${utils.COLORS.yellow}Some uploads failed:${utils.COLORS.reset}`);
1362
+ for (const err of errors) {
1363
+ console.log(` ${utils.COLORS.red}✗${utils.COLORS.reset} ${err.file}: ${err.error}`);
1364
+ }
1365
+ }
1366
+ } catch (error) {
1367
+ spinner.fail(`Upload failed: ${(error as Error).message}`);
1368
+ }
1369
+ }
1370
+
1371
+ interface MergeTask {
1372
+ docType: string;
1373
+ outputFile: string;
1374
+ outputPath: string;
1375
+ title: string;
1376
+ sources: { filename: string; content: string; path: string }[];
1377
+ }
1378
+
1379
+ interface MergeSettings {
1380
+ strategy: string;
1381
+ conflictResolution: string;
1382
+ includeChangelog: boolean;
1383
+ }
1384
+
1385
+ /**
1386
+ * Execute document merges from context folders
1387
+ */
1388
+ async function preseedMerge(_args: ParsedArgs): Promise<void> {
1389
+ const utils = getUtils();
1390
+ const config = getConfig();
1391
+
1392
+ const projectRoot = config.findProjectRoot();
1393
+ const preseedDir = path.join(projectRoot, '.bootspring', 'preseed');
1394
+ const contextDir = path.join(preseedDir, 'context');
1395
+ const manifestPath = path.join(preseedDir, 'MERGE_MANIFEST.json');
1396
+
1397
+ console.log(`
1398
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Merge${utils.COLORS.reset}
1399
+ ${utils.COLORS.dim}Merging source documents from context folders${utils.COLORS.reset}
1400
+ `);
1401
+
1402
+ // Check if context folder exists
1403
+ if (!fs.existsSync(contextDir)) {
1404
+ utils.print.error('No context folder found');
1405
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed setup${utils.COLORS.reset} first`);
1406
+ return;
1407
+ }
1408
+
1409
+ // Document type mappings
1410
+ const docMappings: Record<string, { output: string; folder: string; title: string }> = {
1411
+ 'vision': { output: 'VISION.md', folder: 'vision', title: 'Vision Document' },
1412
+ 'audience': { output: 'AUDIENCE.md', folder: 'audience', title: 'Target Audience' },
1413
+ 'market': { output: 'MARKET.md', folder: 'market', title: 'Market Analysis' },
1414
+ 'competitors': { output: 'COMPETITORS.md', folder: 'competitors', title: 'Competitive Analysis' },
1415
+ 'business': { output: 'BUSINESS_MODEL.md', folder: 'business', title: 'Business Model' },
1416
+ 'prd': { output: 'PRD.md', folder: 'prd', title: 'Product Requirements Document' },
1417
+ 'technical': { output: 'TECHNICAL_SPEC.md', folder: 'technical', title: 'Technical Specification' },
1418
+ 'roadmap': { output: 'ROADMAP.md', folder: 'roadmap', title: 'Product Roadmap' }
1419
+ };
1420
+
1421
+ // Detect what needs to be merged
1422
+ const mergeTasks: MergeTask[] = [];
1423
+
1424
+ for (const [docType, mapping] of Object.entries(docMappings)) {
1425
+ const folderPath = path.join(contextDir, mapping.folder);
1426
+ if (!fs.existsSync(folderPath)) continue;
1427
+
1428
+ const files = fs.readdirSync(folderPath).filter(f =>
1429
+ !f.startsWith('.') && (f.endsWith('.md') || f.endsWith('.txt'))
1430
+ );
1431
+
1432
+ if (files.length === 0) continue;
1433
+
1434
+ // Read all source files
1435
+ const sources: { filename: string; content: string; path: string }[] = [];
1436
+ for (const file of files) {
1437
+ const filePath = path.join(folderPath, file);
1438
+ try {
1439
+ const content = fs.readFileSync(filePath, 'utf-8');
1440
+ sources.push({ filename: file, content, path: filePath });
1441
+ } catch {
1442
+ console.log(` ${utils.COLORS.yellow}!${utils.COLORS.reset} Could not read ${file}`);
1443
+ }
1444
+ }
1445
+
1446
+ if (sources.length > 0) {
1447
+ mergeTasks.push({
1448
+ docType,
1449
+ outputFile: mapping.output,
1450
+ outputPath: path.join(preseedDir, mapping.output),
1451
+ title: mapping.title,
1452
+ sources
1453
+ });
1454
+ }
1455
+ }
1456
+
1457
+ if (mergeTasks.length === 0) {
1458
+ utils.print.warn('No source files found in context folders');
1459
+ console.log(`\nAdd files to ${utils.COLORS.cyan}.bootspring/preseed/context/${utils.COLORS.reset} folders`);
1460
+ return;
1461
+ }
1462
+
1463
+ // Check for existing manifest with settings
1464
+ let mergeSettings: MergeSettings = {
1465
+ strategy: 'conservative',
1466
+ conflictResolution: 'prefer-detailed',
1467
+ includeChangelog: true
1468
+ };
1469
+
1470
+ if (fs.existsSync(manifestPath)) {
1471
+ try {
1472
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { settings?: MergeSettings };
1473
+ if (manifest.settings) {
1474
+ mergeSettings = manifest.settings;
1475
+ }
1476
+ } catch {
1477
+ // Ignore parse errors
1478
+ }
1479
+ }
1480
+
1481
+ console.log(`${utils.COLORS.bold}Merge settings:${utils.COLORS.reset}`);
1482
+ console.log(` Strategy: ${utils.COLORS.cyan}${mergeSettings.strategy}${utils.COLORS.reset}`);
1483
+ console.log(` Conflicts: ${utils.COLORS.cyan}${mergeSettings.conflictResolution}${utils.COLORS.reset}`);
1484
+ console.log(` Changelog: ${utils.COLORS.cyan}${mergeSettings.includeChangelog ? 'Yes' : 'No'}${utils.COLORS.reset}`);
1485
+ console.log();
1486
+
1487
+ // Execute merges
1488
+ const spinner = utils.createSpinner('Merging documents...').start();
1489
+ let merged = 0;
1490
+ let copied = 0;
1491
+
1492
+ for (const task of mergeTasks) {
1493
+ try {
1494
+ let mergedContent: string;
1495
+
1496
+ if (task.sources.length === 1) {
1497
+ // Single file - copy directly with header
1498
+ const src = task.sources[0];
1499
+ if (src) {
1500
+ mergedContent = `<!-- Source: context/${task.docType}/${src.filename} -->\n\n${src.content}`;
1501
+ copied++;
1502
+ } else {
1503
+ continue;
1504
+ }
1505
+ } else {
1506
+ // Multiple files - merge them
1507
+ mergedContent = mergeDocuments(task, mergeSettings);
1508
+ merged++;
1509
+ }
1510
+
1511
+ // Write merged output
1512
+ fs.writeFileSync(task.outputPath, mergedContent);
1513
+ } catch (err) {
1514
+ spinner.fail(`Failed to merge ${task.outputFile}: ${(err as Error).message}`);
1515
+ return;
1516
+ }
1517
+ }
1518
+
1519
+ spinner.succeed(`Merged ${merged + copied} documents (${merged} merged, ${copied} copied)`);
1520
+
1521
+ // Create minimal PRESEED_CONFIG.json so status works
1522
+ const configPath = path.join(preseedDir, 'PRESEED_CONFIG.json');
1523
+ const minimalConfig = {
1524
+ _meta: {
1525
+ version: '1.0.0',
1526
+ created: new Date().toISOString(),
1527
+ updated: new Date().toISOString(),
1528
+ preset: 'merged',
1529
+ source: 'preseed-merge'
1530
+ },
1531
+ identity: {
1532
+ name: path.basename(projectRoot),
1533
+ tagline: '',
1534
+ description: 'Project created from merged preseed documents'
1535
+ },
1536
+ _sync: {
1537
+ lastSync: new Date().toISOString(),
1538
+ mergedDocuments: mergeTasks.map(t => ({
1539
+ type: t.docType,
1540
+ output: t.outputFile,
1541
+ sources: t.sources.map(s => s.filename),
1542
+ mergedAt: new Date().toISOString()
1543
+ }))
1544
+ }
1545
+ };
1546
+
1547
+ fs.writeFileSync(configPath, JSON.stringify(minimalConfig, null, 2));
1548
+
1549
+ // Show results
1550
+ console.log(`\n${utils.COLORS.bold}Created documents:${utils.COLORS.reset}\n`);
1551
+ for (const task of mergeTasks) {
1552
+ const icon = task.sources.length === 1
1553
+ ? `${utils.COLORS.green}✓${utils.COLORS.reset}`
1554
+ : `${utils.COLORS.cyan}⊕${utils.COLORS.reset}`;
1555
+ const action = task.sources.length === 1 ? 'copied' : `merged ${task.sources.length} sources`;
1556
+ console.log(` ${icon} ${task.outputFile} (${action})`);
1557
+ }
1558
+
1559
+ console.log(`\n${utils.COLORS.green}${utils.COLORS.bold}Merge complete!${utils.COLORS.reset}`);
1560
+ console.log(`\n${utils.COLORS.dim}Documents saved to: ${preseedDir}${utils.COLORS.reset}`);
1561
+ console.log(`\n${utils.COLORS.bold}Next steps:${utils.COLORS.reset}`);
1562
+ console.log(` ${utils.COLORS.cyan}bootspring seed synthesize${utils.COLORS.reset} # Generate SEED.md`);
1563
+ console.log(` ${utils.COLORS.cyan}bootspring seed scaffold${utils.COLORS.reset} # Generate project`);
1564
+ }
1565
+
1566
+ /**
1567
+ * Merge multiple source documents into one
1568
+ */
1569
+ function mergeDocuments(task: MergeTask, settings: MergeSettings): string {
1570
+ const sources = task.sources;
1571
+ const sections: string[] = [];
1572
+ const changelog = {
1573
+ sources: [] as string[],
1574
+ duplicatesRemoved: [] as string[],
1575
+ conflicts: [] as string[]
1576
+ };
1577
+
1578
+ // Build merged content
1579
+ sections.push(`# ${task.title}\n`);
1580
+ sections.push('**Merged Document** - Generated by Bootspring');
1581
+ sections.push(`**Date:** ${new Date().toISOString().split('T')[0]}`);
1582
+ sections.push(`**Sources:** ${sources.map(s => s.filename).join(', ')}\n`);
1583
+ sections.push('---\n');
1584
+
1585
+ // Track all content blocks we've seen (for deduplication)
1586
+ const seenContent = new Set<string>();
1587
+
1588
+ for (const source of sources) {
1589
+ changelog.sources.push(source.filename);
1590
+
1591
+ // Split content into paragraphs/blocks
1592
+ const blocks = source.content.split(/\n\n+/);
1593
+
1594
+ for (const block of blocks) {
1595
+ const trimmedBlock = block.trim();
1596
+ if (!trimmedBlock) continue;
1597
+
1598
+ // Normalize for comparison (remove extra whitespace)
1599
+ const normalized = trimmedBlock.replace(/\s+/g, ' ').toLowerCase();
1600
+
1601
+ // Check if we've seen this exact content
1602
+ if (seenContent.has(normalized)) {
1603
+ changelog.duplicatesRemoved.push(`Duplicate block from ${source.filename}`);
1604
+ continue;
1605
+ }
1606
+
1607
+ seenContent.add(normalized);
1608
+ sections.push(trimmedBlock);
1609
+ sections.push(''); // Add spacing
1610
+ }
1611
+ }
1612
+
1613
+ // Add changelog if requested
1614
+ if (settings.includeChangelog) {
1615
+ sections.push('\n---\n');
1616
+ sections.push('## Merge Changelog\n');
1617
+ sections.push(`**Merge Strategy:** ${settings.strategy}`);
1618
+ sections.push(`**Conflict Resolution:** ${settings.conflictResolution}\n`);
1619
+
1620
+ sections.push('### Sources Used');
1621
+ for (const src of changelog.sources) {
1622
+ sections.push(`- ${src}`);
1623
+ }
1624
+
1625
+ if (changelog.duplicatesRemoved.length > 0) {
1626
+ sections.push('\n### Duplicates Removed');
1627
+ sections.push(`- ${changelog.duplicatesRemoved.length} duplicate blocks removed`);
1628
+ }
1629
+
1630
+ sections.push('\n---');
1631
+ sections.push('*Merged by Bootspring CLI*');
1632
+ }
1633
+
1634
+ return sections.join('\n');
1635
+ }
1636
+
1637
+ // =============================================================================
1638
+ // Workflow Commands
1639
+ // =============================================================================
1640
+
1641
+ /**
1642
+ * Start a new workflow or resume existing
1643
+ */
1644
+ async function workflowStart(_args: ParsedArgs): Promise<void> {
1645
+ const utils = getUtils();
1646
+ const config = getConfig();
1647
+ const { PreseedWorkflowEngine } = getPreseedWorkflowModule();
1648
+ const { createInterface, askChoice } = getInteractive();
1649
+
1650
+ const projectRoot = config.findProjectRoot();
1651
+ const workflow = new PreseedWorkflowEngine(projectRoot);
1652
+
1653
+ console.log(`
1654
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Workflow${utils.COLORS.reset}
1655
+ ${utils.COLORS.dim}Professional-grade document approval workflow${utils.COLORS.reset}
1656
+ `);
1657
+
1658
+ // Check for existing workflow
1659
+ if (workflow.hasWorkflow()) {
1660
+ workflow.loadState();
1661
+ const resume = workflow.getResumePoint();
1662
+
1663
+ if (resume) {
1664
+ console.log(`${utils.COLORS.yellow}Existing workflow found${utils.COLORS.reset}`);
1665
+ console.log(`${utils.COLORS.dim}Last updated: ${utils.formatRelativeTime(new Date(resume.lastUpdated))}${utils.COLORS.reset}`);
1666
+ console.log(`${utils.COLORS.dim}Current: ${resume.phaseName} → ${resume.documentName}${utils.COLORS.reset}\n`);
1667
+
1668
+ const rl = createInterface();
1669
+ const choice = await askChoice(rl, 'What would you like to do?', [
1670
+ { label: 'Resume workflow', value: 'resume' },
1671
+ { label: 'View status', value: 'status' },
1672
+ { label: 'Start fresh (reset)', value: 'reset' }
1673
+ ]);
1674
+ rl.close();
1675
+
1676
+ if (choice === 'resume') {
1677
+ return runWorkflowLoop(workflow);
1678
+ } else if (choice === 'status') {
1679
+ return workflowStatus(_args);
1680
+ } else if (choice === 'reset') {
1681
+ workflow.resetWorkflow();
1682
+ console.log(`${utils.COLORS.green}✓${utils.COLORS.reset} Workflow reset\n`);
1683
+ }
1684
+ }
1685
+ }
1686
+
1687
+ // Initialize new workflow
1688
+ const spinner = utils.createSpinner('Initializing workflow...').start();
1689
+ workflow.initializeWorkflow();
1690
+ spinner.succeed('Workflow initialized');
1691
+
1692
+ // Run the workflow loop
1693
+ return runWorkflowLoop(workflow);
1694
+ }
1695
+
1696
+ /**
1697
+ * Resume an existing workflow
1698
+ */
1699
+ async function workflowResume(_args: ParsedArgs): Promise<void> {
1700
+ const utils = getUtils();
1701
+ const config = getConfig();
1702
+ const { PreseedWorkflowEngine } = getPreseedWorkflowModule();
1703
+
1704
+ const projectRoot = config.findProjectRoot();
1705
+ const workflow = new PreseedWorkflowEngine(projectRoot);
1706
+
1707
+ console.log(`
1708
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed Workflow${utils.COLORS.reset}
1709
+ ${utils.COLORS.dim}Resuming workflow...${utils.COLORS.reset}
1710
+ `);
1711
+
1712
+ if (!workflow.hasWorkflow()) {
1713
+ utils.print.error('No workflow found');
1714
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed workflow start${utils.COLORS.reset} to begin`);
1715
+ return;
1716
+ }
1717
+
1718
+ workflow.loadState();
1719
+ const resume = workflow.getResumePoint();
1720
+
1721
+ if (!resume) {
1722
+ utils.print.success('Workflow is complete!');
1723
+ return;
1724
+ }
1725
+
1726
+ console.log(`${utils.COLORS.bold}Resuming:${utils.COLORS.reset} ${resume.phaseName} → ${resume.documentName}`);
1727
+ console.log(`${utils.COLORS.dim}Status: ${resume.documentStatus}${utils.COLORS.reset}\n`);
1728
+
1729
+ return runWorkflowLoop(workflow);
1730
+ }
1731
+
1732
+ /**
1733
+ * Show workflow status
1734
+ */
1735
+ async function workflowStatus(_args: ParsedArgs): Promise<void> {
1736
+ const utils = getUtils();
1737
+ const config = getConfig();
1738
+ const { PreseedWorkflowEngine, DOCUMENT_STATUS } = getPreseedWorkflowModule();
1739
+
1740
+ const projectRoot = config.findProjectRoot();
1741
+ const workflow = new PreseedWorkflowEngine(projectRoot);
1742
+
1743
+ console.log(`
1744
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Preseed Workflow Status${utils.COLORS.reset}
1745
+ `);
1746
+
1747
+ if (!workflow.hasWorkflow()) {
1748
+ console.log(`${utils.COLORS.yellow}○${utils.COLORS.reset} No workflow started`);
1749
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed workflow start${utils.COLORS.reset} to begin`);
1750
+ return;
1751
+ }
1752
+
1753
+ workflow.loadState();
1754
+ const progress = workflow.getProgress();
1755
+
1756
+ if (!progress) {
1757
+ utils.print.error('Could not load workflow state');
1758
+ return;
1759
+ }
1760
+
1761
+ // Overall progress bar
1762
+ const barWidth = 30;
1763
+ const filledWidth = Math.round((progress.overall.percentage / 100) * barWidth);
1764
+ const progressBar = '█'.repeat(filledWidth) + '░'.repeat(barWidth - filledWidth);
1765
+ console.log(`${utils.COLORS.bold}Overall Progress:${utils.COLORS.reset} [${utils.COLORS.green}${progressBar}${utils.COLORS.reset}] ${progress.overall.percentage}%`);
1766
+ console.log(`${utils.COLORS.dim}${progress.overall.approved}/${progress.overall.total} documents approved${utils.COLORS.reset}\n`);
1767
+
1768
+ // Phase details
1769
+ console.log(`${utils.COLORS.bold}Phases:${utils.COLORS.reset}`);
1770
+
1771
+ for (const phase of progress.phases) {
1772
+ let statusIcon: string;
1773
+ let statusColor = utils.COLORS.reset;
1774
+
1775
+ if (phase.status === 'completed') {
1776
+ statusIcon = `${utils.COLORS.green}✓${utils.COLORS.reset}`;
1777
+ } else if (phase.id === progress.currentPhase) {
1778
+ statusIcon = `${utils.COLORS.cyan}▶${utils.COLORS.reset}`;
1779
+ statusColor = utils.COLORS.cyan;
1780
+ } else if (!phase.dependenciesMet) {
1781
+ statusIcon = `${utils.COLORS.dim}○${utils.COLORS.reset}`;
1782
+ statusColor = utils.COLORS.dim;
1783
+ } else {
1784
+ statusIcon = `${utils.COLORS.yellow}○${utils.COLORS.reset}`;
1785
+ }
1786
+
1787
+ console.log(`\n ${statusIcon} ${statusColor}${phase.name}${utils.COLORS.reset} ${utils.COLORS.dim}(${phase.progress.approved}/${phase.progress.total})${utils.COLORS.reset}`);
1788
+
1789
+ for (const doc of phase.documents) {
1790
+ let docIcon: string;
1791
+ let docColor = utils.COLORS.reset;
1792
+
1793
+ switch (doc.status) {
1794
+ case DOCUMENT_STATUS.APPROVED:
1795
+ case DOCUMENT_STATUS.LOCKED:
1796
+ docIcon = `${utils.COLORS.green}✓${utils.COLORS.reset}`;
1797
+ break;
1798
+ case DOCUMENT_STATUS.IN_REVIEW:
1799
+ docIcon = `${utils.COLORS.yellow}◎${utils.COLORS.reset}`;
1800
+ docColor = utils.COLORS.yellow;
1801
+ break;
1802
+ case DOCUMENT_STATUS.DRAFT:
1803
+ docIcon = `${utils.COLORS.blue}◇${utils.COLORS.reset}`;
1804
+ docColor = utils.COLORS.blue;
1805
+ break;
1806
+ case DOCUMENT_STATUS.REJECTED:
1807
+ docIcon = `${utils.COLORS.red}✗${utils.COLORS.reset}`;
1808
+ docColor = utils.COLORS.red;
1809
+ break;
1810
+ default:
1811
+ docIcon = `${utils.COLORS.dim}○${utils.COLORS.reset}`;
1812
+ docColor = utils.COLORS.dim;
1813
+ }
1814
+
1815
+ const qualityBadge = doc.qualityScore !== null
1816
+ ? ` ${utils.COLORS.dim}[${doc.qualityScore}%]${utils.COLORS.reset}`
1817
+ : '';
1818
+
1819
+ const currentMarker = doc.type === progress.currentDocument
1820
+ ? ` ${utils.COLORS.cyan}← current${utils.COLORS.reset}`
1821
+ : '';
1822
+
1823
+ console.log(` ${docIcon} ${docColor}${doc.name}${utils.COLORS.reset}${qualityBadge}${currentMarker}`);
1824
+ }
1825
+ }
1826
+
1827
+ // Legend
1828
+ console.log(`
1829
+ ${utils.COLORS.dim}Legend: ✓ approved ◎ in review ◇ draft ✗ rejected ○ pending${utils.COLORS.reset}`);
1830
+
1831
+ if (progress.isComplete) {
1832
+ console.log(`\n${utils.COLORS.green}${utils.COLORS.bold}🎉 Workflow Complete!${utils.COLORS.reset}`);
1833
+ console.log(`${utils.COLORS.dim}All documents have been approved.${utils.COLORS.reset}`);
1834
+ }
1835
+ }
1836
+
1837
+ /**
1838
+ * Reset workflow
1839
+ */
1840
+ async function workflowReset(_args: ParsedArgs): Promise<void> {
1841
+ const utils = getUtils();
1842
+ const config = getConfig();
1843
+ const { PreseedWorkflowEngine } = getPreseedWorkflowModule();
1844
+ const { createInterface, askChoice } = getInteractive();
1845
+
1846
+ const projectRoot = config.findProjectRoot();
1847
+ const workflow = new PreseedWorkflowEngine(projectRoot);
1848
+
1849
+ if (!workflow.hasWorkflow()) {
1850
+ console.log(`${utils.COLORS.yellow}○${utils.COLORS.reset} No workflow to reset`);
1851
+ return;
1852
+ }
1853
+
1854
+ const rl = createInterface();
1855
+ const confirm = await askChoice(rl, 'Are you sure you want to reset the workflow? This will lose all progress.', [
1856
+ { label: 'Cancel', value: 'cancel' },
1857
+ { label: 'Reset', value: 'reset' }
1858
+ ]);
1859
+ rl.close();
1860
+
1861
+ if (confirm === 'reset') {
1862
+ workflow.resetWorkflow();
1863
+ utils.print.success('Workflow reset');
1864
+ } else {
1865
+ console.log('Cancelled');
1866
+ }
1867
+ }
1868
+
1869
+ /**
1870
+ * Main workflow loop
1871
+ */
1872
+ async function runWorkflowLoop(workflow: PreseedWorkflowEngine): Promise<void> {
1873
+ const utils = getUtils();
1874
+ const config = getConfig();
1875
+ const { PreseedEngine } = getPreseedEngineModule();
1876
+ const { DOCUMENT_STATUS } = getPreseedWorkflowModule();
1877
+ const { createInterface, askChoice, askText } = getInteractive();
1878
+ const { generateDocumentTemplate } = getTemplates();
1879
+
1880
+ const projectRoot = config.findProjectRoot();
1881
+ const engine = new PreseedEngine(projectRoot);
1882
+
1883
+ // Load preseed config if available
1884
+ engine.loadConfig();
1885
+
1886
+ while (true) {
1887
+ const resume = workflow.getResumePoint();
1888
+ if (!resume) {
1889
+ // Workflow complete
1890
+ console.log(`\n${utils.COLORS.green}${utils.COLORS.bold}🎉 Workflow Complete!${utils.COLORS.reset}`);
1891
+ console.log(`${utils.COLORS.dim}All documents have been approved.${utils.COLORS.reset}`);
1892
+ console.log(`\n${utils.COLORS.bold}Approved documents:${utils.COLORS.reset} ${workflow.approvedDir}`);
1893
+ console.log(`\n${utils.COLORS.bold}Next steps:${utils.COLORS.reset}`);
1894
+ console.log(` ${utils.COLORS.cyan}bootspring seed init${utils.COLORS.reset} - Scaffold your project`);
1895
+ break;
1896
+ }
1897
+
1898
+ // Show current position
1899
+ console.log(`\n${utils.COLORS.cyan}${'─'.repeat(60)}${utils.COLORS.reset}`);
1900
+ console.log(`${utils.COLORS.bold}Phase: ${resume.phaseName}${utils.COLORS.reset}`);
1901
+ console.log(`${utils.COLORS.cyan}${'─'.repeat(60)}${utils.COLORS.reset}`);
1902
+ console.log(`\n${utils.COLORS.bold}Document: ${resume.documentName}${utils.COLORS.reset}`);
1903
+
1904
+ const doc = workflow.state.documents[resume.document];
1905
+ const docType = resume.document;
1906
+
1907
+ if (!doc) continue;
1908
+
1909
+ // Handle based on document status
1910
+ if (doc.status === DOCUMENT_STATUS.EMPTY || doc.status === DOCUMENT_STATUS.REJECTED) {
1911
+ // Need to generate or upload
1912
+ if (doc.status === DOCUMENT_STATUS.REJECTED && doc.feedback.length > 0) {
1913
+ const lastFeedback = doc.feedback[doc.feedback.length - 1];
1914
+ if (lastFeedback) {
1915
+ console.log(`\n${utils.COLORS.yellow}Previous rejection:${utils.COLORS.reset} ${lastFeedback.message}`);
1916
+ }
1917
+ }
1918
+
1919
+ const rl = createInterface();
1920
+ const action = await askChoice(rl, '\nNo document found. What would you like to do?', [
1921
+ { label: 'Generate (answer questions)', value: 'generate', description: 'AI-assisted generation' },
1922
+ { label: 'Upload existing file', value: 'upload', description: 'Import from path' },
1923
+ { label: 'Skip (not recommended)', value: 'skip', description: 'Continue without this document' },
1924
+ { label: 'Save and exit', value: 'exit', description: 'Resume later' }
1925
+ ]);
1926
+
1927
+ if (action === 'exit') {
1928
+ rl.close();
1929
+ console.log(`\n${utils.COLORS.dim}Progress saved. Run ${utils.COLORS.cyan}bootspring preseed workflow resume${utils.COLORS.reset}${utils.COLORS.dim} to continue.${utils.COLORS.reset}`);
1930
+ break;
1931
+ }
1932
+
1933
+ if (action === 'skip') {
1934
+ // Ask for confirmation
1935
+ const confirmSkip = await askChoice(rl, 'Are you sure? Skipping may cause issues later.', [
1936
+ { label: 'Go back', value: 'back' },
1937
+ { label: 'Skip anyway', value: 'skip' }
1938
+ ]);
1939
+
1940
+ if (confirmSkip === 'skip') {
1941
+ // Override the phase gate
1942
+ const justification = await askText(rl, 'Provide a brief justification for skipping');
1943
+ workflow.overridePhaseGate(workflow.state.currentPhase, justification || 'User chose to skip');
1944
+
1945
+ // Create empty approved doc
1946
+ const content = `# ${resume.documentName}\n\n> Skipped during workflow\n\nJustification: ${justification || 'No justification provided'}\n`;
1947
+ workflow.createDraft(docType, content, 'skipped');
1948
+ workflow.approveDocument(docType);
1949
+ rl.close();
1950
+ continue;
1951
+ }
1952
+ rl.close();
1953
+ continue;
1954
+ }
1955
+
1956
+ if (action === 'upload') {
1957
+ const filePath = await askText(rl, 'Enter file path');
1958
+ rl.close();
1959
+
1960
+ if (!filePath || !fs.existsSync(filePath)) {
1961
+ utils.print.error('File not found');
1962
+ continue;
1963
+ }
1964
+
1965
+ try {
1966
+ const result = workflow.importDocument(docType, filePath);
1967
+ utils.print.success(`Imported ${result.path}`);
1968
+ } catch (err) {
1969
+ utils.print.error(`Import failed: ${(err as Error).message}`);
1970
+ continue;
1971
+ }
1972
+ }
1973
+
1974
+ if (action === 'generate') {
1975
+ rl.close();
1976
+
1977
+ // Generate using preseed engine
1978
+ console.log(`\n${utils.COLORS.dim}Generating document...${utils.COLORS.reset}`);
1979
+
1980
+ // If we have a config, use it to generate
1981
+ let content: string;
1982
+ if (engine.config) {
1983
+ content = engine.generateDocument(docType);
1984
+ } else {
1985
+ // Fallback: create a template
1986
+ content = generateDocumentTemplate(docType, resume.documentName);
1987
+ }
1988
+
1989
+ const result = workflow.createDraft(docType, content, 'generated');
1990
+ utils.print.success(`Draft created: ${result.path} (v${result.version})`);
1991
+ }
1992
+ }
1993
+
1994
+ // Now we should have a draft - show quality and review options
1995
+ if (doc.status === DOCUMENT_STATUS.DRAFT || doc.status === DOCUMENT_STATUS.IN_REVIEW) {
1996
+ const content = workflow.readDraft(docType);
1997
+ if (!content) continue;
1998
+
1999
+ const quality = workflow.calculateQualityScore(docType, content);
2000
+
2001
+ console.log(`\n${utils.COLORS.bold}Quality Score:${utils.COLORS.reset} ${formatQualityScore(quality.score)}`);
2002
+ console.log(` Completeness: ${formatQualityScore(quality.completeness)}`);
2003
+ console.log(` Clarity: ${formatQualityScore(quality.clarity)}`);
2004
+
2005
+ // Show failed checks
2006
+ const allChecks = [
2007
+ ...(quality.breakdown.completeness?.checks || []),
2008
+ ...(quality.breakdown.clarity?.checks || [])
2009
+ ];
2010
+ const failedChecks = allChecks.filter(c => !c.passed);
2011
+
2012
+ if (failedChecks.length > 0 && failedChecks.length <= 5) {
2013
+ console.log(`\n${utils.COLORS.yellow}Suggestions:${utils.COLORS.reset}`);
2014
+ for (const check of failedChecks.slice(0, 5)) {
2015
+ console.log(` ${utils.COLORS.dim}○${utils.COLORS.reset} ${check.label}`);
2016
+ }
2017
+ }
2018
+
2019
+ const rl = createInterface();
2020
+ const reviewAction = await askChoice(rl, '\nWhat would you like to do?', [
2021
+ { label: 'Approve and continue', value: 'approve', description: 'Accept this version' },
2022
+ { label: 'Edit (opens in $EDITOR)', value: 'edit', description: 'Make changes' },
2023
+ { label: 'Regenerate', value: 'regenerate', description: 'Start fresh' },
2024
+ { label: 'View document', value: 'view', description: 'Show content' },
2025
+ { label: 'Save and exit', value: 'exit', description: 'Resume later' }
2026
+ ]);
2027
+
2028
+ if (reviewAction === 'exit') {
2029
+ rl.close();
2030
+ console.log(`\n${utils.COLORS.dim}Progress saved. Run ${utils.COLORS.cyan}bootspring preseed workflow resume${utils.COLORS.reset}${utils.COLORS.dim} to continue.${utils.COLORS.reset}`);
2031
+ break;
2032
+ }
2033
+
2034
+ if (reviewAction === 'view') {
2035
+ rl.close();
2036
+ console.log(`\n${utils.COLORS.dim}${'─'.repeat(60)}${utils.COLORS.reset}`);
2037
+ console.log(content);
2038
+ console.log(`${utils.COLORS.dim}${'─'.repeat(60)}${utils.COLORS.reset}`);
2039
+ continue;
2040
+ }
2041
+
2042
+ if (reviewAction === 'edit') {
2043
+ rl.close();
2044
+ console.log(`\n${utils.COLORS.dim}Opening in editor...${utils.COLORS.reset}`);
2045
+
2046
+ try {
2047
+ const editResult = await workflow.openInEditor(docType);
2048
+
2049
+ if (editResult.changed) {
2050
+ utils.print.success('Changes detected');
2051
+ } else {
2052
+ console.log(`${utils.COLORS.dim}No changes made${utils.COLORS.reset}`);
2053
+ }
2054
+ } catch (err) {
2055
+ utils.print.error(`Editor failed: ${(err as Error).message}`);
2056
+ }
2057
+ continue;
2058
+ }
2059
+
2060
+ if (reviewAction === 'regenerate') {
2061
+ rl.close();
2062
+
2063
+ let newContent: string;
2064
+ if (engine.config) {
2065
+ newContent = engine.generateDocument(docType);
2066
+ } else {
2067
+ newContent = generateDocumentTemplate(docType, resume.documentName);
2068
+ }
2069
+
2070
+ const result = workflow.createDraft(docType, newContent, 'regenerated');
2071
+ utils.print.success(`New draft created: v${result.version}`);
2072
+ continue;
2073
+ }
2074
+
2075
+ if (reviewAction === 'approve') {
2076
+ rl.close();
2077
+
2078
+ try {
2079
+ const result = workflow.approveDocument(docType);
2080
+ utils.print.success(`${resume.documentName} approved! (v${doc.version})`);
2081
+
2082
+ if (result.workflowComplete) {
2083
+ continue; // Will exit loop at top
2084
+ }
2085
+
2086
+ if (result.next) {
2087
+ console.log(`\n${utils.COLORS.dim}Moving to: ${result.next.document}${utils.COLORS.reset}`);
2088
+ }
2089
+ } catch (err) {
2090
+ utils.print.error(`Approval failed: ${(err as Error).message}`);
2091
+ }
2092
+ continue;
2093
+ }
2094
+
2095
+ rl.close();
2096
+ }
2097
+ }
2098
+ }
2099
+
2100
+ /**
2101
+ * Format quality score with color
2102
+ */
2103
+ function formatQualityScore(score: number): string {
2104
+ const utils = getUtils();
2105
+
2106
+ if (score >= 80) {
2107
+ return `${utils.COLORS.green}${score}%${utils.COLORS.reset}`;
2108
+ } else if (score >= 60) {
2109
+ return `${utils.COLORS.yellow}${score}%${utils.COLORS.reset}`;
2110
+ } else {
2111
+ return `${utils.COLORS.red}${score}%${utils.COLORS.reset}`;
2112
+ }
2113
+ }
2114
+
2115
+ // =============================================================================
2116
+ // Document Commands
2117
+ // =============================================================================
2118
+
2119
+ /**
2120
+ * Handle document-specific commands
2121
+ */
2122
+ async function handleDocCommand(args: ParsedArgs): Promise<void> {
2123
+ const utils = getUtils();
2124
+ const config = getConfig();
2125
+ const { PreseedWorkflowEngine } = getPreseedWorkflowModule();
2126
+ const { DOCUMENT_TYPES } = getPreseedEngineModule();
2127
+ const { createInterface, askText } = getInteractive();
2128
+
2129
+ const projectRoot = config.findProjectRoot();
2130
+ const workflow = new PreseedWorkflowEngine(projectRoot);
2131
+ const docType = (args._ as string[])[0];
2132
+ const action = (args._ as string[])[1];
2133
+
2134
+ if (!docType || !action) {
2135
+ utils.print.error('Usage: bootspring preseed doc <type> <action>');
2136
+ console.log('\nActions: approve, reject, edit, review, view');
2137
+ console.log(`Types: ${Object.keys(DOCUMENT_TYPES).join(', ')}`);
2138
+ return;
2139
+ }
2140
+
2141
+ if (!DOCUMENT_TYPES[docType]) {
2142
+ utils.print.error(`Unknown document type: ${docType}`);
2143
+ console.log(`\nAvailable types: ${Object.keys(DOCUMENT_TYPES).join(', ')}`);
2144
+ return;
2145
+ }
2146
+
2147
+ if (!workflow.hasWorkflow()) {
2148
+ utils.print.error('No workflow found');
2149
+ console.log(`\nRun ${utils.COLORS.cyan}bootspring preseed workflow start${utils.COLORS.reset} first`);
2150
+ return;
2151
+ }
2152
+
2153
+ workflow.loadState();
2154
+
2155
+ switch (action) {
2156
+ case 'approve': {
2157
+ try {
2158
+ const result = workflow.approveDocument(docType);
2159
+ utils.print.success(`${DOCUMENT_TYPES[docType].name} approved!`);
2160
+
2161
+ if (result.next) {
2162
+ console.log(`\n${utils.COLORS.dim}Next: ${result.next.document}${utils.COLORS.reset}`);
2163
+ } else if (result.workflowComplete) {
2164
+ console.log(`\n${utils.COLORS.green}Workflow complete!${utils.COLORS.reset}`);
2165
+ }
2166
+ } catch (err) {
2167
+ utils.print.error((err as Error).message);
2168
+ }
2169
+ break;
2170
+ }
2171
+
2172
+ case 'reject': {
2173
+ const rl = createInterface();
2174
+ const feedback = await askText(rl, 'Rejection feedback');
2175
+ rl.close();
2176
+
2177
+ try {
2178
+ workflow.rejectDocument(docType, feedback);
2179
+ utils.print.success(`${DOCUMENT_TYPES[docType].name} rejected`);
2180
+ console.log(`${utils.COLORS.dim}Feedback: ${feedback}${utils.COLORS.reset}`);
2181
+ } catch (err) {
2182
+ utils.print.error((err as Error).message);
2183
+ }
2184
+ break;
2185
+ }
2186
+
2187
+ case 'edit': {
2188
+ try {
2189
+ console.log(`${utils.COLORS.dim}Opening in editor...${utils.COLORS.reset}`);
2190
+ const result = await workflow.openInEditor(docType);
2191
+
2192
+ if (result.changed) {
2193
+ utils.print.success('Changes saved');
2194
+ } else {
2195
+ console.log(`${utils.COLORS.dim}No changes made${utils.COLORS.reset}`);
2196
+ }
2197
+ } catch (err) {
2198
+ utils.print.error((err as Error).message);
2199
+ }
2200
+ break;
2201
+ }
2202
+
2203
+ case 'review': {
2204
+ try {
2205
+ const quality = workflow.submitForReview(docType);
2206
+ utils.print.success(`${DOCUMENT_TYPES[docType].name} submitted for review`);
2207
+ console.log(`\n${utils.COLORS.bold}Quality Score:${utils.COLORS.reset} ${formatQualityScore(quality.score)}`);
2208
+ } catch (err) {
2209
+ utils.print.error((err as Error).message);
2210
+ }
2211
+ break;
2212
+ }
2213
+
2214
+ case 'view': {
2215
+ const content = workflow.readDraft(docType);
2216
+ if (content) {
2217
+ console.log(`\n${utils.COLORS.dim}${'─'.repeat(60)}${utils.COLORS.reset}`);
2218
+ console.log(content);
2219
+ console.log(`${utils.COLORS.dim}${'─'.repeat(60)}${utils.COLORS.reset}`);
2220
+ } else {
2221
+ utils.print.error(`No draft found for ${docType}`);
2222
+ }
2223
+ break;
2224
+ }
2225
+
2226
+ default:
2227
+ utils.print.error(`Unknown action: ${action}`);
2228
+ console.log('\nActions: approve, reject, edit, review, view');
2229
+ }
2230
+ }
2231
+
2232
+ /**
2233
+ * Show help
2234
+ */
2235
+ function showHelp(): void {
2236
+ const utils = getUtils();
2237
+ const { PRESETS } = getPreseedEngineModule();
2238
+
2239
+ console.log(`
2240
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Preseed${utils.COLORS.reset}
2241
+ ${utils.COLORS.dim}Generate foundational documents from minimal input${utils.COLORS.reset}
2242
+
2243
+ ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
2244
+ bootspring preseed <command> [options]
2245
+
2246
+ ${utils.COLORS.bold}Commands:${utils.COLORS.reset}
2247
+ ${utils.COLORS.green}start${utils.COLORS.reset} ${utils.COLORS.bold}Smart entry point - detects context & guides you${utils.COLORS.reset} ${utils.COLORS.yellow}[PRO]${utils.COLORS.reset}
2248
+ ${utils.COLORS.cyan}setup${utils.COLORS.reset} Create context input folders for early docs
2249
+ ${utils.COLORS.cyan}init${utils.COLORS.reset} Interactive wizard to create preseed
2250
+ ${utils.COLORS.cyan}generate${utils.COLORS.reset} Regenerate all documents
2251
+ ${utils.COLORS.cyan}sync${utils.COLORS.reset} Sync documents with config changes
2252
+ ${utils.COLORS.cyan}status${utils.COLORS.reset} Show preseed status (default)
2253
+ ${utils.COLORS.cyan}update${utils.COLORS.reset} <path> Update a config value
2254
+ ${utils.COLORS.cyan}export${utils.COLORS.reset} Export preseed configuration
2255
+ ${utils.COLORS.cyan}pull${utils.COLORS.reset} Download documents from dashboard ${utils.COLORS.yellow}[PRO]${utils.COLORS.reset}
2256
+ ${utils.COLORS.cyan}push${utils.COLORS.reset} Upload documents to dashboard ${utils.COLORS.yellow}[PRO]${utils.COLORS.reset}
2257
+ ${utils.COLORS.cyan}merge${utils.COLORS.reset} Merge source docs from context folders ${utils.COLORS.yellow}[PRO]${utils.COLORS.reset}
2258
+
2259
+ ${utils.COLORS.bold}Init Options:${utils.COLORS.reset}
2260
+ --preset=<preset> Document preset (essential, startup, full, technical, investor)
2261
+ --quick Quick mode with minimal questions
2262
+
2263
+ ${utils.COLORS.bold}Generate Options:${utils.COLORS.reset}
2264
+ --doc=<type> Generate a single document type
2265
+
2266
+ ${utils.COLORS.bold}Export Options:${utils.COLORS.reset}
2267
+ --format=<fmt> Output format: json (default), yaml
2268
+ --output=<file> Write to file instead of stdout
2269
+
2270
+ ${utils.COLORS.bold}Pull/Push Options:${utils.COLORS.reset}
2271
+ --project=<id> Project ID (uses current project if not specified)
2272
+ --doc=<name> Pull/push a single document (e.g., VISION, PRD)
2273
+ --force Overwrite without prompting
2274
+
2275
+ ${utils.COLORS.bold}Document Types:${utils.COLORS.reset}
2276
+ vision Problem statement, solution overview
2277
+ audience Target audience, personas, ICP
2278
+ market TAM/SAM/SOM analysis
2279
+ competitors Competitive landscape
2280
+ business-model Revenue model, pricing strategy
2281
+ prd Product requirements, user stories
2282
+ technical-spec Architecture, tech stack
2283
+ roadmap Development phases, milestones
2284
+
2285
+ ${utils.COLORS.bold}Presets:${utils.COLORS.reset}
2286
+ essential ${PRESETS.essential?.length || 4} docs - Vision, Audience, Business Model, PRD
2287
+ startup ${PRESETS.startup?.length || 6} docs - Full startup pack (default)
2288
+ full ${PRESETS.full?.length || 8} docs - Complete documentation suite
2289
+ technical ${PRESETS.technical?.length || 4} docs - Technical focus
2290
+ investor ${PRESETS.investor?.length || 6} docs - Investor-ready materials
2291
+
2292
+ ${utils.COLORS.bold}Examples:${utils.COLORS.reset}
2293
+ ${utils.COLORS.green}bootspring preseed start${utils.COLORS.reset} # Smart guide (recommended)
2294
+ bootspring preseed setup # Create context folders first
2295
+ bootspring preseed init # Full interactive wizard
2296
+ bootspring preseed init --quick # Quick mode
2297
+ bootspring preseed init --preset=investor # Investor-ready docs
2298
+ bootspring preseed generate # Regenerate all docs
2299
+ bootspring preseed generate --doc=prd # Regenerate PRD only
2300
+ bootspring preseed sync # Sync after config changes
2301
+ bootspring preseed update identity.name "My App"
2302
+ bootspring preseed pull # Download all from dashboard
2303
+ bootspring preseed pull --doc=VISION # Download single document
2304
+ bootspring preseed push # Upload all to dashboard
2305
+ bootspring preseed push --doc=PRD # Upload single document
2306
+
2307
+ ${utils.COLORS.bold}Workflow Commands:${utils.COLORS.reset} ${utils.COLORS.yellow}[PRO]${utils.COLORS.reset}
2308
+ ${utils.COLORS.cyan}workflow start${utils.COLORS.reset} Begin or resume approval workflow
2309
+ ${utils.COLORS.cyan}workflow resume${utils.COLORS.reset} Continue from last point
2310
+ ${utils.COLORS.cyan}workflow status${utils.COLORS.reset} Show workflow progress
2311
+ ${utils.COLORS.cyan}workflow reset${utils.COLORS.reset} Reset workflow state
2312
+
2313
+ ${utils.COLORS.bold}Document Commands:${utils.COLORS.reset}
2314
+ ${utils.COLORS.cyan}doc <type> approve${utils.COLORS.reset} Approve a document
2315
+ ${utils.COLORS.cyan}doc <type> reject${utils.COLORS.reset} Reject with feedback
2316
+ ${utils.COLORS.cyan}doc <type> edit${utils.COLORS.reset} Open in $EDITOR
2317
+ ${utils.COLORS.cyan}doc <type> view${utils.COLORS.reset} View document content
2318
+ ${utils.COLORS.cyan}doc <type> review${utils.COLORS.reset} Submit for review
2319
+ `);
2320
+ }
2321
+
2322
+ /**
2323
+ * Run preseed command
2324
+ */
2325
+ export async function run(args: string[]): Promise<void> {
2326
+ const utils = getUtils();
2327
+ const tierEnforcement = getTierEnforcement();
2328
+
2329
+ const parsedArgs = utils.parseArgs(args);
2330
+ const subcommand = (parsedArgs._ as string[])[0] || 'status';
2331
+
2332
+ // Check tier access for paid commands
2333
+ const paidCommands = ['start', 'begin', 'new', 'pull', 'download', 'push', 'upload', 'merge', 'workflow'];
2334
+ if (paidCommands.includes(subcommand)) {
2335
+ // Map aliases to actual command names for tier check
2336
+ const commandMap: Record<string, string> = {
2337
+ 'begin': 'start',
2338
+ 'new': 'start',
2339
+ 'download': 'pull',
2340
+ 'upload': 'push',
2341
+ };
2342
+ const actualCommand = commandMap[subcommand] || subcommand;
2343
+
2344
+ try {
2345
+ tierEnforcement.requirePreseedAccess(actualCommand);
2346
+ } catch (error) {
2347
+ const tierError = error as TierError;
2348
+ if (tierError.code === 'TIER_REQUIRED') {
2349
+ console.log(tierError.upgradePrompt);
2350
+ return;
2351
+ }
2352
+ throw error;
2353
+ }
2354
+ }
2355
+
2356
+ switch (subcommand) {
2357
+ case 'start':
2358
+ case 'begin':
2359
+ case 'new':
2360
+ return getPreseedStart().start(parsedArgs);
2361
+
2362
+ case 'setup':
2363
+ return preseedSetup(parsedArgs);
2364
+
2365
+ case 'init':
2366
+ case 'wizard':
2367
+ return preseedInit(parsedArgs);
2368
+
2369
+ case 'generate':
2370
+ case 'gen':
2371
+ return preseedGenerate(parsedArgs);
2372
+
2373
+ case 'sync':
2374
+ return preseedSync(parsedArgs);
2375
+
2376
+ case 'status':
2377
+ return preseedStatus(parsedArgs);
2378
+
2379
+ case 'update':
2380
+ case 'set':
2381
+ parsedArgs._ = (parsedArgs._ as string[]).slice(1);
2382
+ return preseedUpdate(parsedArgs);
2383
+
2384
+ case 'export':
2385
+ return preseedExport(parsedArgs);
2386
+
2387
+ case 'pull':
2388
+ case 'download':
2389
+ return preseedPull(parsedArgs);
2390
+
2391
+ case 'push':
2392
+ case 'upload':
2393
+ return preseedPush(parsedArgs);
2394
+
2395
+ case 'merge':
2396
+ return preseedMerge(parsedArgs);
2397
+
2398
+ // Workflow commands
2399
+ case 'workflow': {
2400
+ const workflowCmd = (parsedArgs._ as string[])[1] || 'status';
2401
+ parsedArgs._ = (parsedArgs._ as string[]).slice(2);
2402
+
2403
+ switch (workflowCmd) {
2404
+ case 'start':
2405
+ return workflowStart(parsedArgs);
2406
+ case 'resume':
2407
+ return workflowResume(parsedArgs);
2408
+ case 'status':
2409
+ return workflowStatus(parsedArgs);
2410
+ case 'reset':
2411
+ return workflowReset(parsedArgs);
2412
+ default:
2413
+ utils.print.error(`Unknown workflow command: ${workflowCmd}`);
2414
+ console.log('\nCommands: start, resume, status, reset');
2415
+ }
2416
+ break;
2417
+ }
2418
+
2419
+ // Document commands
2420
+ case 'doc':
2421
+ case 'document':
2422
+ parsedArgs._ = (parsedArgs._ as string[]).slice(1);
2423
+ return handleDocCommand(parsedArgs);
2424
+
2425
+ case 'help':
2426
+ case '-h':
2427
+ case '--help':
2428
+ return showHelp();
2429
+
2430
+ default:
2431
+ utils.print.error(`Unknown subcommand: ${subcommand}`);
2432
+ showHelp();
2433
+ }
2434
+ }