specsmd 0.0.0-dev.85 → 0.0.0-dev.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +15 -0
  2. package/bin/cli.js +15 -1
  3. package/flows/fire/agents/builder/agent.md +2 -2
  4. package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
  5. package/flows/fire/agents/builder/skills/run-execute/SKILL.md +16 -7
  6. package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +22 -3
  7. package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +63 -20
  8. package/flows/fire/agents/builder/skills/run-execute/scripts/update-checkpoint.cjs +254 -0
  9. package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +17 -6
  10. package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
  11. package/flows/fire/agents/builder/skills/walkthrough-generate/SKILL.md +30 -27
  12. package/flows/fire/agents/orchestrator/agent.md +1 -1
  13. package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
  14. package/flows/fire/memory-bank.yaml +4 -4
  15. package/flows/ideation/agents/orchestrator/agent.md +8 -7
  16. package/flows/ideation/agents/orchestrator/skills/flame/SKILL.md +1 -0
  17. package/flows/ideation/agents/orchestrator/skills/flame/references/evaluation-criteria.md +4 -0
  18. package/flows/ideation/agents/orchestrator/skills/flame/references/six-hats-method.md +12 -0
  19. package/flows/ideation/agents/orchestrator/skills/forge/SKILL.md +1 -0
  20. package/flows/ideation/agents/orchestrator/skills/forge/references/disney-method.md +8 -0
  21. package/flows/ideation/agents/orchestrator/skills/forge/references/pitch-framework.md +15 -0
  22. package/flows/ideation/agents/orchestrator/skills/spark/SKILL.md +1 -0
  23. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/analogy.md +7 -0
  24. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/first-principles.md +5 -0
  25. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/inversion.md +6 -0
  26. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/questorming.md +6 -0
  27. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/random-word.md +1 -0
  28. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/scamper.md +15 -0
  29. package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/what-if.md +6 -0
  30. package/flows/ideation/shared/protocols/anti-bias.md +7 -4
  31. package/flows/ideation/shared/protocols/deep-thinking.md +7 -0
  32. package/flows/ideation/shared/protocols/diverge-converge.md +2 -0
  33. package/flows/ideation/shared/protocols/interaction-adaptation.md +7 -0
  34. package/lib/dashboard/aidlc/parser.js +581 -0
  35. package/lib/dashboard/fire/model.js +382 -0
  36. package/lib/dashboard/fire/parser.js +470 -0
  37. package/lib/dashboard/flow-detect.js +86 -0
  38. package/lib/dashboard/git/changes.js +362 -0
  39. package/lib/dashboard/git/worktrees.js +248 -0
  40. package/lib/dashboard/index.js +709 -0
  41. package/lib/dashboard/runtime/watch-runtime.js +122 -0
  42. package/lib/dashboard/simple/parser.js +293 -0
  43. package/lib/dashboard/tui/app.js +1675 -0
  44. package/lib/dashboard/tui/components/error-banner.js +35 -0
  45. package/lib/dashboard/tui/components/header.js +60 -0
  46. package/lib/dashboard/tui/components/help-footer.js +15 -0
  47. package/lib/dashboard/tui/components/stats-strip.js +35 -0
  48. package/lib/dashboard/tui/file-entries.js +383 -0
  49. package/lib/dashboard/tui/flow-builders.js +991 -0
  50. package/lib/dashboard/tui/git-builders.js +218 -0
  51. package/lib/dashboard/tui/helpers.js +236 -0
  52. package/lib/dashboard/tui/overlays.js +242 -0
  53. package/lib/dashboard/tui/preview.js +220 -0
  54. package/lib/dashboard/tui/renderer.js +76 -0
  55. package/lib/dashboard/tui/row-builders.js +797 -0
  56. package/lib/dashboard/tui/sections.js +45 -0
  57. package/lib/dashboard/tui/store.js +44 -0
  58. package/lib/dashboard/tui/views/overview-view.js +61 -0
  59. package/lib/dashboard/tui/views/runs-view.js +93 -0
  60. package/lib/dashboard/tui/worktree-builders.js +229 -0
  61. package/lib/installers/CodexInstaller.js +72 -1
  62. package/package.json +7 -3
