dev-loops 0.2.1 → 0.2.2
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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/skills/dev-loop/SKILL.md +2 -2
- package/CHANGELOG.md +25 -0
- package/package.json +5 -12
- package/scripts/github/reply-resolve-review-thread.mjs +89 -30
- package/scripts/loop/checkpoint-contract.mjs +79 -30
- package/scripts/loop/outer-loop.mjs +4 -1
- package/scripts/loop/resolve-dev-loop-startup.mjs +5 -1
- package/scripts/repo-wiki.mjs +183 -36
- package/skills/dev-loop/SKILL.md +2 -2
- package/scripts/repo-wiki-local.mjs +0 -156
|
@@ -25,7 +25,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
|
|
|
25
25
|
|
|
26
26
|
The main agent must **always** dispatch the `dev-loop` async subagent for any dev-loop work.
|
|
27
27
|
Do not run `dev-loops loop startup` or any startup resolver in the main agent.
|
|
28
|
-
|
|
28
|
+
For async-required routes (config `workflow.asyncStartMode`, default `required`) the resolver needs an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) that the Pi harness injects when it dispatches the async subagent; under the Claude Code harness the requirement is relaxed automatically (no marker needed). The startup resolver also runs without a marker for non-async routes. Regardless, only the `dev-loop` subagent runs it — never the main agent.
|
|
29
29
|
|
|
30
30
|
### Dev-loop subagent (post-dispatch)
|
|
31
31
|
|
|
@@ -85,7 +85,7 @@ Do not preload route packs before the resolver selects the strategy.
|
|
|
85
85
|
|
|
86
86
|
## Async dispatch
|
|
87
87
|
|
|
88
|
-
**Async dispatch rule (enforced):** the resolver
|
|
88
|
+
**Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
## Fallback gate-comment poster
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.2
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Claude Code: dev-loop no longer dead-ends on the async-start contract** (#830). Running
|
|
10
|
+
`/dev-loop` from the installed plugin failed immediately because `dev-loops loop startup`
|
|
11
|
+
enforces an async-start contract — it requires a run-id env marker (`DEVLOOPS_RUN_ID` /
|
|
12
|
+
`PI_SUBAGENT_RUN_ID`) that Pi injects when dispatching an async subagent but Claude Code's
|
|
13
|
+
Agent tool does not. That contract guards against detached, uninspectable background
|
|
14
|
+
processes, a risk that does not exist under Claude's Agent model (each subagent run is
|
|
15
|
+
visible and inspectable). The async requirement remains configurable via
|
|
16
|
+
`workflow.asyncStartMode` (`required` | `allowed`); under the Claude harness it is now
|
|
17
|
+
**relaxed to `allowed` at runtime** via `resolveEffectiveAsyncStartMode`, which consults the
|
|
18
|
+
new `isClaudeHarness` helper (`CLAUDECODE=1`) in `@dev-loops/core/loop/run-context`. An
|
|
19
|
+
explicit `DEVLOOPS_RUN_ID` still resolves as `valid`, and Pi behavior is unchanged (outside
|
|
20
|
+
Claude the configured mode is honored verbatim).
|
|
21
|
+
- The async-start CLI contract test is now hermetic — it clears `CLAUDECODE` (and the run-id
|
|
22
|
+
markers) so the rejection path is exercised regardless of the harness the suite runs under.
|
|
23
|
+
- The generated `dev-loop` skill prose no longer claims `PI_SUBAGENT_RUN_ID` is *required* — it
|
|
24
|
+
now describes the async run-id marker (`DEVLOOPS_RUN_ID` / `PI_SUBAGENT_RUN_ID` alias) and notes
|
|
25
|
+
the Claude-harness relaxation, so the plugin's docs match the runtime behavior. Subagent
|
|
26
|
+
spawning via the `dev-loop` agent is confirmed correctly wired: it grants the `Agent` tool
|
|
27
|
+
(the current subagent-spawning tool, renamed from `Task` in Claude Code v2.1.63) and the
|
|
28
|
+
strategy skills delegate to the worker agents (`developer`/`quality`/`refiner`/`fixer`/`review`/`docs`).
|
|
29
|
+
|
|
5
30
|
## 0.2.1
|
|
6
31
|
|
|
7
32
|
### Added
|
package/package.json
CHANGED
|
@@ -40,14 +40,9 @@
|
|
|
40
40
|
"repo-wiki:lint": "node scripts/repo-wiki.mjs lint --repo .",
|
|
41
41
|
"repo-wiki:search": "node scripts/repo-wiki.mjs search --repo .",
|
|
42
42
|
"repo-wiki:init": "node scripts/repo-wiki.mjs init --repo .",
|
|
43
|
-
"repo-wiki:
|
|
44
|
-
"repo-wiki:local
|
|
45
|
-
"repo-wiki:local-
|
|
46
|
-
"repo-wiki:local-plan": "node scripts/repo-wiki-local.mjs plan --repo .",
|
|
47
|
-
"repo-wiki:local-lint-docs": "node scripts/repo-wiki-local.mjs lint-docs --repo .",
|
|
48
|
-
"repo-wiki:local-compile": "node scripts/repo-wiki-local.mjs compile --repo .",
|
|
49
|
-
"repo-wiki:local-lint": "node scripts/repo-wiki-local.mjs lint --repo .",
|
|
50
|
-
"repo-wiki:prepare": "node scripts/repo-wiki-local.mjs prepare"
|
|
43
|
+
"repo-wiki:prepare": "node scripts/repo-wiki.mjs --source local prepare",
|
|
44
|
+
"repo-wiki:local": "node scripts/repo-wiki.mjs --source local",
|
|
45
|
+
"repo-wiki:local-prepare": "node scripts/repo-wiki.mjs --source local prepare"
|
|
51
46
|
},
|
|
52
47
|
"bin": {
|
|
53
48
|
"dev-loops": "./cli/index.mjs"
|
|
@@ -58,7 +53,6 @@
|
|
|
58
53
|
},
|
|
59
54
|
"devDependencies": {
|
|
60
55
|
"@playwright/test": "^1.60.0",
|
|
61
|
-
"c8": "^11.0.0",
|
|
62
56
|
"tsx": "^4.22.0"
|
|
63
57
|
},
|
|
64
58
|
"pi": {
|
|
@@ -74,8 +68,7 @@
|
|
|
74
68
|
},
|
|
75
69
|
"dependencies": {
|
|
76
70
|
"mermaid": "11.15.0",
|
|
77
|
-
"
|
|
78
|
-
"@dev-loops/core": "^0.2.1"
|
|
71
|
+
"@dev-loops/core": "^0.2.2"
|
|
79
72
|
},
|
|
80
73
|
"repository": {
|
|
81
74
|
"type": "git",
|
|
@@ -85,7 +78,7 @@
|
|
|
85
78
|
"url": "https://github.com/mfittko/dev-loops/issues"
|
|
86
79
|
},
|
|
87
80
|
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
88
|
-
"version": "0.2.
|
|
81
|
+
"version": "0.2.2",
|
|
89
82
|
"files": [
|
|
90
83
|
"cli/",
|
|
91
84
|
"lib/",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
6
|
+
import { parsePositiveInteger } from "@dev-loops/core/cli/primitives";
|
|
4
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
8
|
import {
|
|
6
9
|
replyAndMaybeResolve,
|
|
@@ -9,34 +12,90 @@ import {
|
|
|
9
12
|
|
|
10
13
|
export { hasCommitShaReference } from "./_review-thread-mutations.mjs";
|
|
11
14
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
const USAGE = `Usage: dev-loops reply-resolve-review-thread --repo <owner/name> --pr <n> --comment-id <n> --thread-id <id> --body-file <path>
|
|
16
|
+
|
|
17
|
+
Reply to a review thread comment and resolve the thread.
|
|
18
|
+
|
|
19
|
+
Required:
|
|
20
|
+
--repo <owner/name> GitHub repository slug
|
|
21
|
+
--pr <n> Pull request number
|
|
22
|
+
--comment-id <n> GraphQL databaseId of the comment to reply to
|
|
23
|
+
--thread-id <id> GraphQL node ID of the review thread
|
|
24
|
+
--body-file <path> Path to file containing the reply body text`;
|
|
25
|
+
|
|
26
|
+
function parseError(message) {
|
|
27
|
+
return Object.assign(new Error(message), { usage: USAGE });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseCliArgs(argv) {
|
|
31
|
+
let values;
|
|
32
|
+
try {
|
|
33
|
+
({ values } = parseArgs({
|
|
34
|
+
args: argv,
|
|
35
|
+
options: {
|
|
36
|
+
repo: { type: "string" },
|
|
37
|
+
pr: { type: "string" },
|
|
38
|
+
"comment-id": { type: "string" },
|
|
39
|
+
"thread-id": { type: "string" },
|
|
40
|
+
"body-file": { type: "string" },
|
|
41
|
+
help: { type: "boolean", short: "h" },
|
|
42
|
+
},
|
|
43
|
+
strict: true,
|
|
44
|
+
allowPositionals: false,
|
|
45
|
+
}));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw parseError(err instanceof Error ? err.message : String(err));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (values.help) {
|
|
51
|
+
return { help: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!values.repo) throw parseError("Missing required option: --repo");
|
|
55
|
+
if (!values.pr) throw parseError("Missing required option: --pr");
|
|
56
|
+
if (!values["comment-id"]) throw parseError("Missing required option: --comment-id");
|
|
57
|
+
if (!values["thread-id"]) throw parseError("Missing required option: --thread-id");
|
|
58
|
+
if (!values["body-file"]) throw parseError("Missing required option: --body-file");
|
|
59
|
+
|
|
60
|
+
const repoSlug = values.repo;
|
|
61
|
+
parseRepoSlug(repoSlug);
|
|
62
|
+
const pr = parsePositiveInteger(values.pr, "--pr", parseError);
|
|
63
|
+
const commentId = parsePositiveInteger(values["comment-id"], "--comment-id", parseError);
|
|
64
|
+
|
|
65
|
+
return { repo: repoSlug, pr, commentId, threadId: values["thread-id"], bodyFile: values["body-file"] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function run(argv) {
|
|
69
|
+
const parsed = parseCliArgs(argv);
|
|
70
|
+
if (parsed.help) {
|
|
71
|
+
process.stdout.write(`${USAGE}\n`);
|
|
38
72
|
return 0;
|
|
39
|
-
}
|
|
40
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { repo: repoSlug, pr, commentId, threadId, bodyFile } = parsed;
|
|
76
|
+
const rawBody = await readFile(bodyFile, "utf8");
|
|
77
|
+
if (rawBody.trim().length === 0) throw new Error("--body-file must contain non-empty text");
|
|
78
|
+
validateResolutionMessage(rawBody);
|
|
79
|
+
|
|
80
|
+
const result = await replyAndMaybeResolve(
|
|
81
|
+
{ repo: repoSlug, pr, commentId, threadId, body: rawBody, resolve: true },
|
|
82
|
+
{ env: process.env, ghCommand: "gh" },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
process.stdout.write(JSON.stringify({
|
|
86
|
+
ok: true, repo: repoSlug, pr, commentId, threadId,
|
|
87
|
+
replyId: result.replyId, replyUrl: result.replyUrl, resolved: true,
|
|
88
|
+
}) + "\n");
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
41
91
|
|
|
42
|
-
if (isDirectCliRun(import.meta.url)) {
|
|
92
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
93
|
+
run(process.argv.slice(2)).then(
|
|
94
|
+
(code) => { process.exitCode = typeof code === "number" ? code : 0; },
|
|
95
|
+
(error) => {
|
|
96
|
+
const usage = error instanceof Error && typeof error.usage === "string" ? error.usage : undefined;
|
|
97
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error), ...(usage && { usage }) })}\n`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
5
7
|
|
|
6
8
|
const CHECKPOINT_FILE = ".pi/dev-loop-retrospective-checkpoint.json";
|
|
7
9
|
const ALLOWED_STATES = new Set(["required", "complete", "skipped", "none", "missing"]);
|
|
8
10
|
|
|
11
|
+
const USAGE = `Usage: dev-loops checkpoint-contract --state <state> [--notes <text>] [--reason <text>]
|
|
12
|
+
|
|
13
|
+
Write .pi/dev-loop-retrospective-checkpoint.json using the retrospective contract format.
|
|
14
|
+
|
|
15
|
+
Required:
|
|
16
|
+
--state <state> Checkpoint state (required, complete, skipped, none, missing)
|
|
17
|
+
|
|
18
|
+
Optional:
|
|
19
|
+
--notes <text> Required when --state is complete
|
|
20
|
+
--reason <text> Required when --state is skipped`;
|
|
21
|
+
|
|
22
|
+
function parseError(message) {
|
|
23
|
+
return Object.assign(new Error(message), { usage: USAGE });
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
export function buildRetrospectiveCheckpointPayload({ state, notes = null, reason = null }, now = new Date()) {
|
|
10
27
|
const timestamp = now.toISOString();
|
|
11
28
|
if (state === "complete") return { state, completedAt: timestamp, notes };
|
|
@@ -16,34 +33,66 @@ export function buildRetrospectiveCheckpointPayload({ state, notes = null, reaso
|
|
|
16
33
|
throw new Error(`Unsupported state: ${state}`);
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
function parseCliArgs(argv) {
|
|
37
|
+
let values;
|
|
38
|
+
try {
|
|
39
|
+
({ values } = parseArgs({
|
|
40
|
+
args: argv,
|
|
41
|
+
options: {
|
|
42
|
+
state: { type: "string" },
|
|
43
|
+
notes: { type: "string" },
|
|
44
|
+
reason: { type: "string" },
|
|
45
|
+
help: { type: "boolean", short: "h" },
|
|
46
|
+
},
|
|
47
|
+
strict: true,
|
|
48
|
+
allowPositionals: false,
|
|
49
|
+
}));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw parseError(err instanceof Error ? err.message : String(err));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (values.help) {
|
|
55
|
+
return { help: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!values.state) throw parseError("Missing required option: --state");
|
|
59
|
+
const state = values.state;
|
|
60
|
+
if (!ALLOWED_STATES.has(state)) {
|
|
61
|
+
throw parseError(`--state must be one of: ${[...ALLOWED_STATES].join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
if (state === "complete" && !values.notes) {
|
|
64
|
+
throw parseError('state "complete" requires --notes');
|
|
65
|
+
}
|
|
66
|
+
if (state === "skipped" && !values.reason) {
|
|
67
|
+
throw parseError('state "skipped" requires --reason');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { state, notes: values.notes ?? null, reason: values.reason ?? null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function run(argv) {
|
|
74
|
+
const parsed = parseCliArgs(argv);
|
|
75
|
+
if (parsed.help) {
|
|
76
|
+
process.stdout.write(`${USAGE}\n`);
|
|
45
77
|
return 0;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
78
|
+
}
|
|
48
79
|
|
|
49
|
-
|
|
80
|
+
const { state, notes, reason } = parsed;
|
|
81
|
+
const payload = buildRetrospectiveCheckpointPayload({ state, notes, reason });
|
|
82
|
+
const checkpointPath = path.join(process.cwd(), CHECKPOINT_FILE);
|
|
83
|
+
await mkdir(path.dirname(checkpointPath), { recursive: true });
|
|
84
|
+
await writeFile(checkpointPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
85
|
+
process.stdout.write(JSON.stringify({ ok: true, path: CHECKPOINT_FILE, checkpoint: payload }) + "\n");
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
90
|
+
run(process.argv.slice(2)).then(
|
|
91
|
+
(code) => { process.exitCode = typeof code === "number" ? code : 0; },
|
|
92
|
+
(error) => {
|
|
93
|
+
const usage = error instanceof Error && typeof error.usage === "string" ? error.usage : undefined;
|
|
94
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error), ...(usage && { usage }) })}\n`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
ASYNC_START_STATUS,
|
|
23
23
|
buildAsyncStartRejection,
|
|
24
24
|
validateAsyncStartContext,
|
|
25
|
+
resolveEffectiveAsyncStartMode,
|
|
25
26
|
} from "@dev-loops/core/loop/async-start-contract";
|
|
26
27
|
import { loadDevLoopConfig, resolveConductorModel, resolveAutonomyStopAt, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
27
28
|
const USAGE = `Usage: outer-loop.mjs --repo <owner/name> --pr <number>
|
|
@@ -286,9 +287,11 @@ export async function runOuterLoop(options, { env = process.env, ghCommand = "gh
|
|
|
286
287
|
devLoopConfig = loaded.config;
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
|
-
const
|
|
290
|
+
const configuredAsyncStartMode = devLoopConfig === null
|
|
290
291
|
? "required"
|
|
291
292
|
: resolveWorkflowConfig(devLoopConfig, "asyncStartMode");
|
|
293
|
+
// Relaxed to "allowed" under the Claude harness (Pi async-start contract does not apply).
|
|
294
|
+
const asyncStartMode = resolveEffectiveAsyncStartMode(configuredAsyncStartMode, env);
|
|
292
295
|
const asyncStartValidation = validateAsyncStartContext({ env, isSnapshotMode, asyncStartMode });
|
|
293
296
|
if (asyncStartValidation.status === ASYNC_START_STATUS.REJECTED) {
|
|
294
297
|
return buildAsyncStartRejection(asyncStartValidation);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
validateAsyncStartContext,
|
|
18
18
|
buildAsyncStartRejection,
|
|
19
|
+
resolveEffectiveAsyncStartMode,
|
|
19
20
|
ASYNC_START_STATUS,
|
|
20
21
|
} from "@dev-loops/core/loop/async-start-contract";
|
|
21
22
|
import { detectRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
@@ -411,7 +412,10 @@ export function buildResolveDevLoopStartupResult(input, { adapter = createPiAdap
|
|
|
411
412
|
? (STRATEGY_ASYNC_DISPATCH[bundle.selectedStrategy] ?? false)
|
|
412
413
|
: false;
|
|
413
414
|
if (requiresAsyncDispatch) {
|
|
414
|
-
|
|
415
|
+
// The configured async-start mode is relaxed to "allowed" under the Claude
|
|
416
|
+
// harness, where the Pi async-subagent start contract does not apply.
|
|
417
|
+
const effectiveAsyncStartMode = resolveEffectiveAsyncStartMode(asyncStartMode, effectiveEnv);
|
|
418
|
+
const validation = validateAsyncStartContext({ env: effectiveEnv, asyncStartMode: effectiveAsyncStartMode });
|
|
415
419
|
if (validation.status === ASYNC_START_STATUS.REJECTED) {
|
|
416
420
|
return buildAsyncStartRejection(validation);
|
|
417
421
|
}
|
package/scripts/repo-wiki.mjs
CHANGED
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Primary repo-wiki wrapper. Proxies to the published @mfittko/repo-wiki
|
|
3
|
-
//
|
|
2
|
+
// Primary repo-wiki wrapper. Proxies to either the published @mfittko/repo-wiki
|
|
3
|
+
// npm package (default) or a pinned local source checkout when --source local or
|
|
4
|
+
// REPO_WIKI_SOURCE=local is set.
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import process from "node:process";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
9
12
|
|
|
10
13
|
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
11
14
|
|
|
12
15
|
// Pinned to the latest published release at the time this slice was opened.
|
|
13
|
-
// Bump deliberately when the consumer repo wants to adopt a newer release.
|
|
14
16
|
export const REPO_WIKI_NPM_PACKAGE = "@mfittko/repo-wiki";
|
|
15
17
|
export const REPO_WIKI_NPM_VERSION = "0.2.6";
|
|
18
|
+
export const REPO_WIKI_MIN_NODE_MAJOR = 20;
|
|
19
|
+
|
|
20
|
+
export const REPO_WIKI_GIT_URL = "https://github.com/mfittko/repo-wiki.git";
|
|
21
|
+
export const REPO_WIKI_REF = "d7e772e3d702a75896a6f4eec574a4e4e5bfa6dd";
|
|
22
|
+
export const REPO_WIKI_LOCAL_MIN_NODE_MAJOR = 24;
|
|
16
23
|
|
|
17
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
25
|
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
19
26
|
|
|
20
|
-
export const
|
|
27
|
+
export const REPO_WIKI_LOCAL_CONFIG_PATH = resolveRepoWikiConfigPath();
|
|
28
|
+
export const REPO_WIKI_SCHEMA_PATH = path.join(PROJECT_ROOT, ".llmwiki", "schema.md");
|
|
21
29
|
|
|
22
30
|
export function resolveRepoWikiConfigPath(projectRoot = PROJECT_ROOT) {
|
|
23
31
|
return path.join(projectRoot, ".llmwiki", "config.json");
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
export const REPO_WIKI_CONFIG_PATH = resolveRepoWikiConfigPath();
|
|
27
|
-
export const REPO_WIKI_SCHEMA_PATH = path.join(PROJECT_ROOT, ".llmwiki", "schema.md");
|
|
28
|
-
|
|
29
|
-
export function parseCliArgs(argv) {
|
|
30
|
-
const args = Array.isArray(argv) ? [...argv] : [];
|
|
31
|
-
if (args.length === 0) {
|
|
32
|
-
return { passthroughArgs: ["--help"] };
|
|
33
|
-
}
|
|
34
|
-
if (args[0] === "--help" || args[0] === "-h") {
|
|
35
|
-
return { passthroughArgs: ["--help"] };
|
|
36
|
-
}
|
|
37
|
-
return { passthroughArgs: args };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
34
|
export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
41
35
|
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
42
36
|
if (!Number.isInteger(major) || major < REPO_WIKI_MIN_NODE_MAJOR) {
|
|
@@ -46,15 +40,21 @@ export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
|
|
43
|
+
export function assertSupportedLocalNodeVersion(version = process.versions.node) {
|
|
44
|
+
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
45
|
+
if (!Number.isInteger(major) || major < REPO_WIKI_LOCAL_MIN_NODE_MAJOR) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`repo-wiki local helper requires Node.js ${REPO_WIKI_LOCAL_MIN_NODE_MAJOR}+ because repo-wiki itself requires Node.js ${REPO_WIKI_LOCAL_MIN_NODE_MAJOR}+. Current runtime: ${version}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
49
52
|
export function assertConsumerConfigPresent({
|
|
50
|
-
configPath =
|
|
53
|
+
configPath = REPO_WIKI_LOCAL_CONFIG_PATH,
|
|
51
54
|
projectRoot = PROJECT_ROOT,
|
|
52
55
|
} = {}) {
|
|
53
|
-
// When callers override projectRoot without overriding configPath, derive the
|
|
54
|
-
// expected config path from the override so tests and other call sites stay
|
|
55
|
-
// consistent with the per-project layout.
|
|
56
56
|
const resolvedConfigPath =
|
|
57
|
-
configPath ===
|
|
57
|
+
configPath === REPO_WIKI_LOCAL_CONFIG_PATH ? resolveRepoWikiConfigPath(projectRoot) : configPath;
|
|
58
58
|
if (!existsSync(resolvedConfigPath)) {
|
|
59
59
|
throw new Error(
|
|
60
60
|
`Missing required repo-wiki config at ${path.relative(projectRoot, resolvedConfigPath) || resolvedConfigPath}.\n` +
|
|
@@ -73,38 +73,185 @@ export function buildNpxInvocation({
|
|
|
73
73
|
return ["npx", "--yes", `${packageName}@${version}`, ...passthroughArgs];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
function run(command, args, options = {}) {
|
|
77
|
+
const result = spawnSync(command, args, {
|
|
78
|
+
cwd: options.cwd ?? PROJECT_ROOT,
|
|
79
|
+
env: { ...process.env, ...(options.env ?? {}) },
|
|
80
|
+
stdio: options.stdio ?? "inherit",
|
|
81
|
+
encoding: options.encoding ?? "utf8",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.error) {
|
|
85
|
+
throw result.error;
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0 && !options.allowNonZero) {
|
|
88
|
+
const printable = [command, ...args].join(" ");
|
|
89
|
+
throw new Error(`Command failed (${result.status ?? "unknown"}): ${printable}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runNpxInvocation({
|
|
77
96
|
command,
|
|
78
97
|
args,
|
|
79
98
|
cwd = PROJECT_ROOT,
|
|
80
99
|
env = process.env,
|
|
81
100
|
} = {}) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
return run(command, args, { cwd, env, stdio: "inherit", allowNonZero: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const VALID_REPO_WIKI_SOURCES = new Set(["npm", "local"]);
|
|
105
|
+
|
|
106
|
+
export function parseCliArgs(argv) {
|
|
107
|
+
const args = Array.isArray(argv) ? [...argv] : [];
|
|
108
|
+
const { values } = parseArgs({
|
|
109
|
+
args,
|
|
110
|
+
options: {
|
|
111
|
+
source: { type: "string" },
|
|
112
|
+
},
|
|
113
|
+
strict: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const sourceEnv = process.env.REPO_WIKI_SOURCE;
|
|
117
|
+
const source = values.source || sourceEnv || "npm";
|
|
118
|
+
if (!VALID_REPO_WIKI_SOURCES.has(source)) {
|
|
119
|
+
throw new Error(`Unknown repo-wiki source "${source}". Valid sources: npm, local`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const passthroughArgs = [];
|
|
123
|
+
for (let i = 0; i < args.length; i++) {
|
|
124
|
+
const a = args[i];
|
|
125
|
+
if (a === "--source") {
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (a.startsWith("--source=")) continue;
|
|
130
|
+
passthroughArgs.push(a);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (passthroughArgs.length === 0 || passthroughArgs[0] === "help" || passthroughArgs[0] === "--help" || passthroughArgs[0] === "-h") {
|
|
134
|
+
return { source, prepareOnly: false, passthroughArgs: ["--help"] };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (passthroughArgs[0] === "prepare") {
|
|
138
|
+
return { source, prepareOnly: true, passthroughArgs: [] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { source, prepareOnly: false, passthroughArgs };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveRepoWikiPaths(projectRoot = PROJECT_ROOT, ref = REPO_WIKI_REF) {
|
|
145
|
+
const baseDir = path.join(projectRoot, ".tmp", "repo-wiki", ref);
|
|
146
|
+
const sourceDir = path.join(baseDir, "source");
|
|
147
|
+
const cliPath = path.join(sourceDir, "dist", "bin", "repo-wiki.js");
|
|
148
|
+
const buildStampPath = path.join(baseDir, "build-stamp.json");
|
|
149
|
+
return { projectRoot, baseDir, sourceDir, cliPath, buildStampPath };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function readBuildStamp(buildStampPath) {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(await readFile(buildStampPath, "utf8"));
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function writeBuildStamp(buildStampPath) {
|
|
161
|
+
await writeFile(buildStampPath, JSON.stringify({ ref: REPO_WIKI_REF }, null, 2) + "\n", "utf8");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function ensureRepoWikiPrepared(projectRoot = PROJECT_ROOT) {
|
|
165
|
+
assertSupportedLocalNodeVersion();
|
|
166
|
+
const { baseDir, sourceDir, cliPath, buildStampPath } = resolveRepoWikiPaths(projectRoot);
|
|
167
|
+
await mkdir(baseDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
let currentHead = null;
|
|
170
|
+
try {
|
|
171
|
+
run("git", ["-C", sourceDir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
|
|
172
|
+
currentHead = run("git", ["-C", sourceDir, "rev-parse", "HEAD"], { stdio: "pipe" }).stdout.trim();
|
|
173
|
+
} catch {
|
|
174
|
+
run("git", ["clone", REPO_WIKI_GIT_URL, sourceDir], { cwd: baseDir });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (currentHead !== REPO_WIKI_REF) {
|
|
178
|
+
run("git", ["-C", sourceDir, "fetch", "origin", REPO_WIKI_REF, "--depth", "1"]);
|
|
179
|
+
run("git", ["-C", sourceDir, "checkout", "--force", REPO_WIKI_REF]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const stamp = await readBuildStamp(buildStampPath);
|
|
183
|
+
if (stamp?.ref !== REPO_WIKI_REF) {
|
|
184
|
+
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
185
|
+
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
186
|
+
await writeBuildStamp(buildStampPath);
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
run(process.execPath, [cliPath, "--help"], { stdio: "ignore" });
|
|
190
|
+
} catch {
|
|
191
|
+
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
192
|
+
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
193
|
+
await writeBuildStamp(buildStampPath);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return resolveRepoWikiPaths(projectRoot);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runRepoWikiNpm(passthroughArgs, projectRoot) {
|
|
201
|
+
assertSupportedNodeVersion();
|
|
202
|
+
assertConsumerConfigPresent({ projectRoot });
|
|
203
|
+
const invocation = buildNpxInvocation({ passthroughArgs });
|
|
204
|
+
const result = runNpxInvocation({ command: invocation[0], args: invocation.slice(1), cwd: projectRoot });
|
|
205
|
+
|
|
206
|
+
if (result.status !== 0) {
|
|
207
|
+
return { ok: false, status: result.status ?? 1, source: "npm", invocation };
|
|
208
|
+
}
|
|
209
|
+
return { ok: true, status: 0, source: "npm", invocation };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function _runRepoWikiLocal(passthroughArgs, projectRoot) {
|
|
213
|
+
const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
|
|
214
|
+
const result = spawnSync(process.execPath, [cliPath, ...passthroughArgs], {
|
|
215
|
+
cwd: projectRoot,
|
|
216
|
+
env: process.env,
|
|
85
217
|
stdio: "inherit",
|
|
86
218
|
});
|
|
87
219
|
if (result.error) {
|
|
88
220
|
throw result.error;
|
|
89
221
|
}
|
|
90
|
-
|
|
222
|
+
if (result.status !== 0) {
|
|
223
|
+
return { ok: false, status: result.status ?? 1, source: "local", cliPath };
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, status: 0, source: "local", cliPath };
|
|
91
226
|
}
|
|
92
227
|
|
|
93
228
|
// Composable entry point: returns a structured result instead of calling
|
|
94
229
|
// process.exit so importers can reuse this wrapper without terminating the host
|
|
95
230
|
// process. The direct-run block below owns the process-level exit-code mapping.
|
|
231
|
+
// Convenience export for callers that explicitly want the pinned source path.
|
|
232
|
+
export async function runRepoWikiLocal(argv, projectRoot = PROJECT_ROOT) {
|
|
233
|
+
const args = argv.some((a) => a === "--source" || a.startsWith("--source="))
|
|
234
|
+
? argv
|
|
235
|
+
: ["--source", "local", ...argv];
|
|
236
|
+
return runRepoWiki(args, projectRoot);
|
|
237
|
+
}
|
|
238
|
+
|
|
96
239
|
export async function runRepoWiki(argv, projectRoot = PROJECT_ROOT) {
|
|
97
|
-
|
|
98
|
-
assertConsumerConfigPresent({ projectRoot });
|
|
99
|
-
const { passthroughArgs } = parseCliArgs(argv);
|
|
100
|
-
const invocation = buildNpxInvocation({ passthroughArgs });
|
|
240
|
+
const { source, prepareOnly, passthroughArgs } = parseCliArgs(argv);
|
|
101
241
|
|
|
102
|
-
|
|
242
|
+
if (source === "local") {
|
|
243
|
+
if (prepareOnly) {
|
|
244
|
+
const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
|
|
245
|
+
return { ok: true, status: 0, source: "local", prepared: true, cliPath };
|
|
246
|
+
}
|
|
247
|
+
return _runRepoWikiLocal(passthroughArgs, projectRoot);
|
|
248
|
+
}
|
|
103
249
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
250
|
+
if (prepareOnly) {
|
|
251
|
+
// prepare is meaningless for the npm source; treat as help.
|
|
252
|
+
return runRepoWikiNpm(["--help"], projectRoot);
|
|
106
253
|
}
|
|
107
|
-
return
|
|
254
|
+
return runRepoWikiNpm(passthroughArgs, projectRoot);
|
|
108
255
|
}
|
|
109
256
|
|
|
110
257
|
if (isDirectCliRun(import.meta.url)) {
|
package/skills/dev-loop/SKILL.md
CHANGED
|
@@ -26,7 +26,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
|
|
|
26
26
|
|
|
27
27
|
The main agent must **always** dispatch the `dev-loop` async subagent for any dev-loop work.
|
|
28
28
|
Do not run `dev-loops loop startup` or any startup resolver in the main agent.
|
|
29
|
-
|
|
29
|
+
For async-required routes (config `workflow.asyncStartMode`, default `required`) the resolver needs an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) that the Pi harness injects when it dispatches the async subagent; under the Claude Code harness the requirement is relaxed automatically (no marker needed). The startup resolver also runs without a marker for non-async routes. Regardless, only the `dev-loop` subagent runs it — never the main agent.
|
|
30
30
|
|
|
31
31
|
### Dev-loop subagent (post-dispatch)
|
|
32
32
|
|
|
@@ -86,7 +86,7 @@ Do not preload route packs before the resolver selects the strategy.
|
|
|
86
86
|
|
|
87
87
|
## Async dispatch
|
|
88
88
|
|
|
89
|
-
**Async dispatch rule (enforced):** the resolver
|
|
89
|
+
**Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
## Fallback gate-comment poster
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Pinned-source fallback repo-wiki wrapper. Clones mfittko/repo-wiki at a fixed
|
|
3
|
-
// commit, installs/builds it locally, and proxies the requested command against
|
|
4
|
-
// this repo.
|
|
5
|
-
//
|
|
6
|
-
// This helper is retained for environments that prefer a pinned source checkout
|
|
7
|
-
// over the published npm install path (deterministic source pin, controlled
|
|
8
|
-
// GitHub-only network access, offline reproduction after the initial clone).
|
|
9
|
-
// It still requires git access to https://github.com/mfittko/repo-wiki.git for
|
|
10
|
-
// the initial clone/fetch step.
|
|
11
|
-
//
|
|
12
|
-
// The primary repo-wiki entrypoint is `scripts/repo-wiki.mjs` (npm-installed).
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import process from "node:process";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
|
|
20
|
-
|
|
21
|
-
export const REPO_WIKI_GIT_URL = "https://github.com/mfittko/repo-wiki.git";
|
|
22
|
-
export const REPO_WIKI_REF = "d7e772e3d702a75896a6f4eec574a4e4e5bfa6dd";
|
|
23
|
-
export const REPO_WIKI_MIN_NODE_MAJOR = 24;
|
|
24
|
-
|
|
25
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
27
|
-
|
|
28
|
-
export function parseCliArgs(argv) {
|
|
29
|
-
const args = Array.isArray(argv) ? [...argv] : [];
|
|
30
|
-
if (args.length === 0) {
|
|
31
|
-
return { prepareOnly: false, passthroughArgs: ["--help"] };
|
|
32
|
-
}
|
|
33
|
-
if (args[0] === "prepare") {
|
|
34
|
-
return { prepareOnly: true, passthroughArgs: [] };
|
|
35
|
-
}
|
|
36
|
-
return { prepareOnly: false, passthroughArgs: args };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function resolveRepoWikiPaths(projectRoot = PROJECT_ROOT, ref = REPO_WIKI_REF) {
|
|
40
|
-
const baseDir = path.join(projectRoot, ".tmp", "repo-wiki", ref);
|
|
41
|
-
const sourceDir = path.join(baseDir, "source");
|
|
42
|
-
const cliPath = path.join(sourceDir, "dist", "bin", "repo-wiki.js");
|
|
43
|
-
const buildStampPath = path.join(baseDir, "build-stamp.json");
|
|
44
|
-
return { projectRoot, baseDir, sourceDir, cliPath, buildStampPath };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
48
|
-
const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
|
|
49
|
-
if (!Number.isInteger(major) || major < REPO_WIKI_MIN_NODE_MAJOR) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
`repo-wiki local helper requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+ because repo-wiki itself requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+. Current runtime: ${version}`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function run(command, args, options = {}) {
|
|
57
|
-
const result = spawnSync(command, args, {
|
|
58
|
-
cwd: options.cwd ?? PROJECT_ROOT,
|
|
59
|
-
env: { ...process.env, ...(options.env ?? {}) },
|
|
60
|
-
stdio: options.stdio ?? "inherit",
|
|
61
|
-
encoding: options.encoding ?? "utf8",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (result.error) {
|
|
65
|
-
throw result.error;
|
|
66
|
-
}
|
|
67
|
-
if (result.status !== 0) {
|
|
68
|
-
const printable = [command, ...args].join(" ");
|
|
69
|
-
throw new Error(`Command failed (${result.status ?? "unknown"}): ${printable}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function readBuildStamp(buildStampPath) {
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(await readFile(buildStampPath, "utf8"));
|
|
78
|
-
} catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function writeBuildStamp(buildStampPath) {
|
|
84
|
-
await writeFile(buildStampPath, JSON.stringify({ ref: REPO_WIKI_REF }, null, 2) + "\n", "utf8");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function ensureRepoWikiPrepared(projectRoot = PROJECT_ROOT) {
|
|
88
|
-
assertSupportedNodeVersion();
|
|
89
|
-
const { baseDir, sourceDir, cliPath, buildStampPath } = resolveRepoWikiPaths(projectRoot);
|
|
90
|
-
await mkdir(baseDir, { recursive: true });
|
|
91
|
-
|
|
92
|
-
let currentHead = null;
|
|
93
|
-
try {
|
|
94
|
-
run("git", ["-C", sourceDir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
|
|
95
|
-
currentHead = run("git", ["-C", sourceDir, "rev-parse", "HEAD"], { stdio: "pipe" }).stdout.trim();
|
|
96
|
-
} catch {
|
|
97
|
-
run("git", ["clone", REPO_WIKI_GIT_URL, sourceDir], { cwd: baseDir });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (currentHead !== REPO_WIKI_REF) {
|
|
101
|
-
run("git", ["-C", sourceDir, "fetch", "origin", REPO_WIKI_REF, "--depth", "1"]);
|
|
102
|
-
run("git", ["-C", sourceDir, "checkout", "--force", REPO_WIKI_REF]);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const stamp = await readBuildStamp(buildStampPath);
|
|
106
|
-
if (stamp?.ref !== REPO_WIKI_REF) {
|
|
107
|
-
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
108
|
-
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
109
|
-
await writeBuildStamp(buildStampPath);
|
|
110
|
-
} else {
|
|
111
|
-
try {
|
|
112
|
-
run(process.execPath, [cliPath, "--help"], { stdio: "ignore" });
|
|
113
|
-
} catch {
|
|
114
|
-
run("npm", ["install", "--silent"], { cwd: sourceDir });
|
|
115
|
-
run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
|
|
116
|
-
await writeBuildStamp(buildStampPath);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return resolveRepoWikiPaths(projectRoot);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Composable entry point: returns a structured result instead of calling
|
|
124
|
-
// process.exit so importers can reuse this wrapper without terminating the host
|
|
125
|
-
// process. The direct-run block below owns the process-level exit-code mapping.
|
|
126
|
-
export async function runRepoWikiLocal(argv, projectRoot = PROJECT_ROOT) {
|
|
127
|
-
const { prepareOnly, passthroughArgs } = parseCliArgs(argv);
|
|
128
|
-
const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
|
|
129
|
-
if (prepareOnly) {
|
|
130
|
-
return { ok: true, status: 0, prepared: true, cliPath };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const result = spawnSync(process.execPath, [cliPath, ...passthroughArgs], {
|
|
134
|
-
cwd: projectRoot,
|
|
135
|
-
env: process.env,
|
|
136
|
-
stdio: "inherit",
|
|
137
|
-
});
|
|
138
|
-
if (result.error) {
|
|
139
|
-
throw result.error;
|
|
140
|
-
}
|
|
141
|
-
if (result.status !== 0) {
|
|
142
|
-
return { ok: false, status: result.status ?? 1, cliPath };
|
|
143
|
-
}
|
|
144
|
-
return { ok: true, status: 0, prepared: true, cliPath };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (isDirectCliRun(import.meta.url)) {
|
|
148
|
-
runRepoWikiLocal(process.argv.slice(2)).then((result) => {
|
|
149
|
-
if (!result.ok) {
|
|
150
|
-
process.exitCode = result.status;
|
|
151
|
-
}
|
|
152
|
-
}).catch((error) => {
|
|
153
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
154
|
-
process.exitCode = 1;
|
|
155
|
-
});
|
|
156
|
-
}
|