@cardor/agent-harness-kit 2.1.0 → 2.1.1
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 -3
- package/dist/cli.js +309 -230
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,11 +16,11 @@ import {
|
|
|
16
16
|
|
|
17
17
|
// src/cli.ts
|
|
18
18
|
import { Command, InvalidArgumentError } from "commander";
|
|
19
|
-
import
|
|
19
|
+
import pc21 from "picocolors";
|
|
20
20
|
|
|
21
21
|
// src/commands/build.ts
|
|
22
22
|
import { watch } from "fs";
|
|
23
|
-
import * as
|
|
23
|
+
import * as p2 from "@clack/prompts";
|
|
24
24
|
import pc2 from "picocolors";
|
|
25
25
|
|
|
26
26
|
// src/core/materializer/claude-code.ts
|
|
@@ -53,31 +53,39 @@ import { fileURLToPath } from "url";
|
|
|
53
53
|
var require2 = createRequire(import.meta.url);
|
|
54
54
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
55
55
|
var candidates = [join2(here, "..", "..", "package.json"), join2(here, "..", "package.json")];
|
|
56
|
-
var pkgPath = candidates.find((
|
|
56
|
+
var pkgPath = candidates.find((p9) => existsSync(p9)) ?? candidates[0];
|
|
57
57
|
var pkg = require2(pkgPath);
|
|
58
58
|
|
|
59
59
|
// src/core/local-install-guard.ts
|
|
60
60
|
function isLocalInstallSatisfied(cwd2) {
|
|
61
61
|
const selfPkgPath = join3(cwd2, "package.json");
|
|
62
|
-
let projectPkg = null;
|
|
63
62
|
if (existsSync2(selfPkgPath)) {
|
|
64
63
|
try {
|
|
65
64
|
const selfPkg = JSON.parse(readFileSync(selfPkgPath, "utf8"));
|
|
66
65
|
if (selfPkg?.name === pkg.name) return true;
|
|
67
|
-
projectPkg = selfPkg;
|
|
68
66
|
} catch {
|
|
69
67
|
}
|
|
70
68
|
}
|
|
69
|
+
return hasRealLocalInstall(cwd2);
|
|
70
|
+
}
|
|
71
|
+
function hasRealLocalInstall(cwd2) {
|
|
71
72
|
const [scope, name] = pkg.name.split("/");
|
|
72
73
|
const localPath = pkg.name.startsWith("@") ? join3(cwd2, "node_modules", scope, name) : join3(cwd2, "node_modules", pkg.name);
|
|
73
74
|
if (existsSync2(localPath)) return true;
|
|
74
75
|
const isPnp = existsSync2(join3(cwd2, ".pnp.cjs")) || existsSync2(join3(cwd2, ".pnp.loader.mjs"));
|
|
75
|
-
if (isPnp
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
if (isPnp) {
|
|
77
|
+
const pkgPath2 = join3(cwd2, "package.json");
|
|
78
|
+
if (existsSync2(pkgPath2)) {
|
|
79
|
+
try {
|
|
80
|
+
const projectPkg = JSON.parse(readFileSync(pkgPath2, "utf8"));
|
|
81
|
+
const deps = {
|
|
82
|
+
...projectPkg?.dependencies ?? {},
|
|
83
|
+
...projectPkg?.devDependencies ?? {}
|
|
84
|
+
};
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
90
|
return false;
|
|
83
91
|
}
|
|
@@ -132,7 +140,7 @@ function detectFromPackageManagerField(cwd2) {
|
|
|
132
140
|
}
|
|
133
141
|
function getMcpCommandParts(pm, port, cwd2) {
|
|
134
142
|
const portStr = String(port);
|
|
135
|
-
if (!
|
|
143
|
+
if (!hasRealLocalInstall(cwd2)) {
|
|
136
144
|
return ["ahk", "serve", "--port", portStr];
|
|
137
145
|
}
|
|
138
146
|
switch (pm) {
|
|
@@ -1083,7 +1091,7 @@ No tasks in progress.
|
|
|
1083
1091
|
],
|
|
1084
1092
|
{ force: opts.force, backupRoot: join7(cwd2, config.storage.dir, "backups") }
|
|
1085
1093
|
);
|
|
1086
|
-
const agents = writeAgentFiles(cwd2, claudeAgentFiles(config), {
|
|
1094
|
+
const agents = writeAgentFiles(cwd2, claudeAgentFiles(config, opts.claudeAgentModels), {
|
|
1087
1095
|
force: opts.force,
|
|
1088
1096
|
backupRoot: join7(cwd2, config.storage.dir, "backups")
|
|
1089
1097
|
});
|
|
@@ -1326,20 +1334,53 @@ function getMaterializer(provider) {
|
|
|
1326
1334
|
}
|
|
1327
1335
|
}
|
|
1328
1336
|
|
|
1337
|
+
// src/commands/claude-model-prompt.ts
|
|
1338
|
+
import * as p from "@clack/prompts";
|
|
1339
|
+
var AGENT_LABELS = [
|
|
1340
|
+
{ key: "lead", label: "Lead" },
|
|
1341
|
+
{ key: "explorer", label: "Explorer" },
|
|
1342
|
+
{ key: "consultant", label: "Consultant" },
|
|
1343
|
+
{ key: "builder", label: "Builder" },
|
|
1344
|
+
{ key: "reviewer", label: "Reviewer" }
|
|
1345
|
+
];
|
|
1346
|
+
async function promptClaudeAgentModels(provider) {
|
|
1347
|
+
const claudeAgentModels = {};
|
|
1348
|
+
if (provider !== "claude-code") return claudeAgentModels;
|
|
1349
|
+
for (const agent of AGENT_LABELS) {
|
|
1350
|
+
const val = await p.select({
|
|
1351
|
+
message: `Model for ${agent.label}`,
|
|
1352
|
+
options: [
|
|
1353
|
+
{ value: "inherit", label: "inherit (default)" },
|
|
1354
|
+
{ value: "haiku", label: "haiku" },
|
|
1355
|
+
{ value: "sonnet", label: "sonnet" },
|
|
1356
|
+
{ value: "opus", label: "opus" },
|
|
1357
|
+
{ value: "fable", label: "fable" }
|
|
1358
|
+
],
|
|
1359
|
+
initialValue: "inherit"
|
|
1360
|
+
});
|
|
1361
|
+
if (p.isCancel(val)) {
|
|
1362
|
+
p.cancel("Cancelled.");
|
|
1363
|
+
process.exit(0);
|
|
1364
|
+
}
|
|
1365
|
+
claudeAgentModels[agent.key] = val;
|
|
1366
|
+
}
|
|
1367
|
+
return claudeAgentModels;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1329
1370
|
// src/commands/build.ts
|
|
1330
1371
|
async function runBuild(cwd2, opts) {
|
|
1331
1372
|
await buildOnce(cwd2, opts.force);
|
|
1332
1373
|
if (opts.sync) {
|
|
1333
|
-
|
|
1374
|
+
p2.log.step("Syncing agent permissions...");
|
|
1334
1375
|
const config = await loadConfig(cwd2);
|
|
1335
1376
|
const materializer = getMaterializer(config.provider);
|
|
1336
1377
|
await materializer.syncPermissions(cwd2);
|
|
1337
1378
|
}
|
|
1338
1379
|
if (opts.watch) {
|
|
1339
|
-
|
|
1380
|
+
p2.log.info(`Watching agent-harness-kit.config.ts for changes...`);
|
|
1340
1381
|
watch(cwd2, { recursive: false }, async (_, filename) => {
|
|
1341
1382
|
if (filename?.startsWith("agent-harness-kit.config")) {
|
|
1342
|
-
|
|
1383
|
+
p2.log.step("Config changed \u2014 rebuilding...");
|
|
1343
1384
|
await buildOnce(cwd2, false);
|
|
1344
1385
|
}
|
|
1345
1386
|
});
|
|
@@ -1348,36 +1389,45 @@ async function runBuild(cwd2, opts) {
|
|
|
1348
1389
|
}
|
|
1349
1390
|
}
|
|
1350
1391
|
async function buildOnce(cwd2, force) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1392
|
+
let config;
|
|
1393
|
+
try {
|
|
1394
|
+
config = await loadConfig(cwd2);
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
let claudeAgentModels;
|
|
1400
|
+
if (force && config.provider === "claude-code") {
|
|
1401
|
+
claudeAgentModels = await promptClaudeAgentModels(config.provider);
|
|
1402
|
+
}
|
|
1403
|
+
const spinner6 = p2.spinner();
|
|
1404
|
+
spinner6.start("Rebuilding files...");
|
|
1353
1405
|
try {
|
|
1354
|
-
const config = await loadConfig(cwd2);
|
|
1355
|
-
spinner6.message("Rebuilding files...");
|
|
1356
1406
|
const materializer = getMaterializer(config.provider);
|
|
1357
|
-
const report = await materializer.build(config, cwd2, { force });
|
|
1407
|
+
const report = await materializer.build(config, cwd2, { force, claudeAgentModels });
|
|
1358
1408
|
spinner6.stop(pc2.green("Build complete"));
|
|
1359
1409
|
const d = report.derived;
|
|
1360
1410
|
const upToDate = [...d.created, ...d.current, ...d.propagated];
|
|
1361
1411
|
if (upToDate.length > 0) {
|
|
1362
|
-
|
|
1412
|
+
p2.log.success(upToDate.join(", "));
|
|
1363
1413
|
}
|
|
1364
1414
|
if (d.propagated.length > 0) {
|
|
1365
|
-
|
|
1415
|
+
p2.log.info(`Propagated config changes to ${d.propagated.length} generated file(s):
|
|
1366
1416
|
${d.propagated.join("\n ")}`);
|
|
1367
1417
|
}
|
|
1368
1418
|
if (d.overwritten.length > 0) {
|
|
1369
|
-
|
|
1419
|
+
p2.log.warn(
|
|
1370
1420
|
pc2.yellow(
|
|
1371
1421
|
`--force REGENERATED ${d.overwritten.length} hand-edited generated file(s), discarding your edits:
|
|
1372
1422
|
` + d.overwritten.join("\n ")
|
|
1373
1423
|
)
|
|
1374
1424
|
);
|
|
1375
1425
|
if (d.backupDir) {
|
|
1376
|
-
|
|
1426
|
+
p2.log.info(pc2.yellow(` Previous content backed up \u2192 ${d.backupDir}`));
|
|
1377
1427
|
}
|
|
1378
1428
|
}
|
|
1379
1429
|
if (d.preserved.length > 0) {
|
|
1380
|
-
|
|
1430
|
+
p2.log.warn(
|
|
1381
1431
|
pc2.yellow(
|
|
1382
1432
|
`Left ${d.preserved.length} hand-edited generated file(s) UNTOUCHED \u2014 your edits are safe:
|
|
1383
1433
|
` + d.preserved.join("\n ") + `
|
|
@@ -1386,26 +1436,26 @@ async function buildOnce(cwd2, force) {
|
|
|
1386
1436
|
)
|
|
1387
1437
|
);
|
|
1388
1438
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1439
|
+
p2.log.success(`Agent definitions (${config.provider})`);
|
|
1440
|
+
p2.log.success("MCP config");
|
|
1391
1441
|
const { created, overwritten, preserved, backupDir } = report.agents;
|
|
1392
1442
|
if (created.length > 0) {
|
|
1393
|
-
|
|
1443
|
+
p2.log.info(`Created ${created.length} missing agent file(s):
|
|
1394
1444
|
${created.join("\n ")}`);
|
|
1395
1445
|
}
|
|
1396
1446
|
if (overwritten.length > 0) {
|
|
1397
|
-
|
|
1447
|
+
p2.log.warn(
|
|
1398
1448
|
pc2.yellow(
|
|
1399
1449
|
`--force REGENERATED ${overwritten.length} existing agent file(s), discarding any customizations:
|
|
1400
1450
|
` + overwritten.join("\n ")
|
|
1401
1451
|
)
|
|
1402
1452
|
);
|
|
1403
1453
|
if (backupDir) {
|
|
1404
|
-
|
|
1454
|
+
p2.log.info(pc2.yellow(` Previous content backed up \u2192 ${backupDir}`));
|
|
1405
1455
|
}
|
|
1406
1456
|
}
|
|
1407
1457
|
if (preserved.length > 0) {
|
|
1408
|
-
|
|
1458
|
+
p2.log.info(
|
|
1409
1459
|
`Left ${preserved.length} existing agent file(s) untouched \u2014 agent files are yours to edit.
|
|
1410
1460
|
Re-run with --force to regenerate them from the packaged templates (this DESTROYS your edits;
|
|
1411
1461
|
a backup is written first).`
|
|
@@ -1413,7 +1463,7 @@ async function buildOnce(cwd2, force) {
|
|
|
1413
1463
|
}
|
|
1414
1464
|
} catch (err) {
|
|
1415
1465
|
spinner6.stop(pc2.red("Build failed"));
|
|
1416
|
-
|
|
1466
|
+
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
1417
1467
|
process.exit(1);
|
|
1418
1468
|
}
|
|
1419
1469
|
}
|
|
@@ -2027,7 +2077,7 @@ function getProviderHealthFiles(provider) {
|
|
|
2027
2077
|
// src/commands/init.ts
|
|
2028
2078
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
2029
2079
|
import { join as join16 } from "path";
|
|
2030
|
-
import * as
|
|
2080
|
+
import * as p4 from "@clack/prompts";
|
|
2031
2081
|
import pc8 from "picocolors";
|
|
2032
2082
|
|
|
2033
2083
|
// src/schema/init.ts
|
|
@@ -2066,7 +2116,7 @@ var taskDescriptionSchema = v2.pipe(
|
|
|
2066
2116
|
);
|
|
2067
2117
|
|
|
2068
2118
|
// src/utils/form.ts
|
|
2069
|
-
import * as
|
|
2119
|
+
import * as p3 from "@clack/prompts";
|
|
2070
2120
|
import * as v3 from "valibot";
|
|
2071
2121
|
var cliFormWithRetry = async (formFn, schema) => {
|
|
2072
2122
|
while (true) {
|
|
@@ -2074,8 +2124,8 @@ var cliFormWithRetry = async (formFn, schema) => {
|
|
|
2074
2124
|
const result = v3.safeParse(schema, res);
|
|
2075
2125
|
if (result.success) return result.output;
|
|
2076
2126
|
const messages = result.issues.map((i) => i.message).join(", ");
|
|
2077
|
-
|
|
2078
|
-
|
|
2127
|
+
p3.log.error(messages);
|
|
2128
|
+
p3.log.info("Please try again.\n");
|
|
2079
2129
|
}
|
|
2080
2130
|
};
|
|
2081
2131
|
|
|
@@ -2236,25 +2286,25 @@ async function runInit(cwd2, flags) {
|
|
|
2236
2286
|
name = flags.name;
|
|
2237
2287
|
} else {
|
|
2238
2288
|
name = await cliFormWithRetry(async () => {
|
|
2239
|
-
const val = await
|
|
2289
|
+
const val = await p4.text({
|
|
2240
2290
|
message: "Project name",
|
|
2241
2291
|
placeholder: "my-app",
|
|
2242
2292
|
...detectedName && { initialValue: detectedName }
|
|
2243
2293
|
});
|
|
2244
|
-
if (
|
|
2245
|
-
|
|
2294
|
+
if (p4.isCancel(val)) {
|
|
2295
|
+
p4.cancel("Cancelled.");
|
|
2246
2296
|
process.exit(0);
|
|
2247
2297
|
}
|
|
2248
2298
|
return val;
|
|
2249
2299
|
}, initNameSchema);
|
|
2250
2300
|
}
|
|
2251
2301
|
const description = await cliFormWithRetry(async () => {
|
|
2252
|
-
const val = await
|
|
2302
|
+
const val = await p4.text({
|
|
2253
2303
|
message: "Short description (shown to agents as context)",
|
|
2254
2304
|
placeholder: "A REST API for managing notes"
|
|
2255
2305
|
});
|
|
2256
|
-
if (
|
|
2257
|
-
|
|
2306
|
+
if (p4.isCancel(val)) {
|
|
2307
|
+
p4.cancel("Cancelled.");
|
|
2258
2308
|
process.exit(0);
|
|
2259
2309
|
}
|
|
2260
2310
|
return val;
|
|
@@ -2263,7 +2313,7 @@ async function runInit(cwd2, flags) {
|
|
|
2263
2313
|
if (flags.provider && ["claude-code", "opencode", "codex-cli", "grok-cli"].includes(flags.provider)) {
|
|
2264
2314
|
provider = flags.provider;
|
|
2265
2315
|
} else {
|
|
2266
|
-
const val = await
|
|
2316
|
+
const val = await p4.select({
|
|
2267
2317
|
message: "AI provider",
|
|
2268
2318
|
options: [
|
|
2269
2319
|
{ value: "opencode", label: "OpenCode" },
|
|
@@ -2272,51 +2322,24 @@ async function runInit(cwd2, flags) {
|
|
|
2272
2322
|
{ value: "grok-cli", label: "Grok CLI" }
|
|
2273
2323
|
]
|
|
2274
2324
|
});
|
|
2275
|
-
if (
|
|
2276
|
-
|
|
2325
|
+
if (p4.isCancel(val)) {
|
|
2326
|
+
p4.cancel("Cancelled.");
|
|
2277
2327
|
process.exit(0);
|
|
2278
2328
|
}
|
|
2279
2329
|
provider = val;
|
|
2280
2330
|
}
|
|
2281
|
-
const
|
|
2282
|
-
{ key: "lead", label: "Lead" },
|
|
2283
|
-
{ key: "explorer", label: "Explorer" },
|
|
2284
|
-
{ key: "consultant", label: "Consultant" },
|
|
2285
|
-
{ key: "builder", label: "Builder" },
|
|
2286
|
-
{ key: "reviewer", label: "Reviewer" }
|
|
2287
|
-
];
|
|
2288
|
-
const claudeAgentModels = {};
|
|
2289
|
-
if (provider === "claude-code") {
|
|
2290
|
-
for (const agent of AGENT_LABELS) {
|
|
2291
|
-
const val = await p3.select({
|
|
2292
|
-
message: `Model for ${agent.label}`,
|
|
2293
|
-
options: [
|
|
2294
|
-
{ value: "inherit", label: "inherit (default)" },
|
|
2295
|
-
{ value: "haiku", label: "haiku" },
|
|
2296
|
-
{ value: "sonnet", label: "sonnet" },
|
|
2297
|
-
{ value: "opus", label: "opus" },
|
|
2298
|
-
{ value: "fable", label: "fable" }
|
|
2299
|
-
],
|
|
2300
|
-
initialValue: "inherit"
|
|
2301
|
-
});
|
|
2302
|
-
if (p3.isCancel(val)) {
|
|
2303
|
-
p3.cancel("Cancelled.");
|
|
2304
|
-
process.exit(0);
|
|
2305
|
-
}
|
|
2306
|
-
claudeAgentModels[agent.key] = val;
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2331
|
+
const claudeAgentModels = await promptClaudeAgentModels(provider);
|
|
2309
2332
|
let docsPath;
|
|
2310
2333
|
if (flags.docs) {
|
|
2311
2334
|
docsPath = flags.docs;
|
|
2312
2335
|
} else {
|
|
2313
2336
|
docsPath = await cliFormWithRetry(async () => {
|
|
2314
|
-
const val = await
|
|
2337
|
+
const val = await p4.text({
|
|
2315
2338
|
message: "Docs folder path (agents will search here)",
|
|
2316
2339
|
initialValue: "./docs"
|
|
2317
2340
|
});
|
|
2318
|
-
if (
|
|
2319
|
-
|
|
2341
|
+
if (p4.isCancel(val)) {
|
|
2342
|
+
p4.cancel("Cancelled.");
|
|
2320
2343
|
process.exit(0);
|
|
2321
2344
|
}
|
|
2322
2345
|
return val;
|
|
@@ -2326,7 +2349,7 @@ async function runInit(cwd2, flags) {
|
|
|
2326
2349
|
if (flags.storageScope && ["local", "global"].includes(flags.storageScope)) {
|
|
2327
2350
|
storageScope = flags.storageScope;
|
|
2328
2351
|
} else {
|
|
2329
|
-
const val = await
|
|
2352
|
+
const val = await p4.select({
|
|
2330
2353
|
message: "Storage scope",
|
|
2331
2354
|
options: [
|
|
2332
2355
|
{ value: "local", label: "Local \u2014 .harness/harness.db lives in this project" },
|
|
@@ -2337,8 +2360,8 @@ async function runInit(cwd2, flags) {
|
|
|
2337
2360
|
],
|
|
2338
2361
|
initialValue: "local"
|
|
2339
2362
|
});
|
|
2340
|
-
if (
|
|
2341
|
-
|
|
2363
|
+
if (p4.isCancel(val)) {
|
|
2364
|
+
p4.cancel("Cancelled.");
|
|
2342
2365
|
process.exit(0);
|
|
2343
2366
|
}
|
|
2344
2367
|
storageScope = val;
|
|
@@ -2347,7 +2370,7 @@ async function runInit(cwd2, flags) {
|
|
|
2347
2370
|
if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
|
|
2348
2371
|
tasksAdapter = flags.tasks;
|
|
2349
2372
|
} else {
|
|
2350
|
-
const val = await
|
|
2373
|
+
const val = await p4.select({
|
|
2351
2374
|
message: "Task adapter",
|
|
2352
2375
|
options: [
|
|
2353
2376
|
{ value: "local", label: "Local (feature_list.json)" },
|
|
@@ -2355,50 +2378,50 @@ async function runInit(cwd2, flags) {
|
|
|
2355
2378
|
{ value: "linear", label: "Linear (coming soon)" }
|
|
2356
2379
|
]
|
|
2357
2380
|
});
|
|
2358
|
-
if (
|
|
2359
|
-
|
|
2381
|
+
if (p4.isCancel(val)) {
|
|
2382
|
+
p4.cancel("Cancelled");
|
|
2360
2383
|
process.exit(0);
|
|
2361
2384
|
}
|
|
2362
2385
|
tasksAdapter = val;
|
|
2363
2386
|
}
|
|
2364
|
-
const addFirstTask = await
|
|
2365
|
-
if (
|
|
2366
|
-
|
|
2387
|
+
const addFirstTask = await p4.confirm({ message: "Add your first task now?", initialValue: false });
|
|
2388
|
+
if (p4.isCancel(addFirstTask)) {
|
|
2389
|
+
p4.cancel("Cancelled");
|
|
2367
2390
|
process.exit(0);
|
|
2368
2391
|
}
|
|
2369
2392
|
let firstTask;
|
|
2370
2393
|
if (addFirstTask) {
|
|
2371
2394
|
const taskTitle = await cliFormWithRetry(async () => {
|
|
2372
|
-
const val = await
|
|
2373
|
-
if (
|
|
2374
|
-
|
|
2395
|
+
const val = await p4.text({ message: "Task title" });
|
|
2396
|
+
if (p4.isCancel(val)) {
|
|
2397
|
+
p4.cancel("Cancelled");
|
|
2375
2398
|
process.exit(0);
|
|
2376
2399
|
}
|
|
2377
2400
|
return val.trim();
|
|
2378
2401
|
}, taskTitleSchema);
|
|
2379
2402
|
const taskDesc = await cliFormWithRetry(async () => {
|
|
2380
|
-
const val = await
|
|
2381
|
-
if (
|
|
2382
|
-
|
|
2403
|
+
const val = await p4.text({ message: "Task description", placeholder: "What and why" });
|
|
2404
|
+
if (p4.isCancel(val)) {
|
|
2405
|
+
p4.cancel("Cancelled");
|
|
2383
2406
|
process.exit(0);
|
|
2384
2407
|
}
|
|
2385
2408
|
return val.trim();
|
|
2386
2409
|
}, taskDescriptionSchema);
|
|
2387
2410
|
const acceptance = [];
|
|
2388
|
-
|
|
2411
|
+
p4.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
2389
2412
|
while (true) {
|
|
2390
|
-
const criterionVal = await
|
|
2413
|
+
const criterionVal = await p4.text({
|
|
2391
2414
|
message: ">",
|
|
2392
2415
|
placeholder: "Criterion (or press Enter to finish)"
|
|
2393
2416
|
});
|
|
2394
|
-
if (
|
|
2417
|
+
if (p4.isCancel(criterionVal) || !criterionVal || !criterionVal.trim()) break;
|
|
2395
2418
|
acceptance.push(criterionVal.trim());
|
|
2396
2419
|
}
|
|
2397
2420
|
firstTask = { title: taskTitle, description: taskDesc, acceptance };
|
|
2398
2421
|
}
|
|
2399
2422
|
let configExt = "ts";
|
|
2400
2423
|
let featureListParseFailedPath = null;
|
|
2401
|
-
const spinner6 =
|
|
2424
|
+
const spinner6 = p4.spinner();
|
|
2402
2425
|
spinner6.start("Scaffolding...");
|
|
2403
2426
|
try {
|
|
2404
2427
|
const config = applyConfigDefaults({
|
|
@@ -2437,7 +2460,7 @@ async function runInit(cwd2, flags) {
|
|
|
2437
2460
|
spinner6.stop("");
|
|
2438
2461
|
} catch (err) {
|
|
2439
2462
|
spinner6.stop("Failed");
|
|
2440
|
-
|
|
2463
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
2441
2464
|
throw err;
|
|
2442
2465
|
}
|
|
2443
2466
|
if (featureListParseFailedPath) {
|
|
@@ -2489,7 +2512,7 @@ async function runInit(cwd2, flags) {
|
|
|
2489
2512
|
}
|
|
2490
2513
|
|
|
2491
2514
|
// src/commands/migrate.ts
|
|
2492
|
-
import * as
|
|
2515
|
+
import * as p5 from "@clack/prompts";
|
|
2493
2516
|
import pc9 from "picocolors";
|
|
2494
2517
|
async function runMigrate(cwd2, opts) {
|
|
2495
2518
|
const config = await loadConfig(cwd2);
|
|
@@ -2497,7 +2520,7 @@ async function runMigrate(cwd2, opts) {
|
|
|
2497
2520
|
if (opts.to && ["claude-code", "opencode", "codex-cli", "grok-cli"].includes(opts.to)) {
|
|
2498
2521
|
target = opts.to;
|
|
2499
2522
|
} else {
|
|
2500
|
-
const val = await
|
|
2523
|
+
const val = await p5.select({
|
|
2501
2524
|
message: "Migrate to provider",
|
|
2502
2525
|
options: [
|
|
2503
2526
|
{ value: "claude-code", label: "Claude Code" },
|
|
@@ -2506,8 +2529,8 @@ async function runMigrate(cwd2, opts) {
|
|
|
2506
2529
|
{ value: "grok-cli", label: "Grok CLI" }
|
|
2507
2530
|
]
|
|
2508
2531
|
});
|
|
2509
|
-
if (
|
|
2510
|
-
|
|
2532
|
+
if (p5.isCancel(val)) {
|
|
2533
|
+
p5.cancel("Cancelled.");
|
|
2511
2534
|
process.exit(0);
|
|
2512
2535
|
}
|
|
2513
2536
|
target = val;
|
|
@@ -2516,17 +2539,17 @@ async function runMigrate(cwd2, opts) {
|
|
|
2516
2539
|
console.log(pc9.dim(`Already on ${target} \u2014 nothing to migrate.`));
|
|
2517
2540
|
return;
|
|
2518
2541
|
}
|
|
2519
|
-
const spinner6 =
|
|
2542
|
+
const spinner6 = p5.spinner();
|
|
2520
2543
|
spinner6.start(`Migrating from ${config.provider} to ${target}...`);
|
|
2521
2544
|
try {
|
|
2522
2545
|
const targetMaterializer = getMaterializer(target);
|
|
2523
2546
|
await targetMaterializer.build(config, cwd2);
|
|
2524
2547
|
spinner6.stop(pc9.green(`Migrated to ${target}`));
|
|
2525
|
-
|
|
2526
|
-
|
|
2548
|
+
p5.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
|
|
2549
|
+
p5.log.warn(`Then run: ahk build`);
|
|
2527
2550
|
} catch (err) {
|
|
2528
2551
|
spinner6.stop(pc9.red("Migration failed"));
|
|
2529
|
-
|
|
2552
|
+
p5.log.error(err instanceof Error ? err.message : String(err));
|
|
2530
2553
|
process.exit(1);
|
|
2531
2554
|
}
|
|
2532
2555
|
}
|
|
@@ -2771,12 +2794,65 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
|
|
|
2771
2794
|
}
|
|
2772
2795
|
}
|
|
2773
2796
|
|
|
2797
|
+
// src/commands/models.ts
|
|
2798
|
+
import { join as join18 } from "path";
|
|
2799
|
+
import pc11 from "picocolors";
|
|
2800
|
+
async function resolveModelsContext(cwd2) {
|
|
2801
|
+
let config;
|
|
2802
|
+
try {
|
|
2803
|
+
config = await loadConfig(cwd2);
|
|
2804
|
+
} catch {
|
|
2805
|
+
return { ok: false, reason: "no-config" };
|
|
2806
|
+
}
|
|
2807
|
+
if (config.provider !== "claude-code") {
|
|
2808
|
+
return { ok: false, reason: "not-claude-code", provider: config.provider };
|
|
2809
|
+
}
|
|
2810
|
+
return { ok: true, config };
|
|
2811
|
+
}
|
|
2812
|
+
async function runModels(cwd2) {
|
|
2813
|
+
const ctx = await resolveModelsContext(cwd2);
|
|
2814
|
+
if (!ctx.ok) {
|
|
2815
|
+
if (ctx.reason === "no-config") {
|
|
2816
|
+
console.log("");
|
|
2817
|
+
console.log(` ${pc11.cyan("config".padEnd(16))}${pc11.yellow("[!]")} no agent-harness-kit.config found`);
|
|
2818
|
+
console.log(` ${"".padEnd(16)} ${pc11.dim("run: ahk init")}`);
|
|
2819
|
+
console.log("");
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
console.log(
|
|
2823
|
+
pc11.dim(
|
|
2824
|
+
`ahk models only applies to Claude Code projects (this project uses '${ctx.provider}') \u2014 nothing to do.`
|
|
2825
|
+
)
|
|
2826
|
+
);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
const { config } = ctx;
|
|
2830
|
+
const claudeAgentModels = await promptClaudeAgentModels(config.provider);
|
|
2831
|
+
const agents = writeAgentFiles(cwd2, claudeAgentFiles(config, claudeAgentModels), {
|
|
2832
|
+
force: true,
|
|
2833
|
+
backupRoot: join18(cwd2, config.storage.dir, "backups")
|
|
2834
|
+
});
|
|
2835
|
+
console.log("");
|
|
2836
|
+
if (agents.overwritten.length > 0) {
|
|
2837
|
+
console.log(pc11.green(`\u2713 Regenerated ${agents.overwritten.length} agent file(s) with updated models:`));
|
|
2838
|
+
for (const file of agents.overwritten) console.log(pc11.green(` \u2713 ${file}`));
|
|
2839
|
+
if (agents.backupDir) {
|
|
2840
|
+
console.log(pc11.dim(` Previous content backed up \u2192 ${agents.backupDir}`));
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
if (agents.created.length > 0) {
|
|
2844
|
+
console.log(pc11.green(`\u2713 Created ${agents.created.length} missing agent file(s):`));
|
|
2845
|
+
for (const file of agents.created) console.log(pc11.green(` \u2713 ${file}`));
|
|
2846
|
+
}
|
|
2847
|
+
console.log("");
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2774
2850
|
// src/commands/reset.ts
|
|
2775
2851
|
import { existsSync as existsSync16, readdirSync, rmSync as rmSync2 } from "fs";
|
|
2776
2852
|
import { homedir as homedir4 } from "os";
|
|
2777
|
-
import { join as
|
|
2778
|
-
import * as
|
|
2779
|
-
import
|
|
2853
|
+
import { join as join19, resolve as resolve8 } from "path";
|
|
2854
|
+
import * as p6 from "@clack/prompts";
|
|
2855
|
+
import pc12 from "picocolors";
|
|
2780
2856
|
var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
|
|
2781
2857
|
var PROVIDER_AGENT_DIRS = {
|
|
2782
2858
|
"claude-code": ".claude/agents",
|
|
@@ -2795,7 +2871,7 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
2795
2871
|
const agentDirPath = resolve8(cwd2, agentDir);
|
|
2796
2872
|
const agentExt = PROVIDER_AGENT_EXT[provider];
|
|
2797
2873
|
if (!existsSync16(agentDirPath)) {
|
|
2798
|
-
console.log(
|
|
2874
|
+
console.log(pc12.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
|
|
2799
2875
|
return;
|
|
2800
2876
|
}
|
|
2801
2877
|
const existingFiles = [];
|
|
@@ -2807,32 +2883,32 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
2807
2883
|
}
|
|
2808
2884
|
}
|
|
2809
2885
|
} catch {
|
|
2810
|
-
console.log(
|
|
2886
|
+
console.log(pc12.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
|
|
2811
2887
|
return;
|
|
2812
2888
|
}
|
|
2813
2889
|
if (existingFiles.length === 0) {
|
|
2814
|
-
console.log(
|
|
2890
|
+
console.log(pc12.yellow(` No agent MD files found in ${agentDir}/`));
|
|
2815
2891
|
return;
|
|
2816
2892
|
}
|
|
2817
2893
|
for (const file of existingFiles) {
|
|
2818
|
-
const confirm3 = await
|
|
2894
|
+
const confirm3 = await p6.confirm({
|
|
2819
2895
|
message: `Remove ${file}?`,
|
|
2820
2896
|
initialValue: true
|
|
2821
2897
|
});
|
|
2822
|
-
if (
|
|
2823
|
-
console.log(
|
|
2898
|
+
if (p6.isCancel(confirm3)) {
|
|
2899
|
+
console.log(pc12.red(" Cancelled by user."));
|
|
2824
2900
|
return;
|
|
2825
2901
|
}
|
|
2826
2902
|
if (confirm3) {
|
|
2827
2903
|
try {
|
|
2828
|
-
const filePath =
|
|
2904
|
+
const filePath = join19(agentDirPath, file);
|
|
2829
2905
|
rmSync2(filePath, { force: true });
|
|
2830
|
-
console.log(
|
|
2906
|
+
console.log(pc12.green(` Removed ${file}`));
|
|
2831
2907
|
} catch {
|
|
2832
|
-
console.error(
|
|
2908
|
+
console.error(pc12.red(` Failed to remove ${file}`));
|
|
2833
2909
|
}
|
|
2834
2910
|
} else {
|
|
2835
|
-
console.log(
|
|
2911
|
+
console.log(pc12.cyan(` Skipped ${file}`));
|
|
2836
2912
|
}
|
|
2837
2913
|
}
|
|
2838
2914
|
}
|
|
@@ -2841,7 +2917,7 @@ async function runReset(cwd2, opts) {
|
|
|
2841
2917
|
try {
|
|
2842
2918
|
config = await loadConfig(cwd2);
|
|
2843
2919
|
} catch {
|
|
2844
|
-
console.error(
|
|
2920
|
+
console.error(pc12.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
|
|
2845
2921
|
process.exit(1);
|
|
2846
2922
|
}
|
|
2847
2923
|
const storageDir = config.storage.dir || ".harness";
|
|
@@ -2855,33 +2931,33 @@ async function runReset(cwd2, opts) {
|
|
|
2855
2931
|
resetDb = true;
|
|
2856
2932
|
} else {
|
|
2857
2933
|
if (config.database.type !== "sqlite") {
|
|
2858
|
-
console.log(
|
|
2934
|
+
console.log(pc12.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
|
|
2859
2935
|
resetDb = false;
|
|
2860
2936
|
} else {
|
|
2861
|
-
const confirm3 = await
|
|
2937
|
+
const confirm3 = await p6.confirm({
|
|
2862
2938
|
message: `Delete database (${dbPath})?`,
|
|
2863
2939
|
initialValue: true
|
|
2864
2940
|
});
|
|
2865
|
-
if (
|
|
2866
|
-
console.log(
|
|
2941
|
+
if (p6.isCancel(confirm3)) {
|
|
2942
|
+
console.log(pc12.red(" Cancelled by user."));
|
|
2867
2943
|
return;
|
|
2868
2944
|
}
|
|
2869
2945
|
resetDb = confirm3;
|
|
2870
2946
|
}
|
|
2871
2947
|
}
|
|
2872
2948
|
} else if (!dbPath) {
|
|
2873
|
-
console.log(
|
|
2949
|
+
console.log(pc12.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
|
|
2874
2950
|
}
|
|
2875
2951
|
if (existsSync16(featureListPath)) {
|
|
2876
2952
|
if (opts.force) {
|
|
2877
2953
|
resetFeatureList = true;
|
|
2878
2954
|
} else {
|
|
2879
|
-
const confirm3 = await
|
|
2955
|
+
const confirm3 = await p6.confirm({
|
|
2880
2956
|
message: `Delete feature list (${storageDir}/feature_list.json)?`,
|
|
2881
2957
|
initialValue: true
|
|
2882
2958
|
});
|
|
2883
|
-
if (
|
|
2884
|
-
console.log(
|
|
2959
|
+
if (p6.isCancel(confirm3)) {
|
|
2960
|
+
console.log(pc12.red(" Cancelled by user."));
|
|
2885
2961
|
return;
|
|
2886
2962
|
}
|
|
2887
2963
|
resetFeatureList = confirm3;
|
|
@@ -2895,17 +2971,17 @@ async function runReset(cwd2, opts) {
|
|
|
2895
2971
|
rmSync2(dbPath, { force: true });
|
|
2896
2972
|
rmSync2(`${dbPath}-wal`, { force: true });
|
|
2897
2973
|
rmSync2(`${dbPath}-shm`, { force: true });
|
|
2898
|
-
console.log(
|
|
2974
|
+
console.log(pc12.green(` \u2713 Removed ${dbPath}`));
|
|
2899
2975
|
} catch {
|
|
2900
|
-
console.error(
|
|
2976
|
+
console.error(pc12.red(` \u2717 Failed to remove ${dbPath}`));
|
|
2901
2977
|
}
|
|
2902
2978
|
}
|
|
2903
2979
|
if (resetFeatureList) {
|
|
2904
2980
|
try {
|
|
2905
2981
|
rmSync2(featureListPath, { force: true });
|
|
2906
|
-
console.log(
|
|
2982
|
+
console.log(pc12.green(` \u2713 Removed ${storageDir}/feature_list.json`));
|
|
2907
2983
|
} catch {
|
|
2908
|
-
console.error(
|
|
2984
|
+
console.error(pc12.red(` \u2717 Failed to remove ${featureListPath}`));
|
|
2909
2985
|
}
|
|
2910
2986
|
}
|
|
2911
2987
|
if (resetAgentMdsFlag) {
|
|
@@ -2913,16 +2989,16 @@ async function runReset(cwd2, opts) {
|
|
|
2913
2989
|
await resetAgentMds(cwd2, opts.provider || "claude-code");
|
|
2914
2990
|
}
|
|
2915
2991
|
if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
|
|
2916
|
-
console.log(
|
|
2992
|
+
console.log(pc12.yellow(" Nothing to reset (all items missing or skipped)."));
|
|
2917
2993
|
return;
|
|
2918
2994
|
}
|
|
2919
2995
|
console.log("");
|
|
2920
|
-
console.log(
|
|
2996
|
+
console.log(pc12.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
|
|
2921
2997
|
}
|
|
2922
2998
|
|
|
2923
2999
|
// src/core/mcp-server.ts
|
|
2924
3000
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
|
|
2925
|
-
import { join as
|
|
3001
|
+
import { join as join21, resolve as resolve9 } from "path";
|
|
2926
3002
|
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
2927
3003
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2928
3004
|
import {
|
|
@@ -2932,7 +3008,7 @@ import {
|
|
|
2932
3008
|
|
|
2933
3009
|
// src/core/permissions-check.ts
|
|
2934
3010
|
import { existsSync as existsSync17 } from "fs";
|
|
2935
|
-
import { join as
|
|
3011
|
+
import { join as join20 } from "path";
|
|
2936
3012
|
var AGENTS = ["lead", "explorer", "consultant", "builder", "reviewer"];
|
|
2937
3013
|
function checkPermissionsSync(cwd2, config) {
|
|
2938
3014
|
if (config.provider !== "claude-code") {
|
|
@@ -2941,7 +3017,7 @@ function checkPermissionsSync(cwd2, config) {
|
|
|
2941
3017
|
const agents = {};
|
|
2942
3018
|
let in_sync = true;
|
|
2943
3019
|
for (const agent of AGENTS) {
|
|
2944
|
-
const filePath =
|
|
3020
|
+
const filePath = join20(cwd2, ".claude", "agents", `${agent}.md`);
|
|
2945
3021
|
const exists = existsSync17(filePath);
|
|
2946
3022
|
if (!exists) in_sync = false;
|
|
2947
3023
|
agents[agent] = exists ? { ok: true } : { ok: false, reason: "missing_file" };
|
|
@@ -3403,7 +3479,7 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3403
3479
|
return ok2(JSON.stringify(result));
|
|
3404
3480
|
}
|
|
3405
3481
|
case "deps.snapshot": {
|
|
3406
|
-
const pkgPath2 =
|
|
3482
|
+
const pkgPath2 = join21(cwd2, "package.json");
|
|
3407
3483
|
if (!existsSync18(pkgPath2)) {
|
|
3408
3484
|
return ok2("package.json not found in project root", true);
|
|
3409
3485
|
}
|
|
@@ -3413,9 +3489,9 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3413
3489
|
dependencies: pkg2.dependencies ?? {},
|
|
3414
3490
|
devDependencies: pkg2.devDependencies ?? {}
|
|
3415
3491
|
};
|
|
3416
|
-
const harnessDir =
|
|
3492
|
+
const harnessDir = join21(cwd2, ".harness");
|
|
3417
3493
|
mkdirSync9(harnessDir, { recursive: true });
|
|
3418
|
-
writeFileSync10(
|
|
3494
|
+
writeFileSync10(join21(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
|
|
3419
3495
|
return ok2(
|
|
3420
3496
|
JSON.stringify({
|
|
3421
3497
|
message: "Snapshot saved to .harness/deps-lock.json",
|
|
@@ -3424,8 +3500,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3424
3500
|
);
|
|
3425
3501
|
}
|
|
3426
3502
|
case "deps.check": {
|
|
3427
|
-
const pkgPath2 =
|
|
3428
|
-
const lockPath =
|
|
3503
|
+
const pkgPath2 = join21(cwd2, "package.json");
|
|
3504
|
+
const lockPath = join21(cwd2, ".harness", "deps-lock.json");
|
|
3429
3505
|
if (!existsSync18(pkgPath2)) {
|
|
3430
3506
|
return ok2("package.json not found in project root", true);
|
|
3431
3507
|
}
|
|
@@ -3531,7 +3607,7 @@ function collectMarkdownFiles(dir) {
|
|
|
3531
3607
|
const files = [];
|
|
3532
3608
|
try {
|
|
3533
3609
|
for (const entry of readdirSync2(dir)) {
|
|
3534
|
-
const full =
|
|
3610
|
+
const full = join21(dir, entry);
|
|
3535
3611
|
const stat = statSync(full);
|
|
3536
3612
|
if (stat.isDirectory()) {
|
|
3537
3613
|
files.push(...collectMarkdownFiles(full));
|
|
@@ -3588,12 +3664,12 @@ async function runServe(cwd2, opts) {
|
|
|
3588
3664
|
|
|
3589
3665
|
// src/commands/status.ts
|
|
3590
3666
|
import Table from "cli-table3";
|
|
3591
|
-
import
|
|
3667
|
+
import pc13 from "picocolors";
|
|
3592
3668
|
var STATUS_COLOR = {
|
|
3593
|
-
pending: (s) =>
|
|
3594
|
-
in_progress: (s) =>
|
|
3595
|
-
done: (s) =>
|
|
3596
|
-
blocked: (s) =>
|
|
3669
|
+
pending: (s) => pc13.dim(s),
|
|
3670
|
+
in_progress: (s) => pc13.cyan(s),
|
|
3671
|
+
done: (s) => pc13.green(s),
|
|
3672
|
+
blocked: (s) => pc13.red(s)
|
|
3597
3673
|
};
|
|
3598
3674
|
async function runStatus(cwd2, opts) {
|
|
3599
3675
|
const config = await loadConfig(cwd2);
|
|
@@ -3614,11 +3690,11 @@ async function runStatus(cwd2, opts) {
|
|
|
3614
3690
|
return;
|
|
3615
3691
|
}
|
|
3616
3692
|
if (tasks.length === 0) {
|
|
3617
|
-
console.log(
|
|
3693
|
+
console.log(pc13.dim("No tasks yet. Run: ahk task add"));
|
|
3618
3694
|
return;
|
|
3619
3695
|
}
|
|
3620
3696
|
const table = new Table({
|
|
3621
|
-
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) =>
|
|
3697
|
+
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc13.bold(h)),
|
|
3622
3698
|
style: { head: [], border: [] }
|
|
3623
3699
|
});
|
|
3624
3700
|
for (const t of tasks) {
|
|
@@ -3636,12 +3712,12 @@ async function runStatus(cwd2, opts) {
|
|
|
3636
3712
|
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
3637
3713
|
if (inProgress.length > 0) {
|
|
3638
3714
|
console.log("");
|
|
3639
|
-
console.log(
|
|
3715
|
+
console.log(pc13.bold("Active actions:"));
|
|
3640
3716
|
for (const t of inProgress) {
|
|
3641
3717
|
const actions = await db.getActionsForTask(t.id);
|
|
3642
3718
|
const active = actions.filter((a) => a.status === "in_progress");
|
|
3643
3719
|
for (const a of active) {
|
|
3644
|
-
console.log(` ${
|
|
3720
|
+
console.log(` ${pc13.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
|
|
3645
3721
|
}
|
|
3646
3722
|
}
|
|
3647
3723
|
}
|
|
@@ -3650,10 +3726,10 @@ async function runStatus(cwd2, opts) {
|
|
|
3650
3726
|
const fn = STATUS_COLOR[s.status] ?? ((x) => x);
|
|
3651
3727
|
return `${fn(s.status)}: ${s.total}`;
|
|
3652
3728
|
});
|
|
3653
|
-
console.log(
|
|
3729
|
+
console.log(pc13.dim("Tasks \u2014 ") + parts.join(pc13.dim(" | ")));
|
|
3654
3730
|
const archivedTasks = await db.getArchivedTasks();
|
|
3655
3731
|
if (archivedTasks.length > 0) {
|
|
3656
|
-
console.log(
|
|
3732
|
+
console.log(pc13.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
|
|
3657
3733
|
}
|
|
3658
3734
|
} finally {
|
|
3659
3735
|
await db.close();
|
|
@@ -3662,12 +3738,12 @@ async function runStatus(cwd2, opts) {
|
|
|
3662
3738
|
|
|
3663
3739
|
// src/commands/sync.ts
|
|
3664
3740
|
import { existsSync as existsSync19, readFileSync as readFileSync11 } from "fs";
|
|
3665
|
-
import { join as
|
|
3666
|
-
import
|
|
3741
|
+
import { join as join22, resolve as resolve10 } from "path";
|
|
3742
|
+
import pc14 from "picocolors";
|
|
3667
3743
|
async function runSync(cwd2, opts) {
|
|
3668
3744
|
const config = await loadConfig(cwd2);
|
|
3669
3745
|
const direction = opts.direction ?? "both";
|
|
3670
|
-
const featureListPath = resolve10(
|
|
3746
|
+
const featureListPath = resolve10(join22(cwd2, config.storage.dir, "feature_list.json"));
|
|
3671
3747
|
const db = await openDB(config, cwd2);
|
|
3672
3748
|
try {
|
|
3673
3749
|
if (direction === "in" || direction === "both") {
|
|
@@ -3682,48 +3758,48 @@ async function runSync(cwd2, opts) {
|
|
|
3682
3758
|
}
|
|
3683
3759
|
async function syncIn(featureListPath, db, dryRun) {
|
|
3684
3760
|
if (!existsSync19(featureListPath)) {
|
|
3685
|
-
console.log(
|
|
3761
|
+
console.log(pc14.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
|
|
3686
3762
|
return;
|
|
3687
3763
|
}
|
|
3688
3764
|
let seeds;
|
|
3689
3765
|
try {
|
|
3690
3766
|
seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
|
|
3691
3767
|
} catch (err) {
|
|
3692
|
-
console.error(
|
|
3768
|
+
console.error(pc14.red(`Failed to parse feature_list.json: ${err}`));
|
|
3693
3769
|
process.exit(1);
|
|
3694
3770
|
}
|
|
3695
3771
|
if (dryRun) {
|
|
3696
|
-
console.log(
|
|
3772
|
+
console.log(pc14.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
|
|
3697
3773
|
for (const t of seeds) {
|
|
3698
3774
|
const existing = await db.getTaskBySlug(t.slug);
|
|
3699
|
-
console.log(` ${existing ?
|
|
3775
|
+
console.log(` ${existing ? pc14.dim("skip") : pc14.green("add ")} ${t.slug}`);
|
|
3700
3776
|
}
|
|
3701
3777
|
return;
|
|
3702
3778
|
}
|
|
3703
3779
|
const result = await db.syncFromFeatureList(seeds);
|
|
3704
|
-
console.log(
|
|
3780
|
+
console.log(pc14.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
|
|
3705
3781
|
}
|
|
3706
3782
|
async function syncOut(db, cwd2, dryRun) {
|
|
3707
3783
|
if (dryRun) {
|
|
3708
3784
|
const tasks = await db.getTasks();
|
|
3709
|
-
console.log(
|
|
3785
|
+
console.log(pc14.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
|
|
3710
3786
|
console.log(` ${tasks.length} tasks would be written`);
|
|
3711
3787
|
return;
|
|
3712
3788
|
}
|
|
3713
3789
|
await db.writeFeatureList(cwd2);
|
|
3714
|
-
console.log(
|
|
3790
|
+
console.log(pc14.green("\u2713 Out-sync: feature_list.json updated"));
|
|
3715
3791
|
}
|
|
3716
3792
|
|
|
3717
3793
|
// src/commands/task/add.ts
|
|
3718
|
-
import * as
|
|
3719
|
-
import
|
|
3794
|
+
import * as p7 from "@clack/prompts";
|
|
3795
|
+
import pc15 from "picocolors";
|
|
3720
3796
|
async function runTaskAdd(cwd2) {
|
|
3721
|
-
|
|
3797
|
+
p7.intro(pc15.bold("agent-harness-kit \u2014 add task"));
|
|
3722
3798
|
const title = await cliFormWithRetry(
|
|
3723
3799
|
async () => {
|
|
3724
|
-
const val = await
|
|
3725
|
-
if (
|
|
3726
|
-
|
|
3800
|
+
const val = await p7.text({ message: "Task title" });
|
|
3801
|
+
if (p7.isCancel(val)) {
|
|
3802
|
+
p7.cancel("Cancelled.");
|
|
3727
3803
|
process.exit(0);
|
|
3728
3804
|
}
|
|
3729
3805
|
return val.trim();
|
|
@@ -3732,12 +3808,12 @@ async function runTaskAdd(cwd2) {
|
|
|
3732
3808
|
);
|
|
3733
3809
|
const description = await cliFormWithRetry(
|
|
3734
3810
|
async () => {
|
|
3735
|
-
const val = await
|
|
3811
|
+
const val = await p7.text({
|
|
3736
3812
|
message: "Description (what and why)",
|
|
3737
3813
|
placeholder: "Describe the task in more detail, including any relevant context or instructions for the agents."
|
|
3738
3814
|
});
|
|
3739
|
-
if (
|
|
3740
|
-
|
|
3815
|
+
if (p7.isCancel(val)) {
|
|
3816
|
+
p7.cancel("Cancelled.");
|
|
3741
3817
|
process.exit(0);
|
|
3742
3818
|
}
|
|
3743
3819
|
return val.trim();
|
|
@@ -3745,13 +3821,13 @@ async function runTaskAdd(cwd2) {
|
|
|
3745
3821
|
taskDescriptionSchema
|
|
3746
3822
|
);
|
|
3747
3823
|
const acceptance = [];
|
|
3748
|
-
|
|
3824
|
+
p7.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
|
|
3749
3825
|
while (true) {
|
|
3750
|
-
const val = await
|
|
3751
|
-
if (
|
|
3826
|
+
const val = await p7.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
|
|
3827
|
+
if (p7.isCancel(val) || !val || !val.trim()) break;
|
|
3752
3828
|
acceptance.push(val.trim());
|
|
3753
3829
|
}
|
|
3754
|
-
const spinner6 =
|
|
3830
|
+
const spinner6 = p7.spinner();
|
|
3755
3831
|
spinner6.start("Saving...");
|
|
3756
3832
|
try {
|
|
3757
3833
|
const config = await loadConfig(cwd2);
|
|
@@ -3761,11 +3837,11 @@ async function runTaskAdd(cwd2) {
|
|
|
3761
3837
|
await db.writeFeatureList(cwd2);
|
|
3762
3838
|
await db.close();
|
|
3763
3839
|
spinner6.stop("");
|
|
3764
|
-
console.log(
|
|
3765
|
-
console.log(
|
|
3840
|
+
console.log(pc15.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
|
|
3841
|
+
console.log(pc15.cyan("\u2192") + " " + pc15.cyan("ahk status") + " to see all tasks");
|
|
3766
3842
|
} catch (err) {
|
|
3767
|
-
spinner6.stop(
|
|
3768
|
-
|
|
3843
|
+
spinner6.stop(pc15.red("Failed"));
|
|
3844
|
+
p7.log.error(err instanceof Error ? err.message : String(err));
|
|
3769
3845
|
process.exit(1);
|
|
3770
3846
|
}
|
|
3771
3847
|
}
|
|
@@ -3774,7 +3850,7 @@ async function runTaskAdd(cwd2) {
|
|
|
3774
3850
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3775
3851
|
import { existsSync as existsSync20 } from "fs";
|
|
3776
3852
|
import { resolve as resolve11 } from "path";
|
|
3777
|
-
import
|
|
3853
|
+
import pc16 from "picocolors";
|
|
3778
3854
|
async function runTaskDone(cwd2, idOrSlug) {
|
|
3779
3855
|
const config = await loadConfig(cwd2);
|
|
3780
3856
|
if (config.health.required) {
|
|
@@ -3782,7 +3858,7 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
3782
3858
|
if (existsSync20(scriptPath)) {
|
|
3783
3859
|
const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
|
|
3784
3860
|
if (result.status !== 0) {
|
|
3785
|
-
console.error(
|
|
3861
|
+
console.error(pc16.red("\u2717 Health check failed \u2014 cannot mark task as done."));
|
|
3786
3862
|
if (result.stdout) console.error(result.stdout);
|
|
3787
3863
|
if (result.stderr) console.error(result.stderr);
|
|
3788
3864
|
process.exit(1);
|
|
@@ -3795,79 +3871,79 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
3795
3871
|
const isId = !isNaN(parsed);
|
|
3796
3872
|
const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
|
|
3797
3873
|
if (!task2) {
|
|
3798
|
-
console.error(
|
|
3874
|
+
console.error(pc16.red(`Task not found: ${idOrSlug}`));
|
|
3799
3875
|
process.exit(1);
|
|
3800
3876
|
}
|
|
3801
3877
|
if (task2.status === "done") {
|
|
3802
|
-
console.log(
|
|
3878
|
+
console.log(pc16.dim(`Task #${task2.id} is already done.`));
|
|
3803
3879
|
return;
|
|
3804
3880
|
}
|
|
3805
3881
|
await db.updateTaskStatus(task2.id, "done");
|
|
3806
3882
|
await db.writeFeatureList(cwd2);
|
|
3807
|
-
console.log(
|
|
3883
|
+
console.log(pc16.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
|
|
3808
3884
|
} finally {
|
|
3809
3885
|
await db.close();
|
|
3810
3886
|
}
|
|
3811
3887
|
}
|
|
3812
3888
|
|
|
3813
3889
|
// src/commands/task/edit.ts
|
|
3814
|
-
import * as
|
|
3815
|
-
import
|
|
3890
|
+
import * as p8 from "@clack/prompts";
|
|
3891
|
+
import pc17 from "picocolors";
|
|
3816
3892
|
async function runTaskEdit(cwd2) {
|
|
3817
|
-
|
|
3893
|
+
p8.intro(pc17.bold("agent-harness-kit \u2014 edit task"));
|
|
3818
3894
|
const config = await loadConfig(cwd2);
|
|
3819
3895
|
const db = await openDB(config, cwd2);
|
|
3820
3896
|
try {
|
|
3821
3897
|
const allTasks = await db.getTasks();
|
|
3822
3898
|
const activeTasks = allTasks.filter((t) => t.status !== "done");
|
|
3823
3899
|
if (activeTasks.length === 0) {
|
|
3824
|
-
|
|
3900
|
+
p8.log.error("No active tasks to edit.");
|
|
3825
3901
|
return;
|
|
3826
3902
|
}
|
|
3827
|
-
const taskId = await
|
|
3903
|
+
const taskId = await p8.select({
|
|
3828
3904
|
message: "Select a task to edit",
|
|
3829
3905
|
options: activeTasks.map((t) => ({
|
|
3830
3906
|
label: `#${t.id} \u2014 ${t.title} (${t.slug})`,
|
|
3831
3907
|
value: t.id
|
|
3832
3908
|
}))
|
|
3833
3909
|
});
|
|
3834
|
-
if (
|
|
3835
|
-
|
|
3910
|
+
if (p8.isCancel(taskId)) {
|
|
3911
|
+
p8.cancel("Cancelled.");
|
|
3836
3912
|
process.exit(0);
|
|
3837
3913
|
}
|
|
3838
3914
|
const task2 = await db.getTaskById(taskId);
|
|
3839
3915
|
if (!task2) {
|
|
3840
|
-
|
|
3916
|
+
p8.log.error("Task not found");
|
|
3841
3917
|
process.exit(1);
|
|
3842
3918
|
}
|
|
3843
|
-
const title = await
|
|
3919
|
+
const title = await p8.text({
|
|
3844
3920
|
message: "Title",
|
|
3845
3921
|
initialValue: task2.title
|
|
3846
3922
|
});
|
|
3847
|
-
if (
|
|
3848
|
-
|
|
3923
|
+
if (p8.isCancel(title)) {
|
|
3924
|
+
p8.cancel("Cancelled.");
|
|
3849
3925
|
process.exit(0);
|
|
3850
3926
|
}
|
|
3851
|
-
const description = await
|
|
3927
|
+
const description = await p8.text({
|
|
3852
3928
|
message: "Description (what and why)",
|
|
3853
3929
|
initialValue: task2.description ?? ""
|
|
3854
3930
|
});
|
|
3855
|
-
if (
|
|
3856
|
-
|
|
3931
|
+
if (p8.isCancel(description)) {
|
|
3932
|
+
p8.cancel("Cancelled.");
|
|
3857
3933
|
process.exit(0);
|
|
3858
3934
|
}
|
|
3859
3935
|
const currentAcceptance = await db.getTaskAcceptance(task2.id);
|
|
3860
3936
|
const newAcceptance = [];
|
|
3861
|
-
|
|
3937
|
+
p8.log.info("Acceptance criteria \u2014 edit each, empty to delete. Add new ones at the end.");
|
|
3862
3938
|
for (let i = 0; i < currentAcceptance.length; i++) {
|
|
3863
3939
|
const ac = currentAcceptance[i];
|
|
3864
|
-
const val = await
|
|
3940
|
+
const val = await p8.text({
|
|
3865
3941
|
message: `#${i + 1}/${currentAcceptance.length}`,
|
|
3866
3942
|
initialValue: ac.criterion,
|
|
3867
3943
|
defaultValue: ""
|
|
3868
3944
|
});
|
|
3869
|
-
if (
|
|
3870
|
-
|
|
3945
|
+
if (p8.isCancel(val)) {
|
|
3946
|
+
p8.cancel("Cancelled.");
|
|
3871
3947
|
process.exit(0);
|
|
3872
3948
|
}
|
|
3873
3949
|
const trimmed = val.trim();
|
|
@@ -3876,11 +3952,11 @@ async function runTaskEdit(cwd2) {
|
|
|
3876
3952
|
}
|
|
3877
3953
|
}
|
|
3878
3954
|
while (true) {
|
|
3879
|
-
const val = await
|
|
3880
|
-
if (
|
|
3955
|
+
const val = await p8.text({ message: "New acceptance criterion", placeholder: "(press Enter to finish)" });
|
|
3956
|
+
if (p8.isCancel(val) || !val || !val.trim()) break;
|
|
3881
3957
|
newAcceptance.push(val.trim());
|
|
3882
3958
|
}
|
|
3883
|
-
const spinner6 =
|
|
3959
|
+
const spinner6 = p8.spinner();
|
|
3884
3960
|
spinner6.start("Saving...");
|
|
3885
3961
|
try {
|
|
3886
3962
|
const newSlug = slugify(title);
|
|
@@ -3892,10 +3968,10 @@ async function runTaskEdit(cwd2) {
|
|
|
3892
3968
|
await db.updateTaskAcceptance(task2.id, newAcceptance);
|
|
3893
3969
|
await db.writeFeatureList(cwd2);
|
|
3894
3970
|
spinner6.stop("");
|
|
3895
|
-
console.log(
|
|
3971
|
+
console.log(pc17.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
|
|
3896
3972
|
} catch (err) {
|
|
3897
|
-
spinner6.stop(
|
|
3898
|
-
|
|
3973
|
+
spinner6.stop(pc17.red("Failed"));
|
|
3974
|
+
p8.log.error(err instanceof Error ? err.message : String(err));
|
|
3899
3975
|
process.exit(1);
|
|
3900
3976
|
}
|
|
3901
3977
|
} finally {
|
|
@@ -3905,12 +3981,12 @@ async function runTaskEdit(cwd2) {
|
|
|
3905
3981
|
|
|
3906
3982
|
// src/commands/task/list.ts
|
|
3907
3983
|
import Table2 from "cli-table3";
|
|
3908
|
-
import
|
|
3984
|
+
import pc18 from "picocolors";
|
|
3909
3985
|
var STATUS_COLOR2 = {
|
|
3910
|
-
pending: (s) =>
|
|
3911
|
-
in_progress: (s) =>
|
|
3912
|
-
done: (s) =>
|
|
3913
|
-
blocked: (s) =>
|
|
3986
|
+
pending: (s) => pc18.dim(s),
|
|
3987
|
+
in_progress: (s) => pc18.cyan(s),
|
|
3988
|
+
done: (s) => pc18.green(s),
|
|
3989
|
+
blocked: (s) => pc18.red(s)
|
|
3914
3990
|
};
|
|
3915
3991
|
async function runTaskList(cwd2, opts) {
|
|
3916
3992
|
const config = await loadConfig(cwd2);
|
|
@@ -3927,11 +4003,11 @@ async function runTaskList(cwd2, opts) {
|
|
|
3927
4003
|
let msg = "No tasks";
|
|
3928
4004
|
if (filterStatus) msg += ` with status: ${filterStatus}`;
|
|
3929
4005
|
if (opts.archived) msg += " (archived)";
|
|
3930
|
-
console.log(
|
|
4006
|
+
console.log(pc18.dim(msg + "."));
|
|
3931
4007
|
return;
|
|
3932
4008
|
}
|
|
3933
4009
|
const table = new Table2({
|
|
3934
|
-
head: ["ID", "Slug", "Title", "Status"].map((h) =>
|
|
4010
|
+
head: ["ID", "Slug", "Title", "Status"].map((h) => pc18.bold(h)),
|
|
3935
4011
|
style: { head: [], border: [] }
|
|
3936
4012
|
});
|
|
3937
4013
|
for (const t of tasks) {
|
|
@@ -3942,7 +4018,7 @@ async function runTaskList(cwd2, opts) {
|
|
|
3942
4018
|
if (!opts.archived && !opts.includeArchived) {
|
|
3943
4019
|
const archivedTasks = await db.getArchivedTasks();
|
|
3944
4020
|
if (archivedTasks.length > 0) {
|
|
3945
|
-
console.log(
|
|
4021
|
+
console.log(pc18.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
|
|
3946
4022
|
}
|
|
3947
4023
|
}
|
|
3948
4024
|
} finally {
|
|
@@ -3952,8 +4028,8 @@ async function runTaskList(cwd2, opts) {
|
|
|
3952
4028
|
|
|
3953
4029
|
// src/core/path-probe.ts
|
|
3954
4030
|
import { accessSync, constants, readdirSync as readdirSync3 } from "fs";
|
|
3955
|
-
import { join as
|
|
3956
|
-
import
|
|
4031
|
+
import { join as join23 } from "path";
|
|
4032
|
+
import pc19 from "picocolors";
|
|
3957
4033
|
var DEFAULT_PATHEXT = [".COM", ".EXE", ".BAT", ".CMD"];
|
|
3958
4034
|
function defaultIsExecutable(filePath) {
|
|
3959
4035
|
try {
|
|
@@ -3997,7 +4073,7 @@ function resolveOnPath(name, options = {}) {
|
|
|
3997
4073
|
}
|
|
3998
4074
|
for (const dir of dirs) {
|
|
3999
4075
|
try {
|
|
4000
|
-
if (isExecutable(
|
|
4076
|
+
if (isExecutable(join23(dir, name))) return true;
|
|
4001
4077
|
} catch {
|
|
4002
4078
|
continue;
|
|
4003
4079
|
}
|
|
@@ -4008,16 +4084,16 @@ function isExecutableOnPath(name) {
|
|
|
4008
4084
|
return resolveOnPath(name);
|
|
4009
4085
|
}
|
|
4010
4086
|
function printMissingGlobalBinaryWarning() {
|
|
4011
|
-
console.error(
|
|
4012
|
-
console.error(
|
|
4013
|
-
console.error(
|
|
4014
|
-
console.error(
|
|
4015
|
-
console.error(
|
|
4016
|
-
console.error(
|
|
4087
|
+
console.error(pc19.yellow("\u26A0 `ahk` was not found on your PATH."));
|
|
4088
|
+
console.error(pc19.dim(" Your project has no local install, so the generated MCP config launches"));
|
|
4089
|
+
console.error(pc19.dim(" `ahk serve` directly. Without `ahk` on your PATH, starting the MCP server"));
|
|
4090
|
+
console.error(pc19.dim(" from that config will fail. This is only a warning \u2014 the command continues."));
|
|
4091
|
+
console.error(pc19.dim(` Run: npm i -g ${pkg.name} (install globally)`));
|
|
4092
|
+
console.error(pc19.dim(` or: npm install --save-dev ${pkg.name} (install locally in this project)`));
|
|
4017
4093
|
}
|
|
4018
4094
|
|
|
4019
4095
|
// src/core/update-check.ts
|
|
4020
|
-
import
|
|
4096
|
+
import pc20 from "picocolors";
|
|
4021
4097
|
var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
4022
4098
|
var TIMEOUT_MS2 = 2500;
|
|
4023
4099
|
function checkForUpdate(currentVersion) {
|
|
@@ -4035,8 +4111,8 @@ function checkForUpdate(currentVersion) {
|
|
|
4035
4111
|
}
|
|
4036
4112
|
function printUpdateMessage({ current, latest }) {
|
|
4037
4113
|
const lines = [
|
|
4038
|
-
` Update available ${
|
|
4039
|
-
` Run: ${
|
|
4114
|
+
` Update available ${pc20.dim(current)} \u2192 ${pc20.green(latest)} `,
|
|
4115
|
+
` Run: ${pc20.cyan(`pnpm i ${pkg.name}@${latest}`)} `
|
|
4040
4116
|
];
|
|
4041
4117
|
drawBox(lines);
|
|
4042
4118
|
}
|
|
@@ -4113,7 +4189,7 @@ migrate.command("storage").description(
|
|
|
4113
4189
|
try {
|
|
4114
4190
|
await runMigrateStorage(cwd, { force: opts.force, dryRun: opts["dry-run"] });
|
|
4115
4191
|
} catch (err) {
|
|
4116
|
-
console.error(
|
|
4192
|
+
console.error(pc21.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
|
|
4117
4193
|
process.exit(1);
|
|
4118
4194
|
}
|
|
4119
4195
|
});
|
|
@@ -4126,6 +4202,9 @@ program.command("reset").description("Reset/clear harness data (DB, feature list
|
|
|
4126
4202
|
program.command("doctor").description("Check lib version, agent files, and harness skills sync status").action(async () => {
|
|
4127
4203
|
await runDoctor(cwd);
|
|
4128
4204
|
});
|
|
4205
|
+
program.command("models").description("Re-prompt per-role Claude Code models and regenerate .claude/agents/*.md (claude-code projects only)").action(async () => {
|
|
4206
|
+
await runModels(cwd);
|
|
4207
|
+
});
|
|
4129
4208
|
program.hook("preAction", () => {
|
|
4130
4209
|
if (!isLocalInstallSatisfied(cwd)) {
|
|
4131
4210
|
printLocalInstallWarning();
|