@@ -0,0 +1,470 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+
5
+ const {
6
+ normalizeStatus,
7
+ normalizeMode,
8
+ normalizeScope,
9
+ normalizeComplexity,
10
+ normalizeState,
11
+ deriveIntentStatus,
12
+ calculateStats,
13
+ parseDependencies,
14
+ buildPendingItems,
15
+ normalizeRunWorkItem
16
+ } = require('./model');
17
+
18
+ const STANDARD_TYPES = [
19
+ 'constitution',
20
+ 'tech-stack',
21
+ 'coding-standards',
22
+ 'testing-standards',
23
+ 'system-architecture'
24
+ ];
25
+
26
+ function parseFrontmatter(content) {
27
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
28
+ if (!match) {
29
+ return {};
30
+ }
31
+
32
+ try {
33
+ return yaml.load(match[1]) || {};
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function readFileSafe(filePath) {
40
+ try {
41
+ return fs.readFileSync(filePath, 'utf8');
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function listSubdirectories(dirPath) {
48
+ try {
49
+ return fs.readdirSync(dirPath, { withFileTypes: true })
50
+ .filter((entry) => entry.isDirectory())
51
+ .map((entry) => entry.name)
52
+ .sort((a, b) => a.localeCompare(b));
53
+ } catch {
54
+ return [];
55
+ }
56
+ }
57
+
58
+ function listMarkdownFiles(dirPath) {
59
+ try {
60
+ return fs.readdirSync(dirPath)
61
+ .filter((file) => file.endsWith('.md'))
62
+ .sort((a, b) => a.localeCompare(b));
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ function getFirstStringValue(record, keys) {
69
+ if (!record || typeof record !== 'object') {
70
+ return undefined;
71
+ }
72
+
73
+ for (const key of keys) {
74
+ const value = record[key];
75
+ if (typeof value === 'string' && value.trim() !== '') {
76
+ return value;
77
+ }
78
+ }
79
+
80
+ return undefined;
81
+ }
82
+
83
+ function parseRunLog(runLogPath) {
84
+ const content = readFileSafe(runLogPath);
85
+ if (!content) {
86
+ return {
87
+ scope: undefined,
88
+ workItems: [],
89
+ currentItem: null,
90
+ startedAt: undefined,
91
+ completedAt: undefined,
92
+ checkpointState: undefined,
93
+ currentCheckpoint: undefined
94
+ };
95
+ }
96
+
97
+ const frontmatter = parseFrontmatter(content);
98
+ const currentItem = getFirstStringValue(frontmatter, ['current_item', 'currentItem', 'work_item', 'workItem']);
99
+ const itemMode = getFirstStringValue(frontmatter, ['mode']);
100
+ const itemStatus = getFirstStringValue(frontmatter, ['status']);
101
+ const itemPhase = getFirstStringValue(frontmatter, ['current_phase', 'currentPhase']);
102
+ const itemCheckpointState = getFirstStringValue(frontmatter, [
103
+ 'checkpoint_state',
104
+ 'checkpointState',
105
+ 'approval_state',
106
+ 'approvalState'
107
+ ]);
108
+ const itemCheckpoint = getFirstStringValue(frontmatter, ['current_checkpoint', 'currentCheckpoint', 'checkpoint']);
109
+
110
+ const workItemsRaw = Array.isArray(frontmatter.work_items)
111
+ ? frontmatter.work_items
112
+ : (Array.isArray(frontmatter.workItems) ? frontmatter.workItems : []);
113
+
114
+ if (workItemsRaw.length === 0 && typeof currentItem === 'string' && currentItem !== '') {
115
+ workItemsRaw.push({
116
+ id: currentItem,
117
+ mode: itemMode,
118
+ status: itemStatus,
119
+ current_phase: itemPhase,
120
+ checkpoint_state: itemCheckpointState,
121
+ current_checkpoint: itemCheckpoint
122
+ });
123
+ }
124
+
125
+ const workItems = workItemsRaw
126
+ .map((item) => normalizeRunWorkItem(item))
127
+ .filter((item) => item.id !== '');
128
+
129
+ return {
130
+ scope: normalizeScope(frontmatter.scope),
131
+ workItems,
132
+ currentItem: currentItem || null,
133
+ startedAt: typeof frontmatter.started === 'string' ? frontmatter.started : undefined,
134
+ completedAt: typeof frontmatter.completed === 'string'
135
+ ? frontmatter.completed
136
+ : undefined,
137
+ checkpointState: itemCheckpointState,
138
+ currentCheckpoint: itemCheckpoint
139
+ };
140
+ }
141
+
142
+ function mergeRunWorkItems(primaryItems, fallbackItems) {
143
+ const primary = Array.isArray(primaryItems) ? primaryItems : [];
144
+ const fallback = Array.isArray(fallbackItems) ? fallbackItems : [];
145
+
146
+ if (primary.length === 0) {
147
+ return fallback;
148
+ }
149
+
150
+ if (fallback.length === 0) {
151
+ return primary;
152
+ }
153
+
154
+ const fallbackById = new Map(fallback.map((item) => [item.id, item]));
155
+ const merged = primary.map((item) => {
156
+ const fallbackItem = fallbackById.get(item.id);
157
+ if (!fallbackItem) {
158
+ return item;
159
+ }
160
+
161
+ return {
162
+ ...fallbackItem,
163
+ ...item,
164
+ checkpointState: item.checkpointState || fallbackItem.checkpointState,
165
+ currentCheckpoint: item.currentCheckpoint || fallbackItem.currentCheckpoint,
166
+ currentPhase: item.currentPhase || fallbackItem.currentPhase
167
+ };
168
+ });
169
+
170
+ const knownIds = new Set(merged.map((item) => item.id));
171
+ for (const fallbackItem of fallback) {
172
+ if (!knownIds.has(fallbackItem.id)) {
173
+ merged.push(fallbackItem);
174
+ }
175
+ }
176
+
177
+ return merged;
178
+ }
179
+
180
+ function scanWorkItems(intentPath, intentId, stateWorkItems, warnings) {
181
+ const workItemsPath = path.join(intentPath, 'work-items');
182
+ const fileWorkItemIds = listMarkdownFiles(workItemsPath)
183
+ .map((file) => file.replace(/\.md$/, ''));
184
+
185
+ const stateWorkItemIds = (stateWorkItems || []).map((item) => item.id).filter(Boolean);
186
+ const uniqueIds = Array.from(new Set([...fileWorkItemIds, ...stateWorkItemIds])).sort((a, b) => a.localeCompare(b));
187
+
188
+ const stateMap = new Map((stateWorkItems || []).map((item) => [item.id, item]));
189
+
190
+ return uniqueIds.map((workItemId) => {
191
+ const stateItem = stateMap.get(workItemId);
192
+ const filePath = path.join(workItemsPath, `${workItemId}.md`);
193
+
194
+ let frontmatter = {};
195
+ if (fs.existsSync(filePath)) {
196
+ frontmatter = parseFrontmatter(readFileSafe(filePath) || '');
197
+ } else if (stateItem) {
198
+ warnings.push(`Work item ${intentId}/${workItemId} exists in state.yaml but markdown file is missing.`);
199
+ }
200
+
201
+ const dependencies = parseDependencies(frontmatter.depends_on ?? frontmatter.dependencies);
202
+
203
+ return {
204
+ id: workItemId,
205
+ intentId,
206
+ title: typeof frontmatter.title === 'string' ? frontmatter.title : workItemId,
207
+ status: normalizeStatus(stateItem?.status || frontmatter.status) || 'pending',
208
+ mode: normalizeMode(stateItem?.mode || frontmatter.mode) || 'confirm',
209
+ complexity: normalizeComplexity(frontmatter.complexity) || 'medium',
210
+ filePath,
211
+ description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
212
+ dependencies,
213
+ createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
214
+ completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
215
+ };
216
+ });
217
+ }
218
+
219
+ function scanIntents(rootPath, normalizedState, warnings) {
220
+ const intentsPath = path.join(rootPath, 'intents');
221
+ const dirIntentIds = listSubdirectories(intentsPath);
222
+ const stateIntentIds = (normalizedState.intents || []).map((intent) => intent.id).filter(Boolean);
223
+ const uniqueIntentIds = Array.from(new Set([...dirIntentIds, ...stateIntentIds])).sort((a, b) => a.localeCompare(b));
224
+
225
+ const stateIntentMap = new Map((normalizedState.intents || []).map((intent) => [intent.id, intent]));
226
+
227
+ return uniqueIntentIds.map((intentId) => {
228
+ const stateIntent = stateIntentMap.get(intentId);
229
+ const intentPath = path.join(intentsPath, intentId);
230
+ const briefPath = path.join(intentPath, 'brief.md');
231
+
232
+ let frontmatter = {};
233
+ if (fs.existsSync(briefPath)) {
234
+ frontmatter = parseFrontmatter(readFileSafe(briefPath) || '');
235
+ } else if (stateIntent) {
236
+ warnings.push(`Intent ${intentId} exists in state.yaml but brief.md is missing.`);
237
+ }
238
+
239
+ const workItems = scanWorkItems(
240
+ intentPath,
241
+ intentId,
242
+ stateIntent?.workItems || [],
243
+ warnings
244
+ );
245
+
246
+ return {
247
+ id: intentId,
248
+ title: typeof frontmatter.title === 'string'
249
+ ? frontmatter.title
250
+ : (stateIntent?.title || intentId),
251
+ status: deriveIntentStatus(stateIntent?.status, workItems),
252
+ filePath: briefPath,
253
+ description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
254
+ workItems,
255
+ createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
256
+ completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
257
+ };
258
+ });
259
+ }
260
+
261
+ function scanRuns(rootPath, normalizedState) {
262
+ const runsPath = path.join(rootPath, 'runs');
263
+ const runDirs = listSubdirectories(runsPath).filter((name) => name.startsWith('run-'));
264
+
265
+ const stateActiveMap = new Map((normalizedState.runs?.active || []).map((run) => [run.id, run]));
266
+ const stateCompletedMap = new Map((normalizedState.runs?.completed || []).map((run) => [run.id, run]));
267
+
268
+ const runIds = Array.from(new Set([
269
+ ...runDirs,
270
+ ...Array.from(stateActiveMap.keys()),
271
+ ...Array.from(stateCompletedMap.keys())
272
+ ])).sort((a, b) => a.localeCompare(b));
273
+
274
+ return runIds.map((runId) => {
275
+ const folderPath = path.join(runsPath, runId);
276
+ const runLogPath = path.join(folderPath, 'run.md');
277
+ const parsedRunLog = parseRunLog(runLogPath);
278
+
279
+ const stateActiveRun = stateActiveMap.get(runId);
280
+ const stateCompletedRun = stateCompletedMap.get(runId);
281
+
282
+ const stateRunWorkItems = (stateActiveRun?.workItems && stateActiveRun.workItems.length > 0)
283
+ ? stateActiveRun.workItems
284
+ : ((stateCompletedRun?.workItems && stateCompletedRun.workItems.length > 0)
285
+ ? stateCompletedRun.workItems
286
+ : []);
287
+ const workItems = mergeRunWorkItems(
288
+ stateRunWorkItems.length > 0 ? stateRunWorkItems : parsedRunLog.workItems,
289
+ parsedRunLog.workItems
290
+ );
291
+
292
+ const completedAt = stateCompletedRun?.completed || parsedRunLog.completedAt || undefined;
293
+
294
+ return {
295
+ id: runId,
296
+ scope: stateActiveRun?.scope || parsedRunLog.scope || 'single',
297
+ workItems,
298
+ currentItem: stateActiveRun?.currentItem || parsedRunLog.currentItem || null,
299
+ checkpointState: stateActiveRun?.checkpointState || parsedRunLog.checkpointState,
300
+ currentCheckpoint: stateActiveRun?.currentCheckpoint || parsedRunLog.currentCheckpoint,
301
+ folderPath,
302
+ startedAt: stateActiveRun?.started || parsedRunLog.startedAt || '',
303
+ completedAt: completedAt === 'null' ? undefined : completedAt,
304
+ hasPlan: fs.existsSync(path.join(folderPath, 'plan.md')),
305
+ hasWalkthrough: fs.existsSync(path.join(folderPath, 'walkthrough.md')),
306
+ hasTestReport: fs.existsSync(path.join(folderPath, 'test-report.md'))
307
+ };
308
+ });
309
+ }
310
+
311
+ function scanStandards(rootPath) {
312
+ const standardsPath = path.join(rootPath, 'standards');
313
+
314
+ return STANDARD_TYPES
315
+ .filter((type) => fs.existsSync(path.join(standardsPath, `${type}.md`)))
316
+ .map((type) => ({
317
+ type,
318
+ filePath: path.join(standardsPath, `${type}.md`),
319
+ scope: 'root'
320
+ }));
321
+ }
322
+
323
+ function buildActiveRuns(runs, normalizedState) {
324
+ const byId = new Map((runs || []).map((run) => [run.id, run]));
325
+
326
+ return (normalizedState.runs?.active || [])
327
+ .map((active) => byId.get(active.id) || null)
328
+ .filter(Boolean);
329
+ }
330
+
331
+ function buildCompletedRuns(runs) {
332
+ return (runs || [])
333
+ .filter((run) => run.completedAt != null)
334
+ .sort((a, b) => {
335
+ const aTime = a.completedAt ? Date.parse(a.completedAt) : 0;
336
+ const bTime = b.completedAt ? Date.parse(b.completedAt) : 0;
337
+ if (bTime !== aTime) {
338
+ return bTime - aTime;
339
+ }
340
+ return b.id.localeCompare(a.id);
341
+ });
342
+ }
343
+
344
+ function createUninitializedSnapshot(workspacePath, rootPath) {
345
+ return {
346
+ flow: 'fire',
347
+ isProject: true,
348
+ initialized: false,
349
+ workspacePath,
350
+ rootPath,
351
+ version: '0.0.0',
352
+ project: null,
353
+ workspace: null,
354
+ intents: [],
355
+ runs: [],
356
+ activeRuns: [],
357
+ completedRuns: [],
358
+ pendingItems: [],
359
+ standards: scanStandards(rootPath),
360
+ stats: {
361
+ totalIntents: 0,
362
+ completedIntents: 0,
363
+ inProgressIntents: 0,
364
+ pendingIntents: 0,
365
+ blockedIntents: 0,
366
+ totalWorkItems: 0,
367
+ completedWorkItems: 0,
368
+ inProgressWorkItems: 0,
369
+ pendingWorkItems: 0,
370
+ blockedWorkItems: 0,
371
+ totalRuns: 0,
372
+ completedRuns: 0,
373
+ activeRunsCount: 0
374
+ },
375
+ warnings: ['FIRE folder exists but state.yaml has not been created yet.'],
376
+ generatedAt: new Date().toISOString()
377
+ };
378
+ }
379
+
380
+ function parseStateFile(statePath) {
381
+ const content = readFileSafe(statePath);
382
+ if (content == null) {
383
+ throw new Error('Unable to read state.yaml');
384
+ }
385
+
386
+ const parsed = yaml.load(content);
387
+ if (!parsed || typeof parsed !== 'object') {
388
+ throw new Error('state.yaml is empty or invalid.');
389
+ }
390
+
391
+ return parsed;
392
+ }
393
+
394
+ function parseFireDashboard(workspacePath) {
395
+ const rootPath = path.join(workspacePath, '.specs-fire');
396
+
397
+ if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
398
+ return {
399
+ ok: false,
400
+ error: {
401
+ code: 'FIRE_NOT_FOUND',
402
+ message: `No FIRE workspace found at ${rootPath}`,
403
+ hint: 'Install FIRE flow or run this command from a FIRE project root.'
404
+ }
405
+ };
406
+ }
407
+
408
+ const statePath = path.join(rootPath, 'state.yaml');
409
+ if (!fs.existsSync(statePath)) {
410
+ return {
411
+ ok: true,
412
+ snapshot: createUninitializedSnapshot(workspacePath, rootPath)
413
+ };
414
+ }
415
+
416
+ let rawState;
417
+ try {
418
+ rawState = parseStateFile(statePath);
419
+ } catch (error) {
420
+ return {
421
+ ok: false,
422
+ error: {
423
+ code: 'STATE_PARSE_ERROR',
424
+ message: `Failed to parse ${statePath}`,
425
+ details: error.message,
426
+ path: statePath
427
+ }
428
+ };
429
+ }
430
+
431
+ const warnings = [];
432
+ const normalizedState = normalizeState(rawState);
433
+ const intents = scanIntents(rootPath, normalizedState, warnings);
434
+ const runs = scanRuns(rootPath, normalizedState);
435
+ const activeRuns = buildActiveRuns(runs, normalizedState);
436
+ const completedRuns = buildCompletedRuns(runs);
437
+ const standards = scanStandards(rootPath);
438
+ const pendingItems = buildPendingItems(intents);
439
+ const stats = calculateStats(intents, runs, activeRuns);
440
+
441
+ return {
442
+ ok: true,
443
+ snapshot: {
444
+ flow: 'fire',
445
+ isProject: true,
446
+ initialized: true,
447
+ workspacePath,
448
+ rootPath,
449
+ version: normalizedState.project?.fireVersion || '0.0.0',
450
+ project: normalizedState.project,
451
+ workspace: normalizedState.workspace,
452
+ intents,
453
+ runs,
454
+ activeRuns,
455
+ completedRuns,
456
+ pendingItems,
457
+ standards,
458
+ stats,
459
+ warnings,
460
+ generatedAt: new Date().toISOString()
461
+ }
462
+ };
463
+ }
464
+
465
+ module.exports = {
466
+ STANDARD_TYPES,
467
+ parseFrontmatter,
468
+ parseRunLog,
469
+ parseFireDashboard
470
+ };
@@ -0,0 +1,86 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SUPPORTED_FLOWS = ['fire', 'aidlc', 'simple'];
5
+
6
+ function directoryExists(dirPath) {
7
+ try {
8
+ return fs.statSync(dirPath).isDirectory();
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ function getFlowMarkerPath(workspacePath, flow) {
15
+ switch (flow) {
16
+ case 'fire':
17
+ return path.join(workspacePath, '.specs-fire');
18
+ case 'aidlc':
19
+ return path.join(workspacePath, 'memory-bank');
20
+ case 'simple':
21
+ return path.join(workspacePath, 'specs');
22
+ default:
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function detectAvailableFlows(workspacePath) {
28
+ return SUPPORTED_FLOWS.filter((flow) => {
29
+ const markerPath = getFlowMarkerPath(workspacePath, flow);
30
+ return markerPath && directoryExists(markerPath);
31
+ });
32
+ }
33
+
34
+ function detectFlow(workspacePath, explicitFlow) {
35
+ const availableFlows = detectAvailableFlows(workspacePath);
36
+
37
+ if (explicitFlow) {
38
+ if (!SUPPORTED_FLOWS.includes(explicitFlow)) {
39
+ const valid = SUPPORTED_FLOWS.join(', ');
40
+ throw new Error(`Invalid flow \"${explicitFlow}\". Valid options: ${valid}`);
41
+ }
42
+
43
+ const markerPath = getFlowMarkerPath(workspacePath, explicitFlow);
44
+ const exists = markerPath ? directoryExists(markerPath) : false;
45
+
46
+ return {
47
+ flow: explicitFlow,
48
+ source: 'flag',
49
+ markerPath,
50
+ detected: exists,
51
+ availableFlows,
52
+ warning: exists
53
+ ? undefined
54
+ : `Flow \"${explicitFlow}\" was selected explicitly but ${markerPath} was not found.`
55
+ };
56
+ }
57
+
58
+ for (const flow of SUPPORTED_FLOWS) {
59
+ const markerPath = getFlowMarkerPath(workspacePath, flow);
60
+ if (markerPath && directoryExists(markerPath)) {
61
+ return {
62
+ flow,
63
+ source: 'auto',
64
+ markerPath,
65
+ detected: true,
66
+ availableFlows
67
+ };
68
+ }
69
+ }
70
+
71
+ return {
72
+ flow: null,
73
+ source: 'auto',
74
+ markerPath: null,
75
+ detected: false,
76
+ availableFlows
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ SUPPORTED_FLOWS,
82
+ directoryExists,
83
+ getFlowMarkerPath,
84
+ detectAvailableFlows,
85
+ detectFlow
86
+ };