forgedock 1.0.13 → 1.0.14
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/bin/forgedock.mjs +485 -410
- package/bin/tui.mjs +400 -0
- package/package.json +1 -1
- package/scripts/gen-logo.mjs +391 -0
package/bin/forgedock.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { dirname, join, relative } from "path";
|
|
5
|
-
import { mkdir, symlink, readlink, lstat, readdir
|
|
5
|
+
import { mkdir, symlink, readlink, lstat, readdir } from "fs/promises";
|
|
6
6
|
import {
|
|
7
7
|
existsSync,
|
|
8
8
|
appendFileSync,
|
|
@@ -13,6 +13,24 @@ import {
|
|
|
13
13
|
} from "fs";
|
|
14
14
|
import { execSync } from "child_process";
|
|
15
15
|
import readline from "readline";
|
|
16
|
+
import {
|
|
17
|
+
renderLogo,
|
|
18
|
+
runSteps,
|
|
19
|
+
box,
|
|
20
|
+
table,
|
|
21
|
+
bold,
|
|
22
|
+
dim,
|
|
23
|
+
green,
|
|
24
|
+
red,
|
|
25
|
+
yellow,
|
|
26
|
+
cyan,
|
|
27
|
+
RESET,
|
|
28
|
+
BOLD,
|
|
29
|
+
GREEN,
|
|
30
|
+
RED,
|
|
31
|
+
YELLOW,
|
|
32
|
+
CYAN,
|
|
33
|
+
} from "./tui.mjs";
|
|
16
34
|
|
|
17
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
36
|
const __dirname = dirname(__filename);
|
|
@@ -20,8 +38,8 @@ const FORGE_HOME = dirname(__dirname);
|
|
|
20
38
|
const COMMANDS_DIR = join(FORGE_HOME, "commands");
|
|
21
39
|
|
|
22
40
|
if (!process.env.HOME) {
|
|
23
|
-
|
|
24
|
-
"Error: HOME environment variable is not set. Cannot determine install location
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
"Error: HOME environment variable is not set. Cannot determine install location.\n",
|
|
25
43
|
);
|
|
26
44
|
process.exit(1);
|
|
27
45
|
}
|
|
@@ -31,24 +49,10 @@ const TARGET_DIR = join(process.env.HOME, ".claude", "commands");
|
|
|
31
49
|
const args = process.argv.slice(2);
|
|
32
50
|
const command = args[0] || "install";
|
|
33
51
|
|
|
34
|
-
const BOLD = "\x1b[1m";
|
|
35
|
-
const GREEN = "\x1b[32m";
|
|
36
|
-
const YELLOW = "\x1b[33m";
|
|
37
|
-
const CYAN = "\x1b[36m";
|
|
38
|
-
const RED = "\x1b[31m";
|
|
39
|
-
const RESET = "\x1b[0m";
|
|
40
|
-
const DIM = "\x1b[2m";
|
|
41
|
-
|
|
42
52
|
// ---------------------------------------------------------------------------
|
|
43
|
-
// Splash — the ForgeDock
|
|
53
|
+
// Splash — the ForgeDock F-monogram logo shown at startup
|
|
44
54
|
// ---------------------------------------------------------------------------
|
|
45
55
|
|
|
46
|
-
const LOGO_LINES = [
|
|
47
|
-
" ╔═╗╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗╔═╗╦╔═",
|
|
48
|
-
" ╠╣ ║ ║╠╦╝║ ╦║╣ ║║║ ║║ ╠╩╗",
|
|
49
|
-
" ╚ ╚═╝╩╚═╚═╝╚═╝═╩╝╚═╝╚═╝╩ ╩",
|
|
50
|
-
];
|
|
51
|
-
|
|
52
56
|
function getVersion() {
|
|
53
57
|
try {
|
|
54
58
|
const pkg = JSON.parse(
|
|
@@ -62,16 +66,7 @@ function getVersion() {
|
|
|
62
66
|
|
|
63
67
|
function splash() {
|
|
64
68
|
const version = getVersion();
|
|
65
|
-
|
|
66
|
-
for (const line of LOGO_LINES) {
|
|
67
|
-
console.log(`${BOLD}${CYAN}${line}${RESET}`);
|
|
68
|
-
}
|
|
69
|
-
console.log("");
|
|
70
|
-
console.log(` ${DIM}GitHub as a knowledge graph for AI agents${RESET}`);
|
|
71
|
-
if (version) {
|
|
72
|
-
console.log(` ${DIM}v${version}${RESET}`);
|
|
73
|
-
}
|
|
74
|
-
console.log("");
|
|
69
|
+
process.stderr.write(renderLogo({ version }) + "\n");
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
/**
|
|
@@ -80,15 +75,15 @@ function splash() {
|
|
|
80
75
|
*/
|
|
81
76
|
async function confirmOverwrite(question) {
|
|
82
77
|
if (!process.stdin.isTTY) {
|
|
83
|
-
|
|
84
|
-
` ${YELLOW}Non-interactive environment detected — aborting to protect existing config.${RESET}`,
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
` ${YELLOW}Non-interactive environment detected — aborting to protect existing config.${RESET}\n`,
|
|
85
80
|
);
|
|
86
81
|
return false;
|
|
87
82
|
}
|
|
88
83
|
return new Promise((resolve) => {
|
|
89
84
|
const rl = readline.createInterface({
|
|
90
85
|
input: process.stdin,
|
|
91
|
-
output: process.
|
|
86
|
+
output: process.stderr,
|
|
92
87
|
});
|
|
93
88
|
rl.question(question, (answer) => {
|
|
94
89
|
rl.close();
|
|
@@ -134,21 +129,24 @@ async function findMarkdownFiles(dir) {
|
|
|
134
129
|
return results.sort();
|
|
135
130
|
}
|
|
136
131
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log(` Source: ${CYAN}${COMMANDS_DIR}/${RESET}`);
|
|
141
|
-
console.log(` Target: ${CYAN}${TARGET_DIR}/${RESET}`);
|
|
142
|
-
console.log("");
|
|
143
|
-
|
|
144
|
-
await mkdir(TARGET_DIR, { recursive: true });
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Shared file-linking logic — used by both install() and update()
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
145
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Link or copy all commands from COMMANDS_DIR into TARGET_DIR.
|
|
138
|
+
* Calls step.progress(idx+1, total) on each file.
|
|
139
|
+
* Returns { installed, updated, skipped }.
|
|
140
|
+
*/
|
|
141
|
+
async function linkCommands(step) {
|
|
146
142
|
const files = await findMarkdownFiles(COMMANDS_DIR);
|
|
143
|
+
const total = files.length;
|
|
147
144
|
let installed = 0;
|
|
148
145
|
let updated = 0;
|
|
149
146
|
let skipped = 0;
|
|
150
147
|
|
|
151
|
-
for (
|
|
148
|
+
for (let idx = 0; idx < files.length; idx++) {
|
|
149
|
+
const file = files[idx];
|
|
152
150
|
const rel = relative(COMMANDS_DIR, file);
|
|
153
151
|
const target = join(TARGET_DIR, rel);
|
|
154
152
|
const targetDir = dirname(target);
|
|
@@ -166,7 +164,6 @@ async function install() {
|
|
|
166
164
|
await symlink(file, target + ".tmp");
|
|
167
165
|
const { rename } = await import("fs/promises");
|
|
168
166
|
await rename(target + ".tmp", target);
|
|
169
|
-
console.log(` ${YELLOW}Updated${RESET}: ${rel}`);
|
|
170
167
|
updated++;
|
|
171
168
|
}
|
|
172
169
|
} else {
|
|
@@ -178,15 +175,11 @@ async function install() {
|
|
|
178
175
|
skipped++;
|
|
179
176
|
} else {
|
|
180
177
|
copyFileSync(file, target);
|
|
181
|
-
console.log(` ${YELLOW}Updated${RESET}: ${rel}`);
|
|
182
178
|
updated++;
|
|
183
179
|
}
|
|
184
180
|
}
|
|
185
181
|
} catch (err) {
|
|
186
182
|
if (err.code !== "ENOENT") {
|
|
187
|
-
console.error(
|
|
188
|
-
` ${RED}Error${RESET}: Cannot access ${rel} — ${err.code ?? err.message}`,
|
|
189
|
-
);
|
|
190
183
|
throw err;
|
|
191
184
|
}
|
|
192
185
|
// Doesn't exist — create it. Prefer a symlink; fall back to a copy on
|
|
@@ -194,182 +187,51 @@ async function install() {
|
|
|
194
187
|
// Developer Mode / admin), which would otherwise throw EPERM. See #587.
|
|
195
188
|
try {
|
|
196
189
|
await symlink(file, target);
|
|
197
|
-
console.log(` ${GREEN}Installed${RESET}: ${rel}`);
|
|
198
190
|
} catch (linkErr) {
|
|
199
191
|
if (linkErr.code === "EPERM" || linkErr.code === "EACCES") {
|
|
200
192
|
copyFileSync(file, target);
|
|
201
|
-
console.log(` ${GREEN}Installed${RESET} (copy): ${rel}`);
|
|
202
193
|
} else {
|
|
203
194
|
throw linkErr;
|
|
204
195
|
}
|
|
205
196
|
}
|
|
206
197
|
installed++;
|
|
207
198
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
console.log("");
|
|
211
|
-
console.log(
|
|
212
|
-
`Done. ${GREEN}Installed: ${installed}${RESET}, Updated: ${updated}, Skipped: ${skipped}`,
|
|
213
|
-
);
|
|
214
|
-
console.log("");
|
|
215
|
-
|
|
216
|
-
// Set FORGE_HOME in shell profiles
|
|
217
|
-
let profileUpdated = false;
|
|
218
|
-
for (const profile of [
|
|
219
|
-
join(process.env.HOME, ".bashrc"),
|
|
220
|
-
join(process.env.HOME, ".zshrc"),
|
|
221
|
-
]) {
|
|
222
|
-
if (existsSync(profile)) {
|
|
223
|
-
const content = readFileSync(profile, "utf-8");
|
|
224
|
-
if (!content.includes("FORGE_HOME")) {
|
|
225
|
-
appendFileSync(
|
|
226
|
-
profile,
|
|
227
|
-
`\n# ForgeDock — autonomous development pipeline\nexport FORGE_HOME="${FORGE_HOME}"\n`,
|
|
228
|
-
);
|
|
229
|
-
console.log(` Added FORGE_HOME to ${profile}`);
|
|
230
|
-
profileUpdated = true;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
console.log(
|
|
236
|
-
`${GREEN}ForgeDock commands are now available as slash commands in any Claude Code session.${RESET}`,
|
|
237
|
-
);
|
|
238
|
-
console.log("");
|
|
239
|
-
|
|
240
|
-
// Auto-generate forge.yaml if missing — no second command needed.
|
|
241
|
-
// Only do this inside a real git project; otherwise cwd may be a system or
|
|
242
|
-
// read-only directory (e.g. C:\Windows when npx is launched from a WSL path),
|
|
243
|
-
// and writing forge.yaml there would crash. See #585.
|
|
244
|
-
const cwd = process.cwd();
|
|
245
|
-
const forgeYamlPath = join(cwd, "forge.yaml");
|
|
246
|
-
if (!existsSync(forgeYamlPath)) {
|
|
247
|
-
if (isGitWorkTree(cwd)) {
|
|
248
|
-
await init(true);
|
|
249
|
-
} else {
|
|
250
|
-
console.log(
|
|
251
|
-
` ${YELLOW}Skipping forge.yaml generation${RESET} — current directory is not a git project.`,
|
|
252
|
-
);
|
|
253
|
-
console.log(
|
|
254
|
-
` Run ${CYAN}npx forgedock init${RESET} inside your project directory to generate it.`,
|
|
255
|
-
);
|
|
256
|
-
console.log("");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function uninstall() {
|
|
262
|
-
console.log("");
|
|
263
|
-
console.log(`${BOLD}ForgeDock${RESET} — Removing pipeline commands`);
|
|
264
|
-
console.log("");
|
|
265
|
-
|
|
266
|
-
const files = await findMarkdownFiles(COMMANDS_DIR);
|
|
267
|
-
let removed = 0;
|
|
268
199
|
|
|
269
|
-
|
|
270
|
-
const rel = relative(COMMANDS_DIR, file);
|
|
271
|
-
const target = join(TARGET_DIR, rel);
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const stats = await lstat(target);
|
|
275
|
-
const { unlink } = await import("fs/promises");
|
|
276
|
-
if (stats.isSymbolicLink()) {
|
|
277
|
-
const current = await readlink(target);
|
|
278
|
-
if (current === file) {
|
|
279
|
-
await unlink(target);
|
|
280
|
-
console.log(` ${RED}Removed${RESET}: ${rel}`);
|
|
281
|
-
removed++;
|
|
282
|
-
}
|
|
283
|
-
} else if (
|
|
284
|
-
readFileSync(file, "utf-8") === readFileSync(target, "utf-8")
|
|
285
|
-
) {
|
|
286
|
-
// Regular file installed by ForgeDock in copy mode (content matches the
|
|
287
|
-
// shipped command). Safe to remove. Content-differing files are left
|
|
288
|
-
// in place in case the user edited them.
|
|
289
|
-
await unlink(target);
|
|
290
|
-
console.log(` ${RED}Removed${RESET}: ${rel}`);
|
|
291
|
-
removed++;
|
|
292
|
-
}
|
|
293
|
-
} catch (err) {
|
|
294
|
-
if (err.code !== "ENOENT") {
|
|
295
|
-
console.error(
|
|
296
|
-
` ${RED}Error${RESET}: Cannot access ${rel} — ${err.code ?? err.message}`,
|
|
297
|
-
);
|
|
298
|
-
throw err;
|
|
299
|
-
}
|
|
300
|
-
// Doesn't exist — nothing to do
|
|
301
|
-
}
|
|
200
|
+
step.progress(idx + 1, total);
|
|
302
201
|
}
|
|
303
202
|
|
|
304
|
-
|
|
305
|
-
console.log(`Done. Removed: ${removed} commands.`);
|
|
306
|
-
console.log("");
|
|
203
|
+
return { installed, updated, skipped };
|
|
307
204
|
}
|
|
308
205
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
console.log("");
|
|
313
|
-
|
|
314
|
-
// Check if installed via npm (no .git directory) or via git clone
|
|
315
|
-
const gitDir = join(FORGE_HOME, ".git");
|
|
316
|
-
if (existsSync(gitDir)) {
|
|
317
|
-
try {
|
|
318
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
319
|
-
cwd: FORGE_HOME,
|
|
320
|
-
encoding: "utf-8",
|
|
321
|
-
}).trim();
|
|
322
|
-
if (branch !== "main") {
|
|
323
|
-
console.log(` Not on main branch (${branch}) — skipping`);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const before = execSync("git rev-parse HEAD", {
|
|
328
|
-
cwd: FORGE_HOME,
|
|
329
|
-
encoding: "utf-8",
|
|
330
|
-
}).trim();
|
|
331
|
-
execSync("git fetch origin main --quiet", { cwd: FORGE_HOME });
|
|
332
|
-
execSync("git merge --ff-only origin/main --quiet", {
|
|
333
|
-
cwd: FORGE_HOME,
|
|
334
|
-
});
|
|
335
|
-
const after = execSync("git rev-parse HEAD", {
|
|
336
|
-
cwd: FORGE_HOME,
|
|
337
|
-
encoding: "utf-8",
|
|
338
|
-
}).trim();
|
|
339
|
-
|
|
340
|
-
if (before === after) {
|
|
341
|
-
console.log(` Already up to date.`);
|
|
342
|
-
} else {
|
|
343
|
-
console.log(` ${GREEN}Updated to latest.${RESET}`);
|
|
344
|
-
await install();
|
|
345
|
-
}
|
|
346
|
-
} catch (err) {
|
|
347
|
-
console.log(
|
|
348
|
-
` ${YELLOW}Cannot fast-forward — local changes exist. Skipping.${RESET}`,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
console.log(
|
|
353
|
-
` Installed via npm. Run ${CYAN}npm update -g forgedock${RESET} to update.`,
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
console.log("");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function init(fromInstall = false) {
|
|
360
|
-
console.log("");
|
|
361
|
-
console.log(`${BOLD}ForgeDock${RESET} — Generating forge.yaml`);
|
|
362
|
-
console.log("");
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Core forge.yaml detection + write — used by both install() and init()
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
363
209
|
|
|
364
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Detect project info from the current working directory and write forge.yaml.
|
|
212
|
+
* Returns the detected config object.
|
|
213
|
+
* This is pure logic — no TUI output. Called from inside step callbacks.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} cwd - Current working directory
|
|
216
|
+
* @param {object} [opts]
|
|
217
|
+
* @param {(text: string) => void} [opts.onNote] - Called with incremental progress notes
|
|
218
|
+
* @returns {Promise<{owner, repo, remoteDetected, defaultBranch, stagingBranch, description, outputPath, wrote: boolean, skipped: boolean, skipReason: string}>}
|
|
219
|
+
*/
|
|
220
|
+
async function detectAndBuildForgeYaml(cwd, opts = {}) {
|
|
221
|
+
const { onNote = () => {} } = opts;
|
|
365
222
|
const worktreeBase = join(cwd, ".claude", "worktrees");
|
|
366
223
|
const outputPath = join(cwd, "forge.yaml");
|
|
367
224
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
225
|
+
const detected = {
|
|
226
|
+
owner: "your-github-org",
|
|
227
|
+
repo: "your-repo-name",
|
|
228
|
+
remoteDetected: false,
|
|
229
|
+
defaultBranch: "main",
|
|
230
|
+
stagingBranch: "staging",
|
|
231
|
+
description: "",
|
|
232
|
+
};
|
|
372
233
|
|
|
234
|
+
// --- Detect git remote URL and parse owner/repo ---
|
|
373
235
|
try {
|
|
374
236
|
const remoteUrl = execSync("git remote get-url origin", {
|
|
375
237
|
cwd,
|
|
@@ -385,210 +247,102 @@ async function init(fromInstall = false) {
|
|
|
385
247
|
);
|
|
386
248
|
|
|
387
249
|
if (sshMatch) {
|
|
388
|
-
owner = sshMatch[1];
|
|
389
|
-
repo = sshMatch[2];
|
|
390
|
-
remoteDetected = true;
|
|
250
|
+
detected.owner = sshMatch[1];
|
|
251
|
+
detected.repo = sshMatch[2];
|
|
252
|
+
detected.remoteDetected = true;
|
|
391
253
|
} else if (httpsMatch) {
|
|
392
|
-
owner = httpsMatch[1];
|
|
393
|
-
repo = httpsMatch[2];
|
|
394
|
-
remoteDetected = true;
|
|
395
|
-
} else {
|
|
396
|
-
console.log(
|
|
397
|
-
` ${YELLOW}Warning${RESET}: Could not parse git remote URL — using placeholder values`,
|
|
398
|
-
);
|
|
254
|
+
detected.owner = httpsMatch[1];
|
|
255
|
+
detected.repo = httpsMatch[2];
|
|
256
|
+
detected.remoteDetected = true;
|
|
399
257
|
}
|
|
400
258
|
} catch {
|
|
401
|
-
|
|
402
|
-
` ${YELLOW}Warning${RESET}: No git remote found — using placeholder values`,
|
|
403
|
-
);
|
|
259
|
+
// No git remote — use placeholders
|
|
404
260
|
}
|
|
405
261
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
262
|
+
onNote(
|
|
263
|
+
detected.remoteDetected
|
|
264
|
+
? `${detected.owner}/${detected.repo}`
|
|
265
|
+
: "no git remote — using placeholders",
|
|
266
|
+
);
|
|
409
267
|
|
|
410
268
|
// --- Detect default branch ---
|
|
411
|
-
let defaultBranch = "main";
|
|
412
269
|
try {
|
|
413
270
|
const headRef = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
414
271
|
cwd,
|
|
415
272
|
encoding: "utf-8",
|
|
416
273
|
stdio: ["pipe", "pipe", "pipe"],
|
|
417
274
|
}).trim();
|
|
418
|
-
|
|
419
|
-
defaultBranch = headRef.replace(/^refs\/remotes\/origin\//, "");
|
|
420
|
-
console.log(` Default branch: ${CYAN}${defaultBranch}${RESET}`);
|
|
275
|
+
detected.defaultBranch = headRef.replace(/^refs\/remotes\/origin\//, "");
|
|
421
276
|
} catch {
|
|
422
|
-
// Fallback: try git rev-parse
|
|
423
277
|
try {
|
|
424
|
-
defaultBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
278
|
+
detected.defaultBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
425
279
|
cwd,
|
|
426
280
|
encoding: "utf-8",
|
|
427
281
|
stdio: ["pipe", "pipe", "pipe"],
|
|
428
282
|
}).trim();
|
|
429
|
-
if (defaultBranch === "HEAD") defaultBranch = "main";
|
|
430
|
-
console.log(
|
|
431
|
-
` Default branch: ${CYAN}${defaultBranch}${RESET} (from current branch)`,
|
|
432
|
-
);
|
|
283
|
+
if (detected.defaultBranch === "HEAD") detected.defaultBranch = "main";
|
|
433
284
|
} catch {
|
|
434
|
-
|
|
435
|
-
` ${YELLOW}Warning${RESET}: Could not detect default branch — defaulting to "main"`,
|
|
436
|
-
);
|
|
285
|
+
// Use default "main"
|
|
437
286
|
}
|
|
438
287
|
}
|
|
439
288
|
|
|
440
289
|
// --- Detect staging branch ---
|
|
441
|
-
let stagingBranch = "staging";
|
|
442
290
|
try {
|
|
443
291
|
const remoteBranches = execSync("git branch -r", {
|
|
444
292
|
cwd,
|
|
445
293
|
encoding: "utf-8",
|
|
446
294
|
stdio: ["pipe", "pipe", "pipe"],
|
|
447
295
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
} else {
|
|
452
|
-
stagingBranch = defaultBranch;
|
|
453
|
-
console.log(
|
|
454
|
-
` Staging branch: ${CYAN}${defaultBranch}${RESET} (no staging branch found — using default)`,
|
|
455
|
-
);
|
|
456
|
-
}
|
|
296
|
+
detected.stagingBranch = remoteBranches.includes("origin/staging")
|
|
297
|
+
? "staging"
|
|
298
|
+
: detected.defaultBranch;
|
|
457
299
|
} catch {
|
|
458
|
-
|
|
459
|
-
` ${YELLOW}Warning${RESET}: Could not read remote branches — defaulting staging to "${defaultBranch}"`,
|
|
460
|
-
);
|
|
461
|
-
stagingBranch = defaultBranch;
|
|
300
|
+
detected.stagingBranch = detected.defaultBranch;
|
|
462
301
|
}
|
|
463
302
|
|
|
464
|
-
// --- Auto-detect project description from README.md ---
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (!inFirstParagraph && line.match(/^#/)) continue;
|
|
477
|
-
// Skip blank lines before paragraph starts
|
|
478
|
-
if (!inFirstParagraph && line.trim() === "") continue;
|
|
479
|
-
// Skip lines that are just badges, HTML, code fences, horizontal rules, or tables
|
|
480
|
-
if (!inFirstParagraph && line.match(/^[!<\[`|]/)) continue;
|
|
481
|
-
if (!inFirstParagraph && line.match(/^---/)) continue;
|
|
482
|
-
// Start collecting
|
|
483
|
-
if (!inFirstParagraph) {
|
|
484
|
-
inFirstParagraph = true;
|
|
485
|
-
}
|
|
486
|
-
// Stop at blank line (end of paragraph)
|
|
487
|
-
if (line.trim() === "") break;
|
|
488
|
-
paragraphLines.push(line.trim());
|
|
489
|
-
}
|
|
490
|
-
if (paragraphLines.length > 0) {
|
|
491
|
-
// Flatten to single line, strip markdown links/bold/inline code
|
|
492
|
-
description = paragraphLines
|
|
493
|
-
.join(" ")
|
|
494
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
495
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
496
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
497
|
-
.slice(0, 200)
|
|
498
|
-
.trim();
|
|
303
|
+
// --- Auto-detect project description from README.md / CLAUDE.md ---
|
|
304
|
+
function extractFirstParagraph(text) {
|
|
305
|
+
const lines = text.split("\n");
|
|
306
|
+
let inFirstParagraph = false;
|
|
307
|
+
const paragraphLines = [];
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
if (!inFirstParagraph && line.match(/^#/)) continue;
|
|
310
|
+
if (!inFirstParagraph && line.trim() === "") continue;
|
|
311
|
+
if (!inFirstParagraph && line.match(/^[!<\[`|]/)) continue;
|
|
312
|
+
if (!inFirstParagraph && line.match(/^---/)) continue;
|
|
313
|
+
if (!inFirstParagraph) {
|
|
314
|
+
inFirstParagraph = true;
|
|
499
315
|
}
|
|
316
|
+
if (line.trim() === "") break;
|
|
317
|
+
paragraphLines.push(line.trim());
|
|
500
318
|
}
|
|
501
|
-
|
|
502
|
-
|
|
319
|
+
if (paragraphLines.length === 0) return "";
|
|
320
|
+
return paragraphLines
|
|
321
|
+
.join(" ")
|
|
322
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
323
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
324
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
325
|
+
.slice(0, 200)
|
|
326
|
+
.trim();
|
|
503
327
|
}
|
|
504
328
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
` Description: ${CYAN}${description.slice(0, 60)}${description.length > 60 ? "…" : ""}${RESET} (from README.md)`,
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// --- Auto-detect project description from CLAUDE.md (fallback) ---
|
|
512
|
-
if (!description) {
|
|
329
|
+
for (const fname of ["README.md", "CLAUDE.md"]) {
|
|
330
|
+
if (detected.description) break;
|
|
513
331
|
try {
|
|
514
|
-
const
|
|
515
|
-
if (existsSync(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const paragraphLines = [];
|
|
520
|
-
for (const line of lines) {
|
|
521
|
-
// Skip heading lines at the top
|
|
522
|
-
if (!inFirstParagraph && line.match(/^#/)) continue;
|
|
523
|
-
// Skip blank lines before paragraph starts
|
|
524
|
-
if (!inFirstParagraph && line.trim() === "") continue;
|
|
525
|
-
// Skip lines that are just badges, HTML comments, code fences, horizontal rules, or tables
|
|
526
|
-
if (!inFirstParagraph && line.match(/^[!<\[`|]/)) continue;
|
|
527
|
-
if (!inFirstParagraph && line.match(/^---/)) continue;
|
|
528
|
-
// Start collecting
|
|
529
|
-
if (!inFirstParagraph) {
|
|
530
|
-
inFirstParagraph = true;
|
|
531
|
-
}
|
|
532
|
-
// Stop at blank line (end of paragraph)
|
|
533
|
-
if (line.trim() === "") break;
|
|
534
|
-
paragraphLines.push(line.trim());
|
|
535
|
-
}
|
|
536
|
-
if (paragraphLines.length > 0) {
|
|
537
|
-
// Flatten to single line, strip markdown links/bold/inline code
|
|
538
|
-
description = paragraphLines
|
|
539
|
-
.join(" ")
|
|
540
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
541
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
542
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
543
|
-
.slice(0, 200)
|
|
544
|
-
.trim();
|
|
545
|
-
}
|
|
332
|
+
const fpath = join(cwd, fname);
|
|
333
|
+
if (existsSync(fpath)) {
|
|
334
|
+
detected.description = extractFirstParagraph(
|
|
335
|
+
readFileSync(fpath, "utf-8").slice(0, 2048),
|
|
336
|
+
);
|
|
546
337
|
}
|
|
547
338
|
} catch {
|
|
548
|
-
// Best-effort only
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (description) {
|
|
552
|
-
console.log(
|
|
553
|
-
` Description: ${CYAN}${description.slice(0, 60)}${description.length > 60 ? "…" : ""}${RESET} (from CLAUDE.md)`,
|
|
554
|
-
);
|
|
339
|
+
// Best-effort only
|
|
555
340
|
}
|
|
556
341
|
}
|
|
557
342
|
|
|
558
|
-
// ---
|
|
559
|
-
|
|
560
|
-
if (!fromInstall) {
|
|
561
|
-
console.log(
|
|
562
|
-
` ${YELLOW}Warning:${RESET} forge.yaml already exists at ${outputPath}`,
|
|
563
|
-
);
|
|
564
|
-
console.log(
|
|
565
|
-
` Continuing will back up your existing config and replace it with a freshly generated one.`,
|
|
566
|
-
);
|
|
567
|
-
console.log(``);
|
|
568
|
-
const confirmed = await confirmOverwrite(` Overwrite? [y/N] `);
|
|
569
|
-
if (!confirmed) {
|
|
570
|
-
console.log(``);
|
|
571
|
-
console.log(
|
|
572
|
-
` ${YELLOW}Aborted.${RESET} Your forge.yaml was not modified.`,
|
|
573
|
-
);
|
|
574
|
-
console.log(``);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
console.log(``);
|
|
578
|
-
}
|
|
579
|
-
const baseBak = join(cwd, "forge.yaml.bak");
|
|
580
|
-
const backupPath = existsSync(baseBak)
|
|
581
|
-
? join(
|
|
582
|
-
cwd,
|
|
583
|
-
`forge.yaml.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
584
|
-
)
|
|
585
|
-
: baseBak;
|
|
586
|
-
const backupName = backupPath.split("/").pop();
|
|
587
|
-
renameSync(outputPath, backupPath);
|
|
588
|
-
console.log(` ${YELLOW}Backed up${RESET}: forge.yaml → ${backupName}`);
|
|
589
|
-
}
|
|
343
|
+
// --- Build forge.yaml content ---
|
|
344
|
+
const { owner, repo, defaultBranch, stagingBranch, description } = detected;
|
|
590
345
|
|
|
591
|
-
// --- Generate forge.yaml content ---
|
|
592
346
|
const projectName = repo
|
|
593
347
|
.split(/[-_]/)
|
|
594
348
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
@@ -687,70 +441,391 @@ branches:
|
|
|
687
441
|
try {
|
|
688
442
|
writeFileSync(outputPath, content, "utf-8");
|
|
689
443
|
} catch (err) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Could not write forge.yaml to ${outputPath} — ${err.code ?? err.message}. ` +
|
|
446
|
+
`Run npx forgedock init from a writable project directory.`,
|
|
693
447
|
);
|
|
694
|
-
|
|
695
|
-
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { ...detected, outputPath, wrote: true };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Subcommands
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
async function install() {
|
|
458
|
+
const result = await runSteps([
|
|
459
|
+
{
|
|
460
|
+
label: "Checking environment",
|
|
461
|
+
async run(step) {
|
|
462
|
+
await mkdir(TARGET_DIR, { recursive: true });
|
|
463
|
+
step.note(cyan(TARGET_DIR));
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
label: "Linking commands",
|
|
468
|
+
async run(step) {
|
|
469
|
+
const { installed, updated, skipped } = await linkCommands(step);
|
|
470
|
+
step.note(
|
|
471
|
+
`${green(String(installed))} installed, ${updated} updated, ${dim(String(skipped))} skipped`,
|
|
472
|
+
);
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
label: "Configuring session hook",
|
|
477
|
+
async run(step) {
|
|
478
|
+
let profileUpdated = false;
|
|
479
|
+
for (const profile of [
|
|
480
|
+
join(process.env.HOME, ".bashrc"),
|
|
481
|
+
join(process.env.HOME, ".zshrc"),
|
|
482
|
+
]) {
|
|
483
|
+
if (existsSync(profile)) {
|
|
484
|
+
const content = readFileSync(profile, "utf-8");
|
|
485
|
+
if (!content.includes("FORGE_HOME")) {
|
|
486
|
+
appendFileSync(
|
|
487
|
+
profile,
|
|
488
|
+
`\n# ForgeDock — autonomous development pipeline\nexport FORGE_HOME="${FORGE_HOME}"\n`,
|
|
489
|
+
);
|
|
490
|
+
profileUpdated = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!profileUpdated) {
|
|
495
|
+
step.skip("FORGE_HOME already set");
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
label: "Generating forge.yaml",
|
|
501
|
+
async run(step) {
|
|
502
|
+
// Auto-generate forge.yaml if missing — no second command needed.
|
|
503
|
+
// Only do this inside a real git project; otherwise cwd may be a system or
|
|
504
|
+
// read-only directory (e.g. C:\Windows when npx is launched from a WSL path),
|
|
505
|
+
// and writing forge.yaml there would crash. See #585.
|
|
506
|
+
const cwd = process.cwd();
|
|
507
|
+
const forgeYamlPath = join(cwd, "forge.yaml");
|
|
508
|
+
if (existsSync(forgeYamlPath)) {
|
|
509
|
+
step.skip("forge.yaml already exists");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (!isGitWorkTree(cwd)) {
|
|
513
|
+
step.skip(
|
|
514
|
+
"not a git project — run npx forgedock init inside your project",
|
|
515
|
+
);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const detected = await detectAndBuildForgeYaml(cwd, {
|
|
519
|
+
onNote: () => {},
|
|
520
|
+
});
|
|
521
|
+
if (!detected.remoteDetected) {
|
|
522
|
+
step.note("created — fill in project.owner and project.repo");
|
|
523
|
+
} else {
|
|
524
|
+
step.note(`created for ${detected.owner}/${detected.repo}`);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
if (result.ok) {
|
|
531
|
+
process.stderr.write(
|
|
532
|
+
box(
|
|
533
|
+
[
|
|
534
|
+
`${green("✔")} ${bold("ForgeDock is ready")}`,
|
|
535
|
+
"",
|
|
536
|
+
`Commands installed to ${cyan(TARGET_DIR)}`,
|
|
537
|
+
`Next: ${cyan("cd <your-project>")} then ${cyan("npx forgedock init")}`,
|
|
538
|
+
],
|
|
539
|
+
{ title: "ForgeDock Installed" },
|
|
540
|
+
) + "\n",
|
|
696
541
|
);
|
|
697
|
-
process.exit(1);
|
|
698
542
|
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function uninstall() {
|
|
546
|
+
const result = await runSteps([
|
|
547
|
+
{
|
|
548
|
+
label: "Removing commands",
|
|
549
|
+
async run(step) {
|
|
550
|
+
const files = await findMarkdownFiles(COMMANDS_DIR);
|
|
551
|
+
const total = files.length;
|
|
552
|
+
let removed = 0;
|
|
553
|
+
|
|
554
|
+
for (let idx = 0; idx < files.length; idx++) {
|
|
555
|
+
const file = files[idx];
|
|
556
|
+
const rel = relative(COMMANDS_DIR, file);
|
|
557
|
+
const target = join(TARGET_DIR, rel);
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const stats = await lstat(target);
|
|
561
|
+
const { unlink } = await import("fs/promises");
|
|
562
|
+
if (stats.isSymbolicLink()) {
|
|
563
|
+
const current = await readlink(target);
|
|
564
|
+
if (current === file) {
|
|
565
|
+
await unlink(target);
|
|
566
|
+
removed++;
|
|
567
|
+
}
|
|
568
|
+
} else if (
|
|
569
|
+
readFileSync(file, "utf-8") === readFileSync(target, "utf-8")
|
|
570
|
+
) {
|
|
571
|
+
// Regular file installed by ForgeDock in copy mode (content matches the
|
|
572
|
+
// shipped command). Safe to remove. Content-differing files are left
|
|
573
|
+
// in place in case the user edited them.
|
|
574
|
+
await unlink(target);
|
|
575
|
+
removed++;
|
|
576
|
+
}
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (err.code !== "ENOENT") {
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
// Doesn't exist — nothing to do
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
step.progress(idx + 1, total);
|
|
585
|
+
}
|
|
699
586
|
|
|
700
|
-
|
|
701
|
-
|
|
587
|
+
step.note(`${red(String(removed))} removed`);
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
if (result.ok) {
|
|
593
|
+
process.stderr.write(
|
|
594
|
+
box(
|
|
595
|
+
[
|
|
596
|
+
`${green("✔")} ForgeDock commands removed`,
|
|
597
|
+
"",
|
|
598
|
+
`Re-install anytime: ${cyan("npx forgedock")}`,
|
|
599
|
+
],
|
|
600
|
+
{ title: "ForgeDock Uninstalled" },
|
|
601
|
+
) + "\n",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
702
605
|
|
|
606
|
+
async function update() {
|
|
607
|
+
// Check if installed via npm (no .git directory) or via git clone
|
|
608
|
+
const gitDir = join(FORGE_HOME, ".git");
|
|
609
|
+
if (!existsSync(gitDir)) {
|
|
610
|
+
await runSteps([
|
|
611
|
+
{
|
|
612
|
+
label: "Checking update path",
|
|
613
|
+
async run(step) {
|
|
614
|
+
step.skip(`installed via npm — run ${cyan("npm update -g forgedock")}`);
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
]);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const result = await runSteps([
|
|
622
|
+
{
|
|
623
|
+
label: "Checking current branch",
|
|
624
|
+
async run(step) {
|
|
625
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
626
|
+
cwd: FORGE_HOME,
|
|
627
|
+
encoding: "utf-8",
|
|
628
|
+
}).trim();
|
|
629
|
+
if (branch !== "main") {
|
|
630
|
+
step.skip(`not on main (${branch})`);
|
|
631
|
+
} else {
|
|
632
|
+
step.note("on main");
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
label: "Fetching latest",
|
|
638
|
+
async run(step) {
|
|
639
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
640
|
+
cwd: FORGE_HOME,
|
|
641
|
+
encoding: "utf-8",
|
|
642
|
+
}).trim();
|
|
643
|
+
if (branch !== "main") {
|
|
644
|
+
step.skip("skipped — not on main");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const before = execSync("git rev-parse HEAD", {
|
|
648
|
+
cwd: FORGE_HOME,
|
|
649
|
+
encoding: "utf-8",
|
|
650
|
+
}).trim();
|
|
651
|
+
try {
|
|
652
|
+
execSync("git fetch origin main --quiet", { cwd: FORGE_HOME });
|
|
653
|
+
execSync("git merge --ff-only origin/main --quiet", {
|
|
654
|
+
cwd: FORGE_HOME,
|
|
655
|
+
});
|
|
656
|
+
} catch {
|
|
657
|
+
step.skip("cannot fast-forward — local changes exist");
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const after = execSync("git rev-parse HEAD", {
|
|
661
|
+
cwd: FORGE_HOME,
|
|
662
|
+
encoding: "utf-8",
|
|
663
|
+
}).trim();
|
|
664
|
+
if (before === after) {
|
|
665
|
+
step.skip("already up to date");
|
|
666
|
+
} else {
|
|
667
|
+
step.note(`${before.slice(0, 7)} → ${after.slice(0, 7)}`);
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
label: "Reinstalling commands",
|
|
673
|
+
async run(step) {
|
|
674
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
675
|
+
cwd: FORGE_HOME,
|
|
676
|
+
encoding: "utf-8",
|
|
677
|
+
}).trim();
|
|
678
|
+
if (branch !== "main") {
|
|
679
|
+
step.skip("skipped — not on main");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const { installed, updated, skipped } = await linkCommands(step);
|
|
683
|
+
step.note(
|
|
684
|
+
`${green(String(installed))} installed, ${updated} updated, ${dim(String(skipped))} skipped`,
|
|
685
|
+
);
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
]);
|
|
689
|
+
|
|
690
|
+
if (result.ok) {
|
|
691
|
+
process.stderr.write(
|
|
692
|
+
box([`${green("✔")} ForgeDock is up to date`], {
|
|
693
|
+
title: "ForgeDock Updated",
|
|
694
|
+
}) + "\n",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function init(fromInstall = false) {
|
|
700
|
+
const cwd = process.cwd();
|
|
701
|
+
const outputPath = join(cwd, "forge.yaml");
|
|
702
|
+
|
|
703
|
+
// When called from install(), we already know forge.yaml doesn't exist
|
|
704
|
+
// and cwd is a git work tree — go straight to detection and write.
|
|
703
705
|
if (fromInstall) {
|
|
704
|
-
|
|
705
|
-
if (!remoteDetected) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
706
|
+
const detected = await detectAndBuildForgeYaml(cwd, { onNote: () => {} });
|
|
707
|
+
if (!detected.remoteDetected) {
|
|
708
|
+
process.stderr.write(
|
|
709
|
+
box(
|
|
710
|
+
[
|
|
711
|
+
`${yellow("Action required:")}`,
|
|
712
|
+
`Edit ${cyan("forge.yaml")} — fill in ${cyan("project.owner")} and ${cyan("project.repo")}`,
|
|
713
|
+
"(git remote not detected)",
|
|
714
|
+
],
|
|
715
|
+
{ title: "forge.yaml Generated" },
|
|
716
|
+
) + "\n",
|
|
709
717
|
);
|
|
710
|
-
console.log("");
|
|
711
718
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Interactive path — run as a step-checklist with a full summary box.
|
|
723
|
+
let detected = null;
|
|
724
|
+
let aborted = false;
|
|
725
|
+
|
|
726
|
+
// Handle existing forge.yaml before entering runSteps (needs interactive prompt)
|
|
727
|
+
if (existsSync(outputPath)) {
|
|
728
|
+
process.stderr.write(
|
|
729
|
+
`\n ${YELLOW}Warning:${RESET} forge.yaml already exists at ${outputPath}\n`,
|
|
730
|
+
);
|
|
731
|
+
process.stderr.write(
|
|
732
|
+
` Continuing will back up your existing config and replace it.\n\n`,
|
|
733
|
+
);
|
|
734
|
+
const confirmed = await confirmOverwrite(` Overwrite? [y/N] `);
|
|
735
|
+
if (!confirmed) {
|
|
736
|
+
process.stderr.write(
|
|
737
|
+
`\n ${YELLOW}Aborted.${RESET} Your forge.yaml was not modified.\n\n`,
|
|
722
738
|
);
|
|
739
|
+
return;
|
|
723
740
|
}
|
|
724
|
-
|
|
725
|
-
|
|
741
|
+
process.stderr.write("\n");
|
|
742
|
+
|
|
743
|
+
// Back up existing file before proceeding
|
|
744
|
+
const baseBak = join(cwd, "forge.yaml.bak");
|
|
745
|
+
const backupPath = existsSync(baseBak)
|
|
746
|
+
? join(
|
|
747
|
+
cwd,
|
|
748
|
+
`forge.yaml.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
749
|
+
)
|
|
750
|
+
: baseBak;
|
|
751
|
+
const backupName = backupPath.split("/").pop();
|
|
752
|
+
renameSync(outputPath, backupPath);
|
|
753
|
+
process.stderr.write(` ${YELLOW}Backed up${RESET}: forge.yaml → ${backupName}\n\n`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const result = await runSteps([
|
|
757
|
+
{
|
|
758
|
+
label: "Detecting project",
|
|
759
|
+
async run(step) {
|
|
760
|
+
detected = await detectAndBuildForgeYaml(cwd, {
|
|
761
|
+
onNote: (msg) => step.note(msg),
|
|
762
|
+
});
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
]);
|
|
766
|
+
|
|
767
|
+
if (!result.ok || !detected) return;
|
|
768
|
+
|
|
769
|
+
const {
|
|
770
|
+
owner,
|
|
771
|
+
repo,
|
|
772
|
+
defaultBranch,
|
|
773
|
+
stagingBranch,
|
|
774
|
+
remoteDetected,
|
|
775
|
+
description,
|
|
776
|
+
} = detected;
|
|
777
|
+
|
|
778
|
+
const summaryLines = [
|
|
779
|
+
`${green("✔")} forge.yaml generated`,
|
|
780
|
+
"",
|
|
781
|
+
` repo: ${cyan(owner + "/" + repo)}`,
|
|
782
|
+
` default: ${cyan(defaultBranch)}`,
|
|
783
|
+
` staging: ${cyan(stagingBranch)}`,
|
|
784
|
+
];
|
|
785
|
+
if (description) {
|
|
786
|
+
summaryLines.push(
|
|
787
|
+
` desc: ${dim(
|
|
788
|
+
description.slice(0, 60) + (description.length > 60 ? "…" : ""),
|
|
789
|
+
)}`,
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
summaryLines.push("");
|
|
793
|
+
if (!remoteDetected) {
|
|
794
|
+
summaryLines.push(
|
|
795
|
+
`${yellow("→")} Edit ${cyan("forge.yaml")} — fill in ${cyan("project.owner")} and ${cyan("project.repo")}`,
|
|
726
796
|
);
|
|
727
|
-
|
|
728
|
-
|
|
797
|
+
} else {
|
|
798
|
+
summaryLines.push(
|
|
799
|
+
`${green("→")} Review ${cyan("forge.yaml")} — all required fields were auto-detected`,
|
|
729
800
|
);
|
|
730
|
-
console.log("");
|
|
731
801
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
function help() {
|
|
735
|
-
console.log("");
|
|
736
|
-
console.log(
|
|
737
|
-
`${BOLD}ForgeDock${RESET} — GitHub as a knowledge graph for AI agents`,
|
|
802
|
+
summaryLines.push(
|
|
803
|
+
`${green("→")} Add ${cyan("forge.yaml")} to ${cyan(".gitignore")} if it contains sensitive paths`,
|
|
738
804
|
);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
console.log(
|
|
742
|
-
` ${CYAN}npx forgedock${RESET} Install commands (default)`,
|
|
805
|
+
summaryLines.push(
|
|
806
|
+
`${green("→")} Run ${cyan("/forgedock-init")} inside Claude Code for guided AI-powered setup`,
|
|
743
807
|
);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
808
|
+
|
|
809
|
+
process.stderr.write(
|
|
810
|
+
box(summaryLines, { title: "forge.yaml Generated" }) + "\n",
|
|
747
811
|
);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function help() {
|
|
815
|
+
// renderLogo already shown by splash() above — show the command table
|
|
816
|
+
const commands = [
|
|
817
|
+
["Command", "Description"],
|
|
818
|
+
["npx forgedock", "Install commands (default)"],
|
|
819
|
+
["npx forgedock install", "Install commands"],
|
|
820
|
+
["npx forgedock init", "Generate forge.yaml config for your project"],
|
|
821
|
+
["npx forgedock uninstall", "Remove commands"],
|
|
822
|
+
["npx forgedock update", "Pull latest & reinstall"],
|
|
823
|
+
["npx forgedock help", "Show this help"],
|
|
824
|
+
];
|
|
825
|
+
|
|
826
|
+
process.stderr.write(
|
|
827
|
+
box(table(commands, { header: true }), { title: "Usage" }) + "\n",
|
|
751
828
|
);
|
|
752
|
-
console.log(` ${CYAN}npx forgedock help${RESET} Show this help`);
|
|
753
|
-
console.log("");
|
|
754
829
|
}
|
|
755
830
|
|
|
756
831
|
splash();
|
|
@@ -774,7 +849,7 @@ switch (command) {
|
|
|
774
849
|
help();
|
|
775
850
|
break;
|
|
776
851
|
default:
|
|
777
|
-
|
|
852
|
+
process.stderr.write(`${RED}Unknown command: ${command}${RESET}\n`);
|
|
778
853
|
help();
|
|
779
854
|
process.exit(1);
|
|
780
855
|
}
|