@okf-harness/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/chunk-CU2XPEBG.js +1028 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +8 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +7 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @okf-harness/cli
|
|
2
|
+
|
|
3
|
+
Command-line package for OKF Harness local workspaces. It provides the `okfh` command for initializing workspaces, registering sources, linting wiki content, searching and reading pages, generating graph reports, and installing Claude Code or Codex guidance.
|
|
4
|
+
|
|
5
|
+
OKF Harness is an independent open-source project built on [Andrej Karpathy's LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) pattern and Google's [Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing) / [OKF specification](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md).
|
|
6
|
+
|
|
7
|
+
Install:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @okf-harness/cli
|
|
11
|
+
okfh doctor --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Try without a global install:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx --package @okf-harness/cli okfh doctor --json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For project overview, workflows, and security notes, see the [main repository README](https://github.com/pumblus/okf-harness#readme).
|
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { execFile as execFile2 } from "child_process";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
import { installAgentAdapters } from "@okf-harness/agent-pack";
|
|
5
|
+
import {
|
|
6
|
+
addSource,
|
|
7
|
+
buildWorkspaceGraph,
|
|
8
|
+
createIngestPlan,
|
|
9
|
+
initWorkspace,
|
|
10
|
+
lintWorkspace,
|
|
11
|
+
listSources,
|
|
12
|
+
readWorkspaceDocument,
|
|
13
|
+
readWorkspaceStatus as readWorkspaceStatus2,
|
|
14
|
+
resolveWorkspaceRoot as resolveWorkspaceRoot2,
|
|
15
|
+
SourceManagementError,
|
|
16
|
+
searchWorkspace,
|
|
17
|
+
WorkspaceInitError as WorkspaceInitError2
|
|
18
|
+
} from "@okf-harness/core";
|
|
19
|
+
import { Command } from "commander";
|
|
20
|
+
|
|
21
|
+
// src/doctor/index.ts
|
|
22
|
+
import { execFile } from "child_process";
|
|
23
|
+
import { access, readFile } from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { promisify } from "util";
|
|
26
|
+
import {
|
|
27
|
+
readWorkspaceStatus,
|
|
28
|
+
resolveWorkspaceRoot,
|
|
29
|
+
WorkspaceResolutionError
|
|
30
|
+
} from "@okf-harness/core";
|
|
31
|
+
var execFileAsync = promisify(execFile);
|
|
32
|
+
var requiredSkills = [
|
|
33
|
+
"okf-harness-init",
|
|
34
|
+
"okf-harness-ingest",
|
|
35
|
+
"okf-harness-query",
|
|
36
|
+
"okf-harness-maintain"
|
|
37
|
+
];
|
|
38
|
+
async function runDoctor(options = {}) {
|
|
39
|
+
const checks = [
|
|
40
|
+
checkOkfh(),
|
|
41
|
+
checkNode(),
|
|
42
|
+
await checkExecutable("git", ["--version"], {
|
|
43
|
+
id: "git",
|
|
44
|
+
label: "git",
|
|
45
|
+
missingMessage: "git executable was not found."
|
|
46
|
+
}),
|
|
47
|
+
await checkExecutable("pnpm", ["--version"], {
|
|
48
|
+
id: "pnpm",
|
|
49
|
+
label: "pnpm",
|
|
50
|
+
missingMessage: "pnpm executable was not found.",
|
|
51
|
+
outputPrefix: "pnpm "
|
|
52
|
+
})
|
|
53
|
+
];
|
|
54
|
+
const workspaceRoot = await resolveDoctorWorkspace(options, checks);
|
|
55
|
+
if (workspaceRoot === null) {
|
|
56
|
+
checks.push(skipCheck("workspace-status", "Workspace status", "No workspace was resolved."));
|
|
57
|
+
checks.push(skipCheck("claude-adapter", "Claude Code adapter", "No workspace was resolved."));
|
|
58
|
+
checks.push(skipCheck("codex-adapter", "Codex adapter", "No workspace was resolved."));
|
|
59
|
+
} else {
|
|
60
|
+
checks.push(await checkWorkspaceStatus(workspaceRoot));
|
|
61
|
+
checks.push(await checkAdapter(workspaceRoot, "claude"));
|
|
62
|
+
checks.push(await checkAdapter(workspaceRoot, "codex"));
|
|
63
|
+
}
|
|
64
|
+
const summary = summarizeChecks(checks);
|
|
65
|
+
return {
|
|
66
|
+
ok: summary.fail === 0,
|
|
67
|
+
workspace: workspaceRoot,
|
|
68
|
+
checks,
|
|
69
|
+
summary
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function checkOkfh() {
|
|
73
|
+
return {
|
|
74
|
+
id: "okfh",
|
|
75
|
+
label: "okfh CLI",
|
|
76
|
+
status: "pass",
|
|
77
|
+
message: "The current okfh CLI entrypoint is running.",
|
|
78
|
+
details: {
|
|
79
|
+
argv0: process.argv[1] ?? null,
|
|
80
|
+
pid: process.pid
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function checkNode() {
|
|
85
|
+
const version = process.versions.node;
|
|
86
|
+
const major = Number.parseInt(version.split(".")[0] ?? "", 10);
|
|
87
|
+
if (Number.isFinite(major) && major >= 22) {
|
|
88
|
+
return {
|
|
89
|
+
id: "node",
|
|
90
|
+
label: "Node.js",
|
|
91
|
+
status: "pass",
|
|
92
|
+
message: `Node.js ${version} satisfies the >=22 runtime requirement.`,
|
|
93
|
+
details: { version }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
id: "node",
|
|
98
|
+
label: "Node.js",
|
|
99
|
+
status: "fail",
|
|
100
|
+
message: `Node.js ${version} does not satisfy the >=22 runtime requirement.`,
|
|
101
|
+
details: { version, required: ">=22.0.0" }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function checkExecutable(executable, args, options) {
|
|
105
|
+
try {
|
|
106
|
+
const { stdout, stderr } = await execFileAsync(executable, args);
|
|
107
|
+
const output = `${stdout}${stderr}`.trim();
|
|
108
|
+
return {
|
|
109
|
+
id: options.id,
|
|
110
|
+
label: options.label,
|
|
111
|
+
status: "pass",
|
|
112
|
+
message: output.length > 0 ? `${options.outputPrefix ?? ""}${output}` : `${executable} is available.`,
|
|
113
|
+
details: { executable }
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const code = nodeErrorCode(error);
|
|
117
|
+
return {
|
|
118
|
+
id: options.id,
|
|
119
|
+
label: options.label,
|
|
120
|
+
status: "fail",
|
|
121
|
+
message: code === "ENOENT" ? options.missingMessage : `${executable} check failed.`,
|
|
122
|
+
details: {
|
|
123
|
+
executable,
|
|
124
|
+
error: error instanceof Error ? error.message : String(error)
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function resolveDoctorWorkspace(options, checks) {
|
|
130
|
+
try {
|
|
131
|
+
return await resolveWorkspaceRoot({
|
|
132
|
+
workspaceRoot: options.workspaceRoot,
|
|
133
|
+
startDir: options.startDir
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof WorkspaceResolutionError) {
|
|
137
|
+
checks.push({
|
|
138
|
+
id: "workspace-resolution",
|
|
139
|
+
label: "Workspace resolution",
|
|
140
|
+
status: options.workspaceRoot === void 0 ? "warn" : "fail",
|
|
141
|
+
message: options.workspaceRoot === void 0 ? "No okfh.config.yaml was found from the current directory or its parents." : "The requested workspace could not be resolved.",
|
|
142
|
+
details: { startDir: error.startDir }
|
|
143
|
+
});
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function checkWorkspaceStatus(workspaceRoot) {
|
|
150
|
+
const status = await readWorkspaceStatus(workspaceRoot);
|
|
151
|
+
if (!status.initialized) {
|
|
152
|
+
return {
|
|
153
|
+
id: "workspace-status",
|
|
154
|
+
label: "Workspace status",
|
|
155
|
+
status: "fail",
|
|
156
|
+
message: "Workspace is not initialized or okfh.config.yaml is invalid.",
|
|
157
|
+
details: {
|
|
158
|
+
workspace: status.workspaceRoot,
|
|
159
|
+
lintIssues: status.lint.issues.length
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id: "workspace-status",
|
|
165
|
+
label: "Workspace status",
|
|
166
|
+
status: status.lint.ok ? "pass" : "warn",
|
|
167
|
+
message: status.lint.ok ? `Workspace ${status.name ?? workspaceRoot} is initialized and lint passes.` : `Workspace ${status.name ?? workspaceRoot} is initialized but lint has issues.`,
|
|
168
|
+
details: {
|
|
169
|
+
workspace: status.workspaceRoot,
|
|
170
|
+
name: status.name ?? null,
|
|
171
|
+
wikiFiles: status.wikiFiles,
|
|
172
|
+
concepts: status.concepts,
|
|
173
|
+
lintOk: status.lint.ok,
|
|
174
|
+
lintIssues: status.lint.issues.length
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function checkAdapter(workspaceRoot, adapter) {
|
|
179
|
+
const rootGuidance = adapter === "claude" ? "CLAUDE.md" : "AGENTS.md";
|
|
180
|
+
const skillRoot = adapter === "claude" ? ".claude/skills" : ".agents/skills";
|
|
181
|
+
const missingFiles = [];
|
|
182
|
+
const rootPath = path.join(workspaceRoot, rootGuidance);
|
|
183
|
+
const rootContents = await readOptionalText(rootPath);
|
|
184
|
+
if (rootContents === void 0) {
|
|
185
|
+
missingFiles.push(rootGuidance);
|
|
186
|
+
}
|
|
187
|
+
for (const skill of requiredSkills) {
|
|
188
|
+
const skillPath = `${skillRoot}/${skill}/SKILL.md`;
|
|
189
|
+
if (!await fileExists(path.join(workspaceRoot, skillPath))) {
|
|
190
|
+
missingFiles.push(skillPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const hasManagedBlock = rootContents?.includes("<!-- OKF Harness: start -->") === true && rootContents.includes("<!-- OKF Harness: end -->");
|
|
194
|
+
if (missingFiles.length === 0 && hasManagedBlock) {
|
|
195
|
+
return {
|
|
196
|
+
id: `${adapter}-adapter`,
|
|
197
|
+
label: adapter === "claude" ? "Claude Code adapter" : "Codex adapter",
|
|
198
|
+
status: "pass",
|
|
199
|
+
message: `${adapter === "claude" ? "Claude Code" : "Codex"} adapter files are installed.`,
|
|
200
|
+
details: { rootGuidance, skillRoot }
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
id: `${adapter}-adapter`,
|
|
205
|
+
label: adapter === "claude" ? "Claude Code adapter" : "Codex adapter",
|
|
206
|
+
status: "warn",
|
|
207
|
+
message: `${adapter === "claude" ? "Claude Code" : "Codex"} adapter support is incomplete.`,
|
|
208
|
+
details: {
|
|
209
|
+
rootGuidance,
|
|
210
|
+
skillRoot,
|
|
211
|
+
hasManagedBlock,
|
|
212
|
+
missingFiles,
|
|
213
|
+
repairCommand: `okfh agent install ${adapter} --workspace <workspace> --json`
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function skipCheck(id, label, message) {
|
|
218
|
+
return {
|
|
219
|
+
id,
|
|
220
|
+
label,
|
|
221
|
+
status: "skip",
|
|
222
|
+
message
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function summarizeChecks(checks) {
|
|
226
|
+
return checks.reduce(
|
|
227
|
+
(summary, check) => {
|
|
228
|
+
summary[check.status] += 1;
|
|
229
|
+
return summary;
|
|
230
|
+
},
|
|
231
|
+
{ pass: 0, warn: 0, fail: 0, skip: 0 }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
async function fileExists(filePath) {
|
|
235
|
+
try {
|
|
236
|
+
await access(filePath);
|
|
237
|
+
return true;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (nodeErrorCode(error) === "ENOENT") {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function readOptionalText(filePath) {
|
|
246
|
+
try {
|
|
247
|
+
return await readFile(filePath, "utf8");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (nodeErrorCode(error) === "ENOENT") {
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function nodeErrorCode(error) {
|
|
256
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
const code = error.code;
|
|
260
|
+
return typeof code === "string" ? code : void 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/errors/index.ts
|
|
264
|
+
import {
|
|
265
|
+
GraphWorkspaceError,
|
|
266
|
+
ReadWorkspaceError,
|
|
267
|
+
WorkspaceInitError,
|
|
268
|
+
WorkspaceResolutionError as WorkspaceResolutionError2
|
|
269
|
+
} from "@okf-harness/core";
|
|
270
|
+
function handleCliError(error, io, options) {
|
|
271
|
+
if (error instanceof WorkspaceInitError) {
|
|
272
|
+
writeCliError(io, {
|
|
273
|
+
command: "init",
|
|
274
|
+
error,
|
|
275
|
+
json: options.json
|
|
276
|
+
});
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
if (error instanceof WorkspaceResolutionError2) {
|
|
280
|
+
writeCliError(io, {
|
|
281
|
+
command: options.command,
|
|
282
|
+
error,
|
|
283
|
+
workspace: null,
|
|
284
|
+
next: ["Run from inside an OKF Harness workspace or pass --workspace <path>."],
|
|
285
|
+
json: options.json
|
|
286
|
+
});
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
if (isCommanderError(error)) {
|
|
290
|
+
if (options.json) {
|
|
291
|
+
writeCliError(io, {
|
|
292
|
+
command: options.command,
|
|
293
|
+
error: {
|
|
294
|
+
code: error.code,
|
|
295
|
+
message: error.message
|
|
296
|
+
},
|
|
297
|
+
json: true
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
io.writeErr(options.capturedStderr);
|
|
301
|
+
}
|
|
302
|
+
return error.exitCode;
|
|
303
|
+
}
|
|
304
|
+
writeCliError(io, {
|
|
305
|
+
command: "unknown",
|
|
306
|
+
error: {
|
|
307
|
+
code: "UNKNOWN",
|
|
308
|
+
message: error instanceof Error ? error.message : "Unknown error."
|
|
309
|
+
},
|
|
310
|
+
json: options.json
|
|
311
|
+
});
|
|
312
|
+
return 5;
|
|
313
|
+
}
|
|
314
|
+
function writeCliError(io, options) {
|
|
315
|
+
const normalized = normalizeCliError(options.error);
|
|
316
|
+
if (normalized === void 0) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const envelope = {
|
|
320
|
+
ok: false,
|
|
321
|
+
command: options.command,
|
|
322
|
+
data: {},
|
|
323
|
+
warnings: [],
|
|
324
|
+
error: normalized,
|
|
325
|
+
next: options.next ?? []
|
|
326
|
+
};
|
|
327
|
+
if (options.workspace !== void 0) {
|
|
328
|
+
envelope.workspace = options.workspace;
|
|
329
|
+
}
|
|
330
|
+
if (options.json === true) {
|
|
331
|
+
io.writeErr(`${JSON.stringify(envelope)}
|
|
332
|
+
`);
|
|
333
|
+
} else {
|
|
334
|
+
io.writeErr(renderHumanError(normalized.message, options.next ?? []));
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
function writeValidationError(io, options) {
|
|
339
|
+
writeCliError(io, {
|
|
340
|
+
command: options.command,
|
|
341
|
+
error: options.details === void 0 ? {
|
|
342
|
+
code: options.code,
|
|
343
|
+
message: options.message
|
|
344
|
+
} : {
|
|
345
|
+
code: options.code,
|
|
346
|
+
message: options.message,
|
|
347
|
+
details: options.details
|
|
348
|
+
},
|
|
349
|
+
workspace: options.workspace,
|
|
350
|
+
next: options.next ?? [],
|
|
351
|
+
json: options.json
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function renderHumanError(message, next) {
|
|
355
|
+
const nextStep = next[0];
|
|
356
|
+
return nextStep === void 0 ? `${message}
|
|
357
|
+
` : `${message}
|
|
358
|
+
Next: ${nextStep}
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
function normalizeCliError(error) {
|
|
362
|
+
if (isNormalizedCliError(error)) {
|
|
363
|
+
return normalizeObject(error);
|
|
364
|
+
}
|
|
365
|
+
if (error instanceof WorkspaceResolutionError2) {
|
|
366
|
+
return {
|
|
367
|
+
code: error.code,
|
|
368
|
+
message: error.message,
|
|
369
|
+
details: { startDir: error.startDir }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (error instanceof ReadWorkspaceError) {
|
|
373
|
+
const normalized = {
|
|
374
|
+
code: error.code,
|
|
375
|
+
message: error.message
|
|
376
|
+
};
|
|
377
|
+
return Object.keys(error.details).length > 0 ? { ...normalized, details: error.details } : normalized;
|
|
378
|
+
}
|
|
379
|
+
if (error instanceof GraphWorkspaceError) {
|
|
380
|
+
return {
|
|
381
|
+
code: error.code,
|
|
382
|
+
message: error.message,
|
|
383
|
+
details: error.details
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (isErrorWithCode(error)) {
|
|
387
|
+
return {
|
|
388
|
+
code: error.code,
|
|
389
|
+
message: error.message
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
function normalizeObject(error) {
|
|
395
|
+
return error.details === void 0 ? {
|
|
396
|
+
code: error.code,
|
|
397
|
+
message: error.message
|
|
398
|
+
} : error;
|
|
399
|
+
}
|
|
400
|
+
function isCommanderError(error) {
|
|
401
|
+
if (typeof error !== "object" || error === null) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
const candidate = error;
|
|
405
|
+
return typeof candidate.code === "string" && typeof candidate.exitCode === "number" && typeof candidate.message === "string";
|
|
406
|
+
}
|
|
407
|
+
function isErrorWithCode(error) {
|
|
408
|
+
if (typeof error !== "object" || error === null) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const candidate = error;
|
|
412
|
+
return typeof candidate.code === "string" && typeof candidate.message === "string";
|
|
413
|
+
}
|
|
414
|
+
function isNormalizedCliError(error) {
|
|
415
|
+
if (!isErrorWithCode(error)) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
const candidate = error;
|
|
419
|
+
return candidate.details === void 0 || isRecord(candidate.details);
|
|
420
|
+
}
|
|
421
|
+
function isRecord(value) {
|
|
422
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/options/index.ts
|
|
426
|
+
function parseIntegerOption(value) {
|
|
427
|
+
const parsed = Number.parseInt(value, 10);
|
|
428
|
+
if (!Number.isFinite(parsed)) {
|
|
429
|
+
throw new Error(`Expected an integer option value, received: ${value}`);
|
|
430
|
+
}
|
|
431
|
+
return parsed;
|
|
432
|
+
}
|
|
433
|
+
function commandFromArgv(argv) {
|
|
434
|
+
const command = argv.slice(2).find((arg) => !arg.startsWith("-"));
|
|
435
|
+
return command ?? "unknown";
|
|
436
|
+
}
|
|
437
|
+
function parseAgentInstallTarget(input) {
|
|
438
|
+
if (input === "claude" || input === "codex" || input === "all") {
|
|
439
|
+
return input;
|
|
440
|
+
}
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
function parseInitAgentTarget(input) {
|
|
444
|
+
if (input === "none") {
|
|
445
|
+
return "none";
|
|
446
|
+
}
|
|
447
|
+
if (input === "claude,codex" || input === "codex,claude") {
|
|
448
|
+
return "all";
|
|
449
|
+
}
|
|
450
|
+
return parseAgentInstallTarget(input);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/render/result.ts
|
|
454
|
+
function writeResult(io, envelope, json = false) {
|
|
455
|
+
if (json) {
|
|
456
|
+
io.writeOut(`${JSON.stringify(envelope)}
|
|
457
|
+
`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
io.writeOut(renderHumanResult(envelope));
|
|
461
|
+
}
|
|
462
|
+
function renderHumanResult(envelope) {
|
|
463
|
+
if (envelope.command === "search") {
|
|
464
|
+
const data = envelope.data;
|
|
465
|
+
const rows = (data.results ?? []).map((result, index) => {
|
|
466
|
+
const title = result.title ?? "(untitled)";
|
|
467
|
+
const pathValue = result.path ?? "(unknown path)";
|
|
468
|
+
const type = result.type ?? "Unknown";
|
|
469
|
+
const score = result.score === void 0 ? "" : ` score=${result.score}`;
|
|
470
|
+
return `${index + 1}. ${title} [${type}] ${pathValue}${score}`;
|
|
471
|
+
});
|
|
472
|
+
const summary = `Found ${data.totalMatches ?? rows.length}${data.truncated ? " (truncated)" : ""}`;
|
|
473
|
+
return `${summary}
|
|
474
|
+
${rows.join("\n")}${rows.length > 0 ? "\n" : ""}`;
|
|
475
|
+
}
|
|
476
|
+
if (envelope.command === "read") {
|
|
477
|
+
const data = envelope.data;
|
|
478
|
+
const title = data.metadata?.title ?? "(untitled)";
|
|
479
|
+
const type = data.metadata?.type ?? "Unknown";
|
|
480
|
+
const pathValue = data.target?.path ?? "(unknown path)";
|
|
481
|
+
const truncated = data.content?.truncated ? " truncated" : "";
|
|
482
|
+
return `${title} [${type}] ${pathValue}${truncated}
|
|
483
|
+
|
|
484
|
+
${data.content?.text ?? ""}
|
|
485
|
+
`;
|
|
486
|
+
}
|
|
487
|
+
if (envelope.command === "graph") {
|
|
488
|
+
const data = envelope.data;
|
|
489
|
+
return `Graph report: ${data.report?.htmlPath ?? "(not written)"}
|
|
490
|
+
Backlinks: ${data.report?.backlinksPath ?? "(not written)"}
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
if (envelope.command === "doctor") {
|
|
494
|
+
const data = envelope.data;
|
|
495
|
+
const summary = data.summary ?? {};
|
|
496
|
+
const rows = (data.checks ?? []).map((check) => {
|
|
497
|
+
const label = check.label ?? "Check";
|
|
498
|
+
const status = (check.status ?? "unknown").toUpperCase();
|
|
499
|
+
const message = check.message ?? "";
|
|
500
|
+
return `${status} ${label}: ${message}`;
|
|
501
|
+
});
|
|
502
|
+
return `Doctor: ${summary.pass ?? 0} pass, ${summary.warn ?? 0} warn, ${summary.fail ?? 0} fail, ${summary.skip ?? 0} skip
|
|
503
|
+
${rows.join("\n")}${rows.length > 0 ? "\n" : ""}`;
|
|
504
|
+
}
|
|
505
|
+
if (!envelope.ok) {
|
|
506
|
+
return `FAILED ${envelope.command}
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
return `${envelope.ok ? "OK" : "FAILED"} ${envelope.command}
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/index.ts
|
|
514
|
+
var packageInfo = {
|
|
515
|
+
name: "@okf-harness/cli",
|
|
516
|
+
role: "cli"
|
|
517
|
+
};
|
|
518
|
+
async function runCli(argv = process.argv, io = {
|
|
519
|
+
writeOut: (chunk) => process.stdout.write(chunk),
|
|
520
|
+
writeErr: (chunk) => process.stderr.write(chunk)
|
|
521
|
+
}) {
|
|
522
|
+
const program = new Command();
|
|
523
|
+
let exitCode = 0;
|
|
524
|
+
const jsonRequested = argv.includes("--json");
|
|
525
|
+
const capturedCommanderErrors = [];
|
|
526
|
+
program.name("okfh").description("OKF Harness command line interface.");
|
|
527
|
+
program.exitOverride();
|
|
528
|
+
program.command("init <workspace>").description("Initialize an OKF Harness workspace.").storeOptionsAsProperties(false).requiredOption("--name <name>", "workspace display name").option("--agents <agents>", "agent adapters to install: claude, codex, all, none", "all").option("--dry-run", "return the planned writes without creating files").option("--git", "initialize a git repository without committing").option("--json", "write machine-readable JSON").action(async (workspace, command) => {
|
|
529
|
+
const options = command.opts();
|
|
530
|
+
const agentTarget = parseInitAgentTarget(options.agents);
|
|
531
|
+
if (agentTarget === void 0) {
|
|
532
|
+
writeValidationError(io, {
|
|
533
|
+
command: "init",
|
|
534
|
+
code: "INVALID_AGENT_TARGET",
|
|
535
|
+
message: "Agents must be one of: claude, codex, all, none, claude,codex.",
|
|
536
|
+
workspace: path2.resolve(workspace),
|
|
537
|
+
next: ["Rerun okfh init with --agents all, claude, codex, none, or claude,codex."],
|
|
538
|
+
json: options.json === true
|
|
539
|
+
});
|
|
540
|
+
exitCode = 1;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
let result;
|
|
544
|
+
try {
|
|
545
|
+
result = await initWorkspace({
|
|
546
|
+
workspaceRoot: workspace,
|
|
547
|
+
name: options.name,
|
|
548
|
+
dryRun: options.dryRun === true,
|
|
549
|
+
git: options.git === true
|
|
550
|
+
});
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof WorkspaceInitError2) {
|
|
553
|
+
writeCliError(io, {
|
|
554
|
+
command: "init",
|
|
555
|
+
error,
|
|
556
|
+
workspace: path2.resolve(workspace),
|
|
557
|
+
next: error.code === "INIT_NOT_EMPTY" ? ["Choose an empty directory, or run okfh doctor --workspace <path> --json."] : ["Fix the initialization input and rerun okfh init --json."],
|
|
558
|
+
json: options.json === true
|
|
559
|
+
});
|
|
560
|
+
exitCode = error.code === "DEPENDENCY_MISSING" ? 4 : 1;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
const agentInstall = agentTarget === "none" ? void 0 : await installAgentAdapters({
|
|
566
|
+
workspaceRoot: result.workspaceRoot,
|
|
567
|
+
adapter: agentTarget,
|
|
568
|
+
dryRun: result.dryRun
|
|
569
|
+
});
|
|
570
|
+
const ok = result.lint.ok && (agentInstall?.conflicts.length ?? 0) === 0;
|
|
571
|
+
const plannedFiles = uniqueStrings(
|
|
572
|
+
result.dryRun ? [...result.files, ...agentInstall?.plannedFiles ?? []] : []
|
|
573
|
+
);
|
|
574
|
+
const files = uniqueStrings(
|
|
575
|
+
result.dryRun ? result.files : [
|
|
576
|
+
...result.files,
|
|
577
|
+
...agentInstall?.writtenFiles ?? [],
|
|
578
|
+
...agentInstall?.replacedFiles ?? []
|
|
579
|
+
]
|
|
580
|
+
);
|
|
581
|
+
const envelope = {
|
|
582
|
+
ok,
|
|
583
|
+
command: "init",
|
|
584
|
+
workspace: result.workspaceRoot,
|
|
585
|
+
data: {
|
|
586
|
+
name: result.name,
|
|
587
|
+
dryRun: result.dryRun,
|
|
588
|
+
git: result.git,
|
|
589
|
+
agents: renderInitAgentData(agentTarget, agentInstall),
|
|
590
|
+
files,
|
|
591
|
+
plannedFiles,
|
|
592
|
+
directories: result.directories,
|
|
593
|
+
lint: result.lint
|
|
594
|
+
},
|
|
595
|
+
warnings: filterAgentPackPendingWarnings(result.warnings),
|
|
596
|
+
next: [
|
|
597
|
+
"Use the generated OKF Harness skills from Claude Code or Codex.",
|
|
598
|
+
"Run okfh lint --workspace <path> --json after editing wiki files."
|
|
599
|
+
]
|
|
600
|
+
};
|
|
601
|
+
writeResult(io, envelope, options.json);
|
|
602
|
+
exitCode = ok ? 0 : 1;
|
|
603
|
+
});
|
|
604
|
+
program.command("status").description("Report OKF Harness workspace status.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (command) => {
|
|
605
|
+
const options = command.opts();
|
|
606
|
+
const workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
|
|
607
|
+
const result = await readWorkspaceStatus2(workspaceRoot);
|
|
608
|
+
const envelope = {
|
|
609
|
+
ok: result.initialized && result.lint.ok,
|
|
610
|
+
command: "status",
|
|
611
|
+
workspace: result.workspaceRoot,
|
|
612
|
+
data: {
|
|
613
|
+
initialized: result.initialized,
|
|
614
|
+
name: result.name,
|
|
615
|
+
wikiFiles: result.wikiFiles,
|
|
616
|
+
concepts: result.concepts,
|
|
617
|
+
lint: result.lint,
|
|
618
|
+
capabilities: {
|
|
619
|
+
search: "available",
|
|
620
|
+
read: "available",
|
|
621
|
+
graph: "available",
|
|
622
|
+
queryCommand: "not_available"
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
warnings: filterAgentPackPendingWarnings(result.warnings),
|
|
626
|
+
next: result.initialized ? ["Use okfh search and okfh read to answer from the synthesized wiki."] : []
|
|
627
|
+
};
|
|
628
|
+
writeResult(io, envelope, options.json);
|
|
629
|
+
exitCode = envelope.ok ? 0 : 1;
|
|
630
|
+
});
|
|
631
|
+
program.command("lint").description("Lint an OKF Harness workspace.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (command) => {
|
|
632
|
+
const options = command.opts();
|
|
633
|
+
const workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
|
|
634
|
+
const lint = await lintWorkspace(workspaceRoot);
|
|
635
|
+
const envelope = {
|
|
636
|
+
ok: lint.ok,
|
|
637
|
+
command: "lint",
|
|
638
|
+
workspace: workspaceRoot,
|
|
639
|
+
data: lint,
|
|
640
|
+
warnings: [],
|
|
641
|
+
next: lint.ok ? [] : ["Fix lint errors and rerun okfh lint --workspace <path> --json."]
|
|
642
|
+
};
|
|
643
|
+
writeResult(io, envelope, options.json);
|
|
644
|
+
exitCode = lint.ok ? 0 : 1;
|
|
645
|
+
});
|
|
646
|
+
program.command("search <query>").description("Search synthesized OKF wiki concept documents.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--limit <number>", "maximum results to return", parseIntegerOption).option("--json", "write machine-readable JSON").action(async (query, command) => {
|
|
647
|
+
const options = command.opts();
|
|
648
|
+
let workspaceRoot = null;
|
|
649
|
+
try {
|
|
650
|
+
workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
|
|
651
|
+
const result = await searchWorkspace({
|
|
652
|
+
workspaceRoot,
|
|
653
|
+
query,
|
|
654
|
+
limit: options.limit
|
|
655
|
+
});
|
|
656
|
+
const { workspaceRoot: _workspaceRoot, warnings, ...data } = result;
|
|
657
|
+
const envelope = {
|
|
658
|
+
ok: true,
|
|
659
|
+
command: "search",
|
|
660
|
+
workspace: result.workspaceRoot,
|
|
661
|
+
data,
|
|
662
|
+
warnings,
|
|
663
|
+
next: result.totalMatches === 0 ? [
|
|
664
|
+
"Run okfh read index --json to inspect the wiki map.",
|
|
665
|
+
"Try broader keywords, or ingest sources first if the material is only registered raw source."
|
|
666
|
+
] : ["Run okfh read <concept-id> --json for the most relevant candidate."]
|
|
667
|
+
};
|
|
668
|
+
writeResult(io, envelope, options.json);
|
|
669
|
+
exitCode = 0;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
const handled = writeCliError(io, {
|
|
672
|
+
command: "search",
|
|
673
|
+
error,
|
|
674
|
+
workspace: workspaceRoot,
|
|
675
|
+
next: ["Check the workspace path and rerun okfh search --json."],
|
|
676
|
+
json: options.json === true
|
|
677
|
+
});
|
|
678
|
+
if (handled) {
|
|
679
|
+
exitCode = 1;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
program.command("read <target>").description("Read a bounded OKF wiki document.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--section <heading>", "read a section by heading").option("--section-id <id>", "read a section by stable section id").option("--offset <number>", "read from a character offset", parseIntegerOption).option("--limit <number>", "maximum characters for range reads", parseIntegerOption).option("--full", "explicitly request a full bounded read").option("--json", "write machine-readable JSON").action(async (target, command) => {
|
|
686
|
+
const options = command.opts();
|
|
687
|
+
let workspaceRoot = null;
|
|
688
|
+
try {
|
|
689
|
+
workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
|
|
690
|
+
const result = await readWorkspaceDocument({
|
|
691
|
+
workspaceRoot,
|
|
692
|
+
target,
|
|
693
|
+
section: options.section,
|
|
694
|
+
sectionId: options.sectionId,
|
|
695
|
+
offset: options.offset,
|
|
696
|
+
limit: options.limit,
|
|
697
|
+
full: options.full === true
|
|
698
|
+
});
|
|
699
|
+
const { workspaceRoot: _workspaceRoot, warnings, ...data } = result;
|
|
700
|
+
const envelope = {
|
|
701
|
+
ok: true,
|
|
702
|
+
command: "read",
|
|
703
|
+
workspace: result.workspaceRoot,
|
|
704
|
+
data,
|
|
705
|
+
warnings,
|
|
706
|
+
next: result.content.truncated ? ["Use --section, --section-id, --offset/--limit, or --full to continue reading."] : []
|
|
707
|
+
};
|
|
708
|
+
writeResult(io, envelope, options.json);
|
|
709
|
+
exitCode = 0;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
const handled = writeCliError(io, {
|
|
712
|
+
command: "read",
|
|
713
|
+
error,
|
|
714
|
+
workspace: workspaceRoot,
|
|
715
|
+
next: ["Run okfh search with broader keywords, then read one returned concept path."],
|
|
716
|
+
json: options.json === true
|
|
717
|
+
});
|
|
718
|
+
if (handled) {
|
|
719
|
+
exitCode = 1;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
program.command("graph").description("Generate OKF backlinks data and a self-contained graph report.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--open", "open the generated graph report in the default macOS browser").option("--json", "write machine-readable JSON").action(async (command) => {
|
|
726
|
+
const options = command.opts();
|
|
727
|
+
let workspaceRoot = null;
|
|
728
|
+
try {
|
|
729
|
+
workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
|
|
730
|
+
const result = await buildWorkspaceGraph({ workspaceRoot });
|
|
731
|
+
if (options.open === true) {
|
|
732
|
+
await openGraphReport(result.report.htmlPath);
|
|
733
|
+
}
|
|
734
|
+
const { workspaceRoot: _workspaceRoot, ...data } = result;
|
|
735
|
+
const envelope = {
|
|
736
|
+
ok: true,
|
|
737
|
+
command: "graph",
|
|
738
|
+
workspace: result.workspaceRoot,
|
|
739
|
+
data,
|
|
740
|
+
warnings: [],
|
|
741
|
+
next: options.open === true ? [] : ["Open the graph HTML report in a browser if needed."]
|
|
742
|
+
};
|
|
743
|
+
writeResult(io, envelope, options.json);
|
|
744
|
+
exitCode = 0;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
const handled = writeCliError(io, {
|
|
747
|
+
command: "graph",
|
|
748
|
+
error,
|
|
749
|
+
workspace: workspaceRoot,
|
|
750
|
+
next: ["Check write permissions under .okfh and rerun okfh graph --json."],
|
|
751
|
+
json: options.json === true
|
|
752
|
+
});
|
|
753
|
+
if (handled) {
|
|
754
|
+
exitCode = 1;
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
program.command("doctor").description("Check okfh, local shell dependencies, and workspace readiness.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (command) => {
|
|
761
|
+
const options = command.opts();
|
|
762
|
+
const result = await runDoctor({ workspaceRoot: options.workspace });
|
|
763
|
+
const envelope = {
|
|
764
|
+
ok: result.ok,
|
|
765
|
+
command: "doctor",
|
|
766
|
+
workspace: result.workspace,
|
|
767
|
+
data: {
|
|
768
|
+
checks: result.checks,
|
|
769
|
+
summary: result.summary
|
|
770
|
+
},
|
|
771
|
+
warnings: result.checks.filter((check) => check.status === "warn").map((check) => ({
|
|
772
|
+
code: check.id.toUpperCase().replaceAll("-", "_"),
|
|
773
|
+
message: check.message
|
|
774
|
+
})),
|
|
775
|
+
next: result.ok ? ["Use okfh --json commands through the local shell for OKF Harness workflows."] : ["Fix failed checks, then rerun okfh doctor --json."]
|
|
776
|
+
};
|
|
777
|
+
writeResult(io, envelope, options.json);
|
|
778
|
+
exitCode = result.ok ? 0 : 1;
|
|
779
|
+
});
|
|
780
|
+
program.command("source <action> [input]").description("Register and list OKF Harness raw sources.").storeOptionsAsProperties(false).requiredOption("--workspace <path>", "workspace path").option("--dry-run", "return the planned source registration without writing files").option("--json", "write machine-readable JSON").action(async (actionInput, input, command) => {
|
|
781
|
+
const options = command.opts();
|
|
782
|
+
if (actionInput === "list") {
|
|
783
|
+
try {
|
|
784
|
+
const result = await listSources({ workspaceRoot: options.workspace });
|
|
785
|
+
const envelope = {
|
|
786
|
+
ok: true,
|
|
787
|
+
command: "source list",
|
|
788
|
+
workspace: result.workspaceRoot,
|
|
789
|
+
data: { sources: result.sources },
|
|
790
|
+
warnings: [],
|
|
791
|
+
next: []
|
|
792
|
+
};
|
|
793
|
+
writeResult(io, envelope, options.json);
|
|
794
|
+
exitCode = 0;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (error instanceof SourceManagementError) {
|
|
797
|
+
writeCliError(io, {
|
|
798
|
+
command: "source list",
|
|
799
|
+
error,
|
|
800
|
+
workspace: path2.resolve(options.workspace),
|
|
801
|
+
next: ["Check the workspace path and rerun okfh source list --json."],
|
|
802
|
+
json: options.json === true
|
|
803
|
+
});
|
|
804
|
+
exitCode = 1;
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (actionInput !== "add") {
|
|
812
|
+
writeValidationError(io, {
|
|
813
|
+
command: "source",
|
|
814
|
+
code: "INVALID_SOURCE_ACTION",
|
|
815
|
+
message: "Source action must be one of: add, list.",
|
|
816
|
+
workspace: path2.resolve(options.workspace),
|
|
817
|
+
next: ["Use okfh source add <path-or-url> --workspace <path> --json."],
|
|
818
|
+
json: options.json === true
|
|
819
|
+
});
|
|
820
|
+
exitCode = 1;
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (input === void 0) {
|
|
824
|
+
writeValidationError(io, {
|
|
825
|
+
command: "source add",
|
|
826
|
+
code: "SOURCE_INPUT_REQUIRED",
|
|
827
|
+
message: "source add requires a file path or URL.",
|
|
828
|
+
workspace: path2.resolve(options.workspace),
|
|
829
|
+
next: ["Pass a local file path or URL to okfh source add."],
|
|
830
|
+
json: options.json === true
|
|
831
|
+
});
|
|
832
|
+
exitCode = 2;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const result = await addSource({
|
|
837
|
+
workspaceRoot: options.workspace,
|
|
838
|
+
input,
|
|
839
|
+
dryRun: options.dryRun === true
|
|
840
|
+
});
|
|
841
|
+
const envelope = {
|
|
842
|
+
ok: true,
|
|
843
|
+
command: "source add",
|
|
844
|
+
workspace: result.workspaceRoot,
|
|
845
|
+
data: {
|
|
846
|
+
action: result.action,
|
|
847
|
+
dryRun: result.dryRun,
|
|
848
|
+
source: result.source
|
|
849
|
+
},
|
|
850
|
+
warnings: [],
|
|
851
|
+
next: [`Run okfh ingest plan ${result.source.id} --workspace <path> --json.`]
|
|
852
|
+
};
|
|
853
|
+
writeResult(io, envelope, options.json);
|
|
854
|
+
exitCode = 0;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (error instanceof SourceManagementError) {
|
|
857
|
+
writeCliError(io, {
|
|
858
|
+
command: "source add",
|
|
859
|
+
error,
|
|
860
|
+
workspace: path2.resolve(options.workspace),
|
|
861
|
+
next: ["Check the source input and workspace path, then rerun okfh source add --json."],
|
|
862
|
+
json: options.json === true
|
|
863
|
+
});
|
|
864
|
+
exitCode = error.code === "SOURCE_INPUT_UNSUPPORTED" ? 1 : 5;
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
program.command("ingest <action> <source>").description("Plan source ingestion into the OKF wiki.").storeOptionsAsProperties(false).requiredOption("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (actionInput, sourceInput, command) => {
|
|
871
|
+
const options = command.opts();
|
|
872
|
+
if (actionInput !== "plan") {
|
|
873
|
+
writeValidationError(io, {
|
|
874
|
+
command: "ingest",
|
|
875
|
+
code: "INVALID_INGEST_ACTION",
|
|
876
|
+
message: "Ingest action must be: plan.",
|
|
877
|
+
workspace: path2.resolve(options.workspace),
|
|
878
|
+
next: ["Use okfh ingest plan <source-id-or-path> --workspace <path> --json."],
|
|
879
|
+
json: options.json === true
|
|
880
|
+
});
|
|
881
|
+
exitCode = 1;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const result = await createIngestPlan({
|
|
886
|
+
workspaceRoot: options.workspace,
|
|
887
|
+
source: sourceInput
|
|
888
|
+
});
|
|
889
|
+
const envelope = {
|
|
890
|
+
ok: true,
|
|
891
|
+
command: "ingest plan",
|
|
892
|
+
workspace: result.workspaceRoot,
|
|
893
|
+
data: {
|
|
894
|
+
source: result.source,
|
|
895
|
+
recommendedReferencePath: result.recommendedReferencePath,
|
|
896
|
+
candidateConcepts: result.candidateConcepts,
|
|
897
|
+
checklist: result.checklist
|
|
898
|
+
},
|
|
899
|
+
warnings: [],
|
|
900
|
+
next: ["Use the ingest plan as the Agent checklist before editing wiki files."]
|
|
901
|
+
};
|
|
902
|
+
writeResult(io, envelope, options.json);
|
|
903
|
+
exitCode = 0;
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if (error instanceof SourceManagementError) {
|
|
906
|
+
writeCliError(io, {
|
|
907
|
+
command: "ingest plan",
|
|
908
|
+
error,
|
|
909
|
+
workspace: path2.resolve(options.workspace),
|
|
910
|
+
next: ["Register the source first with okfh source add, then rerun okfh ingest plan."],
|
|
911
|
+
json: options.json === true
|
|
912
|
+
});
|
|
913
|
+
exitCode = error.code === "SOURCE_NOT_REGISTERED" ? 1 : 5;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
program.command("agent <action> <adapter>").description("Install or repair Claude Code and Codex adapter files.").storeOptionsAsProperties(false).requiredOption("--workspace <path>", "workspace path").option("--dry-run", "return the planned writes without creating files").option("--force", "replace conflicting same-name adapter files").option("--json", "write machine-readable JSON").action(async (actionInput, adapterInput, command) => {
|
|
920
|
+
const options = command.opts();
|
|
921
|
+
if (actionInput !== "install") {
|
|
922
|
+
writeValidationError(io, {
|
|
923
|
+
command: "agent",
|
|
924
|
+
code: "INVALID_AGENT_ACTION",
|
|
925
|
+
message: "Agent action must be: install.",
|
|
926
|
+
workspace: path2.resolve(options.workspace),
|
|
927
|
+
next: ["Use okfh agent install claude|codex|all --workspace <path> --json."],
|
|
928
|
+
json: options.json === true
|
|
929
|
+
});
|
|
930
|
+
exitCode = 1;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const adapter = parseAgentInstallTarget(adapterInput);
|
|
934
|
+
if (adapter === void 0) {
|
|
935
|
+
writeValidationError(io, {
|
|
936
|
+
command: "agent install",
|
|
937
|
+
code: "INVALID_AGENT_ADAPTER",
|
|
938
|
+
message: "Adapter must be one of: claude, codex, all.",
|
|
939
|
+
workspace: path2.resolve(options.workspace),
|
|
940
|
+
next: ["Rerun with adapter claude, codex, or all."],
|
|
941
|
+
json: options.json === true
|
|
942
|
+
});
|
|
943
|
+
exitCode = 1;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const workspaceStatus = await readWorkspaceStatus2(options.workspace);
|
|
947
|
+
if (!workspaceStatus.initialized) {
|
|
948
|
+
writeValidationError(io, {
|
|
949
|
+
command: "agent install",
|
|
950
|
+
code: "WORKSPACE_NOT_INITIALIZED",
|
|
951
|
+
message: "Workspace is not initialized. Run okfh init first.",
|
|
952
|
+
workspace: workspaceStatus.workspaceRoot,
|
|
953
|
+
next: ["Run okfh init <workspace> --name <name> --agents all --json first."],
|
|
954
|
+
json: options.json === true
|
|
955
|
+
});
|
|
956
|
+
exitCode = 1;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const result = await installAgentAdapters({
|
|
960
|
+
workspaceRoot: workspaceStatus.workspaceRoot,
|
|
961
|
+
adapter,
|
|
962
|
+
dryRun: options.dryRun === true,
|
|
963
|
+
force: options.force === true
|
|
964
|
+
});
|
|
965
|
+
const ok = result.conflicts.length === 0;
|
|
966
|
+
const envelope = {
|
|
967
|
+
ok,
|
|
968
|
+
command: "agent install",
|
|
969
|
+
workspace: workspaceStatus.workspaceRoot,
|
|
970
|
+
data: result,
|
|
971
|
+
warnings: [],
|
|
972
|
+
next: ok ? ["Run okfh status --workspace <path> --json to verify the workspace."] : ["Resolve conflicts or rerun with --force after reviewing the files."]
|
|
973
|
+
};
|
|
974
|
+
writeResult(io, envelope, options.json);
|
|
975
|
+
exitCode = ok ? 0 : 1;
|
|
976
|
+
});
|
|
977
|
+
const restoreConsoleError = captureCommanderConsoleError(capturedCommanderErrors);
|
|
978
|
+
try {
|
|
979
|
+
await program.parseAsync(argv);
|
|
980
|
+
return exitCode;
|
|
981
|
+
} catch (error) {
|
|
982
|
+
return handleCliError(error, io, {
|
|
983
|
+
command: commandFromArgv(argv),
|
|
984
|
+
json: jsonRequested,
|
|
985
|
+
capturedStderr: capturedCommanderErrors.join("")
|
|
986
|
+
});
|
|
987
|
+
} finally {
|
|
988
|
+
restoreConsoleError();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async function openGraphReport(htmlPath) {
|
|
992
|
+
await new Promise((resolve, reject) => {
|
|
993
|
+
execFile2("open", [htmlPath], (error) => {
|
|
994
|
+
if (error !== null) {
|
|
995
|
+
reject(error);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
resolve();
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
function captureCommanderConsoleError(capturedErrors) {
|
|
1003
|
+
const originalConsoleError = console.error;
|
|
1004
|
+
console.error = (...args) => {
|
|
1005
|
+
capturedErrors.push(`${args.map((arg) => String(arg)).join(" ")}
|
|
1006
|
+
`);
|
|
1007
|
+
};
|
|
1008
|
+
return () => {
|
|
1009
|
+
console.error = originalConsoleError;
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function renderInitAgentData(requested, install) {
|
|
1013
|
+
if (install === void 0) {
|
|
1014
|
+
return { requested };
|
|
1015
|
+
}
|
|
1016
|
+
return { requested, install };
|
|
1017
|
+
}
|
|
1018
|
+
function filterAgentPackPendingWarnings(warnings) {
|
|
1019
|
+
return warnings.filter((warning) => warning.code !== "AGENT_PACK_PENDING");
|
|
1020
|
+
}
|
|
1021
|
+
function uniqueStrings(values) {
|
|
1022
|
+
return [...new Set(values)];
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export {
|
|
1026
|
+
packageInfo,
|
|
1027
|
+
runCli
|
|
1028
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type CliIo = {
|
|
2
|
+
writeOut: (chunk: string) => void;
|
|
3
|
+
writeErr: (chunk: string) => void;
|
|
4
|
+
};
|
|
5
|
+
type JsonEnvelope = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
command: string;
|
|
8
|
+
workspace?: string | null;
|
|
9
|
+
data: unknown;
|
|
10
|
+
warnings: Array<{
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
next: string[];
|
|
15
|
+
error?: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
details?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
declare const packageInfo: {
|
|
23
|
+
readonly name: "@okf-harness/cli";
|
|
24
|
+
readonly role: "cli";
|
|
25
|
+
};
|
|
26
|
+
type PackageInfo = typeof packageInfo;
|
|
27
|
+
declare function runCli(argv?: string[], io?: CliIo): Promise<number>;
|
|
28
|
+
|
|
29
|
+
export { type CliIo, type JsonEnvelope, type PackageInfo, packageInfo, runCli };
|
package/dist/index.js
ADDED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/main.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@okf-harness/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The okfh command-line package for local OKF Harness workspaces.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Eric Zhou",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.0.0"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/pumblus/okf-harness#readme",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/pumblus/okf-harness.git",
|
|
15
|
+
"directory": "packages/cli"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/pumblus/okf-harness/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"okf",
|
|
22
|
+
"open-knowledge-format",
|
|
23
|
+
"llm-wiki",
|
|
24
|
+
"agent-skills",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"codex",
|
|
27
|
+
"macos",
|
|
28
|
+
"knowledge-management"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"bin": {
|
|
36
|
+
"okfh": "dist/main.js",
|
|
37
|
+
"okf-harness": "dist/main.js"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "pnpm --dir ../.. --filter @okf-harness/core build && pnpm --dir ../.. --filter @okf-harness/agent-pack build && tsup src/index.ts src/main.ts --format esm --dts --clean",
|
|
50
|
+
"prepublishOnly": "pnpm run build"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@okf-harness/agent-pack": "0.1.0",
|
|
54
|
+
"@okf-harness/core": "0.1.0",
|
|
55
|
+
"commander": "4.1.1"
|
|
56
|
+
}
|
|
57
|
+
}
|