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,45 @@
1
+ function getSectionOrderForView(view, options = {}) {
2
+ const includeWorktrees = options.includeWorktrees === true;
3
+ const includeOtherWorktrees = options.includeOtherWorktrees === true;
4
+
5
+ if (view === 'intents') {
6
+ return ['intent-status'];
7
+ }
8
+ if (view === 'completed') {
9
+ return ['completed-runs'];
10
+ }
11
+ if (view === 'health') {
12
+ return ['standards', 'stats', 'warnings', 'error-details'];
13
+ }
14
+ if (view === 'git') {
15
+ return ['git-status', 'git-changes', 'git-commits', 'git-diff'];
16
+ }
17
+ const sections = [];
18
+ if (includeWorktrees) {
19
+ sections.push('worktrees');
20
+ }
21
+ sections.push('current-run', 'run-files');
22
+ if (includeOtherWorktrees) {
23
+ sections.push('other-worktrees-active');
24
+ }
25
+ return sections;
26
+ }
27
+
28
+ function cycleSection(view, currentSectionKey, direction = 1, availableSections = null) {
29
+ const order = Array.isArray(availableSections) && availableSections.length > 0
30
+ ? availableSections
31
+ : getSectionOrderForView(view);
32
+ if (order.length === 0) {
33
+ return currentSectionKey;
34
+ }
35
+
36
+ const currentIndex = order.indexOf(currentSectionKey);
37
+ const safeIndex = currentIndex >= 0 ? currentIndex : 0;
38
+ const nextIndex = (safeIndex + direction + order.length) % order.length;
39
+ return order[nextIndex];
40
+ }
41
+
42
+ module.exports = {
43
+ getSectionOrderForView,
44
+ cycleSection
45
+ };
@@ -0,0 +1,44 @@
1
+ function createInitialUIState() {
2
+ return {
3
+ view: 'runs',
4
+ showHelp: false
5
+ };
6
+ }
7
+
8
+ function cycleView(current) {
9
+ if (current === 'runs') {
10
+ return 'intents';
11
+ }
12
+ if (current === 'intents') {
13
+ return 'completed';
14
+ }
15
+ if (current === 'completed') {
16
+ return 'health';
17
+ }
18
+ if (current === 'health') {
19
+ return 'git';
20
+ }
21
+ return 'runs';
22
+ }
23
+
24
+ function cycleViewBackward(current) {
25
+ if (current === 'runs') {
26
+ return 'git';
27
+ }
28
+ if (current === 'intents') {
29
+ return 'runs';
30
+ }
31
+ if (current === 'completed') {
32
+ return 'intents';
33
+ }
34
+ if (current === 'health') {
35
+ return 'completed';
36
+ }
37
+ return 'health';
38
+ }
39
+
40
+ module.exports = {
41
+ createInitialUIState,
42
+ cycleView,
43
+ cycleViewBackward
44
+ };
@@ -0,0 +1,61 @@
1
+ const { truncate } = require('../components/header');
2
+
3
+ const STANDARD_TYPES = [
4
+ 'constitution',
5
+ 'tech-stack',
6
+ 'coding-standards',
7
+ 'testing-standards',
8
+ 'system-architecture'
9
+ ];
10
+
11
+ function renderOverviewViewLines(snapshot, width) {
12
+ const lines = ['Overview'];
13
+
14
+ if (!snapshot?.initialized) {
15
+ lines.push('FIRE project folder exists but state.yaml is missing.');
16
+ lines.push('Run initialization and the overview will appear automatically.');
17
+ return lines.map((line) => truncate(line, width));
18
+ }
19
+
20
+ const project = snapshot.project || {};
21
+ const workspace = snapshot.workspace || {};
22
+
23
+ lines.push(`Project: ${project.name || 'Unknown'} | FIRE version: ${project.fireVersion || snapshot.version || '0.0.0'}`);
24
+ lines.push(`Workspace: ${workspace.type || 'unknown'} / ${workspace.structure || 'unknown'} | autonomy: ${workspace.autonomyBias || 'unknown'} | run scope pref: ${workspace.runScopePreference || 'unknown'}`);
25
+ lines.push('');
26
+
27
+ lines.push('Intent Summary');
28
+ lines.push(` total: ${snapshot.stats.totalIntents} | completed: ${snapshot.stats.completedIntents} | in_progress: ${snapshot.stats.inProgressIntents} | pending: ${snapshot.stats.pendingIntents} | blocked: ${snapshot.stats.blockedIntents}`);
29
+ lines.push('');
30
+
31
+ lines.push('Work Item Summary');
32
+ lines.push(` total: ${snapshot.stats.totalWorkItems} | completed: ${snapshot.stats.completedWorkItems} | in_progress: ${snapshot.stats.inProgressWorkItems} | pending: ${snapshot.stats.pendingWorkItems} | blocked: ${snapshot.stats.blockedWorkItems}`);
33
+ lines.push('');
34
+
35
+ const standardSet = new Set((snapshot.standards || []).map((item) => item.type));
36
+ lines.push('Standards');
37
+ for (const type of STANDARD_TYPES) {
38
+ const marker = standardSet.has(type) ? '[x]' : '[ ]';
39
+ lines.push(` ${marker} ${type}.md`);
40
+ }
41
+
42
+ lines.push('');
43
+ lines.push('Top Intents');
44
+
45
+ const intents = (snapshot.intents || []).slice(0, 6);
46
+ if (intents.length === 0) {
47
+ lines.push(' - none');
48
+ } else {
49
+ for (const intent of intents) {
50
+ const totalWorkItems = (intent.workItems || []).length;
51
+ const completedWorkItems = (intent.workItems || []).filter((item) => item.status === 'completed').length;
52
+ lines.push(` - ${intent.id}: ${intent.status} (${completedWorkItems}/${totalWorkItems} work items completed)`);
53
+ }
54
+ }
55
+
56
+ return lines.map((line) => truncate(line, width));
57
+ }
58
+
59
+ module.exports = {
60
+ renderOverviewViewLines
61
+ };
@@ -0,0 +1,93 @@
1
+ const { truncate } = require('../components/header');
2
+
3
+ function safeArray(value) {
4
+ return Array.isArray(value) ? value : [];
5
+ }
6
+
7
+ function formatRunProgress(run) {
8
+ const workItems = safeArray(run.workItems);
9
+ const total = workItems.length;
10
+ const completed = workItems.filter((item) => item.status === 'completed').length;
11
+ const inProgress = workItems.filter((item) => item.status === 'in_progress').length;
12
+ return `${completed}/${total} completed${inProgress > 0 ? `, ${inProgress} in_progress` : ''}`;
13
+ }
14
+
15
+ function renderActiveRunLines(activeRuns, width) {
16
+ const lines = ['Active Runs'];
17
+ if (!activeRuns || activeRuns.length === 0) {
18
+ lines.push(' - none');
19
+ return lines.map((line) => truncate(line, width));
20
+ }
21
+
22
+ for (const run of activeRuns) {
23
+ const currentItem = run.currentItem || 'n/a';
24
+ const artifacts = [
25
+ run.hasPlan ? 'plan' : null,
26
+ run.hasWalkthrough ? 'walkthrough' : null,
27
+ run.hasTestReport ? 'test-report' : null
28
+ ].filter(Boolean).join(', ') || 'no artifacts yet';
29
+
30
+ lines.push(` - ${run.id} [${run.scope}] current: ${currentItem}`);
31
+ lines.push(` progress: ${formatRunProgress(run)} | artifacts: ${artifacts}`);
32
+ }
33
+
34
+ return lines.map((line) => truncate(line, width));
35
+ }
36
+
37
+ function renderPendingQueueLines(pendingItems, width) {
38
+ const lines = ['Pending Queue'];
39
+ if (!pendingItems || pendingItems.length === 0) {
40
+ lines.push(' - none');
41
+ return lines.map((line) => truncate(line, width));
42
+ }
43
+
44
+ for (const item of pendingItems.slice(0, 12)) {
45
+ const deps = item.dependencies && item.dependencies.length > 0
46
+ ? ` deps:${item.dependencies.join(',')}`
47
+ : '';
48
+ lines.push(` - ${item.id} (${item.mode}/${item.complexity}) in ${item.intentTitle}${deps}`);
49
+ }
50
+
51
+ if (pendingItems.length > 12) {
52
+ lines.push(` ... ${pendingItems.length - 12} more pending items`);
53
+ }
54
+
55
+ return lines.map((line) => truncate(line, width));
56
+ }
57
+
58
+ function renderCompletedRunLines(completedRuns, width) {
59
+ const lines = ['Recent Completed Runs'];
60
+ if (!completedRuns || completedRuns.length === 0) {
61
+ lines.push(' - none');
62
+ return lines.map((line) => truncate(line, width));
63
+ }
64
+
65
+ for (const run of completedRuns.slice(0, 5)) {
66
+ const completedAt = run.completedAt || 'unknown';
67
+ lines.push(` - ${run.id} [${run.scope}] completed: ${completedAt} | ${formatRunProgress(run)}`);
68
+ }
69
+
70
+ return lines.map((line) => truncate(line, width));
71
+ }
72
+
73
+ function renderRunsViewLines(snapshot, width) {
74
+ const lines = [];
75
+
76
+ if (!snapshot?.initialized) {
77
+ lines.push('FIRE detected, but state.yaml is not initialized yet.');
78
+ lines.push('Initialize FIRE project context, then the dashboard will auto-populate.');
79
+ return lines.map((line) => truncate(line, width));
80
+ }
81
+
82
+ lines.push(...renderActiveRunLines(snapshot.activeRuns, width));
83
+ lines.push('');
84
+ lines.push(...renderPendingQueueLines(snapshot.pendingItems, width));
85
+ lines.push('');
86
+ lines.push(...renderCompletedRunLines(snapshot.completedRuns, width));
87
+
88
+ return lines.map((line) => truncate(line, width));
89
+ }
90
+
91
+ module.exports = {
92
+ renderRunsViewLines
93
+ };
@@ -0,0 +1,229 @@
1
+ const path = require('path');
2
+ const { clampIndex } = require('./helpers');
3
+ const { truncate } = require('./helpers');
4
+ const { getEffectiveFlow, getCurrentBolt } = require('./flow-builders');
5
+ const { collectAidlcBoltFiles } = require('./file-entries');
6
+ const { collectFireRunFiles } = require('./file-entries');
7
+
8
+ function getDashboardWorktreeMeta(snapshot) {
9
+ if (!snapshot || typeof snapshot !== 'object') {
10
+ return null;
11
+ }
12
+ const meta = snapshot.dashboardWorktrees;
13
+ if (!meta || typeof meta !== 'object') {
14
+ return null;
15
+ }
16
+ const items = Array.isArray(meta.items) ? meta.items : [];
17
+ if (items.length === 0) {
18
+ return null;
19
+ }
20
+ return {
21
+ ...meta,
22
+ items
23
+ };
24
+ }
25
+
26
+ function getWorktreeItems(snapshot) {
27
+ return getDashboardWorktreeMeta(snapshot)?.items || [];
28
+ }
29
+
30
+ function getSelectedWorktree(snapshot) {
31
+ const meta = getDashboardWorktreeMeta(snapshot);
32
+ if (!meta) {
33
+ return null;
34
+ }
35
+ return meta.items.find((item) => item.id === meta.selectedWorktreeId) || null;
36
+ }
37
+
38
+ function hasMultipleWorktrees(snapshot) {
39
+ return getWorktreeItems(snapshot).length > 1;
40
+ }
41
+
42
+ function isSelectedWorktreeMain(snapshot) {
43
+ const selected = getSelectedWorktree(snapshot);
44
+ return Boolean(selected?.isMainBranch);
45
+ }
46
+
47
+ function getWorktreeDisplayName(worktree) {
48
+ if (!worktree || typeof worktree !== 'object') {
49
+ return 'unknown';
50
+ }
51
+ if (typeof worktree.displayBranch === 'string' && worktree.displayBranch.trim() !== '') {
52
+ return worktree.displayBranch;
53
+ }
54
+ if (typeof worktree.branch === 'string' && worktree.branch.trim() !== '') {
55
+ return worktree.branch;
56
+ }
57
+ if (typeof worktree.name === 'string' && worktree.name.trim() !== '') {
58
+ return worktree.name;
59
+ }
60
+ return path.basename(worktree.path || '') || 'unknown';
61
+ }
62
+
63
+ function buildWorktreeRows(snapshot, flow) {
64
+ const meta = getDashboardWorktreeMeta(snapshot);
65
+ if (!meta) {
66
+ return [{
67
+ kind: 'info',
68
+ key: 'worktrees:none',
69
+ label: 'No git worktrees detected',
70
+ selectable: false
71
+ }];
72
+ }
73
+
74
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
75
+ const entityLabel = effectiveFlow === 'aidlc'
76
+ ? 'active bolts'
77
+ : (effectiveFlow === 'simple' ? 'active specs' : 'active runs');
78
+
79
+ const rows = [];
80
+ for (const item of meta.items) {
81
+ const currentLabel = item.isSelected ? '[CURRENT] ' : '';
82
+ const mainLabel = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
83
+ const availabilityLabel = item.flowAvailable ? '' : ' (flow unavailable)';
84
+ const statusLabel = item.status === 'loading'
85
+ ? ' loading...'
86
+ : (item.status === 'error' ? ' error' : ` ${item.activeCount || 0} ${entityLabel}`);
87
+ const scopeLabel = item.name ? ` (${item.name})` : '';
88
+
89
+ rows.push({
90
+ kind: 'info',
91
+ key: `worktree:item:${item.id}`,
92
+ label: `${currentLabel}${mainLabel}${getWorktreeDisplayName(item)}${scopeLabel}${availabilityLabel}${statusLabel}`,
93
+ color: item.isSelected ? 'green' : (item.flowAvailable ? 'gray' : 'red'),
94
+ bold: item.isSelected,
95
+ selectable: true
96
+ });
97
+ }
98
+
99
+ return rows;
100
+ }
101
+
102
+ function buildOtherWorktreeActiveGroups(snapshot, flow) {
103
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
104
+ if (effectiveFlow === 'simple') {
105
+ return [];
106
+ }
107
+
108
+ const meta = getDashboardWorktreeMeta(snapshot);
109
+ if (!meta) {
110
+ return [];
111
+ }
112
+
113
+ const selectedWorktree = getSelectedWorktree(snapshot);
114
+ if (!selectedWorktree || !selectedWorktree.isMainBranch) {
115
+ return [];
116
+ }
117
+
118
+ const groups = [];
119
+ const otherItems = meta.items.filter((item) => item.id !== meta.selectedWorktreeId);
120
+ for (const item of otherItems) {
121
+ if (!item.flowAvailable || item.status === 'unavailable' || item.status === 'error') {
122
+ continue;
123
+ }
124
+
125
+ if (effectiveFlow === 'aidlc') {
126
+ const activeBolts = Array.isArray(item.activity?.activeBolts) ? item.activity.activeBolts : [];
127
+ for (const bolt of activeBolts) {
128
+ const stages = Array.isArray(bolt?.stages) ? bolt.stages : [];
129
+ const completedStages = stages.filter((stage) => stage?.status === 'completed').length;
130
+ groups.push({
131
+ key: `other:wt:${item.id}:bolt:${bolt.id}`,
132
+ label: `[WT ${getWorktreeDisplayName(item)}] ${bolt.id} [${bolt.type || 'bolt'}] ${completedStages}/${stages.length || 0} stages`,
133
+ files: collectAidlcBoltFiles(bolt).map((file) => ({ ...file, scope: 'active' }))
134
+ });
135
+ }
136
+ continue;
137
+ }
138
+
139
+ const activeRuns = Array.isArray(item.activity?.activeRuns) ? item.activity.activeRuns : [];
140
+ for (const run of activeRuns) {
141
+ const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
142
+ const completed = workItems.filter((workItem) => workItem?.status === 'completed').length;
143
+ groups.push({
144
+ key: `other:wt:${item.id}:run:${run.id}`,
145
+ label: `[WT ${getWorktreeDisplayName(item)}] ${run.id} [${run.scope || 'single'}] ${completed}/${workItems.length} items`,
146
+ files: collectFireRunFiles(run).map((file) => ({ ...file, scope: 'active' }))
147
+ });
148
+ }
149
+ }
150
+
151
+ return groups;
152
+ }
153
+
154
+ function getOtherWorktreeEmptyMessage(snapshot, flow) {
155
+ const effectiveFlow = getEffectiveFlow(flow, snapshot);
156
+ if (!hasMultipleWorktrees(snapshot)) {
157
+ return 'No additional worktrees';
158
+ }
159
+ if (!isSelectedWorktreeMain(snapshot)) {
160
+ return 'Switch to main worktree to view active items from other worktrees';
161
+ }
162
+ if (effectiveFlow === 'aidlc') {
163
+ return 'No active bolts in other worktrees';
164
+ }
165
+ if (effectiveFlow === 'simple') {
166
+ return 'No active specs in other worktrees';
167
+ }
168
+ return 'No active runs in other worktrees';
169
+ }
170
+
171
+ function buildWorktreeOverlayLines(snapshot, selectedIndex, width) {
172
+ const meta = getDashboardWorktreeMeta(snapshot);
173
+ if (!meta) {
174
+ return [{
175
+ text: truncate('No worktrees available', width),
176
+ color: 'gray',
177
+ bold: false
178
+ }];
179
+ }
180
+
181
+ const items = Array.isArray(meta.items) ? meta.items : [];
182
+ const clampedIndex = clampIndex(selectedIndex, items.length || 1);
183
+ const lines = [{
184
+ text: truncate('Use ↑/↓ and Enter to switch. Esc closes.', width),
185
+ color: 'gray',
186
+ bold: false
187
+ }];
188
+
189
+ for (let index = 0; index < items.length; index += 1) {
190
+ const item = items[index];
191
+ const marker = index === clampedIndex ? '>' : ' ';
192
+ const current = item.isSelected ? '[CURRENT] ' : '';
193
+ const main = item.isMainBranch && !item.detached ? '[MAIN] ' : '';
194
+ const status = item.status === 'loading'
195
+ ? 'loading'
196
+ : (item.status === 'error'
197
+ ? 'error'
198
+ : (item.flowAvailable ? `${item.activeCount || 0} active` : 'flow unavailable'));
199
+ const pathLabel = item.path ? path.basename(item.path) : 'unknown';
200
+ lines.push({
201
+ text: truncate(`${marker} ${current}${main}${getWorktreeDisplayName(item)} (${pathLabel}) | ${status}`, width),
202
+ color: index === clampedIndex ? 'green' : (item.isSelected ? 'cyan' : 'gray'),
203
+ bold: index === clampedIndex || item.isSelected
204
+ });
205
+ }
206
+
207
+ if (meta.hasPendingScans) {
208
+ lines.push({
209
+ text: truncate('Background scan in progress for additional worktrees...', width),
210
+ color: 'yellow',
211
+ bold: false
212
+ });
213
+ }
214
+
215
+ return lines;
216
+ }
217
+
218
+ module.exports = {
219
+ getDashboardWorktreeMeta,
220
+ getWorktreeItems,
221
+ getSelectedWorktree,
222
+ hasMultipleWorktrees,
223
+ isSelectedWorktreeMain,
224
+ getWorktreeDisplayName,
225
+ buildWorktreeRows,
226
+ buildOtherWorktreeActiveGroups,
227
+ getOtherWorktreeEmptyMessage,
228
+ buildWorktreeOverlayLines
229
+ };
@@ -1,4 +1,8 @@
1
1
  const ToolInstaller = require('./ToolInstaller');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const CLIUtils = require('../cli-utils');
