@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.
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +20 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- 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
|
+
}
|