@forgelore/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1652 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1652 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import { mkdir } from "fs/promises";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import {
|
|
12
|
+
createDefaultConfig,
|
|
13
|
+
writeConfig,
|
|
14
|
+
configExists,
|
|
15
|
+
getForgeloreDir
|
|
16
|
+
} from "@forgelore/core";
|
|
17
|
+
import { scaffoldSpecDirs } from "@forgelore/core";
|
|
18
|
+
import { scaffoldKnowledge } from "@forgelore/core";
|
|
19
|
+
|
|
20
|
+
// src/ui/banner.ts
|
|
21
|
+
import boxen from "boxen";
|
|
22
|
+
import chalk2 from "chalk";
|
|
23
|
+
import gradient2 from "gradient-string";
|
|
24
|
+
|
|
25
|
+
// src/ui/theme.ts
|
|
26
|
+
import chalk from "chalk";
|
|
27
|
+
import gradient from "gradient-string";
|
|
28
|
+
var colors = {
|
|
29
|
+
primary: chalk.hex("#FF6B35"),
|
|
30
|
+
// forge orange
|
|
31
|
+
secondary: chalk.hex("#06B6D4"),
|
|
32
|
+
// lore cyan
|
|
33
|
+
accent: chalk.hex("#FFD700"),
|
|
34
|
+
// spark gold
|
|
35
|
+
lore: chalk.hex("#7C3AED"),
|
|
36
|
+
// lore violet
|
|
37
|
+
success: chalk.hex("#10B981"),
|
|
38
|
+
// emerald
|
|
39
|
+
warning: chalk.hex("#F97316"),
|
|
40
|
+
// orange
|
|
41
|
+
error: chalk.hex("#EF4444"),
|
|
42
|
+
// red
|
|
43
|
+
muted: chalk.hex("#6B7280"),
|
|
44
|
+
// gray
|
|
45
|
+
dim: chalk.dim,
|
|
46
|
+
bold: chalk.bold,
|
|
47
|
+
white: chalk.white
|
|
48
|
+
};
|
|
49
|
+
var gradients = {
|
|
50
|
+
brand: gradient(["#FF6B35", "#FFD700", "#7C3AED", "#06B6D4"]),
|
|
51
|
+
forge: gradient(["#FF6B35", "#FFD700"]),
|
|
52
|
+
lore: gradient(["#7C3AED", "#06B6D4"]),
|
|
53
|
+
warm: gradient(["#F59E0B", "#EF4444"]),
|
|
54
|
+
cool: gradient(["#06B6D4", "#7C3AED"]),
|
|
55
|
+
success: gradient(["#10B981", "#06B6D4"]),
|
|
56
|
+
sunset: gradient(["#F97316", "#EF4444", "#7C3AED"])
|
|
57
|
+
};
|
|
58
|
+
var icons = {
|
|
59
|
+
success: colors.success("\u2713"),
|
|
60
|
+
error: colors.error("\u2717"),
|
|
61
|
+
warning: colors.warning("\u26A0"),
|
|
62
|
+
info: colors.secondary("\u25C6"),
|
|
63
|
+
pending: colors.muted("\u25CB"),
|
|
64
|
+
inProgress: colors.accent("\u25CF"),
|
|
65
|
+
arrow: colors.primary("\u2192"),
|
|
66
|
+
bullet: colors.muted("\u2022"),
|
|
67
|
+
star: colors.accent("\u2605"),
|
|
68
|
+
spark: colors.accent("\u2726"),
|
|
69
|
+
anvil: colors.primary("\u2692")
|
|
70
|
+
};
|
|
71
|
+
function statusColor(status) {
|
|
72
|
+
switch (status) {
|
|
73
|
+
case "passed":
|
|
74
|
+
case "validated":
|
|
75
|
+
case "archived":
|
|
76
|
+
return colors.success(status);
|
|
77
|
+
case "failed":
|
|
78
|
+
return colors.error(status);
|
|
79
|
+
case "in-progress":
|
|
80
|
+
case "claimed":
|
|
81
|
+
case "implementing":
|
|
82
|
+
case "validating":
|
|
83
|
+
return colors.accent(status);
|
|
84
|
+
case "blocked":
|
|
85
|
+
return colors.error(status);
|
|
86
|
+
case "pending":
|
|
87
|
+
case "proposed":
|
|
88
|
+
case "planning":
|
|
89
|
+
return colors.muted(status);
|
|
90
|
+
default:
|
|
91
|
+
return colors.white(status);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function progressBar(percent, width = 20) {
|
|
95
|
+
const filled = Math.round(percent / 100 * width);
|
|
96
|
+
const empty = width - filled;
|
|
97
|
+
const bar = colors.success("\u2588".repeat(filled)) + colors.muted("\u2591".repeat(empty));
|
|
98
|
+
return `${bar} ${percent}%`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/ui/banner.ts
|
|
102
|
+
var forgeGradient = gradient2(["#FF6B35", "#FFD700"]);
|
|
103
|
+
var loreGradient = gradient2(["#7C3AED", "#06B6D4"]);
|
|
104
|
+
var brandGradient = gradient2(["#FF6B35", "#FFD700", "#7C3AED", "#06B6D4"]);
|
|
105
|
+
var sparkColor = chalk2.hex("#FFD700");
|
|
106
|
+
var emberColor = chalk2.hex("#FF6B35");
|
|
107
|
+
var TITLE_GRADIENT = [
|
|
108
|
+
"#FF6B35",
|
|
109
|
+
// F - forge orange
|
|
110
|
+
"#FF8E3C",
|
|
111
|
+
// O
|
|
112
|
+
"#FFB347",
|
|
113
|
+
// R
|
|
114
|
+
"#FFD700",
|
|
115
|
+
// G - spark gold
|
|
116
|
+
"#C9A832",
|
|
117
|
+
// E
|
|
118
|
+
"#9B6FC0",
|
|
119
|
+
// L - transitioning
|
|
120
|
+
"#7C3AED",
|
|
121
|
+
// O - lore violet
|
|
122
|
+
"#4196CB",
|
|
123
|
+
// R
|
|
124
|
+
"#06B6D4"
|
|
125
|
+
// E - lore cyan
|
|
126
|
+
];
|
|
127
|
+
var BOOK_LINES = [
|
|
128
|
+
" .\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500.",
|
|
129
|
+
" \u2571 \u2261 \u2261 \u2261 \u2261 \u2261 \u2571\u2502",
|
|
130
|
+
" \u2571 \u2261 \u2261 \u2261 \u2261 \u2261 \u2571 \u2502",
|
|
131
|
+
" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u2502",
|
|
132
|
+
" \u2502 \u2261 \u2261 \u2261 \u2261 \u2261 \u2502 \u2502",
|
|
133
|
+
" \u2502 \u2261 \u2261 \u2261 \u2261 \u2261 \u2502 \u2571",
|
|
134
|
+
" \u2502 \u2261 \u2261 \u2261 \u2261 \u2261 \u2502\u2571",
|
|
135
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"
|
|
136
|
+
];
|
|
137
|
+
var ANVIL_LINES = [
|
|
138
|
+
" \u2554\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2550\u2557",
|
|
139
|
+
" \u2501\u2501\u2501\u2501\u2501\u2563\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2560\u2501\u2501\u2501\u2501\u2501",
|
|
140
|
+
" \u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551",
|
|
141
|
+
" \u255A\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2550\u255D",
|
|
142
|
+
" \u2588\u2588\u2588\u2588\u2588\u2567\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
143
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
|
|
144
|
+
];
|
|
145
|
+
var SPARK_CHARS = ["\u2726", "\u2727", "\xB7", "\u2736", "\u2217"];
|
|
146
|
+
function getColoredBook() {
|
|
147
|
+
return BOOK_LINES.map((l) => loreGradient(l));
|
|
148
|
+
}
|
|
149
|
+
function getColoredAnvil() {
|
|
150
|
+
return ANVIL_LINES.map((l) => forgeGradient(l));
|
|
151
|
+
}
|
|
152
|
+
function getTitle() {
|
|
153
|
+
return "FORGELORE".split("").map((ch, i) => chalk2.bold(chalk2.hex(TITLE_GRADIENT[i])(ch))).join("");
|
|
154
|
+
}
|
|
155
|
+
function randomSpark() {
|
|
156
|
+
return sparkColor(
|
|
157
|
+
SPARK_CHARS[Math.floor(Math.random() * SPARK_CHARS.length)]
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
161
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
162
|
+
var CLEAR_LINE = "\x1B[2K";
|
|
163
|
+
function moveUp(n) {
|
|
164
|
+
return n > 0 ? `\x1B[${n}A` : "";
|
|
165
|
+
}
|
|
166
|
+
function sleep(ms) {
|
|
167
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
168
|
+
}
|
|
169
|
+
function renderBanner() {
|
|
170
|
+
const book = getColoredBook().join("\n");
|
|
171
|
+
const anvil = getColoredAnvil().join("\n");
|
|
172
|
+
const title = ` ${getTitle()}`;
|
|
173
|
+
const tagline = colors.muted(" forge knowledge, shape code");
|
|
174
|
+
const version = chalk2.dim(" v0.1.0");
|
|
175
|
+
return `
|
|
176
|
+
${book}
|
|
177
|
+
${anvil}
|
|
178
|
+
|
|
179
|
+
${title}
|
|
180
|
+
${tagline}${version}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
async function renderAnimatedBanner() {
|
|
184
|
+
if (!process.stdout.isTTY) {
|
|
185
|
+
console.log(renderBanner());
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const bookColored = getColoredBook();
|
|
189
|
+
const anvilColored = getColoredAnvil();
|
|
190
|
+
const allLines = [...bookColored, ...anvilColored];
|
|
191
|
+
const totalHeight = allLines.length;
|
|
192
|
+
process.stdout.write(HIDE_CURSOR);
|
|
193
|
+
try {
|
|
194
|
+
console.log("");
|
|
195
|
+
for (let i = anvilColored.length - 1; i >= 0; i--) {
|
|
196
|
+
const visible = anvilColored.slice(i);
|
|
197
|
+
for (const line of visible) {
|
|
198
|
+
process.stdout.write(CLEAR_LINE + line + "\n");
|
|
199
|
+
}
|
|
200
|
+
await sleep(45);
|
|
201
|
+
if (i > 0) {
|
|
202
|
+
process.stdout.write(moveUp(visible.length));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const anvilHeight = anvilColored.length;
|
|
206
|
+
process.stdout.write(moveUp(anvilHeight));
|
|
207
|
+
for (let i = bookColored.length - 1; i >= 0; i--) {
|
|
208
|
+
const visibleBook = bookColored.slice(i);
|
|
209
|
+
const frame = [...visibleBook, ...anvilColored];
|
|
210
|
+
for (const line of frame) {
|
|
211
|
+
process.stdout.write(CLEAR_LINE + line + "\n");
|
|
212
|
+
}
|
|
213
|
+
await sleep(35);
|
|
214
|
+
process.stdout.write(moveUp(frame.length));
|
|
215
|
+
}
|
|
216
|
+
for (const line of allLines) {
|
|
217
|
+
process.stdout.write(CLEAR_LINE + line + "\n");
|
|
218
|
+
}
|
|
219
|
+
for (let cycle = 0; cycle < 5; cycle++) {
|
|
220
|
+
await sleep(90);
|
|
221
|
+
process.stdout.write(moveUp(totalHeight));
|
|
222
|
+
for (let i = 0; i < totalHeight; i++) {
|
|
223
|
+
const line = allLines[i];
|
|
224
|
+
let spark = "";
|
|
225
|
+
if (i >= bookColored.length - 2 && i <= bookColored.length + 2) {
|
|
226
|
+
if (Math.random() > 0.3) {
|
|
227
|
+
spark = " " + randomSpark();
|
|
228
|
+
if (Math.random() > 0.5) spark += " " + randomSpark();
|
|
229
|
+
}
|
|
230
|
+
} else if (Math.random() > 0.7) {
|
|
231
|
+
spark = " " + randomSpark();
|
|
232
|
+
}
|
|
233
|
+
process.stdout.write(CLEAR_LINE + line + spark + "\n");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
process.stdout.write(moveUp(totalHeight));
|
|
237
|
+
for (const line of allLines) {
|
|
238
|
+
process.stdout.write(CLEAR_LINE + line + "\n");
|
|
239
|
+
}
|
|
240
|
+
console.log("");
|
|
241
|
+
process.stdout.write(" ");
|
|
242
|
+
const title = "FORGELORE";
|
|
243
|
+
for (let i = 0; i < title.length; i++) {
|
|
244
|
+
process.stdout.write(emberColor("\u2588"));
|
|
245
|
+
await sleep(35);
|
|
246
|
+
process.stdout.write("\b" + chalk2.bold(chalk2.hex(TITLE_GRADIENT[i])(title[i])));
|
|
247
|
+
await sleep(50);
|
|
248
|
+
}
|
|
249
|
+
console.log("");
|
|
250
|
+
await sleep(200);
|
|
251
|
+
console.log(colors.muted(" forge knowledge, shape code") + chalk2.dim(" v0.1.0"));
|
|
252
|
+
console.log("");
|
|
253
|
+
} finally {
|
|
254
|
+
process.stdout.write(SHOW_CURSOR);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function renderBox(content, title, borderColor = "#FF6B35") {
|
|
258
|
+
return boxen(content, {
|
|
259
|
+
padding: 1,
|
|
260
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
261
|
+
borderStyle: "round",
|
|
262
|
+
borderColor,
|
|
263
|
+
title,
|
|
264
|
+
titleAlignment: "left"
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function renderSection(title, content) {
|
|
268
|
+
return `
|
|
269
|
+
${brandGradient(` ${title} `)}
|
|
270
|
+
${content}
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/commands/init.ts
|
|
275
|
+
async function initCommand(options) {
|
|
276
|
+
const projectRoot = resolve(options.cwd || process.cwd());
|
|
277
|
+
console.log(renderBanner());
|
|
278
|
+
if (await configExists(projectRoot)) {
|
|
279
|
+
console.log(
|
|
280
|
+
renderBox(
|
|
281
|
+
`${icons.warning} forgelore is already initialized in this project.
|
|
282
|
+
${colors.muted("Run")} ${colors.primary("forgelore status")} ${colors.muted("to see current state.")}`,
|
|
283
|
+
"Already Initialized"
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
p.intro(gradients.brand(" forgelore init "));
|
|
289
|
+
const mode = await p.select({
|
|
290
|
+
message: "How do you want to manage specs?",
|
|
291
|
+
options: [
|
|
292
|
+
{
|
|
293
|
+
value: "local",
|
|
294
|
+
label: "Local only",
|
|
295
|
+
hint: "Specs live in this repo only"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
value: "local+global",
|
|
299
|
+
label: "Local + Global",
|
|
300
|
+
hint: "Local specs + a shared company spec repo"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
value: "global",
|
|
304
|
+
label: "Global only",
|
|
305
|
+
hint: "Reference a shared spec repo"
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
});
|
|
309
|
+
if (p.isCancel(mode)) {
|
|
310
|
+
p.cancel("Init cancelled.");
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
let globalSource;
|
|
314
|
+
if (mode === "local+global" || mode === "global") {
|
|
315
|
+
const source = await p.text({
|
|
316
|
+
message: "Global spec source (filesystem path or GitHub URL):",
|
|
317
|
+
placeholder: "/path/to/company-specs or https://github.com/org/specs",
|
|
318
|
+
validate: (val) => {
|
|
319
|
+
if (!val) return "Please enter a path or URL";
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (p.isCancel(source)) {
|
|
324
|
+
p.cancel("Init cancelled.");
|
|
325
|
+
process.exit(0);
|
|
326
|
+
}
|
|
327
|
+
globalSource = source;
|
|
328
|
+
}
|
|
329
|
+
const config = createDefaultConfig(mode);
|
|
330
|
+
if (globalSource) {
|
|
331
|
+
const isUrl = globalSource.startsWith("http://") || globalSource.startsWith("https://");
|
|
332
|
+
config.global = {
|
|
333
|
+
source: globalSource,
|
|
334
|
+
path: isUrl ? `~/.cache/forgelore/global/${globalSource.split("/").pop()}` : globalSource,
|
|
335
|
+
autoSync: true
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const spinner = ora({
|
|
339
|
+
text: "Scaffolding forgelore...",
|
|
340
|
+
color: "magenta"
|
|
341
|
+
}).start();
|
|
342
|
+
try {
|
|
343
|
+
const forgeloreDir = getForgeloreDir(projectRoot);
|
|
344
|
+
await mkdir(forgeloreDir, { recursive: true });
|
|
345
|
+
spinner.text = "Creating directory structure...";
|
|
346
|
+
await scaffoldSpecDirs(projectRoot);
|
|
347
|
+
spinner.text = "Setting up knowledge base...";
|
|
348
|
+
await scaffoldKnowledge(projectRoot);
|
|
349
|
+
spinner.text = "Writing config...";
|
|
350
|
+
await writeConfig(projectRoot, config);
|
|
351
|
+
spinner.succeed(colors.success("forgelore initialized"));
|
|
352
|
+
} catch (err) {
|
|
353
|
+
spinner.fail(colors.error("Failed to initialize forgelore"));
|
|
354
|
+
console.error(err);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
const summary = [
|
|
358
|
+
`${icons.success} ${colors.white("Created:")}`,
|
|
359
|
+
` ${icons.bullet} ${colors.muted("forgelore/forgelore.json")}`,
|
|
360
|
+
` ${icons.bullet} ${colors.muted("forgelore/changes/")}`,
|
|
361
|
+
` ${icons.bullet} ${colors.muted("forgelore/knowledge/architecture.md")}`,
|
|
362
|
+
` ${icons.bullet} ${colors.muted("forgelore/knowledge/patterns.md")}`,
|
|
363
|
+
` ${icons.bullet} ${colors.muted("forgelore/knowledge/glossary.md")}`,
|
|
364
|
+
` ${icons.bullet} ${colors.muted("forgelore/knowledge/capabilities/")}`,
|
|
365
|
+
` ${icons.bullet} ${colors.muted("forgelore/knowledge/decisions/")}`,
|
|
366
|
+
"",
|
|
367
|
+
`${icons.info} Mode: ${colors.primary(mode)}`
|
|
368
|
+
];
|
|
369
|
+
if (globalSource) {
|
|
370
|
+
summary.push(`${icons.info} Global: ${colors.secondary(globalSource)}`);
|
|
371
|
+
}
|
|
372
|
+
console.log(renderBox(summary.join("\n"), "Setup Complete", "#10B981"));
|
|
373
|
+
p.outro(
|
|
374
|
+
`Run ${colors.primary("forgelore propose")} ${colors.muted('"your idea"')} to create your first spec.`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/commands/propose.ts
|
|
379
|
+
import * as p2 from "@clack/prompts";
|
|
380
|
+
import ora2 from "ora";
|
|
381
|
+
import { resolve as resolve2 } from "path";
|
|
382
|
+
import {
|
|
383
|
+
configExists as configExists2,
|
|
384
|
+
createChange
|
|
385
|
+
} from "@forgelore/core";
|
|
386
|
+
function slugify(text3) {
|
|
387
|
+
return text3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
388
|
+
}
|
|
389
|
+
async function proposeCommand(idea, options) {
|
|
390
|
+
const projectRoot = resolve2(options?.cwd || process.cwd());
|
|
391
|
+
if (!await configExists2(projectRoot)) {
|
|
392
|
+
console.log(
|
|
393
|
+
renderBox(
|
|
394
|
+
`${icons.error} forgelore is not initialized.
|
|
395
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
396
|
+
"Not Initialized",
|
|
397
|
+
"#EF4444"
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
p2.intro(gradients.brand(" forgelore propose "));
|
|
403
|
+
let ideaText = idea;
|
|
404
|
+
if (!ideaText) {
|
|
405
|
+
const input = await p2.text({
|
|
406
|
+
message: "What do you want to build?",
|
|
407
|
+
placeholder: "e.g., Add dark mode toggle to settings",
|
|
408
|
+
validate: (val) => {
|
|
409
|
+
if (!val) return "Please describe your idea";
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
if (p2.isCancel(input)) {
|
|
414
|
+
p2.cancel("Propose cancelled.");
|
|
415
|
+
process.exit(0);
|
|
416
|
+
}
|
|
417
|
+
ideaText = input;
|
|
418
|
+
}
|
|
419
|
+
const suggestedName = slugify(ideaText).slice(0, 50);
|
|
420
|
+
const nameInput = await p2.text({
|
|
421
|
+
message: "Change name (used as folder name):",
|
|
422
|
+
placeholder: suggestedName,
|
|
423
|
+
defaultValue: suggestedName,
|
|
424
|
+
validate: (val) => {
|
|
425
|
+
if (!val) return "Please enter a name";
|
|
426
|
+
if (!/^[a-z0-9-]+$/.test(val))
|
|
427
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
if (p2.isCancel(nameInput)) {
|
|
432
|
+
p2.cancel("Propose cancelled.");
|
|
433
|
+
process.exit(0);
|
|
434
|
+
}
|
|
435
|
+
const changeName = nameInput || suggestedName;
|
|
436
|
+
const proposalContent = `# Proposal: ${ideaText}
|
|
437
|
+
|
|
438
|
+
## Motivation
|
|
439
|
+
|
|
440
|
+
<!-- Why are we doing this? What problem does it solve? -->
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
## Scope
|
|
444
|
+
|
|
445
|
+
<!-- What is included and excluded from this change? -->
|
|
446
|
+
|
|
447
|
+
### In Scope
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
### Out of Scope
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
## Context
|
|
454
|
+
|
|
455
|
+
<!-- Any relevant context, links, or references -->
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
## Success Criteria
|
|
459
|
+
|
|
460
|
+
<!-- How do we know this is done? -->
|
|
461
|
+
|
|
462
|
+
1.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
*Proposed: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}*
|
|
467
|
+
`;
|
|
468
|
+
const spinner = ora2({
|
|
469
|
+
text: "Creating change...",
|
|
470
|
+
color: "magenta"
|
|
471
|
+
}).start();
|
|
472
|
+
try {
|
|
473
|
+
const change = await createChange(projectRoot, changeName, proposalContent);
|
|
474
|
+
spinner.succeed(colors.success(`Created change: ${changeName}`));
|
|
475
|
+
const files = [
|
|
476
|
+
`forgelore/changes/${changeName}/proposal.md`,
|
|
477
|
+
`forgelore/changes/${changeName}/specs/requirements.md`,
|
|
478
|
+
`forgelore/changes/${changeName}/specs/scenarios.md`,
|
|
479
|
+
`forgelore/changes/${changeName}/design.md`,
|
|
480
|
+
`forgelore/changes/${changeName}/tasks.md`
|
|
481
|
+
];
|
|
482
|
+
const summary = [
|
|
483
|
+
`${icons.success} ${colors.white("Change created:")} ${colors.primary(changeName)}`,
|
|
484
|
+
"",
|
|
485
|
+
`${colors.white("Files:")}`,
|
|
486
|
+
...files.map((f) => ` ${icons.bullet} ${colors.muted(f)}`),
|
|
487
|
+
"",
|
|
488
|
+
`${colors.muted("Next steps:")}`,
|
|
489
|
+
` 1. Fill in ${colors.primary("proposal.md")} with motivation and scope`,
|
|
490
|
+
` 2. Define requirements in ${colors.primary("specs/requirements.md")}`,
|
|
491
|
+
` 3. Add scenarios in ${colors.primary("specs/scenarios.md")}`,
|
|
492
|
+
` 4. Describe approach in ${colors.primary("design.md")}`,
|
|
493
|
+
` 5. Break into tasks in ${colors.primary("tasks.md")}`
|
|
494
|
+
];
|
|
495
|
+
console.log(renderBox(summary.join("\n"), "Proposed", "#7C3AED"));
|
|
496
|
+
} catch (err) {
|
|
497
|
+
spinner.fail(colors.error("Failed to create change"));
|
|
498
|
+
if (err instanceof Error) {
|
|
499
|
+
console.error(colors.muted(err.message));
|
|
500
|
+
}
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
p2.outro(
|
|
504
|
+
`Run ${colors.primary("forgelore clarify")} ${colors.muted(changeName)} to refine requirements.`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/commands/status.ts
|
|
509
|
+
import { resolve as resolve3 } from "path";
|
|
510
|
+
import Table from "cli-table3";
|
|
511
|
+
import {
|
|
512
|
+
configExists as configExists3,
|
|
513
|
+
readConfig,
|
|
514
|
+
listCapabilities,
|
|
515
|
+
getProjectSummary
|
|
516
|
+
} from "@forgelore/core";
|
|
517
|
+
async function statusCommand(options) {
|
|
518
|
+
const projectRoot = resolve3(options?.cwd || process.cwd());
|
|
519
|
+
if (!await configExists3(projectRoot)) {
|
|
520
|
+
console.log(
|
|
521
|
+
renderBox(
|
|
522
|
+
`${icons.error} forgelore is not initialized.
|
|
523
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
524
|
+
"Not Initialized",
|
|
525
|
+
"#EF4444"
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
console.log(renderBanner());
|
|
531
|
+
const config = await readConfig(projectRoot);
|
|
532
|
+
const summary = await getProjectSummary(projectRoot);
|
|
533
|
+
const capabilities = await listCapabilities(projectRoot);
|
|
534
|
+
const statsLine = [
|
|
535
|
+
`${colors.primary(String(summary.activeChanges))} active`,
|
|
536
|
+
`${colors.success(String(summary.archivedChanges))} archived`,
|
|
537
|
+
`${colors.secondary(String(summary.totalCapabilities))} capabilities`,
|
|
538
|
+
`${colors.muted("mode:")} ${colors.accent(config.mode)}`
|
|
539
|
+
].join(colors.muted(" \u2502 "));
|
|
540
|
+
console.log(renderBox(statsLine, "forgelore status", "#7C3AED"));
|
|
541
|
+
if (summary.changes.length > 0) {
|
|
542
|
+
const table = new Table({
|
|
543
|
+
head: [
|
|
544
|
+
colors.bold("Change"),
|
|
545
|
+
colors.bold("Status"),
|
|
546
|
+
colors.bold("Tasks"),
|
|
547
|
+
colors.bold("Progress")
|
|
548
|
+
].map(String),
|
|
549
|
+
style: { head: [], border: ["gray"] },
|
|
550
|
+
colWidths: [30, 14, 10, 30]
|
|
551
|
+
});
|
|
552
|
+
for (const change of summary.changes) {
|
|
553
|
+
const ts = change.taskSummary;
|
|
554
|
+
const taskStr = ts.total > 0 ? `${ts.passed}/${ts.total}` : colors.muted("none");
|
|
555
|
+
const progress = ts.total > 0 ? progressBar(ts.completionPercent) : colors.muted("--");
|
|
556
|
+
table.push([
|
|
557
|
+
colors.white(change.name),
|
|
558
|
+
statusColor(change.status),
|
|
559
|
+
taskStr,
|
|
560
|
+
progress
|
|
561
|
+
]);
|
|
562
|
+
}
|
|
563
|
+
console.log(renderSection("Active Changes", table.toString()));
|
|
564
|
+
} else {
|
|
565
|
+
console.log(
|
|
566
|
+
renderSection(
|
|
567
|
+
"Active Changes",
|
|
568
|
+
` ${colors.muted("No active changes.")} Run ${colors.primary("forgelore propose")} to create one.`
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (capabilities.length > 0) {
|
|
573
|
+
const capTable = new Table({
|
|
574
|
+
head: [
|
|
575
|
+
colors.bold("Capability"),
|
|
576
|
+
colors.bold("Source"),
|
|
577
|
+
colors.bold("Archived")
|
|
578
|
+
].map(String),
|
|
579
|
+
style: { head: [], border: ["gray"] },
|
|
580
|
+
colWidths: [30, 25, 16]
|
|
581
|
+
});
|
|
582
|
+
for (const cap of capabilities.slice(0, 10)) {
|
|
583
|
+
capTable.push([
|
|
584
|
+
colors.white(cap.name),
|
|
585
|
+
colors.muted(cap.sourceChange),
|
|
586
|
+
colors.dim(cap.archivedAt.slice(0, 10))
|
|
587
|
+
]);
|
|
588
|
+
}
|
|
589
|
+
const moreText = capabilities.length > 10 ? `
|
|
590
|
+
${colors.muted(`...and ${capabilities.length - 10} more. Run`)} ${colors.primary("forgelore capabilities")} ${colors.muted("to see all.")}` : "";
|
|
591
|
+
console.log(
|
|
592
|
+
renderSection("Capabilities", capTable.toString() + moreText)
|
|
593
|
+
);
|
|
594
|
+
} else {
|
|
595
|
+
console.log(
|
|
596
|
+
renderSection(
|
|
597
|
+
"Capabilities",
|
|
598
|
+
` ${colors.muted("No capabilities yet.")} Archive a completed change to start building the knowledge base.`
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (config.mode === "local+global" || config.mode === "global") {
|
|
603
|
+
const globalInfo = config.global ? ` ${icons.info} Source: ${colors.secondary(config.global.source)}
|
|
604
|
+
${icons.info} Path: ${colors.muted(config.global.path)}
|
|
605
|
+
${icons.info} Auto-sync: ${config.global.autoSync ? colors.success("enabled") : colors.muted("disabled")}` : ` ${colors.warning("Global spec repo not configured.")}`;
|
|
606
|
+
console.log(renderSection("Global Specs", globalInfo));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/commands/clarify.ts
|
|
611
|
+
import * as p3 from "@clack/prompts";
|
|
612
|
+
import { resolve as resolve4 } from "path";
|
|
613
|
+
import {
|
|
614
|
+
configExists as configExists4,
|
|
615
|
+
readChange,
|
|
616
|
+
readChangeFile,
|
|
617
|
+
updateChangeStatus
|
|
618
|
+
} from "@forgelore/core";
|
|
619
|
+
async function clarifyCommand(changeName, options) {
|
|
620
|
+
const projectRoot = resolve4(options?.cwd || process.cwd());
|
|
621
|
+
if (!await configExists4(projectRoot)) {
|
|
622
|
+
console.log(
|
|
623
|
+
renderBox(
|
|
624
|
+
`${icons.error} forgelore is not initialized.
|
|
625
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
626
|
+
"Not Initialized",
|
|
627
|
+
"#EF4444"
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
p3.intro(gradients.brand(" forgelore clarify "));
|
|
633
|
+
let change;
|
|
634
|
+
try {
|
|
635
|
+
change = await readChange(projectRoot, changeName);
|
|
636
|
+
} catch {
|
|
637
|
+
console.log(
|
|
638
|
+
renderBox(
|
|
639
|
+
`${icons.error} Change ${colors.primary(changeName)} not found.
|
|
640
|
+
Run ${colors.primary("forgelore list")} to see available changes.`,
|
|
641
|
+
"Not Found",
|
|
642
|
+
"#EF4444"
|
|
643
|
+
)
|
|
644
|
+
);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
const proposal = await readChangeFile(projectRoot, changeName, "proposal.md");
|
|
648
|
+
let requirements;
|
|
649
|
+
try {
|
|
650
|
+
requirements = await readChangeFile(projectRoot, changeName, "specs/requirements.md");
|
|
651
|
+
} catch {
|
|
652
|
+
requirements = "(not yet defined)";
|
|
653
|
+
}
|
|
654
|
+
let scenarios;
|
|
655
|
+
try {
|
|
656
|
+
scenarios = await readChangeFile(projectRoot, changeName, "specs/scenarios.md");
|
|
657
|
+
} catch {
|
|
658
|
+
scenarios = "(not yet defined)";
|
|
659
|
+
}
|
|
660
|
+
console.log(
|
|
661
|
+
renderBox(
|
|
662
|
+
[
|
|
663
|
+
`${icons.info} Change: ${colors.primary(changeName)}`,
|
|
664
|
+
`${icons.info} Status: ${colors.accent(change.status)}`,
|
|
665
|
+
`${icons.info} Created: ${colors.muted(change.createdAt.slice(0, 10))}`
|
|
666
|
+
].join("\n"),
|
|
667
|
+
"Change Details"
|
|
668
|
+
)
|
|
669
|
+
);
|
|
670
|
+
console.log(renderSection("Proposal", colors.muted(proposal.slice(0, 500))));
|
|
671
|
+
console.log(renderSection("Requirements", colors.muted(requirements.slice(0, 500))));
|
|
672
|
+
console.log(renderSection("Scenarios", colors.muted(scenarios.slice(0, 500))));
|
|
673
|
+
const checks = await p3.group({
|
|
674
|
+
proposalComplete: () => p3.confirm({
|
|
675
|
+
message: "Is the proposal well-defined with clear motivation and scope?",
|
|
676
|
+
initialValue: false
|
|
677
|
+
}),
|
|
678
|
+
requirementsDefined: () => p3.confirm({
|
|
679
|
+
message: "Are functional and non-functional requirements specified?",
|
|
680
|
+
initialValue: false
|
|
681
|
+
}),
|
|
682
|
+
scenariosCovered: () => p3.confirm({
|
|
683
|
+
message: "Are happy path, edge cases, and error scenarios defined?",
|
|
684
|
+
initialValue: false
|
|
685
|
+
}),
|
|
686
|
+
readyForDesign: () => p3.confirm({
|
|
687
|
+
message: "Is this change ready for design and task breakdown?",
|
|
688
|
+
initialValue: false
|
|
689
|
+
})
|
|
690
|
+
});
|
|
691
|
+
if (p3.isCancel(checks)) {
|
|
692
|
+
p3.cancel("Clarify cancelled.");
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
const allReady = checks.proposalComplete && checks.requirementsDefined && checks.scenariosCovered && checks.readyForDesign;
|
|
696
|
+
if (allReady) {
|
|
697
|
+
await updateChangeStatus(projectRoot, changeName, "planning");
|
|
698
|
+
console.log(
|
|
699
|
+
renderBox(
|
|
700
|
+
`${icons.success} Change ${colors.primary(changeName)} is ready for design.
|
|
701
|
+
Status updated to ${colors.accent("planning")}.
|
|
702
|
+
|
|
703
|
+
Next: Fill in ${colors.primary("design.md")} and ${colors.primary("tasks.md")}`,
|
|
704
|
+
"Ready",
|
|
705
|
+
"#10B981"
|
|
706
|
+
)
|
|
707
|
+
);
|
|
708
|
+
} else {
|
|
709
|
+
const incomplete = [];
|
|
710
|
+
if (!checks.proposalComplete) incomplete.push(` ${icons.pending} Proposal needs refinement`);
|
|
711
|
+
if (!checks.requirementsDefined) incomplete.push(` ${icons.pending} Requirements need specification`);
|
|
712
|
+
if (!checks.scenariosCovered) incomplete.push(` ${icons.pending} Scenarios need coverage`);
|
|
713
|
+
console.log(
|
|
714
|
+
renderBox(
|
|
715
|
+
`${icons.warning} Change ${colors.primary(changeName)} needs more clarification:
|
|
716
|
+
|
|
717
|
+
` + incomplete.join("\n") + `
|
|
718
|
+
|
|
719
|
+
${colors.muted("Edit the spec files and run")} ${colors.primary("forgelore clarify " + changeName)} ${colors.muted("again.")}`,
|
|
720
|
+
"Needs Work",
|
|
721
|
+
"#F59E0B"
|
|
722
|
+
)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
p3.outro(colors.muted("Specs are the foundation. Take the time to get them right."));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/commands/list.ts
|
|
729
|
+
import { resolve as resolve5 } from "path";
|
|
730
|
+
import Table2 from "cli-table3";
|
|
731
|
+
import {
|
|
732
|
+
configExists as configExists5,
|
|
733
|
+
listChanges
|
|
734
|
+
} from "@forgelore/core";
|
|
735
|
+
import { summarizeTasks } from "@forgelore/core";
|
|
736
|
+
async function listCommand(options) {
|
|
737
|
+
const projectRoot = resolve5(options?.cwd || process.cwd());
|
|
738
|
+
if (!await configExists5(projectRoot)) {
|
|
739
|
+
console.log(
|
|
740
|
+
renderBox(
|
|
741
|
+
`${icons.error} forgelore is not initialized.
|
|
742
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
743
|
+
"Not Initialized",
|
|
744
|
+
"#EF4444"
|
|
745
|
+
)
|
|
746
|
+
);
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
let changes = await listChanges(projectRoot, options?.archived ?? false);
|
|
750
|
+
if (options?.status) {
|
|
751
|
+
changes = changes.filter((c) => c.status === options.status);
|
|
752
|
+
}
|
|
753
|
+
if (changes.length === 0) {
|
|
754
|
+
const msg = options?.status ? `No changes with status "${options.status}".` : "No changes found.";
|
|
755
|
+
console.log(
|
|
756
|
+
renderBox(
|
|
757
|
+
`${icons.info} ${msg}
|
|
758
|
+
Run ${colors.primary("forgelore propose")} to create one.`,
|
|
759
|
+
"Changes"
|
|
760
|
+
)
|
|
761
|
+
);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const table = new Table2({
|
|
765
|
+
head: [
|
|
766
|
+
colors.bold("Name"),
|
|
767
|
+
colors.bold("Status"),
|
|
768
|
+
colors.bold("Tasks"),
|
|
769
|
+
colors.bold("Progress"),
|
|
770
|
+
colors.bold("Updated")
|
|
771
|
+
].map(String),
|
|
772
|
+
style: { head: [], border: ["gray"] },
|
|
773
|
+
colWidths: [28, 14, 10, 26, 14]
|
|
774
|
+
});
|
|
775
|
+
for (const change of changes) {
|
|
776
|
+
const ts = summarizeTasks(change.tasks);
|
|
777
|
+
const taskStr = ts.total > 0 ? `${ts.passed}/${ts.total}` : colors.muted("--");
|
|
778
|
+
const progress = ts.total > 0 ? progressBar(ts.completionPercent) : colors.muted("--");
|
|
779
|
+
const updated = colors.dim(change.updatedAt.slice(0, 10));
|
|
780
|
+
table.push([
|
|
781
|
+
colors.white(change.name),
|
|
782
|
+
statusColor(change.status),
|
|
783
|
+
taskStr,
|
|
784
|
+
progress,
|
|
785
|
+
updated
|
|
786
|
+
]);
|
|
787
|
+
}
|
|
788
|
+
const title = options?.archived ? "All Changes (including archived)" : "Active Changes";
|
|
789
|
+
console.log(renderSection(title, table.toString()));
|
|
790
|
+
console.log(
|
|
791
|
+
colors.muted(` ${changes.length} change${changes.length === 1 ? "" : "s"} found`)
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/commands/verify.ts
|
|
796
|
+
import { resolve as resolve6 } from "path";
|
|
797
|
+
import {
|
|
798
|
+
configExists as configExists6,
|
|
799
|
+
readChangeFile as readChangeFile2,
|
|
800
|
+
listChanges as listChanges2,
|
|
801
|
+
getChangePath as getChangePath2
|
|
802
|
+
} from "@forgelore/core";
|
|
803
|
+
async function verifyChange(projectRoot, changeName) {
|
|
804
|
+
const checks = [];
|
|
805
|
+
const changePath = getChangePath2(projectRoot, changeName);
|
|
806
|
+
try {
|
|
807
|
+
const proposal = await readChangeFile2(projectRoot, changeName, "proposal.md");
|
|
808
|
+
const hasContent = proposal.replace(/<!--.*?-->/gs, "").trim().length > 50;
|
|
809
|
+
checks.push({
|
|
810
|
+
name: "Proposal",
|
|
811
|
+
passed: hasContent,
|
|
812
|
+
message: hasContent ? "proposal.md has content" : "proposal.md is mostly empty \u2014 fill in motivation and scope"
|
|
813
|
+
});
|
|
814
|
+
} catch {
|
|
815
|
+
checks.push({
|
|
816
|
+
name: "Proposal",
|
|
817
|
+
passed: false,
|
|
818
|
+
message: "proposal.md is missing"
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const reqs = await readChangeFile2(projectRoot, changeName, "specs/requirements.md");
|
|
823
|
+
const hasContent = reqs.replace(/<!--.*?-->/gs, "").trim().length > 50;
|
|
824
|
+
checks.push({
|
|
825
|
+
name: "Requirements",
|
|
826
|
+
passed: hasContent,
|
|
827
|
+
message: hasContent ? "requirements.md has content" : "requirements.md is mostly empty \u2014 define functional and non-functional requirements"
|
|
828
|
+
});
|
|
829
|
+
} catch {
|
|
830
|
+
checks.push({
|
|
831
|
+
name: "Requirements",
|
|
832
|
+
passed: false,
|
|
833
|
+
message: "specs/requirements.md is missing"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const scenarios = await readChangeFile2(projectRoot, changeName, "specs/scenarios.md");
|
|
838
|
+
const hasContent = scenarios.replace(/<!--.*?-->/gs, "").trim().length > 50;
|
|
839
|
+
checks.push({
|
|
840
|
+
name: "Scenarios",
|
|
841
|
+
passed: hasContent,
|
|
842
|
+
message: hasContent ? "scenarios.md has content" : "scenarios.md is mostly empty \u2014 define happy path, edge cases, error cases"
|
|
843
|
+
});
|
|
844
|
+
} catch {
|
|
845
|
+
checks.push({
|
|
846
|
+
name: "Scenarios",
|
|
847
|
+
passed: false,
|
|
848
|
+
message: "specs/scenarios.md is missing"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
const design = await readChangeFile2(projectRoot, changeName, "design.md");
|
|
853
|
+
const hasContent = design.replace(/<!--.*?-->/gs, "").trim().length > 50;
|
|
854
|
+
checks.push({
|
|
855
|
+
name: "Design",
|
|
856
|
+
passed: hasContent,
|
|
857
|
+
message: hasContent ? "design.md has content" : "design.md is mostly empty \u2014 describe technical approach"
|
|
858
|
+
});
|
|
859
|
+
} catch {
|
|
860
|
+
checks.push({
|
|
861
|
+
name: "Design",
|
|
862
|
+
passed: false,
|
|
863
|
+
message: "design.md is missing"
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
const tasks = await readChangeFile2(projectRoot, changeName, "tasks.md");
|
|
868
|
+
const hasContent = tasks.replace(/<!--.*?-->/gs, "").trim().length > 50;
|
|
869
|
+
checks.push({
|
|
870
|
+
name: "Tasks",
|
|
871
|
+
passed: hasContent,
|
|
872
|
+
message: hasContent ? "tasks.md has content" : "tasks.md is mostly empty \u2014 break down implementation into tasks"
|
|
873
|
+
});
|
|
874
|
+
} catch {
|
|
875
|
+
checks.push({
|
|
876
|
+
name: "Tasks",
|
|
877
|
+
passed: false,
|
|
878
|
+
message: "tasks.md is missing"
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return checks;
|
|
882
|
+
}
|
|
883
|
+
async function verifyCommand(changeName, options) {
|
|
884
|
+
const projectRoot = resolve6(options?.cwd || process.cwd());
|
|
885
|
+
if (!await configExists6(projectRoot)) {
|
|
886
|
+
console.log(
|
|
887
|
+
renderBox(
|
|
888
|
+
`${icons.error} forgelore is not initialized.
|
|
889
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
890
|
+
"Not Initialized",
|
|
891
|
+
"#EF4444"
|
|
892
|
+
)
|
|
893
|
+
);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
const changesToVerify = [];
|
|
897
|
+
if (changeName) {
|
|
898
|
+
changesToVerify.push(changeName);
|
|
899
|
+
} else {
|
|
900
|
+
const changes = await listChanges2(projectRoot, false);
|
|
901
|
+
if (changes.length === 0) {
|
|
902
|
+
console.log(
|
|
903
|
+
renderBox(
|
|
904
|
+
`${icons.info} No active changes to verify.
|
|
905
|
+
Run ${colors.primary("forgelore propose")} to create one.`,
|
|
906
|
+
"Nothing to Verify"
|
|
907
|
+
)
|
|
908
|
+
);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
changesToVerify.push(...changes.map((c) => c.name));
|
|
912
|
+
}
|
|
913
|
+
let totalPassed = 0;
|
|
914
|
+
let totalChecks = 0;
|
|
915
|
+
for (const name of changesToVerify) {
|
|
916
|
+
try {
|
|
917
|
+
const checks = await verifyChange(projectRoot, name);
|
|
918
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
919
|
+
totalPassed += passed;
|
|
920
|
+
totalChecks += checks.length;
|
|
921
|
+
const lines = checks.map(
|
|
922
|
+
(c) => ` ${c.passed ? icons.success : icons.error} ${colors.white(c.name)}: ${c.passed ? colors.muted(c.message) : colors.warning(c.message)}`
|
|
923
|
+
);
|
|
924
|
+
const allPassed = passed === checks.length;
|
|
925
|
+
const borderColor = allPassed ? "#10B981" : "#F59E0B";
|
|
926
|
+
console.log(
|
|
927
|
+
renderBox(
|
|
928
|
+
`${allPassed ? icons.success : icons.warning} ${passed}/${checks.length} checks passed
|
|
929
|
+
|
|
930
|
+
${lines.join("\n")}`,
|
|
931
|
+
name,
|
|
932
|
+
borderColor
|
|
933
|
+
)
|
|
934
|
+
);
|
|
935
|
+
} catch {
|
|
936
|
+
console.log(
|
|
937
|
+
renderBox(
|
|
938
|
+
`${icons.error} Change ${colors.primary(name)} not found.`,
|
|
939
|
+
name,
|
|
940
|
+
"#EF4444"
|
|
941
|
+
)
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (changesToVerify.length > 1) {
|
|
946
|
+
const allPassed = totalPassed === totalChecks;
|
|
947
|
+
console.log(
|
|
948
|
+
renderBox(
|
|
949
|
+
`${allPassed ? icons.success : icons.warning} ${totalPassed}/${totalChecks} total checks passed across ${changesToVerify.length} changes`,
|
|
950
|
+
"Summary",
|
|
951
|
+
allPassed ? "#10B981" : "#F59E0B"
|
|
952
|
+
)
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/commands/archive.ts
|
|
958
|
+
import * as p4 from "@clack/prompts";
|
|
959
|
+
import ora3 from "ora";
|
|
960
|
+
import { resolve as resolve7 } from "path";
|
|
961
|
+
import {
|
|
962
|
+
configExists as configExists7,
|
|
963
|
+
readChange as readChange3,
|
|
964
|
+
archiveChange,
|
|
965
|
+
readOutcome,
|
|
966
|
+
writeOutcome,
|
|
967
|
+
getChangePath as getChangePath3
|
|
968
|
+
} from "@forgelore/core";
|
|
969
|
+
async function archiveCommand(changeName, options) {
|
|
970
|
+
const projectRoot = resolve7(options?.cwd || process.cwd());
|
|
971
|
+
if (!await configExists7(projectRoot)) {
|
|
972
|
+
console.log(
|
|
973
|
+
renderBox(
|
|
974
|
+
`${icons.error} forgelore is not initialized.
|
|
975
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
976
|
+
"Not Initialized",
|
|
977
|
+
"#EF4444"
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
p4.intro(gradients.brand(" forgelore archive "));
|
|
983
|
+
let change;
|
|
984
|
+
try {
|
|
985
|
+
change = await readChange3(projectRoot, changeName);
|
|
986
|
+
} catch {
|
|
987
|
+
console.log(
|
|
988
|
+
renderBox(
|
|
989
|
+
`${icons.error} Change ${colors.primary(changeName)} not found.`,
|
|
990
|
+
"Not Found",
|
|
991
|
+
"#EF4444"
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
const changePath = getChangePath3(projectRoot, changeName);
|
|
997
|
+
const existingOutcome = await readOutcome(changePath);
|
|
998
|
+
if (!existingOutcome && !options?.skipOutcome) {
|
|
999
|
+
console.log(
|
|
1000
|
+
renderBox(
|
|
1001
|
+
[
|
|
1002
|
+
`${icons.info} Before archiving, an ${colors.primary("outcome.md")} should be created.`,
|
|
1003
|
+
"",
|
|
1004
|
+
`This file captures:`,
|
|
1005
|
+
` ${icons.bullet} What was built and the final state`,
|
|
1006
|
+
` ${icons.bullet} Capabilities that emerged from this change`,
|
|
1007
|
+
` ${icons.bullet} Lessons learned and patterns established`,
|
|
1008
|
+
"",
|
|
1009
|
+
`${colors.muted("An AI agent can generate this, or you can write it manually.")}`,
|
|
1010
|
+
`${colors.muted("Create")} ${colors.primary(`forgelore/changes/${changeName}/outcome.md`)} ${colors.muted("and run archive again.")}`
|
|
1011
|
+
].join("\n"),
|
|
1012
|
+
"Step 1: Outcome",
|
|
1013
|
+
"#F59E0B"
|
|
1014
|
+
)
|
|
1015
|
+
);
|
|
1016
|
+
const proceed = await p4.confirm({
|
|
1017
|
+
message: "Create a placeholder outcome.md and continue archiving?",
|
|
1018
|
+
initialValue: false
|
|
1019
|
+
});
|
|
1020
|
+
if (p4.isCancel(proceed) || !proceed) {
|
|
1021
|
+
p4.cancel("Archive paused. Create outcome.md and run archive again.");
|
|
1022
|
+
process.exit(0);
|
|
1023
|
+
}
|
|
1024
|
+
const placeholderOutcome = `# Outcome: ${changeName}
|
|
1025
|
+
|
|
1026
|
+
## What Was Built
|
|
1027
|
+
|
|
1028
|
+
<!-- Summary of what was implemented -->
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
## Capabilities
|
|
1032
|
+
|
|
1033
|
+
<!-- List capabilities that emerged from this change -->
|
|
1034
|
+
<!-- Format: - **Capability Name**: Description -->
|
|
1035
|
+
|
|
1036
|
+
-
|
|
1037
|
+
|
|
1038
|
+
## Lessons Learned
|
|
1039
|
+
|
|
1040
|
+
<!-- What did we learn? Any patterns to document? -->
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
## Files Changed
|
|
1044
|
+
|
|
1045
|
+
<!-- Key files that were created or modified -->
|
|
1046
|
+
|
|
1047
|
+
-
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
1051
|
+
*Archived: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}*
|
|
1052
|
+
`;
|
|
1053
|
+
await writeOutcome(changePath, placeholderOutcome);
|
|
1054
|
+
console.log(` ${icons.success} Created placeholder outcome.md`);
|
|
1055
|
+
}
|
|
1056
|
+
const confirmArchive = await p4.confirm({
|
|
1057
|
+
message: `Archive "${changeName}"? This will move it to forgelore/changes/archive/`,
|
|
1058
|
+
initialValue: true
|
|
1059
|
+
});
|
|
1060
|
+
if (p4.isCancel(confirmArchive) || !confirmArchive) {
|
|
1061
|
+
p4.cancel("Archive cancelled.");
|
|
1062
|
+
process.exit(0);
|
|
1063
|
+
}
|
|
1064
|
+
const spinner = ora3({
|
|
1065
|
+
text: "Archiving change...",
|
|
1066
|
+
color: "magenta"
|
|
1067
|
+
}).start();
|
|
1068
|
+
try {
|
|
1069
|
+
const archivePath = await archiveChange(projectRoot, changeName);
|
|
1070
|
+
spinner.succeed(colors.success("Change archived"));
|
|
1071
|
+
console.log(
|
|
1072
|
+
renderBox(
|
|
1073
|
+
[
|
|
1074
|
+
`${icons.success} ${colors.white("Archived:")} ${colors.primary(changeName)}`,
|
|
1075
|
+
`${icons.arrow} ${colors.muted(archivePath)}`,
|
|
1076
|
+
"",
|
|
1077
|
+
`${colors.muted("Next steps:")}`,
|
|
1078
|
+
` 1. Review ${colors.primary("outcome.md")} and extract capabilities`,
|
|
1079
|
+
` 2. Run ${colors.primary("forgelore capabilities")} to view the knowledge base`,
|
|
1080
|
+
` 3. Update ${colors.primary("forgelore/knowledge/architecture.md")} if needed`
|
|
1081
|
+
].join("\n"),
|
|
1082
|
+
"Archived",
|
|
1083
|
+
"#10B981"
|
|
1084
|
+
)
|
|
1085
|
+
);
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
spinner.fail(colors.error("Failed to archive"));
|
|
1088
|
+
if (err instanceof Error) {
|
|
1089
|
+
console.error(colors.muted(err.message));
|
|
1090
|
+
}
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
p4.outro(colors.muted("Knowledge captured. The spec lives on."));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/commands/sync.ts
|
|
1097
|
+
import ora4 from "ora";
|
|
1098
|
+
import { resolve as resolve8 } from "path";
|
|
1099
|
+
import { execSync } from "child_process";
|
|
1100
|
+
import { existsSync, mkdirSync } from "fs";
|
|
1101
|
+
import {
|
|
1102
|
+
configExists as configExists8,
|
|
1103
|
+
readConfig as readConfig2
|
|
1104
|
+
} from "@forgelore/core";
|
|
1105
|
+
async function syncCommand(options) {
|
|
1106
|
+
const projectRoot = resolve8(options?.cwd || process.cwd());
|
|
1107
|
+
if (!await configExists8(projectRoot)) {
|
|
1108
|
+
console.log(
|
|
1109
|
+
renderBox(
|
|
1110
|
+
`${icons.error} forgelore is not initialized.
|
|
1111
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
1112
|
+
"Not Initialized",
|
|
1113
|
+
"#EF4444"
|
|
1114
|
+
)
|
|
1115
|
+
);
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
}
|
|
1118
|
+
const config = await readConfig2(projectRoot);
|
|
1119
|
+
if (config.mode === "local") {
|
|
1120
|
+
console.log(
|
|
1121
|
+
renderBox(
|
|
1122
|
+
`${icons.info} Spec mode is ${colors.primary("local")}. No global repo to sync.
|
|
1123
|
+
Run ${colors.primary("forgelore config mode local+global")} to enable global specs.`,
|
|
1124
|
+
"No Sync Needed"
|
|
1125
|
+
)
|
|
1126
|
+
);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (!config.global?.source) {
|
|
1130
|
+
console.log(
|
|
1131
|
+
renderBox(
|
|
1132
|
+
`${icons.error} Global spec source not configured.
|
|
1133
|
+
Run ${colors.primary("forgelore config global.source <path-or-url>")} to set it.`,
|
|
1134
|
+
"Not Configured",
|
|
1135
|
+
"#EF4444"
|
|
1136
|
+
)
|
|
1137
|
+
);
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
}
|
|
1140
|
+
const source = config.global.source;
|
|
1141
|
+
const isGitUrl = source.startsWith("http://") || source.startsWith("https://") || source.startsWith("git@");
|
|
1142
|
+
const targetPath = config.global.path;
|
|
1143
|
+
const spinner = ora4({
|
|
1144
|
+
text: `Syncing from ${source}...`,
|
|
1145
|
+
color: "magenta"
|
|
1146
|
+
}).start();
|
|
1147
|
+
try {
|
|
1148
|
+
if (isGitUrl) {
|
|
1149
|
+
if (existsSync(targetPath)) {
|
|
1150
|
+
spinner.text = "Pulling latest changes...";
|
|
1151
|
+
execSync("git pull --ff-only", { cwd: targetPath, stdio: "pipe" });
|
|
1152
|
+
spinner.succeed(colors.success("Global specs updated (git pull)"));
|
|
1153
|
+
} else {
|
|
1154
|
+
spinner.text = "Cloning global spec repo...";
|
|
1155
|
+
mkdirSync(targetPath, { recursive: true });
|
|
1156
|
+
execSync(`git clone ${source} ${targetPath}`, { stdio: "pipe" });
|
|
1157
|
+
spinner.succeed(colors.success("Global specs cloned"));
|
|
1158
|
+
}
|
|
1159
|
+
} else {
|
|
1160
|
+
if (existsSync(source)) {
|
|
1161
|
+
spinner.succeed(
|
|
1162
|
+
colors.success(`Global specs linked: ${colors.muted(source)}`)
|
|
1163
|
+
);
|
|
1164
|
+
} else {
|
|
1165
|
+
spinner.fail(
|
|
1166
|
+
colors.error(`Global spec path not found: ${source}`)
|
|
1167
|
+
);
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
console.log(
|
|
1172
|
+
renderBox(
|
|
1173
|
+
[
|
|
1174
|
+
`${icons.success} Global specs synced`,
|
|
1175
|
+
`${icons.info} Source: ${colors.secondary(source)}`,
|
|
1176
|
+
`${icons.info} Path: ${colors.muted(targetPath)}`
|
|
1177
|
+
].join("\n"),
|
|
1178
|
+
"Sync Complete",
|
|
1179
|
+
"#10B981"
|
|
1180
|
+
)
|
|
1181
|
+
);
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
spinner.fail(colors.error("Sync failed"));
|
|
1184
|
+
if (err instanceof Error) {
|
|
1185
|
+
console.error(colors.muted(err.message));
|
|
1186
|
+
}
|
|
1187
|
+
process.exit(1);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/commands/doctor.ts
|
|
1192
|
+
import { resolve as resolve9, join } from "path";
|
|
1193
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
1194
|
+
import {
|
|
1195
|
+
configExists as configExists9,
|
|
1196
|
+
readConfig as readConfig3,
|
|
1197
|
+
fileExists as fileExists2,
|
|
1198
|
+
getForgeloreDir as getForgeloreDir2,
|
|
1199
|
+
listChanges as listChanges3
|
|
1200
|
+
} from "@forgelore/core";
|
|
1201
|
+
async function doctorCommand(options) {
|
|
1202
|
+
const projectRoot = resolve9(options?.cwd || process.cwd());
|
|
1203
|
+
const forgeloreDir = getForgeloreDir2(projectRoot);
|
|
1204
|
+
const checks = [];
|
|
1205
|
+
console.log(
|
|
1206
|
+
renderBox(
|
|
1207
|
+
`${icons.info} Running health checks...`,
|
|
1208
|
+
"forgelore doctor"
|
|
1209
|
+
)
|
|
1210
|
+
);
|
|
1211
|
+
const hasConfig = await configExists9(projectRoot);
|
|
1212
|
+
checks.push({
|
|
1213
|
+
name: "Configuration",
|
|
1214
|
+
passed: hasConfig,
|
|
1215
|
+
message: hasConfig ? "forgelore.json found" : "forgelore.json not found \u2014 run forgelore init"
|
|
1216
|
+
});
|
|
1217
|
+
if (!hasConfig) {
|
|
1218
|
+
printResults(checks);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
let config;
|
|
1222
|
+
try {
|
|
1223
|
+
config = await readConfig3(projectRoot);
|
|
1224
|
+
checks.push({
|
|
1225
|
+
name: "Config Valid",
|
|
1226
|
+
passed: true,
|
|
1227
|
+
message: `Config parsed (v${config.version}, mode: ${config.mode})`
|
|
1228
|
+
});
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
checks.push({
|
|
1231
|
+
name: "Config Valid",
|
|
1232
|
+
passed: false,
|
|
1233
|
+
message: `Config parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
1234
|
+
});
|
|
1235
|
+
printResults(checks);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const dirs = [
|
|
1239
|
+
{ path: join(forgeloreDir, "changes"), name: "changes/" },
|
|
1240
|
+
{ path: join(forgeloreDir, "changes", "archive"), name: "changes/archive/" },
|
|
1241
|
+
{ path: join(forgeloreDir, "knowledge"), name: "knowledge/" },
|
|
1242
|
+
{ path: join(forgeloreDir, "knowledge", "capabilities"), name: "knowledge/capabilities/" },
|
|
1243
|
+
{ path: join(forgeloreDir, "knowledge", "decisions"), name: "knowledge/decisions/" }
|
|
1244
|
+
];
|
|
1245
|
+
for (const dir of dirs) {
|
|
1246
|
+
const exists = await fileExists2(dir.path);
|
|
1247
|
+
checks.push({
|
|
1248
|
+
name: `Dir: ${dir.name}`,
|
|
1249
|
+
passed: exists,
|
|
1250
|
+
message: exists ? `${dir.name} exists` : `${dir.name} missing`,
|
|
1251
|
+
fixable: true
|
|
1252
|
+
});
|
|
1253
|
+
if (!exists && options?.fix) {
|
|
1254
|
+
await mkdir2(dir.path, { recursive: true });
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const knowledgeFiles = [
|
|
1258
|
+
{ path: join(forgeloreDir, "knowledge", "architecture.md"), name: "architecture.md" },
|
|
1259
|
+
{ path: join(forgeloreDir, "knowledge", "patterns.md"), name: "patterns.md" },
|
|
1260
|
+
{ path: join(forgeloreDir, "knowledge", "glossary.md"), name: "glossary.md" }
|
|
1261
|
+
];
|
|
1262
|
+
for (const file of knowledgeFiles) {
|
|
1263
|
+
const exists = await fileExists2(file.path);
|
|
1264
|
+
checks.push({
|
|
1265
|
+
name: `KB: ${file.name}`,
|
|
1266
|
+
passed: exists,
|
|
1267
|
+
message: exists ? `${file.name} present` : `${file.name} missing`
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
try {
|
|
1271
|
+
const changes = await listChanges3(projectRoot, false);
|
|
1272
|
+
checks.push({
|
|
1273
|
+
name: "Active Changes",
|
|
1274
|
+
passed: true,
|
|
1275
|
+
message: `${changes.length} active change${changes.length === 1 ? "" : "s"}`
|
|
1276
|
+
});
|
|
1277
|
+
for (const change of changes) {
|
|
1278
|
+
const hasProposal = await fileExists2(join(change.path, "proposal.md"));
|
|
1279
|
+
if (!hasProposal) {
|
|
1280
|
+
checks.push({
|
|
1281
|
+
name: `Change: ${change.name}`,
|
|
1282
|
+
passed: false,
|
|
1283
|
+
message: `${change.name} has metadata but missing proposal.md`
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
} catch {
|
|
1288
|
+
checks.push({
|
|
1289
|
+
name: "Active Changes",
|
|
1290
|
+
passed: false,
|
|
1291
|
+
message: "Could not read changes directory"
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
if (config.mode !== "local" && config.global) {
|
|
1295
|
+
const globalExists = await fileExists2(config.global.path);
|
|
1296
|
+
checks.push({
|
|
1297
|
+
name: "Global Repo",
|
|
1298
|
+
passed: globalExists,
|
|
1299
|
+
message: globalExists ? `Global specs available at ${config.global.path}` : `Global specs not found at ${config.global.path} \u2014 run forgelore sync`
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
printResults(checks, options?.fix);
|
|
1303
|
+
}
|
|
1304
|
+
function printResults(checks, didFix) {
|
|
1305
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
1306
|
+
const total = checks.length;
|
|
1307
|
+
const allPassed = passed === total;
|
|
1308
|
+
const lines = checks.map(
|
|
1309
|
+
(c) => ` ${c.passed ? icons.success : icons.error} ${colors.white(c.name)}: ${c.passed ? colors.muted(c.message) : colors.warning(c.message)}${!c.passed && c.fixable && didFix ? colors.success(" (fixed)") : ""}`
|
|
1310
|
+
);
|
|
1311
|
+
const borderColor = allPassed ? "#10B981" : passed > total / 2 ? "#F59E0B" : "#EF4444";
|
|
1312
|
+
console.log(
|
|
1313
|
+
renderBox(
|
|
1314
|
+
`${allPassed ? icons.success : icons.warning} ${passed}/${total} checks passed
|
|
1315
|
+
|
|
1316
|
+
${lines.join("\n")}`,
|
|
1317
|
+
"Results",
|
|
1318
|
+
borderColor
|
|
1319
|
+
)
|
|
1320
|
+
);
|
|
1321
|
+
if (!allPassed && !didFix) {
|
|
1322
|
+
const fixable = checks.filter((c) => !c.passed && c.fixable).length;
|
|
1323
|
+
if (fixable > 0) {
|
|
1324
|
+
console.log(
|
|
1325
|
+
colors.muted(` Run ${colors.primary("forgelore doctor --fix")} to auto-fix ${fixable} issue${fixable === 1 ? "" : "s"}.`)
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/commands/capabilities.ts
|
|
1332
|
+
import { resolve as resolve10 } from "path";
|
|
1333
|
+
import Table3 from "cli-table3";
|
|
1334
|
+
import {
|
|
1335
|
+
configExists as configExists10,
|
|
1336
|
+
listCapabilities as listCapabilities3
|
|
1337
|
+
} from "@forgelore/core";
|
|
1338
|
+
async function capabilitiesCommand(options) {
|
|
1339
|
+
const projectRoot = resolve10(options?.cwd || process.cwd());
|
|
1340
|
+
if (!await configExists10(projectRoot)) {
|
|
1341
|
+
console.log(
|
|
1342
|
+
renderBox(
|
|
1343
|
+
`${icons.error} forgelore is not initialized.
|
|
1344
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
1345
|
+
"Not Initialized",
|
|
1346
|
+
"#EF4444"
|
|
1347
|
+
)
|
|
1348
|
+
);
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
const capabilities = await listCapabilities3(projectRoot);
|
|
1352
|
+
if (capabilities.length === 0) {
|
|
1353
|
+
console.log(
|
|
1354
|
+
renderBox(
|
|
1355
|
+
`${icons.info} No capabilities registered yet.
|
|
1356
|
+
|
|
1357
|
+
Capabilities are extracted when changes are archived.
|
|
1358
|
+
Archive a completed change with ${colors.primary("forgelore archive <change>")} to start building
|
|
1359
|
+
your knowledge base.`,
|
|
1360
|
+
"Capabilities"
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (options?.json) {
|
|
1366
|
+
console.log(JSON.stringify(capabilities, null, 2));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const table = new Table3({
|
|
1370
|
+
head: [
|
|
1371
|
+
colors.bold("Name"),
|
|
1372
|
+
colors.bold("Description"),
|
|
1373
|
+
colors.bold("Source"),
|
|
1374
|
+
colors.bold("Archived"),
|
|
1375
|
+
colors.bold("Tags")
|
|
1376
|
+
].map(String),
|
|
1377
|
+
style: { head: [], border: ["gray"] },
|
|
1378
|
+
colWidths: [22, 30, 18, 14, 16],
|
|
1379
|
+
wordWrap: true
|
|
1380
|
+
});
|
|
1381
|
+
for (const cap of capabilities) {
|
|
1382
|
+
table.push([
|
|
1383
|
+
colors.white(cap.name),
|
|
1384
|
+
colors.muted(cap.description.slice(0, 60)),
|
|
1385
|
+
colors.secondary(cap.sourceChange),
|
|
1386
|
+
colors.dim(cap.archivedAt.slice(0, 10)),
|
|
1387
|
+
cap.tags?.length ? colors.accent(cap.tags.join(", ")) : colors.muted("--")
|
|
1388
|
+
]);
|
|
1389
|
+
}
|
|
1390
|
+
console.log(renderSection("Capabilities", table.toString()));
|
|
1391
|
+
console.log(
|
|
1392
|
+
colors.muted(
|
|
1393
|
+
` ${capabilities.length} capabilit${capabilities.length === 1 ? "y" : "ies"} registered`
|
|
1394
|
+
)
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/commands/config.ts
|
|
1399
|
+
import { resolve as resolve11 } from "path";
|
|
1400
|
+
import {
|
|
1401
|
+
configExists as configExists11,
|
|
1402
|
+
readConfig as readConfig4,
|
|
1403
|
+
getConfigValue,
|
|
1404
|
+
setConfigValue
|
|
1405
|
+
} from "@forgelore/core";
|
|
1406
|
+
async function configCommand(key, value, options) {
|
|
1407
|
+
const projectRoot = resolve11(options?.cwd || process.cwd());
|
|
1408
|
+
if (!await configExists11(projectRoot)) {
|
|
1409
|
+
console.log(
|
|
1410
|
+
renderBox(
|
|
1411
|
+
`${icons.error} forgelore is not initialized.
|
|
1412
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
1413
|
+
"Not Initialized",
|
|
1414
|
+
"#EF4444"
|
|
1415
|
+
)
|
|
1416
|
+
);
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
if (options?.list || !key && !value) {
|
|
1420
|
+
const config = await readConfig4(projectRoot);
|
|
1421
|
+
const lines = formatObject(config, "");
|
|
1422
|
+
console.log(
|
|
1423
|
+
renderSection("Configuration", lines.join("\n"))
|
|
1424
|
+
);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (key && !value) {
|
|
1428
|
+
const val = await getConfigValue(projectRoot, key);
|
|
1429
|
+
if (val === void 0) {
|
|
1430
|
+
console.log(
|
|
1431
|
+
` ${icons.error} Key ${colors.primary(key)} not found in config.`
|
|
1432
|
+
);
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
if (typeof val === "object" && val !== null) {
|
|
1436
|
+
const lines = formatObject(val, "");
|
|
1437
|
+
console.log(renderSection(key, lines.join("\n")));
|
|
1438
|
+
} else {
|
|
1439
|
+
console.log(` ${colors.primary(key)} ${colors.muted("=")} ${colors.white(String(val))}`);
|
|
1440
|
+
}
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (key && value) {
|
|
1444
|
+
let parsed = value;
|
|
1445
|
+
if (value === "true") parsed = true;
|
|
1446
|
+
else if (value === "false") parsed = false;
|
|
1447
|
+
else if (/^\d+$/.test(value)) parsed = parseInt(value, 10);
|
|
1448
|
+
else {
|
|
1449
|
+
try {
|
|
1450
|
+
parsed = JSON.parse(value);
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
await setConfigValue(projectRoot, key, parsed);
|
|
1455
|
+
console.log(
|
|
1456
|
+
` ${icons.success} Set ${colors.primary(key)} ${colors.muted("=")} ${colors.white(String(parsed))}`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function formatObject(obj, prefix) {
|
|
1461
|
+
const lines = [];
|
|
1462
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1463
|
+
const fullKey = prefix ? `${prefix}.${k}` : k;
|
|
1464
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
1465
|
+
lines.push(` ${colors.muted(fullKey + ":")}`);
|
|
1466
|
+
lines.push(...formatObject(v, fullKey));
|
|
1467
|
+
} else {
|
|
1468
|
+
lines.push(
|
|
1469
|
+
` ${colors.primary(fullKey)} ${colors.muted("=")} ${colors.white(String(v))}`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return lines;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/commands/diff.ts
|
|
1477
|
+
import { resolve as resolve12 } from "path";
|
|
1478
|
+
import Table4 from "cli-table3";
|
|
1479
|
+
import {
|
|
1480
|
+
configExists as configExists12,
|
|
1481
|
+
listChanges as listChanges4,
|
|
1482
|
+
readChange as readChange4,
|
|
1483
|
+
readChangeFile as readChangeFile3
|
|
1484
|
+
} from "@forgelore/core";
|
|
1485
|
+
async function diffCommand(changeName, options) {
|
|
1486
|
+
const projectRoot = resolve12(options?.cwd || process.cwd());
|
|
1487
|
+
if (!await configExists12(projectRoot)) {
|
|
1488
|
+
console.log(
|
|
1489
|
+
renderBox(
|
|
1490
|
+
`${icons.error} forgelore is not initialized.
|
|
1491
|
+
Run ${colors.primary("forgelore init")} first.`,
|
|
1492
|
+
"Not Initialized",
|
|
1493
|
+
"#EF4444"
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
process.exit(1);
|
|
1497
|
+
}
|
|
1498
|
+
const driftItems = [];
|
|
1499
|
+
if (changeName) {
|
|
1500
|
+
try {
|
|
1501
|
+
const change = await readChange4(projectRoot, changeName);
|
|
1502
|
+
await analyzeChangeDrift(projectRoot, changeName, driftItems);
|
|
1503
|
+
} catch {
|
|
1504
|
+
console.log(
|
|
1505
|
+
renderBox(
|
|
1506
|
+
`${icons.error} Change ${colors.primary(changeName)} not found.`,
|
|
1507
|
+
"Not Found",
|
|
1508
|
+
"#EF4444"
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
process.exit(1);
|
|
1512
|
+
}
|
|
1513
|
+
} else {
|
|
1514
|
+
const changes = await listChanges4(projectRoot, false);
|
|
1515
|
+
if (changes.length === 0) {
|
|
1516
|
+
console.log(
|
|
1517
|
+
renderBox(
|
|
1518
|
+
`${icons.info} No active changes to analyze.
|
|
1519
|
+
Run ${colors.primary("forgelore propose")} to create one.`,
|
|
1520
|
+
"No Changes"
|
|
1521
|
+
)
|
|
1522
|
+
);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
for (const change of changes) {
|
|
1526
|
+
await analyzeChangeDrift(projectRoot, change.name, driftItems);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (driftItems.length === 0) {
|
|
1530
|
+
console.log(
|
|
1531
|
+
renderBox(
|
|
1532
|
+
`${icons.success} No drift detected. Specs and implementation are in sync.`,
|
|
1533
|
+
"All Clear",
|
|
1534
|
+
"#10B981"
|
|
1535
|
+
)
|
|
1536
|
+
);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const table = new Table4({
|
|
1540
|
+
head: [
|
|
1541
|
+
colors.bold("Severity"),
|
|
1542
|
+
colors.bold("Type"),
|
|
1543
|
+
colors.bold("Message")
|
|
1544
|
+
].map(String),
|
|
1545
|
+
style: { head: [], border: ["gray"] },
|
|
1546
|
+
colWidths: [12, 20, 55],
|
|
1547
|
+
wordWrap: true
|
|
1548
|
+
});
|
|
1549
|
+
for (const item of driftItems) {
|
|
1550
|
+
const sevColor = item.severity === "critical" ? colors.error : item.severity === "warning" ? colors.warning : colors.muted;
|
|
1551
|
+
table.push([
|
|
1552
|
+
sevColor(item.severity),
|
|
1553
|
+
colors.white(item.type),
|
|
1554
|
+
colors.muted(item.message)
|
|
1555
|
+
]);
|
|
1556
|
+
}
|
|
1557
|
+
const critical = driftItems.filter((d) => d.severity === "critical").length;
|
|
1558
|
+
const warnings = driftItems.filter((d) => d.severity === "warning").length;
|
|
1559
|
+
const info = driftItems.filter((d) => d.severity === "info").length;
|
|
1560
|
+
const summaryParts = [];
|
|
1561
|
+
if (critical > 0) summaryParts.push(colors.error(`${critical} critical`));
|
|
1562
|
+
if (warnings > 0) summaryParts.push(colors.warning(`${warnings} warnings`));
|
|
1563
|
+
if (info > 0) summaryParts.push(colors.muted(`${info} info`));
|
|
1564
|
+
console.log(
|
|
1565
|
+
renderSection("Drift Report", table.toString())
|
|
1566
|
+
);
|
|
1567
|
+
console.log(
|
|
1568
|
+
renderBox(
|
|
1569
|
+
`${critical > 0 ? icons.error : icons.warning} ${summaryParts.join(", ")}`,
|
|
1570
|
+
"Summary",
|
|
1571
|
+
critical > 0 ? "#EF4444" : "#F59E0B"
|
|
1572
|
+
)
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
async function analyzeChangeDrift(projectRoot, changeName, items) {
|
|
1576
|
+
try {
|
|
1577
|
+
const change = await readChange4(projectRoot, changeName);
|
|
1578
|
+
const updatedAt = new Date(change.updatedAt);
|
|
1579
|
+
const daysSince = Math.floor(
|
|
1580
|
+
(Date.now() - updatedAt.getTime()) / (1e3 * 60 * 60 * 24)
|
|
1581
|
+
);
|
|
1582
|
+
if (daysSince > 14 && change.status === "proposed") {
|
|
1583
|
+
items.push({
|
|
1584
|
+
type: "stale-proposal",
|
|
1585
|
+
severity: "warning",
|
|
1586
|
+
message: `"${changeName}" proposed ${daysSince} days ago with no progress`
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
if (daysSince > 7 && change.status === "in-progress") {
|
|
1590
|
+
items.push({
|
|
1591
|
+
type: "stale-progress",
|
|
1592
|
+
severity: "info",
|
|
1593
|
+
message: `"${changeName}" in progress for ${daysSince} days since last update`
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
const specFiles = ["proposal.md", "specs/requirements.md", "specs/scenarios.md", "design.md", "tasks.md"];
|
|
1597
|
+
for (const file of specFiles) {
|
|
1598
|
+
try {
|
|
1599
|
+
const content = await readChangeFile3(projectRoot, changeName, file);
|
|
1600
|
+
const stripped = content.replace(/<!--.*?-->/gs, "").trim();
|
|
1601
|
+
if (stripped.length < 30) {
|
|
1602
|
+
items.push({
|
|
1603
|
+
type: "empty-spec",
|
|
1604
|
+
severity: change.status === "in-progress" ? "warning" : "info",
|
|
1605
|
+
message: `"${changeName}/${file}" has minimal content`
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
} catch {
|
|
1609
|
+
items.push({
|
|
1610
|
+
type: "missing-file",
|
|
1611
|
+
severity: "critical",
|
|
1612
|
+
message: `"${changeName}/${file}" is missing`
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (change.status === "in-progress" && change.tasks.length > 0 && change.tasks.every((t) => t.status === "pending")) {
|
|
1617
|
+
items.push({
|
|
1618
|
+
type: "no-task-progress",
|
|
1619
|
+
severity: "warning",
|
|
1620
|
+
message: `"${changeName}" is in-progress but all ${change.tasks.length} tasks are still pending`
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
} catch {
|
|
1624
|
+
items.push({
|
|
1625
|
+
type: "read-error",
|
|
1626
|
+
severity: "critical",
|
|
1627
|
+
message: `Could not read change "${changeName}"`
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/index.ts
|
|
1633
|
+
var program = new Command();
|
|
1634
|
+
program.name("forgelore").description("Spec-driven development for AI-assisted teams \u2014 forge knowledge, shape code").version("0.1.0");
|
|
1635
|
+
program.command("init").description("Initialize forgelore in the current project").option("-C, --cwd <path>", "Working directory").action((opts) => initCommand({ cwd: opts.cwd }));
|
|
1636
|
+
program.command("propose [idea]").description("Create a new change proposal").option("-C, --cwd <path>", "Working directory").action((idea, opts) => proposeCommand(idea, { cwd: opts.cwd }));
|
|
1637
|
+
program.command("clarify <change>").description("Review and refine requirements for a change").option("-C, --cwd <path>", "Working directory").action((change, opts) => clarifyCommand(change, { cwd: opts.cwd }));
|
|
1638
|
+
program.command("status").description("Show project status dashboard").option("-C, --cwd <path>", "Working directory").action((opts) => statusCommand({ cwd: opts.cwd }));
|
|
1639
|
+
program.command("list").description("List all changes").option("-a, --archived", "Include archived changes").option("-s, --status <status>", "Filter by status").option("-C, --cwd <path>", "Working directory").action((opts) => listCommand({ archived: opts.archived, status: opts.status, cwd: opts.cwd }));
|
|
1640
|
+
program.command("verify [change]").description("Verify a change against its specs (structural check)").option("-C, --cwd <path>", "Working directory").action((change, opts) => verifyCommand(change, { cwd: opts.cwd }));
|
|
1641
|
+
program.command("archive <change>").description("Archive a completed change and extract capabilities").option("--skip-outcome", "Skip outcome.md generation step").option("-C, --cwd <path>", "Working directory").action((change, opts) => archiveCommand(change, { skipOutcome: opts.skipOutcome, cwd: opts.cwd }));
|
|
1642
|
+
program.command("sync").description("Sync with the global spec repository").option("--force", "Force sync even if local changes exist").option("-C, --cwd <path>", "Working directory").action((opts) => syncCommand({ force: opts.force, cwd: opts.cwd }));
|
|
1643
|
+
program.command("doctor").description("Check forgelore health and diagnose issues").option("--fix", "Attempt to fix detected issues").option("-C, --cwd <path>", "Working directory").action((opts) => doctorCommand({ fix: opts.fix, cwd: opts.cwd }));
|
|
1644
|
+
program.command("capabilities").alias("caps").description("List all registered capabilities").option("--json", "Output as JSON").option("-C, --cwd <path>", "Working directory").action((opts) => capabilitiesCommand({ json: opts.json, cwd: opts.cwd }));
|
|
1645
|
+
program.command("config [key] [value]").description("Get or set configuration values").option("--list", "List all config values").option("-C, --cwd <path>", "Working directory").action((key, value, opts) => configCommand(key, value, { list: opts.list, cwd: opts.cwd }));
|
|
1646
|
+
program.command("diff [change]").description("Show drift between specs and implementation").option("-C, --cwd <path>", "Working directory").action((change, opts) => diffCommand(change, { cwd: opts.cwd }));
|
|
1647
|
+
program.action(async () => {
|
|
1648
|
+
await renderAnimatedBanner();
|
|
1649
|
+
program.outputHelp();
|
|
1650
|
+
});
|
|
1651
|
+
program.parse();
|
|
1652
|
+
//# sourceMappingURL=index.js.map
|