5
+ const { theme } = CLIUtils;
2
6
 
3
7
  class CodexInstaller extends ToolInstaller {
4
8
  get key() {
@@ -10,12 +14,79 @@ class CodexInstaller extends ToolInstaller {
10
14
  }
11
15
 
12
16
  get commandsDir() {
13
- return '.codex';
17
+ return path.join('.codex', 'skills');
14
18
  }
15
19
 
16
20
  get detectPath() {
17
21
  return '.codex';
18
22
  }
23
+
24
+ async installCommands(flowPath, config) {
25
+ const targetSkillsDir = this.commandsDir;
26
+ console.log(theme.dim(` Installing skills to ${targetSkillsDir}/...`));
27
+ await fs.ensureDir(targetSkillsDir);
28
+
29
+ const commandsSourceDir = path.join(flowPath, 'commands');
30
+ if (!await fs.pathExists(commandsSourceDir)) {
31
+ console.log(theme.warning(` No commands folder found at ${commandsSourceDir}`));
32
+ return [];
33
+ }
34
+
35
+ const commandFiles = await fs.readdir(commandsSourceDir);
36
+ const installedFiles = [];
37
+
38
+ for (const cmdFile of commandFiles) {
39
+ if (!cmdFile.endsWith('.md')) continue;
40
+
41
+ try {
42
+ const sourcePath = path.join(commandsSourceDir, cmdFile);
43
+ const content = await fs.readFile(sourcePath, 'utf8');
44
+ const commandName = cmdFile.replace('.md', '');
45
+ const prefix = (config && config.command && config.command.prefix) ? `${config.command.prefix}-` : '';
46
+ const skillName = `specsmd-${prefix}${commandName}`;
47
+
48
+ const { description, body } = this.parseFrontmatter(content);
49
+
50
+ // Build SKILL.md with Codex frontmatter
51
+ const skillContent = [
52
+ '---',
53
+ `name: ${skillName}`,
54
+ `description: "${description || 'specsmd agent'}"`,
55
+ '---',
56
+ '',
57
+ body
58
+ ].join('\n');
59
+
60
+ // Write SKILL.md
61
+ const skillDir = path.join(targetSkillsDir, skillName);
62
+ await fs.ensureDir(skillDir);
63
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent, 'utf8');
64
+
65
+ installedFiles.push(skillName);
66
+ } catch (err) {
67
+ console.log(theme.warning(` Failed to install ${cmdFile}: ${err.message}`));
68
+ }
69
+ }
70
+
71
+ CLIUtils.displayStatus('', `Installed ${installedFiles.length} skills for ${this.name}`, 'success');
72
+ return installedFiles;
73
+ }
74
+
75
+ /**
76
+ * Parse YAML frontmatter from a markdown file
77
+ */
78
+ parseFrontmatter(content) {
79
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
80
+ if (!match) return { description: '', body: content };
81
+
82
+ const frontmatter = match[1];
83
+ const body = match[2];
84
+ const descMatch = frontmatter.match(/description:\s*["']?(.+?)["']?\s*$/m);
85
+ return {
86
+ description: descMatch ? descMatch[1] : '',
87
+ body: body.trim()
88
+ };
89
+ }
19
90
  }
20
91
 
21
92
  module.exports = CodexInstaller;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.0.0-dev.85",
3
+ "version": "0.0.0-dev.87",
4
4
  "description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {
@@ -40,18 +40,22 @@
40
40
  "README.md"
41
41
  ],
42
42
  "dependencies": {
43
+ "@inkjs/ui": "^2.0.0",
43
44
  "chalk": "^4.1.2",
45
+ "chokidar": "^4.0.3",
44
46
  "commander": "^11.1.0",
45
47
  "figlet": "^1.9.4",
46
48
  "fs-extra": "^11.1.1",
47
49
  "gradient-string": "^2.0.2",
50
+ "ink": "^5.2.1",
48
51
  "js-yaml": "^4.1.0",
49
52
  "mixpanel": "^0.18.0",
50
53
  "oh-my-logo": "^0.4.0",
51
- "prompts": "^2.4.2"
54
+ "prompts": "^2.4.2",
55
+ "react": "^18.3.1"
52
56
  },
53
57
  "engines": {
54
- "node": ">=14.0.0"
58
+ "node": ">=20.0.0"
55
59
  },
56
60
  "devDependencies": {
57
61
  "@types/js-yaml": "^4.0.9",