forgedock 1.0.5 → 1.0.6
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/README.md +7 -1
- package/bin/forgedock.mjs +429 -363
- package/bin/labels.json +152 -0
- package/commands/audit-agents.md +4 -2
- package/commands/autopilot.md +5 -5
- package/commands/incident-response.md +1 -1
- package/commands/orchestrate.md +1 -1
- package/commands/review-pr-staging.md +49 -3
- package/commands/review-pr.md +9 -7
- package/commands/rollback.md +1 -1
- package/commands/security-audit.md +27 -25
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +154 -2
- package/commands/work-on/build/context.md +131 -3
- package/commands/work-on.md +1 -1
- package/docs/CONFIG.md +117 -0
- package/package.json +2 -1
- package/templates/devdocs/README.md +96 -0
- package/templates/devdocs/agent/using-forgedock.md +245 -0
- package/templates/devdocs/agent/using-github.md +325 -0
- package/templates/devdocs/project/architecture.md +122 -0
- package/templates/devdocs/project/conventions.md +158 -0
- package/templates/devdocs/project/custom-instructions.md +73 -0
- package/templates/devdocs/project/glossary.md +84 -0
- package/templates/devdocs/project/stack.md +80 -0
- package/templates/devdocs/templates/commit-convention.md +147 -0
- package/templates/devdocs/templates/issue.md +141 -0
- package/templates/devdocs/templates/pull-request.md +115 -0
- package/templates/devdocs/templates/review-finding.md +147 -0
- package/bin/github-app-manifest.json +0 -24
package/bin/forgedock.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os from "os";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
6
6
|
import {
|
|
7
7
|
mkdir,
|
|
8
8
|
symlink,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readlink,
|
|
11
11
|
lstat,
|
|
12
12
|
readdir,
|
|
13
|
+
realpath,
|
|
13
14
|
stat,
|
|
14
15
|
writeFile,
|
|
15
16
|
unlink as fsUnlink,
|
|
@@ -17,14 +18,12 @@ import {
|
|
|
17
18
|
import {
|
|
18
19
|
existsSync,
|
|
19
20
|
appendFileSync,
|
|
20
|
-
chmodSync,
|
|
21
21
|
readFileSync,
|
|
22
22
|
writeFileSync,
|
|
23
23
|
renameSync,
|
|
24
24
|
unlinkSync,
|
|
25
25
|
} from "fs";
|
|
26
26
|
import { execSync, execFileSync } from "child_process";
|
|
27
|
-
import { createSign } from "crypto";
|
|
28
27
|
import {
|
|
29
28
|
BOLD,
|
|
30
29
|
GREEN,
|
|
@@ -1221,15 +1220,8 @@ function queryNpmRegistry(pkg) {
|
|
|
1221
1220
|
|
|
1222
1221
|
/**
|
|
1223
1222
|
* Path to the update-check cache file.
|
|
1224
|
-
* Stored in ~/.forgedock/
|
|
1225
|
-
* Populated lazily — FORGEDOCK_HOME is defined later in the file, but this
|
|
1226
|
-
* constant is only resolved at module evaluation time so the ordering is fine.
|
|
1223
|
+
* Stored in ~/.forgedock/update-check.json.
|
|
1227
1224
|
*/
|
|
1228
|
-
// NOTE: FORGEDOCK_HOME is declared later in the file (line ~2974). This const
|
|
1229
|
-
// is a forward reference that is only evaluated when the module finishes loading,
|
|
1230
|
-
// which is after FORGEDOCK_HOME is defined. Node ESM top-level await ensures
|
|
1231
|
-
// the module is fully evaluated before any exported binding is read.
|
|
1232
|
-
// We defer the join() call to avoid the forward-reference issue:
|
|
1233
1225
|
function _getUpdateCheckCachePath() {
|
|
1234
1226
|
return join(HOME, ".forgedock", "update-check.json");
|
|
1235
1227
|
}
|
|
@@ -1862,6 +1854,8 @@ async function init() {
|
|
|
1862
1854
|
});
|
|
1863
1855
|
console.log(` ${GREEN}Created${RESET}: forge.yaml`);
|
|
1864
1856
|
console.log("");
|
|
1857
|
+
await injectClaudeMd(cwd);
|
|
1858
|
+
console.log("");
|
|
1865
1859
|
_printNextSteps({ remoteDetected });
|
|
1866
1860
|
await validate(outputPath);
|
|
1867
1861
|
return;
|
|
@@ -2160,12 +2154,187 @@ async function init() {
|
|
|
2160
2154
|
);
|
|
2161
2155
|
}
|
|
2162
2156
|
console.log("");
|
|
2157
|
+
await injectClaudeMd(cwd);
|
|
2158
|
+
console.log("");
|
|
2163
2159
|
_printNextSteps({ remoteDetected: ownerInput !== "your-github-org" });
|
|
2164
2160
|
await validate(outputPath);
|
|
2165
2161
|
}
|
|
2166
2162
|
}
|
|
2167
2163
|
}
|
|
2168
2164
|
|
|
2165
|
+
// ---------------------------------------------------------------------------
|
|
2166
|
+
// CLAUDE.md Integration — injectClaudeMd()
|
|
2167
|
+
// ---------------------------------------------------------------------------
|
|
2168
|
+
|
|
2169
|
+
const CLAUDE_BLOCK_BEGIN = "<!-- BEGIN FORGEDOCK -->";
|
|
2170
|
+
const CLAUDE_BLOCK_END = "<!-- END FORGEDOCK -->";
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Extract the `description:` value from a command file's YAML frontmatter.
|
|
2174
|
+
* Returns null if no frontmatter or no description is found.
|
|
2175
|
+
*
|
|
2176
|
+
* @param {string} content - Full file content
|
|
2177
|
+
* @returns {string|null}
|
|
2178
|
+
*/
|
|
2179
|
+
function _extractFrontmatterDescription(content) {
|
|
2180
|
+
// Match YAML frontmatter block: starts with ---, ends with ---
|
|
2181
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2182
|
+
if (!fmMatch) return null;
|
|
2183
|
+
const fm = fmMatch[1];
|
|
2184
|
+
// Extract description: value (may span the rest of the line)
|
|
2185
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
2186
|
+
if (!descMatch) return null;
|
|
2187
|
+
return descMatch[1].trim();
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* Build the managed ForgeDock block content (between markers, not including them).
|
|
2192
|
+
* Reads all commands/*.md files, extracts frontmatter descriptions, and generates
|
|
2193
|
+
* a concise command index.
|
|
2194
|
+
*
|
|
2195
|
+
* @returns {Promise<string>} Block content
|
|
2196
|
+
*/
|
|
2197
|
+
async function _buildForgeDockBlock() {
|
|
2198
|
+
const files = await findMarkdownFiles(COMMANDS_DIR);
|
|
2199
|
+
|
|
2200
|
+
/** @type {Array<{name: string, description: string}>} */
|
|
2201
|
+
const commands = [];
|
|
2202
|
+
|
|
2203
|
+
for (const file of files) {
|
|
2204
|
+
let content = "";
|
|
2205
|
+
try {
|
|
2206
|
+
content = readFileSync(file, "utf-8");
|
|
2207
|
+
} catch {
|
|
2208
|
+
continue; // skip unreadable files
|
|
2209
|
+
}
|
|
2210
|
+
const description = _extractFrontmatterDescription(content);
|
|
2211
|
+
if (!description) continue;
|
|
2212
|
+
|
|
2213
|
+
// Derive display name: relative path from COMMANDS_DIR, remove .md extension
|
|
2214
|
+
const rel = relative(COMMANDS_DIR, file).replace(/\\/g, "/").replace(/\.md$/, "");
|
|
2215
|
+
commands.push({ name: rel, description });
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// Sort: top-level commands first, then sub-commands alphabetically
|
|
2219
|
+
commands.sort((a, b) => {
|
|
2220
|
+
const aDepth = a.name.split("/").length;
|
|
2221
|
+
const bDepth = b.name.split("/").length;
|
|
2222
|
+
if (aDepth !== bDepth) return aDepth - bDepth;
|
|
2223
|
+
return a.name.localeCompare(b.name);
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
const indexLines = commands
|
|
2227
|
+
.map(({ name, description }) => `- \`/${name}\` — ${description}`)
|
|
2228
|
+
.join("\n");
|
|
2229
|
+
|
|
2230
|
+
return `## ForgeDock — Autonomous Development Pipeline
|
|
2231
|
+
|
|
2232
|
+
This project is driven by **ForgeDock**. GitHub issues are the knowledge graph.
|
|
2233
|
+
Core loop: **issue in → PR out** (PRs target \`staging\` fast-lane or \`milestone/{slug}\`).
|
|
2234
|
+
|
|
2235
|
+
### When to Use What
|
|
2236
|
+
|
|
2237
|
+
- \`/issue\` — file a pipeline-ready issue
|
|
2238
|
+
- \`/work-on N\` — full investigate→build→review→merge for issue N
|
|
2239
|
+
- \`/milestone\` + \`/orchestrate\` — plan and parallelize milestone work
|
|
2240
|
+
- \`/review-pr\` — review a PR with domain agents
|
|
2241
|
+
- \`/quality-gate\` — pre-commit quality check
|
|
2242
|
+
- \`/validate\` — validate forge.yaml configuration
|
|
2243
|
+
|
|
2244
|
+
### Command Index
|
|
2245
|
+
|
|
2246
|
+
${indexLines}
|
|
2247
|
+
|
|
2248
|
+
### Conventions
|
|
2249
|
+
|
|
2250
|
+
- Fast-lane PRs target \`staging\`; milestone PRs target \`milestone/{slug}\`
|
|
2251
|
+
- Review findings become separate issues (not merge blockers)
|
|
2252
|
+
- \`forge.yaml\` is the project config — see \`docs/CONFIG.md\`
|
|
2253
|
+
- Re-generate this block: \`npx forgedock integrate\``;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Idempotently inject (or update) a managed ForgeDock usage block into a file.
|
|
2258
|
+
* The block is bounded by BEGIN FORGEDOCK / END FORGEDOCK HTML comment markers.
|
|
2259
|
+
* Content outside the markers is never modified.
|
|
2260
|
+
*
|
|
2261
|
+
* @param {string} filePath - Absolute path to the target file (CLAUDE.md or AGENTS.md)
|
|
2262
|
+
* @param {string} blockContent - Content to place between the markers
|
|
2263
|
+
* @param {boolean} createIfMissing - Create the file if it does not exist
|
|
2264
|
+
* @returns {'created'|'updated'|'skipped'} Result status
|
|
2265
|
+
*/
|
|
2266
|
+
function _injectManagedBlock(filePath, blockContent, createIfMissing) {
|
|
2267
|
+
const managed = `${CLAUDE_BLOCK_BEGIN}\n${blockContent}\n${CLAUDE_BLOCK_END}`;
|
|
2268
|
+
|
|
2269
|
+
if (!existsSync(filePath)) {
|
|
2270
|
+
if (!createIfMissing) return "skipped";
|
|
2271
|
+
writeFileSync(filePath, `${managed}\n`, "utf-8");
|
|
2272
|
+
return "created";
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
let existing = readFileSync(filePath, "utf-8");
|
|
2276
|
+
|
|
2277
|
+
if (existing.includes(CLAUDE_BLOCK_BEGIN) && existing.includes(CLAUDE_BLOCK_END)) {
|
|
2278
|
+
// Both markers present — replace existing block in place
|
|
2279
|
+
const escaped_begin = CLAUDE_BLOCK_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2280
|
+
const escaped_end = CLAUDE_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2281
|
+
const blockRegex = new RegExp(
|
|
2282
|
+
`${escaped_begin}[\\s\\S]*?${escaped_end}`,
|
|
2283
|
+
"g",
|
|
2284
|
+
);
|
|
2285
|
+
existing = existing.replace(blockRegex, managed);
|
|
2286
|
+
writeFileSync(filePath, existing, "utf-8");
|
|
2287
|
+
return "updated";
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (existing.includes(CLAUDE_BLOCK_BEGIN)) {
|
|
2291
|
+
// BEGIN present but END missing — file is truncated or malformed.
|
|
2292
|
+
// Strip the orphaned marker and everything after it, then fall through
|
|
2293
|
+
// to the append path below so the complete block is written correctly.
|
|
2294
|
+
existing = existing.slice(0, existing.indexOf(CLAUDE_BLOCK_BEGIN)).trimEnd();
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// Append block to existing content, separated by a blank line
|
|
2298
|
+
const separator = existing.length === 0 || existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2299
|
+
writeFileSync(filePath, `${existing}${separator}${managed}\n`, "utf-8");
|
|
2300
|
+
return "updated";
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* Inject or update the managed ForgeDock block into the project CLAUDE.md.
|
|
2305
|
+
* Also mirrors the block into AGENTS.md if that file already exists.
|
|
2306
|
+
* Creates CLAUDE.md if absent; never creates AGENTS.md.
|
|
2307
|
+
*
|
|
2308
|
+
* @param {string} cwd - Project root directory
|
|
2309
|
+
* @returns {Promise<void>}
|
|
2310
|
+
*/
|
|
2311
|
+
async function injectClaudeMd(cwd) {
|
|
2312
|
+
const claudePath = join(cwd, "CLAUDE.md");
|
|
2313
|
+
const agentsPath = join(cwd, "AGENTS.md");
|
|
2314
|
+
|
|
2315
|
+
let blockContent;
|
|
2316
|
+
try {
|
|
2317
|
+
blockContent = await _buildForgeDockBlock();
|
|
2318
|
+
} catch (err) {
|
|
2319
|
+
console.log(
|
|
2320
|
+
` ${YELLOW}!${RESET} Could not generate command index — skipping CLAUDE.md update (${err.message})`,
|
|
2321
|
+
);
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
const claudeResult = _injectManagedBlock(claudePath, blockContent, true);
|
|
2326
|
+
if (claudeResult === "created") {
|
|
2327
|
+
console.log(` ${GREEN}✔${RESET} CLAUDE.md created with ForgeDock integration block`);
|
|
2328
|
+
} else {
|
|
2329
|
+
console.log(` ${GREEN}✔${RESET} CLAUDE.md updated — ForgeDock block refreshed`);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
if (existsSync(agentsPath)) {
|
|
2333
|
+
_injectManagedBlock(agentsPath, blockContent, false);
|
|
2334
|
+
console.log(` ${GREEN}✔${RESET} AGENTS.md mirrored`);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2169
2338
|
// ---------------------------------------------------------------------------
|
|
2170
2339
|
// Helpers for init()
|
|
2171
2340
|
// ---------------------------------------------------------------------------
|
|
@@ -2731,21 +2900,6 @@ ${reviewSection}
|
|
|
2731
2900
|
# =============================================================================
|
|
2732
2901
|
|
|
2733
2902
|
${verificationSection}
|
|
2734
|
-
|
|
2735
|
-
# =============================================================================
|
|
2736
|
-
# BOT (OPTIONAL)
|
|
2737
|
-
# GitHub App bot identity for pipeline operations.
|
|
2738
|
-
# Credentials are stored separately in ~/.forgedock/credentials.json — not here.
|
|
2739
|
-
# Run: npx forgedock bot setup to configure interactively.
|
|
2740
|
-
# Run: npx forgedock bot status to verify.
|
|
2741
|
-
# =============================================================================
|
|
2742
|
-
|
|
2743
|
-
# bot:
|
|
2744
|
-
# app_id: 12345
|
|
2745
|
-
# installation_id: 67890
|
|
2746
|
-
# private_key_path: "~/.forgedock/forgedock-bot.private-key.pem"
|
|
2747
|
-
# # When configured, pipeline commands use the app token instead of personal token.
|
|
2748
|
-
# # Bot-authored comments include <!-- forgedock-bot --> for audit filtering.
|
|
2749
2903
|
`;
|
|
2750
2904
|
}
|
|
2751
2905
|
|
|
@@ -3210,388 +3364,276 @@ function _renderValidationSummary(checks) {
|
|
|
3210
3364
|
}
|
|
3211
3365
|
|
|
3212
3366
|
// ---------------------------------------------------------------------------
|
|
3213
|
-
//
|
|
3367
|
+
// Labels Bootstrap — idempotently create/update all ForgeDock-managed labels
|
|
3214
3368
|
// ---------------------------------------------------------------------------
|
|
3215
3369
|
|
|
3216
|
-
const FORGEDOCK_HOME = join(HOME, ".forgedock");
|
|
3217
|
-
const CREDENTIALS_FILE = join(FORGEDOCK_HOME, "credentials.json");
|
|
3218
|
-
|
|
3219
3370
|
/**
|
|
3220
|
-
*
|
|
3221
|
-
* The private key itself is NOT stored — only the path.
|
|
3371
|
+
* Resolve the target repo for the labels command.
|
|
3222
3372
|
*
|
|
3223
|
-
*
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
// Tighten permissions on pre-existing installs: fs.mkdir({ recursive: true }) ignores
|
|
3228
|
-
// mode when the directory already exists (Linux kernel behaviour). chmodSync enforces
|
|
3229
|
-
// 0o700 unconditionally, matching the intent of the mode option above.
|
|
3230
|
-
chmodSync(FORGEDOCK_HOME, 0o700);
|
|
3231
|
-
const existing = loadBotCredentials() ?? {};
|
|
3232
|
-
const updated = { ...existing, bot: creds };
|
|
3233
|
-
writeFileSync(CREDENTIALS_FILE, JSON.stringify(updated, null, 2) + "\n", {
|
|
3234
|
-
encoding: "utf-8",
|
|
3235
|
-
mode: 0o600,
|
|
3236
|
-
});
|
|
3237
|
-
// Tighten permissions on files created before this fix (mode is only applied at creation time)
|
|
3238
|
-
chmodSync(CREDENTIALS_FILE, 0o600);
|
|
3239
|
-
}
|
|
3240
|
-
|
|
3241
|
-
/**
|
|
3242
|
-
* Load bot credentials from ~/.forgedock/credentials.json.
|
|
3243
|
-
* Returns null if the file is absent or malformed.
|
|
3373
|
+
* Priority order:
|
|
3374
|
+
* 1. --repo <owner/repo> passed on the CLI
|
|
3375
|
+
* 2. forge.yaml → project.owner + project.repo in the current working directory
|
|
3376
|
+
* 3. Returns null if neither is available (caller prints an error)
|
|
3244
3377
|
*
|
|
3245
|
-
* @
|
|
3378
|
+
* @param {string[]} subArgs - CLI args after "labels [setup]"
|
|
3379
|
+
* @returns {string|null} "owner/repo" string or null
|
|
3246
3380
|
*/
|
|
3247
|
-
function
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
return
|
|
3252
|
-
} catch {
|
|
3253
|
-
return null;
|
|
3381
|
+
function resolveLabelsRepo(subArgs) {
|
|
3382
|
+
// 1. Explicit --repo flag
|
|
3383
|
+
const repoFlagIdx = subArgs.indexOf("--repo");
|
|
3384
|
+
if (repoFlagIdx !== -1 && subArgs[repoFlagIdx + 1]) {
|
|
3385
|
+
return subArgs[repoFlagIdx + 1];
|
|
3254
3386
|
}
|
|
3255
|
-
}
|
|
3256
3387
|
|
|
3257
|
-
//
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
*
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
iat: now - 60, // issued 60s ago to account for clock skew
|
|
3272
|
-
exp: now + 600, // expires in 10 minutes (GitHub max)
|
|
3273
|
-
iss: appId,
|
|
3274
|
-
};
|
|
3275
|
-
|
|
3276
|
-
const header = Buffer.from(
|
|
3277
|
-
JSON.stringify({ alg: "RS256", typ: "JWT" }),
|
|
3278
|
-
).toString("base64url");
|
|
3279
|
-
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
3280
|
-
const unsigned = `${header}.${body}`;
|
|
3281
|
-
|
|
3282
|
-
const sign = createSign("RSA-SHA256");
|
|
3283
|
-
sign.update(unsigned);
|
|
3284
|
-
const signature = sign.sign(privateKey, "base64url");
|
|
3388
|
+
// 2. forge.yaml in cwd
|
|
3389
|
+
const forgeYamlPath = join(process.cwd(), "forge.yaml");
|
|
3390
|
+
if (existsSync(forgeYamlPath)) {
|
|
3391
|
+
try {
|
|
3392
|
+
const raw = readFileSync(forgeYamlPath, "utf-8");
|
|
3393
|
+
const ownerMatch = raw.match(/^\s*owner:\s*["']?([^\s"'#]+)["']?/m);
|
|
3394
|
+
const repoMatch = raw.match(/^\s*repo:\s*["']?([^\s"'#]+)["']?/m);
|
|
3395
|
+
if (ownerMatch && repoMatch) {
|
|
3396
|
+
return `${ownerMatch[1]}/${repoMatch[1]}`;
|
|
3397
|
+
}
|
|
3398
|
+
} catch {
|
|
3399
|
+
// fall through
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3285
3402
|
|
|
3286
|
-
return
|
|
3403
|
+
return null;
|
|
3287
3404
|
}
|
|
3288
3405
|
|
|
3289
|
-
// ---------------------------------------------------------------------------
|
|
3290
|
-
// Bot — connect existing GitHub App
|
|
3291
|
-
// ---------------------------------------------------------------------------
|
|
3292
|
-
|
|
3293
3406
|
/**
|
|
3294
|
-
*
|
|
3295
|
-
*
|
|
3407
|
+
* Idempotently create or update all ForgeDock-managed labels on a GitHub repo.
|
|
3408
|
+
*
|
|
3409
|
+
* Reads the canonical manifest from bin/labels.json (co-located with this
|
|
3410
|
+
* script). For each label entry, calls:
|
|
3411
|
+
*
|
|
3412
|
+
* gh label create <name> --color <hex> --description <desc> --force --repo <repo>
|
|
3413
|
+
*
|
|
3414
|
+
* --force makes the call idempotent: it creates the label if absent, or updates
|
|
3415
|
+
* its color and description if it already exists. Safe to re-run at any time.
|
|
3416
|
+
*
|
|
3417
|
+
* @param {string} repo - "owner/repo" string
|
|
3418
|
+
* @returns {{ created: number, failed: string[] }}
|
|
3296
3419
|
*/
|
|
3297
|
-
async function
|
|
3298
|
-
|
|
3299
|
-
console.log(
|
|
3300
|
-
box(
|
|
3301
|
-
[
|
|
3302
|
-
`${bold("Connect an existing GitHub App")}`,
|
|
3303
|
-
"",
|
|
3304
|
-
"You'll need:",
|
|
3305
|
-
` • App ID (GitHub → Settings → Developer settings → GitHub Apps → your app)`,
|
|
3306
|
-
` • Private key file (.pem downloaded from app settings)`,
|
|
3307
|
-
` • Installation ID (GitHub → your org → Settings → Installed Apps → your app → configure → URL)`,
|
|
3308
|
-
].join("\n"),
|
|
3309
|
-
{ title: "Connect GitHub App" },
|
|
3310
|
-
),
|
|
3311
|
-
);
|
|
3312
|
-
console.log("");
|
|
3420
|
+
async function labelsSetup(repo) {
|
|
3421
|
+
const manifestPath = join(__dirname, "labels.json");
|
|
3313
3422
|
|
|
3314
|
-
|
|
3315
|
-
if (!appId || !/^\d+$/.test(appId.trim())) {
|
|
3316
|
-
console.log(` ${RED}Invalid App ID — must be a numeric string.${RESET}`);
|
|
3317
|
-
return false;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
const privateKeyPath = await input("Private key path (.pem):", "");
|
|
3321
|
-
const trimmedKeyPath = privateKeyPath.trim();
|
|
3322
|
-
const home = os.homedir();
|
|
3323
|
-
const expanded = trimmedKeyPath.startsWith("~")
|
|
3324
|
-
? join(home, trimmedKeyPath.slice(1))
|
|
3325
|
-
: trimmedKeyPath;
|
|
3326
|
-
const resolvedKeyPath = resolve(expanded);
|
|
3327
|
-
if (trimmedKeyPath.startsWith("~")) {
|
|
3328
|
-
const rel = relative(home, resolvedKeyPath);
|
|
3329
|
-
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
3330
|
-
console.log(
|
|
3331
|
-
` ${RED}Invalid path: cannot traverse outside home directory.${RESET}`,
|
|
3332
|
-
);
|
|
3333
|
-
return false;
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
if (!existsSync(resolvedKeyPath)) {
|
|
3337
|
-
console.log(` ${RED}File not found: ${resolvedKeyPath}${RESET}`);
|
|
3338
|
-
return false;
|
|
3339
|
-
}
|
|
3340
|
-
|
|
3341
|
-
const installationId = await input("Installation ID (numeric):", "");
|
|
3342
|
-
if (!installationId || !/^\d+$/.test(installationId.trim())) {
|
|
3423
|
+
if (!existsSync(manifestPath)) {
|
|
3343
3424
|
console.log(
|
|
3344
|
-
|
|
3425
|
+
`${RED}Label manifest not found: ${manifestPath}${RESET}`,
|
|
3345
3426
|
);
|
|
3346
|
-
|
|
3427
|
+
process.exit(1);
|
|
3347
3428
|
}
|
|
3348
3429
|
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
console.log(` Validating credentials…`);
|
|
3352
|
-
let privateKey;
|
|
3430
|
+
/** @type {Array<{name: string, color: string, description: string}>} */
|
|
3431
|
+
let labels;
|
|
3353
3432
|
try {
|
|
3354
|
-
|
|
3433
|
+
labels = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
3355
3434
|
} catch (err) {
|
|
3356
|
-
console.log(
|
|
3357
|
-
|
|
3435
|
+
console.log(`${RED}Failed to parse labels.json: ${err.message}${RESET}`);
|
|
3436
|
+
process.exit(1);
|
|
3358
3437
|
}
|
|
3359
3438
|
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3439
|
+
console.log("");
|
|
3440
|
+
console.log(`${BOLD}ForgeDock Label Bootstrap${RESET}`);
|
|
3441
|
+
console.log(` Repository: ${cyan(repo)}`);
|
|
3442
|
+
console.log(` Labels: ${labels.length} managed labels`);
|
|
3443
|
+
console.log("");
|
|
3444
|
+
|
|
3445
|
+
const bar = createProgressBar(labels.length, {
|
|
3446
|
+
label: " Bootstrapping labels...",
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
let created = 0;
|
|
3450
|
+
const failed = [];
|
|
3451
|
+
|
|
3452
|
+
for (const { name, color, description } of labels) {
|
|
3453
|
+
bar.tick(1, dim(name));
|
|
3454
|
+
try {
|
|
3455
|
+
execFileSync(
|
|
3456
|
+
"gh",
|
|
3457
|
+
[
|
|
3458
|
+
"label",
|
|
3459
|
+
"create",
|
|
3460
|
+
name,
|
|
3461
|
+
"--color",
|
|
3462
|
+
color,
|
|
3463
|
+
"--description",
|
|
3464
|
+
description,
|
|
3465
|
+
"--force",
|
|
3466
|
+
"--repo",
|
|
3467
|
+
repo,
|
|
3468
|
+
],
|
|
3469
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
3373
3470
|
);
|
|
3374
|
-
|
|
3471
|
+
created++;
|
|
3472
|
+
} catch (err) {
|
|
3473
|
+
failed.push(name);
|
|
3474
|
+
// Continue — don't abort the whole bootstrap for one failure
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
bar.done(
|
|
3479
|
+
failed.length === 0
|
|
3480
|
+
? `${green("✔")} Bootstrapped ${created}/${labels.length} labels on ${cyan(repo)}`
|
|
3481
|
+
: `${yellow("⚠")} Bootstrapped ${created}/${labels.length} labels — ${failed.length} failed`,
|
|
3482
|
+
);
|
|
3483
|
+
|
|
3484
|
+
if (failed.length > 0) {
|
|
3485
|
+
console.log("");
|
|
3486
|
+
console.log(` ${RED}Failed labels:${RESET}`);
|
|
3487
|
+
for (const name of failed) {
|
|
3488
|
+
console.log(` ${dim("•")} ${name}`);
|
|
3375
3489
|
}
|
|
3376
|
-
const appData = await resp.json();
|
|
3377
3490
|
console.log(
|
|
3378
|
-
|
|
3491
|
+
`\n Run ${cyan("gh auth status")} to verify GitHub authentication.`,
|
|
3379
3492
|
);
|
|
3380
|
-
} catch (err) {
|
|
3381
|
-
console.log(` ${RED}Validation failed: ${err.message}${RESET}`);
|
|
3382
|
-
return false;
|
|
3383
3493
|
}
|
|
3384
3494
|
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
installationId: installationId.trim(),
|
|
3388
|
-
privateKeyPath: resolvedKeyPath,
|
|
3389
|
-
});
|
|
3390
|
-
|
|
3391
|
-
console.log(` Credentials saved to ${cyan(CREDENTIALS_FILE)}`);
|
|
3392
|
-
return true;
|
|
3495
|
+
console.log("");
|
|
3496
|
+
return { created, failed };
|
|
3393
3497
|
}
|
|
3394
3498
|
|
|
3395
3499
|
// ---------------------------------------------------------------------------
|
|
3396
|
-
//
|
|
3500
|
+
// docs init — scaffold devdocs knowledge tree into a project
|
|
3397
3501
|
// ---------------------------------------------------------------------------
|
|
3398
3502
|
|
|
3399
3503
|
/**
|
|
3400
|
-
*
|
|
3401
|
-
*
|
|
3504
|
+
* Walk a directory tree and return all file paths (any extension).
|
|
3505
|
+
* @param {string} dir
|
|
3506
|
+
* @returns {Promise<string[]>}
|
|
3402
3507
|
*/
|
|
3403
|
-
async function
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3508
|
+
async function _findAllFiles(dir) {
|
|
3509
|
+
const results = [];
|
|
3510
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
3511
|
+
for (const entry of entries) {
|
|
3512
|
+
const full = join(dir, entry.name);
|
|
3513
|
+
if (entry.isDirectory()) {
|
|
3514
|
+
results.push(...(await _findAllFiles(full)));
|
|
3515
|
+
} else {
|
|
3516
|
+
results.push(full);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
return results.sort();
|
|
3520
|
+
}
|
|
3407
3521
|
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3522
|
+
async function docsInit() {
|
|
3523
|
+
const cwd = process.cwd();
|
|
3524
|
+
|
|
3525
|
+
// Resolve target devdocs path: forge.yaml → devdocs.path, default "devdocs/"
|
|
3526
|
+
let devdocsRelPath = "devdocs";
|
|
3527
|
+
const forgeYamlPath = join(cwd, "forge.yaml");
|
|
3528
|
+
if (existsSync(forgeYamlPath)) {
|
|
3529
|
+
try {
|
|
3530
|
+
const raw = readFileSync(forgeYamlPath, "utf-8");
|
|
3531
|
+
const parsed = _parseYamlKey(raw, "devdocs.path");
|
|
3532
|
+
if (parsed && parsed.trim()) {
|
|
3533
|
+
devdocsRelPath = _sanitizePathValue(parsed.trim().replace(/\/$/, ""));
|
|
3534
|
+
}
|
|
3535
|
+
} catch {
|
|
3536
|
+
// forge.yaml unreadable — use default
|
|
3537
|
+
}
|
|
3417
3538
|
}
|
|
3418
3539
|
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
console.log("");
|
|
3540
|
+
const targetDir = isAbsolute(devdocsRelPath)
|
|
3541
|
+
? devdocsRelPath
|
|
3542
|
+
: join(cwd, devdocsRelPath);
|
|
3423
3543
|
|
|
3424
|
-
//
|
|
3425
|
-
|
|
3544
|
+
// Security: assert targetDir is confined to the project directory.
|
|
3545
|
+
// Prevents path traversal via ../.. sequences, absolute paths, and symlinks in devdocs.path.
|
|
3546
|
+
// Use realpath() to dereference symlinks — resolve() does not follow them, so a symlink
|
|
3547
|
+
// pre-placed inside the project (e.g. devdocs -> /etc) would bypass a resolve()-only check.
|
|
3548
|
+
// Fall back to resolve() when targetDir does not yet exist (no symlink to dereference).
|
|
3549
|
+
let resolvedTarget;
|
|
3550
|
+
try {
|
|
3551
|
+
resolvedTarget = await realpath(targetDir);
|
|
3552
|
+
} catch {
|
|
3553
|
+
resolvedTarget = resolve(targetDir);
|
|
3554
|
+
}
|
|
3555
|
+
if (resolvedTarget !== cwd && !resolvedTarget.startsWith(cwd + sep)) {
|
|
3426
3556
|
console.log(
|
|
3427
|
-
|
|
3557
|
+
`${RED}Error: devdocs.path must be inside the project directory.${RESET}`,
|
|
3428
3558
|
);
|
|
3429
|
-
console.log(`
|
|
3430
|
-
console.log(
|
|
3431
|
-
|
|
3559
|
+
console.log(` Resolved: ${resolvedTarget}`);
|
|
3560
|
+
console.log(` Project: ${cwd}`);
|
|
3561
|
+
console.log(
|
|
3562
|
+
` Fix: set ${CYAN}devdocs.path${RESET} to a relative path inside your project (e.g. "devdocs" or "docs/knowledge").`,
|
|
3563
|
+
);
|
|
3564
|
+
process.exit(1);
|
|
3432
3565
|
}
|
|
3433
3566
|
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
try {
|
|
3437
|
-
privateKey = readFileSync(creds.privateKeyPath, "utf-8");
|
|
3438
|
-
} catch (err) {
|
|
3439
|
-
console.log(` ${RED}Cannot read private key: ${err.message}${RESET}`);
|
|
3440
|
-
console.log("");
|
|
3441
|
-
return;
|
|
3442
|
-
}
|
|
3567
|
+
// Template source: templates/devdocs/ inside the ForgeDock package
|
|
3568
|
+
const templatesDir = join(FORGE_HOME, "templates", "devdocs");
|
|
3443
3569
|
|
|
3444
|
-
|
|
3445
|
-
const jwt = generateAppJwt(creds.appId, privateKey);
|
|
3446
|
-
|
|
3447
|
-
// App identity
|
|
3448
|
-
const appResp = await fetch("https://api.github.com/app", {
|
|
3449
|
-
headers: {
|
|
3450
|
-
Authorization: `Bearer ${jwt}`,
|
|
3451
|
-
Accept: "application/vnd.github+json",
|
|
3452
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
3453
|
-
},
|
|
3454
|
-
});
|
|
3455
|
-
if (!appResp.ok) {
|
|
3456
|
-
const body = await appResp.json().catch(() => ({}));
|
|
3457
|
-
console.log(
|
|
3458
|
-
` ${RED}GitHub API error ${appResp.status}: ${body.message ?? "unknown"}${RESET}`,
|
|
3459
|
-
);
|
|
3460
|
-
console.log("");
|
|
3461
|
-
return;
|
|
3462
|
-
}
|
|
3463
|
-
const appData = await appResp.json();
|
|
3570
|
+
if (!existsSync(templatesDir)) {
|
|
3464
3571
|
console.log(
|
|
3465
|
-
|
|
3572
|
+
`${RED}DevDocs templates not found: ${templatesDir}${RESET}`,
|
|
3466
3573
|
);
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
},
|
|
3475
|
-
});
|
|
3476
|
-
if (rlResp.ok) {
|
|
3477
|
-
const rl = await rlResp.json();
|
|
3478
|
-
const core = rl.resources?.core;
|
|
3479
|
-
if (core) {
|
|
3480
|
-
const used = core.limit - core.remaining;
|
|
3481
|
-
const resetTime = new Date(core.reset * 1000).toLocaleTimeString();
|
|
3482
|
-
console.log(
|
|
3483
|
-
` Rate limit: ${cyan(core.remaining)}/${core.limit} remaining (${used} used, resets ${resetTime})`,
|
|
3484
|
-
);
|
|
3485
|
-
}
|
|
3486
|
-
}
|
|
3487
|
-
} catch (err) {
|
|
3488
|
-
console.log(` ${RED}Error: ${err.message}${RESET}`);
|
|
3574
|
+
console.log(
|
|
3575
|
+
` This usually means ForgeDock is not installed correctly.`,
|
|
3576
|
+
);
|
|
3577
|
+
console.log(
|
|
3578
|
+
` Try: ${CYAN}npm install -g forgedock${RESET} or ${CYAN}npx forgedock@latest docs init${RESET}`,
|
|
3579
|
+
);
|
|
3580
|
+
process.exit(1);
|
|
3489
3581
|
}
|
|
3490
3582
|
|
|
3491
|
-
|
|
3492
|
-
}
|
|
3583
|
+
const templateFiles = await _findAllFiles(templatesDir);
|
|
3493
3584
|
|
|
3494
|
-
// ---------------------------------------------------------------------------
|
|
3495
|
-
// Bot — setup TUI step (called from StepOrchestrator or directly)
|
|
3496
|
-
// ---------------------------------------------------------------------------
|
|
3497
|
-
|
|
3498
|
-
/**
|
|
3499
|
-
* Interactive bot setup step.
|
|
3500
|
-
* Shows options: Create new app / Connect existing / Skip.
|
|
3501
|
-
* Called as an optional step in tuiOnboarding() or via `npx forgedock bot setup`.
|
|
3502
|
-
*/
|
|
3503
|
-
async function botSetup() {
|
|
3504
3585
|
console.log("");
|
|
3505
|
-
console.log(
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
"",
|
|
3510
|
-
"A GitHub App gives ForgeDock its own identity",
|
|
3511
|
-
"for pipeline operations. Benefits:",
|
|
3512
|
-
` ${cyan("•")} Separate audit trail for bot actions`,
|
|
3513
|
-
` ${cyan("•")} Higher API rate limits`,
|
|
3514
|
-
` ${cyan("•")} Webhook-driven automation (future)`,
|
|
3515
|
-
` ${cyan("•")} Multi-user team support`,
|
|
3516
|
-
].join("\n"),
|
|
3517
|
-
{ title: "GitHub Bot" },
|
|
3518
|
-
),
|
|
3519
|
-
);
|
|
3586
|
+
console.log(`${BOLD}ForgeDock${RESET} — Scaffold DevDocs`);
|
|
3587
|
+
console.log(` Source: ${dim(templatesDir + "/")}`);
|
|
3588
|
+
console.log(` Target: ${dim(targetDir + "/")}`);
|
|
3589
|
+
console.log(` Files: ${templateFiles.length} seed files`);
|
|
3520
3590
|
console.log("");
|
|
3521
3591
|
|
|
3522
|
-
|
|
3523
|
-
{ label: "Create new GitHub App (opens browser)", value: "create" },
|
|
3524
|
-
{ label: "Connect existing GitHub App", value: "connect" },
|
|
3525
|
-
{ label: "Skip — use personal token (current default)", value: "skip" },
|
|
3526
|
-
]);
|
|
3592
|
+
await mkdir(targetDir, { recursive: true });
|
|
3527
3593
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
` ${dim("Skipped.")} Run ${cyan("npx forgedock bot setup")} later to configure a bot identity.`,
|
|
3531
|
-
);
|
|
3532
|
-
return;
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
if (action === "create") {
|
|
3536
|
-
// Build the GitHub App manifest URL — opens the browser-based creation flow
|
|
3537
|
-
const manifestPath = join(dirname(__filename), "github-app-manifest.json");
|
|
3538
|
-
let manifest;
|
|
3539
|
-
try {
|
|
3540
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
3541
|
-
} catch {
|
|
3542
|
-
console.log(
|
|
3543
|
-
` ${RED}Could not read github-app-manifest.json — skipping.${RESET}`,
|
|
3544
|
-
);
|
|
3545
|
-
return;
|
|
3546
|
-
}
|
|
3594
|
+
let copied = 0;
|
|
3595
|
+
let skipped = 0;
|
|
3547
3596
|
|
|
3548
|
-
|
|
3549
|
-
const
|
|
3597
|
+
for (const srcFile of templateFiles) {
|
|
3598
|
+
const rel = relative(templatesDir, srcFile);
|
|
3599
|
+
const destFile = join(targetDir, rel);
|
|
3600
|
+
const destDir = dirname(destFile);
|
|
3550
3601
|
|
|
3551
|
-
|
|
3552
|
-
console.log(
|
|
3553
|
-
` ${bold("Opening GitHub App registration in your browser…")}`,
|
|
3554
|
-
);
|
|
3555
|
-
console.log(` URL: ${cyan(createUrl.substring(0, 80))}…`);
|
|
3556
|
-
console.log("");
|
|
3602
|
+
await mkdir(destDir, { recursive: true });
|
|
3557
3603
|
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
: "xdg-open";
|
|
3566
|
-
// On Windows, `start "url"` treats the first quoted arg as the window title.
|
|
3567
|
-
// Pass an empty title placeholder so the URL is treated as the target: `start "" "url"`.
|
|
3568
|
-
const cmd =
|
|
3569
|
-
process.platform === "win32"
|
|
3570
|
-
? `start "" "${createUrl}"`
|
|
3571
|
-
: `${opener} "${createUrl}"`;
|
|
3572
|
-
execSync(cmd, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3573
|
-
} catch {
|
|
3574
|
-
console.log(` ${YELLOW}Could not open browser automatically.${RESET}`);
|
|
3575
|
-
console.log(` Please open this URL manually:`);
|
|
3576
|
-
console.log(` ${cyan(createUrl)}`);
|
|
3604
|
+
if (existsSync(destFile)) {
|
|
3605
|
+
skipped++;
|
|
3606
|
+
console.log(` ${dim("skip")} ${dim(rel)}`);
|
|
3607
|
+
} else {
|
|
3608
|
+
await copyFile(srcFile, destFile);
|
|
3609
|
+
copied++;
|
|
3610
|
+
console.log(` ${green("+")} ${rel}`);
|
|
3577
3611
|
}
|
|
3612
|
+
}
|
|
3578
3613
|
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
console.log(` 1. Download the private key (.pem) from the app settings`);
|
|
3582
|
-
console.log(` 2. Note your App ID and Installation ID`);
|
|
3614
|
+
console.log("");
|
|
3615
|
+
if (copied > 0 && skipped === 0) {
|
|
3583
3616
|
console.log(
|
|
3584
|
-
|
|
3617
|
+
`${green("DevDocs scaffolded!")} ${copied} file${copied !== 1 ? "s" : ""} created in ${cyan(devdocsRelPath + "/")}.`,
|
|
3618
|
+
);
|
|
3619
|
+
} else if (copied > 0) {
|
|
3620
|
+
console.log(
|
|
3621
|
+
`${green("DevDocs scaffolded!")} ${copied} file${copied !== 1 ? "s" : ""} created, ${skipped} already existed (skipped).`,
|
|
3622
|
+
);
|
|
3623
|
+
} else {
|
|
3624
|
+
console.log(
|
|
3625
|
+
`${yellow("DevDocs already up to date.")} All ${skipped} file${skipped !== 1 ? "s" : ""} already exist — nothing was overwritten.`,
|
|
3585
3626
|
);
|
|
3586
|
-
console.log("");
|
|
3587
|
-
return;
|
|
3588
3627
|
}
|
|
3589
3628
|
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3629
|
+
console.log("");
|
|
3630
|
+
console.log(
|
|
3631
|
+
` ${dim("Tip:")} Customise the files in ${cyan(devdocsRelPath + "/")} for your project.`,
|
|
3632
|
+
);
|
|
3633
|
+
console.log(
|
|
3634
|
+
` ${dim("Agents read these files as authoritative project knowledge.")}`,
|
|
3635
|
+
);
|
|
3636
|
+
console.log("");
|
|
3595
3637
|
}
|
|
3596
3638
|
|
|
3597
3639
|
function help() {
|
|
@@ -3621,13 +3663,19 @@ function help() {
|
|
|
3621
3663
|
` ${CYAN}npx forgedock update${RESET} Pull latest & reinstall`,
|
|
3622
3664
|
);
|
|
3623
3665
|
console.log(
|
|
3624
|
-
` ${CYAN}npx forgedock
|
|
3666
|
+
` ${CYAN}npx forgedock labels${RESET} Bootstrap ForgeDock-managed labels on a GitHub repo`,
|
|
3667
|
+
);
|
|
3668
|
+
console.log(
|
|
3669
|
+
` ${CYAN}npx forgedock labels setup${RESET} Create/update all managed labels (idempotent)`,
|
|
3670
|
+
);
|
|
3671
|
+
console.log(
|
|
3672
|
+
` ${CYAN}npx forgedock labels setup --repo owner/repo${RESET} Target a specific repo`,
|
|
3625
3673
|
);
|
|
3626
3674
|
console.log(
|
|
3627
|
-
` ${CYAN}npx forgedock
|
|
3675
|
+
` ${CYAN}npx forgedock integrate${RESET} Inject/update ForgeDock usage block in project CLAUDE.md`,
|
|
3628
3676
|
);
|
|
3629
3677
|
console.log(
|
|
3630
|
-
` ${CYAN}npx forgedock
|
|
3678
|
+
` ${CYAN}npx forgedock docs init${RESET} Scaffold devdocs knowledge tree into current project`,
|
|
3631
3679
|
);
|
|
3632
3680
|
console.log(` ${CYAN}npx forgedock help${RESET} Show this help`);
|
|
3633
3681
|
console.log("");
|
|
@@ -3984,13 +4032,6 @@ async function tuiOnboarding() {
|
|
|
3984
4032
|
await validate(forgeYamlPath);
|
|
3985
4033
|
},
|
|
3986
4034
|
},
|
|
3987
|
-
{
|
|
3988
|
-
name: "GitHub Bot Setup",
|
|
3989
|
-
optional: true,
|
|
3990
|
-
run: async () => {
|
|
3991
|
-
await botSetup();
|
|
3992
|
-
},
|
|
3993
|
-
},
|
|
3994
4035
|
];
|
|
3995
4036
|
|
|
3996
4037
|
const orchestrator = new StepOrchestrator(steps);
|
|
@@ -4040,15 +4081,40 @@ if (!command) {
|
|
|
4040
4081
|
case "update":
|
|
4041
4082
|
await update();
|
|
4042
4083
|
break;
|
|
4043
|
-
case "
|
|
4084
|
+
case "labels": {
|
|
4044
4085
|
const subcommand = args[1];
|
|
4045
|
-
if (!subcommand || subcommand === "
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4086
|
+
if (!subcommand || subcommand === "setup" || subcommand.startsWith("--")) {
|
|
4087
|
+
const subArgs = args.slice(1);
|
|
4088
|
+
const repo = resolveLabelsRepo(subArgs);
|
|
4089
|
+
if (!repo) {
|
|
4090
|
+
console.log(
|
|
4091
|
+
`${RED}No repository specified.${RESET}\n` +
|
|
4092
|
+
` Pass ${cyan("--repo owner/repo")} or run from a directory with ${cyan("forge.yaml")}.`,
|
|
4093
|
+
);
|
|
4094
|
+
process.exit(1);
|
|
4095
|
+
}
|
|
4096
|
+
await labelsSetup(repo);
|
|
4049
4097
|
} else {
|
|
4050
|
-
console.log(`${RED}Unknown
|
|
4051
|
-
console.log(
|
|
4098
|
+
console.log(`${RED}Unknown labels subcommand: ${subcommand}${RESET}`);
|
|
4099
|
+
console.log(
|
|
4100
|
+
`Usage: ${CYAN}npx forgedock labels [setup] [--repo owner/repo]${RESET}`,
|
|
4101
|
+
);
|
|
4102
|
+
process.exit(1);
|
|
4103
|
+
}
|
|
4104
|
+
break;
|
|
4105
|
+
}
|
|
4106
|
+
case "integrate":
|
|
4107
|
+
await injectClaudeMd(process.cwd());
|
|
4108
|
+
break;
|
|
4109
|
+
case "docs": {
|
|
4110
|
+
const docsSubcommand = args[1];
|
|
4111
|
+
if (!docsSubcommand || docsSubcommand === "init") {
|
|
4112
|
+
await docsInit();
|
|
4113
|
+
} else {
|
|
4114
|
+
console.log(`${RED}Unknown docs subcommand: ${docsSubcommand}${RESET}`);
|
|
4115
|
+
console.log(
|
|
4116
|
+
`Usage: ${CYAN}npx forgedock docs init${RESET}`,
|
|
4117
|
+
);
|
|
4052
4118
|
process.exit(1);
|
|
4053
4119
|
}
|
|
4054
4120
|
break;
|