harnessed 3.0.1 → 3.0.3
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 +4 -0
- package/dist/cli.mjs +222 -213
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/workflows/capabilities.yaml +1 -1
- package/workflows/defaults.yaml +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync, spawn } from 'child_process';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, appendFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { join, resolve, dirname, relative } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
5
6
|
import { Type } from '@sinclair/typebox';
|
|
6
7
|
import { Value } from '@sinclair/typebox/value';
|
|
7
8
|
import { LineCounter, parseDocument, parse, isSeq, isScalar } from 'yaml';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
9
|
import { readFile, readdir, unlink, writeFile, stat, rm, cp, access, mkdir, rename } from 'fs/promises';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { Command } from 'commander';
|
|
@@ -78,6 +78,46 @@ var init_origin_check = __esm({
|
|
|
78
78
|
"src/cli/lib/origin-check.ts"() {
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
|
+
function getHarnessedRoot() {
|
|
82
|
+
const override = process.env.HARNESSED_ROOT_OVERRIDE;
|
|
83
|
+
if (override !== void 0 && override !== "") return override;
|
|
84
|
+
return join(homedir(), ".claude", "harnessed");
|
|
85
|
+
}
|
|
86
|
+
function harnessedSubdir(name) {
|
|
87
|
+
return join(getHarnessedRoot(), name);
|
|
88
|
+
}
|
|
89
|
+
function harnessedFile(name) {
|
|
90
|
+
return join(getHarnessedRoot(), name);
|
|
91
|
+
}
|
|
92
|
+
function migrateLegacyHarnessedRoot() {
|
|
93
|
+
const legacyRoot = join(homedir(), ".harnessed");
|
|
94
|
+
const newRoot = join(homedir(), ".claude", "harnessed");
|
|
95
|
+
const claudeParent = join(homedir(), ".claude");
|
|
96
|
+
if (!existsSync(legacyRoot)) return;
|
|
97
|
+
if (!existsSync(newRoot)) {
|
|
98
|
+
mkdirSync(claudeParent, { recursive: true });
|
|
99
|
+
renameSync(legacyRoot, newRoot);
|
|
100
|
+
console.error(
|
|
101
|
+
`[harnessed] migrated legacy state directory ${legacyRoot} \u2192 ${newRoot} (v3.0.3 path change)`
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const safetyBak = join(homedir(), ".harnessed.legacy-bak");
|
|
106
|
+
if (existsSync(safetyBak)) {
|
|
107
|
+
console.error(
|
|
108
|
+
`[harnessed] WARN: ${legacyRoot} reappeared after a prior migration (existing bak at ${safetyBak}); leaving in place \u2014 inspect manually if needed`
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
renameSync(legacyRoot, safetyBak);
|
|
113
|
+
console.error(
|
|
114
|
+
`[harnessed] both ${legacyRoot} and ${newRoot} existed \u2014 legacy directory preserved at ${safetyBak} (review manually if you need data from it; v3.0.3 path change)`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
var init_harnessedRoot = __esm({
|
|
118
|
+
"src/installers/lib/harnessedRoot.ts"() {
|
|
119
|
+
}
|
|
120
|
+
});
|
|
81
121
|
|
|
82
122
|
// src/cli/lib/probe-gstack.ts
|
|
83
123
|
var probe_gstack_exports = {};
|
|
@@ -408,7 +448,7 @@ function writeCheckpoint(c, customPath) {
|
|
|
408
448
|
throw new CheckpointWriteError(`Schema validation failed: ${errs}`);
|
|
409
449
|
}
|
|
410
450
|
const enforced = enforceBudget(c);
|
|
411
|
-
const path =
|
|
451
|
+
const path = join(harnessedSubdir("checkpoints"), `${enforced.phase}.json`);
|
|
412
452
|
mkdirSync(dirname(path), { recursive: true });
|
|
413
453
|
writeFileSync(path, JSON.stringify(enforced, null, 2), "utf8");
|
|
414
454
|
return path;
|
|
@@ -416,6 +456,7 @@ function writeCheckpoint(c, customPath) {
|
|
|
416
456
|
var BUDGET_TOKEN, CheckpointTooLargeError, CheckpointWriteError;
|
|
417
457
|
var init_template = __esm({
|
|
418
458
|
"src/checkpoint/template.ts"() {
|
|
459
|
+
init_harnessedRoot();
|
|
419
460
|
init_schema();
|
|
420
461
|
BUDGET_TOKEN = 1e3;
|
|
421
462
|
CheckpointTooLargeError = class extends Error {
|
|
@@ -579,10 +620,25 @@ var init_check_planning_with_files = __esm({
|
|
|
579
620
|
REMEDIATION = "install via Claude Code plugin marketplace: `claude plugin install planning-with-files` (requires >=2.2.0 per R20.15 + D-15)";
|
|
580
621
|
}
|
|
581
622
|
});
|
|
623
|
+
function statePath() {
|
|
624
|
+
return harnessedFile("current-workflow.json");
|
|
625
|
+
}
|
|
626
|
+
function lockTarget() {
|
|
627
|
+
return getHarnessedRoot();
|
|
628
|
+
}
|
|
629
|
+
function lockOpts() {
|
|
630
|
+
return {
|
|
631
|
+
stale: 1e4,
|
|
632
|
+
retries: { retries: 3, factor: 2, minTimeout: 100 },
|
|
633
|
+
lockfilePath: harnessedFile(".lock")
|
|
634
|
+
};
|
|
635
|
+
}
|
|
582
636
|
async function withLock(fn) {
|
|
637
|
+
const target = lockTarget();
|
|
638
|
+
await mkdir(target, { recursive: true });
|
|
583
639
|
let release;
|
|
584
640
|
try {
|
|
585
|
-
release = await lockfile.lock(
|
|
641
|
+
release = await lockfile.lock(target, lockOpts());
|
|
586
642
|
} catch (e) {
|
|
587
643
|
if (e.code === "ELOCKED") throw new LockHeldError();
|
|
588
644
|
throw e;
|
|
@@ -596,7 +652,7 @@ async function withLock(fn) {
|
|
|
596
652
|
async function readCurrentWorkflow() {
|
|
597
653
|
let raw;
|
|
598
654
|
try {
|
|
599
|
-
raw = await readFile(
|
|
655
|
+
raw = await readFile(statePath(), "utf8");
|
|
600
656
|
} catch {
|
|
601
657
|
return null;
|
|
602
658
|
}
|
|
@@ -617,9 +673,10 @@ async function writeCurrentWorkflow(s) {
|
|
|
617
673
|
const errs = [...Value.Errors(CurrentWorkflowV1, s)].map((e) => e.message).join("; ");
|
|
618
674
|
throw new WorkflowStateError(`current-workflow schema validation failed: ${errs}`);
|
|
619
675
|
}
|
|
620
|
-
|
|
676
|
+
const path = statePath();
|
|
677
|
+
await mkdir(dirname(path), { recursive: true });
|
|
621
678
|
await withLock(async () => {
|
|
622
|
-
await writeFile(
|
|
679
|
+
await writeFile(path, JSON.stringify(s, null, 2), "utf8");
|
|
623
680
|
});
|
|
624
681
|
}
|
|
625
682
|
async function activate(phase, checkpointPath = null) {
|
|
@@ -636,18 +693,12 @@ async function complete() {
|
|
|
636
693
|
if (!s) return;
|
|
637
694
|
await writeCurrentWorkflow({ ...s, status: "complete", completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
638
695
|
}
|
|
639
|
-
var
|
|
696
|
+
var WorkflowStateError, LockHeldError;
|
|
640
697
|
var init_state = __esm({
|
|
641
698
|
"src/checkpoint/state.ts"() {
|
|
699
|
+
init_harnessedRoot();
|
|
642
700
|
init_schemaVersion();
|
|
643
701
|
init_schema();
|
|
644
|
-
STATE_PATH = ".harnessed/current-workflow.json";
|
|
645
|
-
LOCK_TARGET = ".harnessed";
|
|
646
|
-
LOCK_OPTS = {
|
|
647
|
-
stale: 1e4,
|
|
648
|
-
retries: { retries: 3, factor: 2, minTimeout: 100 },
|
|
649
|
-
lockfilePath: ".harnessed/.lock"
|
|
650
|
-
};
|
|
651
702
|
WorkflowStateError = class extends Error {
|
|
652
703
|
constructor(message) {
|
|
653
704
|
super(message);
|
|
@@ -657,7 +708,7 @@ var init_state = __esm({
|
|
|
657
708
|
LockHeldError = class _LockHeldError extends Error {
|
|
658
709
|
constructor() {
|
|
659
710
|
super(
|
|
660
|
-
|
|
711
|
+
`another harnessed process holds the lock at ${harnessedFile(".lock")} \u2014 wait or kill stale process (try: harnessed status)`
|
|
661
712
|
);
|
|
662
713
|
this.name = "LockHeldError";
|
|
663
714
|
Object.setPrototypeOf(this, _LockHeldError.prototype);
|
|
@@ -740,7 +791,10 @@ __export(resume_exports, {
|
|
|
740
791
|
async function runResume() {
|
|
741
792
|
const current = await readCurrentWorkflow();
|
|
742
793
|
if (!current) {
|
|
743
|
-
return {
|
|
794
|
+
return {
|
|
795
|
+
status: "no-paused-phase",
|
|
796
|
+
error: "no current-workflow.json found under <harnessed-root>"
|
|
797
|
+
};
|
|
744
798
|
}
|
|
745
799
|
if (current.status !== "paused") {
|
|
746
800
|
return {
|
|
@@ -793,7 +847,7 @@ var init_resume = __esm({
|
|
|
793
847
|
|
|
794
848
|
// package.json
|
|
795
849
|
var package_default = {
|
|
796
|
-
version: "3.0.
|
|
850
|
+
version: "3.0.3"};
|
|
797
851
|
|
|
798
852
|
// src/manifest/errors.ts
|
|
799
853
|
function instancePathToKeyPath(instancePath) {
|
|
@@ -1546,7 +1600,12 @@ audited ${yamls.length} manifest${yamls.length === 1 ? "" : "s"} \u2014 ${findin
|
|
|
1546
1600
|
process.exit(errorCount > 0 ? 1 : 0);
|
|
1547
1601
|
});
|
|
1548
1602
|
}
|
|
1549
|
-
|
|
1603
|
+
|
|
1604
|
+
// src/cli/audit-log.ts
|
|
1605
|
+
init_harnessedRoot();
|
|
1606
|
+
function auditPath() {
|
|
1607
|
+
return harnessedFile("audit.log");
|
|
1608
|
+
}
|
|
1550
1609
|
var REDACT_PATTERNS = [
|
|
1551
1610
|
[/api[_-]?key\s*[:=]\s*\S+/gi, "api_key=[REDACTED]"],
|
|
1552
1611
|
[/\btoken\s*[:=]\s*\S+/gi, "token=[REDACTED]"],
|
|
@@ -1594,7 +1653,9 @@ function pipeToJq(filterExpr, lines) {
|
|
|
1594
1653
|
});
|
|
1595
1654
|
}
|
|
1596
1655
|
function registerAuditLog(program2) {
|
|
1597
|
-
program2.command("audit-log").description(
|
|
1656
|
+
program2.command("audit-log").description(
|
|
1657
|
+
"Query routing audit log (<harnessed-root>/audit.log) with optional jq filter (R10.1)"
|
|
1658
|
+
).option("--filter <expr>", `jq filter expression (e.g. '.category=="engineering"')`).option("--tail <n>", "show N most recent records (default 50)", "50").option("--head <n>", "show N oldest records (--head takes priority over --tail)").option("--reverse", "flip output order").option("--json", "output full 12-field JSON instead of human table").action(
|
|
1598
1659
|
async (opts) => {
|
|
1599
1660
|
const tailN = opts.tail !== void 0 ? Number(opts.tail) : 50;
|
|
1600
1661
|
if (Number.isNaN(tailN) || tailN < 1) {
|
|
@@ -1606,11 +1667,12 @@ function registerAuditLog(program2) {
|
|
|
1606
1667
|
console.error("\u2717 --head must be a positive integer");
|
|
1607
1668
|
process.exit(2);
|
|
1608
1669
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1670
|
+
const path = auditPath();
|
|
1671
|
+
if (!existsSync(path)) {
|
|
1672
|
+
console.log(`no audit records found (${path} does not exist)`);
|
|
1611
1673
|
process.exit(0);
|
|
1612
1674
|
}
|
|
1613
|
-
const raw = readFileSync(
|
|
1675
|
+
const raw = readFileSync(path, "utf8");
|
|
1614
1676
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1615
1677
|
if (lines.length === 0) {
|
|
1616
1678
|
console.log("no audit records found (audit.log is empty)");
|
|
@@ -1646,8 +1708,11 @@ function registerAuditLog(program2) {
|
|
|
1646
1708
|
}
|
|
1647
1709
|
);
|
|
1648
1710
|
}
|
|
1711
|
+
|
|
1712
|
+
// src/installers/lib/backup.ts
|
|
1713
|
+
init_harnessedRoot();
|
|
1649
1714
|
function getBackupRoot() {
|
|
1650
|
-
return
|
|
1715
|
+
return harnessedSubdir("backups");
|
|
1651
1716
|
}
|
|
1652
1717
|
var HOME_DIR = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1653
1718
|
function mirrorPath(target, scope, backupDir) {
|
|
@@ -2317,7 +2382,12 @@ function matchesWhen(when, task) {
|
|
|
2317
2382
|
}
|
|
2318
2383
|
return true;
|
|
2319
2384
|
}
|
|
2320
|
-
|
|
2385
|
+
|
|
2386
|
+
// src/audit/log.ts
|
|
2387
|
+
init_harnessedRoot();
|
|
2388
|
+
function auditPath2() {
|
|
2389
|
+
return harnessedFile("audit.log");
|
|
2390
|
+
}
|
|
2321
2391
|
Type.Object(
|
|
2322
2392
|
{
|
|
2323
2393
|
ts: Type.String(),
|
|
@@ -2352,8 +2422,9 @@ function buildAuditRecord(task, decision, matched, ctx) {
|
|
|
2352
2422
|
};
|
|
2353
2423
|
}
|
|
2354
2424
|
function emitAuditRecord(record) {
|
|
2355
|
-
|
|
2356
|
-
|
|
2425
|
+
const path = auditPath2();
|
|
2426
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2427
|
+
appendFileSync(path, `${JSON.stringify(record)}
|
|
2357
2428
|
`);
|
|
2358
2429
|
}
|
|
2359
2430
|
|
|
@@ -2371,18 +2442,19 @@ function emitAudit(task, decision, matched, outcome, sessionId) {
|
|
|
2371
2442
|
}
|
|
2372
2443
|
|
|
2373
2444
|
// src/checkpoint/engineHook.ts
|
|
2445
|
+
init_harnessedRoot();
|
|
2374
2446
|
init_schemaVersion();
|
|
2375
2447
|
init_state();
|
|
2376
2448
|
init_template();
|
|
2377
2449
|
async function activatePhase(phaseId) {
|
|
2378
|
-
const checkpointPath =
|
|
2450
|
+
const checkpointPath = join(harnessedSubdir("checkpoints"), `${phaseId}.json`);
|
|
2379
2451
|
await activate(phaseId, checkpointPath);
|
|
2380
2452
|
return { checkpointPath };
|
|
2381
2453
|
}
|
|
2382
2454
|
async function completePhase(ctx) {
|
|
2383
2455
|
if (ctx.phaseId === "unknown") {
|
|
2384
2456
|
console.error(
|
|
2385
|
-
|
|
2457
|
+
`[harnessed] WARN engineHook: phaseId="unknown" \u2014 checkpoint paths fall back to ${join(harnessedSubdir("checkpoints"), "unknown.json")} (Karpathy fail-loud non-blocking; W-04 mitigation)`
|
|
2386
2458
|
);
|
|
2387
2459
|
}
|
|
2388
2460
|
writeCheckpoint({
|
|
@@ -2395,7 +2467,7 @@ async function completePhase(ctx) {
|
|
|
2395
2467
|
...ctx.sessionId ? { session_id: ctx.sessionId } : {},
|
|
2396
2468
|
cwd: process.cwd(),
|
|
2397
2469
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2398
|
-
archive_path:
|
|
2470
|
+
archive_path: `${join(harnessedSubdir("archive"), `phase-${ctx.phaseId}`)}/`
|
|
2399
2471
|
});
|
|
2400
2472
|
await complete();
|
|
2401
2473
|
}
|
|
@@ -2479,11 +2551,11 @@ var VerbatimCompleteFailError = class extends Error {
|
|
|
2479
2551
|
}
|
|
2480
2552
|
lastMessage;
|
|
2481
2553
|
};
|
|
2482
|
-
async function ralphLoopWrap(
|
|
2554
|
+
async function ralphLoopWrap(spawn6, maxIter) {
|
|
2483
2555
|
let last = "";
|
|
2484
2556
|
let sessionId;
|
|
2485
2557
|
for (let i = 0; i < maxIter; i++) {
|
|
2486
|
-
last = await
|
|
2558
|
+
last = await spawn6(sessionId, (id) => {
|
|
2487
2559
|
sessionId = id;
|
|
2488
2560
|
});
|
|
2489
2561
|
if (isComplete(last)) return last;
|
|
@@ -3239,12 +3311,15 @@ function preflight(ctx) {
|
|
|
3239
3311
|
}
|
|
3240
3312
|
return { ok: errors.length === 0, errors };
|
|
3241
3313
|
}
|
|
3314
|
+
|
|
3315
|
+
// src/installers/lib/state.ts
|
|
3316
|
+
init_harnessedRoot();
|
|
3242
3317
|
var DEFAULT_STATE = { version: "1", installed: {} };
|
|
3243
|
-
function
|
|
3244
|
-
return
|
|
3318
|
+
function statePath2(_cwd) {
|
|
3319
|
+
return harnessedFile("state.json");
|
|
3245
3320
|
}
|
|
3246
3321
|
async function readState(cwd) {
|
|
3247
|
-
const path =
|
|
3322
|
+
const path = statePath2();
|
|
3248
3323
|
let raw;
|
|
3249
3324
|
try {
|
|
3250
3325
|
raw = await readFile(path, "utf8");
|
|
@@ -3265,7 +3340,7 @@ async function readState(cwd) {
|
|
|
3265
3340
|
}
|
|
3266
3341
|
}
|
|
3267
3342
|
async function writeState(cwd, state) {
|
|
3268
|
-
const path =
|
|
3343
|
+
const path = statePath2();
|
|
3269
3344
|
const tmp = `${path}.tmp`;
|
|
3270
3345
|
await mkdir(dirname(path), { recursive: true });
|
|
3271
3346
|
await writeFile(tmp, `${JSON.stringify(state, null, 2)}
|
|
@@ -3273,7 +3348,7 @@ async function writeState(cwd, state) {
|
|
|
3273
3348
|
await rename(tmp, path);
|
|
3274
3349
|
}
|
|
3275
3350
|
async function updateInstalled(cwd, name, version, manifestSha1) {
|
|
3276
|
-
const state = await readState(
|
|
3351
|
+
const state = await readState();
|
|
3277
3352
|
state.installed[name] = {
|
|
3278
3353
|
version,
|
|
3279
3354
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3382,6 +3457,39 @@ var installCcHookAdd = async (ctx) => {
|
|
|
3382
3457
|
await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, "", "");
|
|
3383
3458
|
return { ok: true, backupId: bk.backupId, appliedFiles: [settingsPath] };
|
|
3384
3459
|
};
|
|
3460
|
+
function getUserClaudeJsonPath() {
|
|
3461
|
+
return join(homedir(), ".claude.json");
|
|
3462
|
+
}
|
|
3463
|
+
async function readUserClaudeJson() {
|
|
3464
|
+
const path = getUserClaudeJsonPath();
|
|
3465
|
+
let raw;
|
|
3466
|
+
try {
|
|
3467
|
+
raw = await readFile(path, "utf8");
|
|
3468
|
+
} catch (err2) {
|
|
3469
|
+
if (err2.code === "ENOENT") return {};
|
|
3470
|
+
throw err2;
|
|
3471
|
+
}
|
|
3472
|
+
try {
|
|
3473
|
+
const parsed = JSON.parse(raw);
|
|
3474
|
+
if (parsed === null || typeof parsed !== "object") return {};
|
|
3475
|
+
return parsed;
|
|
3476
|
+
} catch {
|
|
3477
|
+
return {};
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
async function isMcpServerRegistered(name) {
|
|
3481
|
+
const config = await readUserClaudeJson();
|
|
3482
|
+
const servers = config.mcpServers;
|
|
3483
|
+
if (!servers || typeof servers !== "object") return false;
|
|
3484
|
+
return Object.hasOwn(servers, name);
|
|
3485
|
+
}
|
|
3486
|
+
async function isPluginRegistered(pluginName) {
|
|
3487
|
+
const config = await readUserClaudeJson();
|
|
3488
|
+
const plugins = config.enabledPlugins;
|
|
3489
|
+
if (!plugins || typeof plugins !== "object") return false;
|
|
3490
|
+
if (Object.hasOwn(plugins, pluginName)) return true;
|
|
3491
|
+
return Object.keys(plugins).some((k) => k.split("@")[0] === pluginName);
|
|
3492
|
+
}
|
|
3385
3493
|
function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
|
|
3386
3494
|
return new Promise((resolve9) => {
|
|
3387
3495
|
const isWin = process.platform === "win32";
|
|
@@ -3404,6 +3512,9 @@ function runArgs(claudeArgs, cwd, timeoutMs = 15e3) {
|
|
|
3404
3512
|
});
|
|
3405
3513
|
});
|
|
3406
3514
|
}
|
|
3515
|
+
function getMcpSpawnCwd() {
|
|
3516
|
+
return homedir();
|
|
3517
|
+
}
|
|
3407
3518
|
|
|
3408
3519
|
// src/installers/ccPluginMarketplace.ts
|
|
3409
3520
|
function parseCmd(cmd) {
|
|
@@ -3455,8 +3566,8 @@ var installCcPluginMarketplace = async (ctx) => {
|
|
|
3455
3566
|
}
|
|
3456
3567
|
};
|
|
3457
3568
|
}
|
|
3458
|
-
const pluginName = parsed.pluginAtMkt.split("@")[0];
|
|
3459
|
-
const installArgs = ["plugin", "install", parsed.pluginAtMkt, "--scope", "
|
|
3569
|
+
const pluginName = parsed.pluginAtMkt.split("@")[0] ?? parsed.pluginAtMkt;
|
|
3570
|
+
const installArgs = ["plugin", "install", parsed.pluginAtMkt, "--scope", "user"];
|
|
3460
3571
|
const allArgs = [];
|
|
3461
3572
|
if (parsed.marketplaceRef !== null) {
|
|
3462
3573
|
allArgs.push(["plugin", "marketplace", "add", parsed.marketplaceRef]);
|
|
@@ -3464,30 +3575,30 @@ var installCcPluginMarketplace = async (ctx) => {
|
|
|
3464
3575
|
allArgs.push(installArgs);
|
|
3465
3576
|
for (const argSet of allArgs) {
|
|
3466
3577
|
for (const a of argSet) {
|
|
3467
|
-
const
|
|
3468
|
-
if (
|
|
3578
|
+
const violation = checkCmdString(a);
|
|
3579
|
+
if (violation) {
|
|
3469
3580
|
return {
|
|
3470
3581
|
ok: false,
|
|
3471
3582
|
phase: "preflight",
|
|
3472
3583
|
error: err(
|
|
3473
3584
|
ctx,
|
|
3474
3585
|
"/spec/install/cmd",
|
|
3475
|
-
`shell escape detected in constructed cc-plugin arg '${a.slice(0, 60)}': ${
|
|
3586
|
+
`shell escape detected in constructed cc-plugin arg '${a.slice(0, 60)}': ${violation.label} (${violation.hint})`,
|
|
3476
3587
|
"security-gate-bypass"
|
|
3477
3588
|
)
|
|
3478
3589
|
};
|
|
3479
3590
|
}
|
|
3480
3591
|
}
|
|
3481
3592
|
}
|
|
3482
|
-
const settingsFile = `${
|
|
3593
|
+
const settingsFile = `${getMcpSpawnCwd()}/.claude.json`;
|
|
3483
3594
|
const newEntry = JSON.stringify({ enabledPlugins: { [parsed.pluginAtMkt]: true } }, null, 2);
|
|
3484
3595
|
const plan = {
|
|
3485
3596
|
files: [
|
|
3486
3597
|
{
|
|
3487
3598
|
target: settingsFile,
|
|
3488
|
-
scope: "
|
|
3599
|
+
scope: "HOME",
|
|
3489
3600
|
oldText: "",
|
|
3490
|
-
newText: `// will be merged into
|
|
3601
|
+
newText: `// will be merged into ~/.claude.json enabledPlugins map by \`claude plugin install --scope user\`:
|
|
3491
3602
|
${newEntry}
|
|
3492
3603
|
`
|
|
3493
3604
|
}
|
|
@@ -3502,15 +3613,13 @@ ${newEntry}
|
|
|
3502
3613
|
if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
|
|
3503
3614
|
const bk = await backup(plan, ctx);
|
|
3504
3615
|
if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
|
|
3616
|
+
const spawnCwd = install.cwd ?? getMcpSpawnCwd();
|
|
3505
3617
|
let stepOneStderr = "";
|
|
3506
3618
|
if (parsed.marketplaceRef !== null) {
|
|
3507
|
-
const r1 = await runArgs(
|
|
3508
|
-
["plugin", "marketplace", "add", parsed.marketplaceRef],
|
|
3509
|
-
install.cwd ?? ctx.cwd
|
|
3510
|
-
);
|
|
3619
|
+
const r1 = await runArgs(["plugin", "marketplace", "add", parsed.marketplaceRef], spawnCwd);
|
|
3511
3620
|
stepOneStderr = r1.stderr;
|
|
3512
3621
|
}
|
|
3513
|
-
const r2 = await runArgs(installArgs,
|
|
3622
|
+
const r2 = await runArgs(installArgs, spawnCwd);
|
|
3514
3623
|
if (r2.exitCode !== 0) {
|
|
3515
3624
|
return {
|
|
3516
3625
|
ok: false,
|
|
@@ -3524,11 +3633,8 @@ ${newEntry}
|
|
|
3524
3633
|
)
|
|
3525
3634
|
};
|
|
3526
3635
|
}
|
|
3527
|
-
const
|
|
3528
|
-
|
|
3529
|
-
const verifyLine = `claude plugin list --json | grep -q ${pluginName}`;
|
|
3530
|
-
const violation = checkCmdString(verifyLine);
|
|
3531
|
-
if (violation) {
|
|
3636
|
+
const registered = await isPluginRegistered(pluginName);
|
|
3637
|
+
if (!registered) {
|
|
3532
3638
|
return {
|
|
3533
3639
|
ok: false,
|
|
3534
3640
|
phase: "verify",
|
|
@@ -3536,39 +3642,7 @@ ${newEntry}
|
|
|
3536
3642
|
error: err(
|
|
3537
3643
|
ctx,
|
|
3538
3644
|
"/spec/verify/cmd",
|
|
3539
|
-
`verify
|
|
3540
|
-
"security-gate-bypass"
|
|
3541
|
-
)
|
|
3542
|
-
};
|
|
3543
|
-
}
|
|
3544
|
-
const vr = await new Promise((resolve9) => {
|
|
3545
|
-
const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
|
|
3546
|
-
let stderr = "";
|
|
3547
|
-
child.stderr?.setEncoding("utf8").on("data", (c) => {
|
|
3548
|
-
stderr += c;
|
|
3549
|
-
});
|
|
3550
|
-
const timer = setTimeout(() => {
|
|
3551
|
-
child.kill("SIGKILL");
|
|
3552
|
-
resolve9({ exitCode: -1, stderr: `${stderr}[timeout]` });
|
|
3553
|
-
}, 15e3);
|
|
3554
|
-
child.on("error", (e) => {
|
|
3555
|
-
clearTimeout(timer);
|
|
3556
|
-
resolve9({ exitCode: -1, stderr: e.message });
|
|
3557
|
-
});
|
|
3558
|
-
child.on("close", (code) => {
|
|
3559
|
-
clearTimeout(timer);
|
|
3560
|
-
resolve9({ exitCode: code ?? -1, stderr });
|
|
3561
|
-
});
|
|
3562
|
-
});
|
|
3563
|
-
if (vr.exitCode !== 0) {
|
|
3564
|
-
return {
|
|
3565
|
-
ok: false,
|
|
3566
|
-
phase: "verify",
|
|
3567
|
-
backupId: bk.backupId,
|
|
3568
|
-
error: err(
|
|
3569
|
-
ctx,
|
|
3570
|
-
"/spec/verify/cmd",
|
|
3571
|
-
`verify exit ${vr.exitCode}: ${vr.stderr.slice(0, 200)}`,
|
|
3645
|
+
`verify: plugin '${pluginName}' not found in enabledPlugins map of ~/.claude.json after install spawn exit 0 (file may have been overwritten, or claude plugin install wrote to a non-default location)`,
|
|
3572
3646
|
"verify-failed"
|
|
3573
3647
|
)
|
|
3574
3648
|
};
|
|
@@ -3576,8 +3650,9 @@ ${newEntry}
|
|
|
3576
3650
|
await updateInstalled(ctx.cwd, ctx.manifest.metadata.name, install.git_ref, "");
|
|
3577
3651
|
return { ok: true, backupId: bk.backupId, appliedFiles: [settingsFile] };
|
|
3578
3652
|
};
|
|
3579
|
-
var
|
|
3580
|
-
|
|
3653
|
+
var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
|
|
3654
|
+
var DEFAULT_INSTALL_TIMEOUT_MS = 6e4;
|
|
3655
|
+
async function spawnCmd(ctx, cmd, args, timeoutMs) {
|
|
3581
3656
|
const violation = checkCmdString(cmd);
|
|
3582
3657
|
if (violation) {
|
|
3583
3658
|
return {
|
|
@@ -3594,8 +3669,7 @@ async function spawnCmd(ctx, cmd, args) {
|
|
|
3594
3669
|
};
|
|
3595
3670
|
}
|
|
3596
3671
|
const installCfg = ctx.manifest.spec.install;
|
|
3597
|
-
const
|
|
3598
|
-
const timeoutMs = verifyCfg.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
3672
|
+
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
|
|
3599
3673
|
const env = { ...process.env, ...installCfg.env ?? {} };
|
|
3600
3674
|
const cwd = installCfg.cwd ?? ctx.cwd;
|
|
3601
3675
|
let child;
|
|
@@ -3622,13 +3696,13 @@ async function spawnCmd(ctx, cmd, args) {
|
|
|
3622
3696
|
error: {
|
|
3623
3697
|
file: ctx.manifest.metadata.name,
|
|
3624
3698
|
path: "/spec/install/cmd",
|
|
3625
|
-
message: `spawn timed out after ${
|
|
3699
|
+
message: `spawn timed out after ${effectiveTimeoutMs}ms (cmd: ${cmd}); partial stderr: ${stderr.slice(0, 200)}`,
|
|
3626
3700
|
line: null,
|
|
3627
3701
|
column: null,
|
|
3628
3702
|
keyword: "spawn-timeout"
|
|
3629
3703
|
}
|
|
3630
3704
|
});
|
|
3631
|
-
},
|
|
3705
|
+
}, effectiveTimeoutMs);
|
|
3632
3706
|
child.on("error", (err2) => {
|
|
3633
3707
|
clearTimeout(timer);
|
|
3634
3708
|
resolve9({
|
|
@@ -3774,7 +3848,7 @@ var installGitCloneWithSetup = async (ctx) => {
|
|
|
3774
3848
|
if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
|
|
3775
3849
|
const bk = await backup(plan, ctx);
|
|
3776
3850
|
if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
|
|
3777
|
-
const sp = await spawnCmd(ctx, install.cmd, []);
|
|
3851
|
+
const sp = await spawnCmd(ctx, install.cmd, [], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
3778
3852
|
if (!("exitCode" in sp)) return { ...sp, backupId: bk.backupId };
|
|
3779
3853
|
if (sp.exitCode !== 0) {
|
|
3780
3854
|
return {
|
|
@@ -3816,7 +3890,8 @@ var installGitCloneWithSetup = async (ctx) => {
|
|
|
3816
3890
|
)
|
|
3817
3891
|
};
|
|
3818
3892
|
}
|
|
3819
|
-
const
|
|
3893
|
+
const verifyTimeoutMs = ctx.manifest.spec.verify.timeout_ms ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
3894
|
+
const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, [], verifyTimeoutMs);
|
|
3820
3895
|
if (!("exitCode" in vr)) return { ...vr, backupId: bk.backupId };
|
|
3821
3896
|
const expected = ctx.manifest.spec.verify.expected_exit_code ?? 0;
|
|
3822
3897
|
if (vr.exitCode !== expected) {
|
|
@@ -3835,6 +3910,8 @@ var installGitCloneWithSetup = async (ctx) => {
|
|
|
3835
3910
|
await updateInstalled(ctx.cwd, name, install.git_ref, "");
|
|
3836
3911
|
return { ok: true, backupId: bk.backupId, appliedFiles: [cloneTarget] };
|
|
3837
3912
|
};
|
|
3913
|
+
|
|
3914
|
+
// src/installers/mcpHttpAdd.ts
|
|
3838
3915
|
function resolveEnvVars(value) {
|
|
3839
3916
|
const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3840
3917
|
let resolved = value;
|
|
@@ -3919,33 +3996,23 @@ var installMcpHttpAdd = async (ctx) => {
|
|
|
3919
3996
|
}
|
|
3920
3997
|
};
|
|
3921
3998
|
}
|
|
3922
|
-
const addArgs = [
|
|
3923
|
-
"mcp",
|
|
3924
|
-
"add",
|
|
3925
|
-
"--scope",
|
|
3926
|
-
"project",
|
|
3927
|
-
"--transport",
|
|
3928
|
-
"http",
|
|
3929
|
-
...hdr.flat,
|
|
3930
|
-
name,
|
|
3931
|
-
url
|
|
3932
|
-
];
|
|
3999
|
+
const addArgs = ["mcp", "add", "--scope", "user", "--transport", "http", ...hdr.flat, name, url];
|
|
3933
4000
|
for (const a of addArgs) {
|
|
3934
|
-
const
|
|
3935
|
-
if (
|
|
4001
|
+
const violation = checkCmdString(a);
|
|
4002
|
+
if (violation) {
|
|
3936
4003
|
return {
|
|
3937
4004
|
ok: false,
|
|
3938
4005
|
phase: "preflight",
|
|
3939
4006
|
error: err(
|
|
3940
4007
|
ctx,
|
|
3941
4008
|
"/spec/install/cmd",
|
|
3942
|
-
`shell escape detected in constructed mcp-http-add arg '${a.slice(0, 60)}': ${
|
|
4009
|
+
`shell escape detected in constructed mcp-http-add arg '${a.slice(0, 60)}': ${violation.label} (${violation.hint})`,
|
|
3943
4010
|
"security-gate-bypass"
|
|
3944
4011
|
)
|
|
3945
4012
|
};
|
|
3946
4013
|
}
|
|
3947
4014
|
}
|
|
3948
|
-
const mcpFile = `${
|
|
4015
|
+
const mcpFile = `${getMcpSpawnCwd()}/.claude.json`;
|
|
3949
4016
|
const headersObj = {};
|
|
3950
4017
|
for (let i = 0; i < hdr.flat.length; i += 2) {
|
|
3951
4018
|
const kv = hdr.flat[i + 1];
|
|
@@ -3959,9 +4026,9 @@ var installMcpHttpAdd = async (ctx) => {
|
|
|
3959
4026
|
files: [
|
|
3960
4027
|
{
|
|
3961
4028
|
target: mcpFile,
|
|
3962
|
-
scope: "
|
|
4029
|
+
scope: "HOME",
|
|
3963
4030
|
oldText: "",
|
|
3964
|
-
newText: `// will be merged into .
|
|
4031
|
+
newText: `// will be merged into ~/.claude.json mcpServers map by \`claude mcp add --scope user\`:
|
|
3965
4032
|
${newEntry}
|
|
3966
4033
|
`
|
|
3967
4034
|
}
|
|
@@ -3976,9 +4043,10 @@ ${newEntry}
|
|
|
3976
4043
|
if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
|
|
3977
4044
|
const bk = await backup(plan, ctx);
|
|
3978
4045
|
if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
|
|
3979
|
-
const
|
|
4046
|
+
const spawnCwd = install.cwd ?? getMcpSpawnCwd();
|
|
4047
|
+
const r = await runArgs(addArgs, spawnCwd);
|
|
3980
4048
|
if (r.exitCode !== 0) {
|
|
3981
|
-
if (r.stderr.includes("already exists
|
|
4049
|
+
if (r.stderr.includes("already exists")) {
|
|
3982
4050
|
return { ok: true, alreadyInstalled: true, backupId: bk.backupId };
|
|
3983
4051
|
}
|
|
3984
4052
|
return {
|
|
@@ -3993,43 +4061,8 @@ ${newEntry}
|
|
|
3993
4061
|
)
|
|
3994
4062
|
};
|
|
3995
4063
|
}
|
|
3996
|
-
const
|
|
3997
|
-
|
|
3998
|
-
const verifyLine = `claude mcp list | grep -q ${name}`;
|
|
3999
|
-
const violation = checkCmdString(verifyLine);
|
|
4000
|
-
if (violation) {
|
|
4001
|
-
return {
|
|
4002
|
-
ok: false,
|
|
4003
|
-
phase: "verify",
|
|
4004
|
-
backupId: bk.backupId,
|
|
4005
|
-
error: err(
|
|
4006
|
-
ctx,
|
|
4007
|
-
"/spec/verify/cmd",
|
|
4008
|
-
`verify shell escape: ${violation.label}`,
|
|
4009
|
-
"security-gate-bypass"
|
|
4010
|
-
)
|
|
4011
|
-
};
|
|
4012
|
-
}
|
|
4013
|
-
const vr = await new Promise((resolve9) => {
|
|
4014
|
-
const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
|
|
4015
|
-
let stderr = "";
|
|
4016
|
-
child.stderr?.setEncoding("utf8").on("data", (c) => {
|
|
4017
|
-
stderr += c;
|
|
4018
|
-
});
|
|
4019
|
-
const timer = setTimeout(() => {
|
|
4020
|
-
child.kill("SIGKILL");
|
|
4021
|
-
resolve9({ exitCode: -1, stderr: `${stderr}[timeout]` });
|
|
4022
|
-
}, 15e3);
|
|
4023
|
-
child.on("error", (e) => {
|
|
4024
|
-
clearTimeout(timer);
|
|
4025
|
-
resolve9({ exitCode: -1, stderr: e.message });
|
|
4026
|
-
});
|
|
4027
|
-
child.on("close", (code) => {
|
|
4028
|
-
clearTimeout(timer);
|
|
4029
|
-
resolve9({ exitCode: code ?? -1, stderr });
|
|
4030
|
-
});
|
|
4031
|
-
});
|
|
4032
|
-
if (vr.exitCode !== 0) {
|
|
4064
|
+
const registered = await isMcpServerRegistered(name);
|
|
4065
|
+
if (!registered) {
|
|
4033
4066
|
return {
|
|
4034
4067
|
ok: false,
|
|
4035
4068
|
phase: "verify",
|
|
@@ -4037,7 +4070,7 @@ ${newEntry}
|
|
|
4037
4070
|
error: err(
|
|
4038
4071
|
ctx,
|
|
4039
4072
|
"/spec/verify/cmd",
|
|
4040
|
-
`verify
|
|
4073
|
+
`verify: '${name}' not found in mcpServers map of ~/.claude.json after install spawn exit 0 (file may have been overwritten, or claude mcp add wrote to a non-default location)`,
|
|
4041
4074
|
"verify-failed"
|
|
4042
4075
|
)
|
|
4043
4076
|
};
|
|
@@ -4045,6 +4078,8 @@ ${newEntry}
|
|
|
4045
4078
|
await updateInstalled(ctx.cwd, name, install.npm_version, "");
|
|
4046
4079
|
return { ok: true, backupId: bk.backupId, appliedFiles: [mcpFile] };
|
|
4047
4080
|
};
|
|
4081
|
+
|
|
4082
|
+
// src/installers/mcpStdioAdd.ts
|
|
4048
4083
|
var installMcpStdioAdd = async (ctx) => {
|
|
4049
4084
|
const install = ctx.manifest.spec.install;
|
|
4050
4085
|
if (install.method !== "mcp-stdio-add") {
|
|
@@ -4073,7 +4108,7 @@ var installMcpStdioAdd = async (ctx) => {
|
|
|
4073
4108
|
"mcp",
|
|
4074
4109
|
"add",
|
|
4075
4110
|
"--scope",
|
|
4076
|
-
"
|
|
4111
|
+
"user",
|
|
4077
4112
|
"--transport",
|
|
4078
4113
|
"stdio",
|
|
4079
4114
|
name,
|
|
@@ -4083,21 +4118,21 @@ var installMcpStdioAdd = async (ctx) => {
|
|
|
4083
4118
|
`${pkg}@${ver}`
|
|
4084
4119
|
];
|
|
4085
4120
|
for (const a of addArgs) {
|
|
4086
|
-
const
|
|
4087
|
-
if (
|
|
4121
|
+
const violation = checkCmdString(a);
|
|
4122
|
+
if (violation) {
|
|
4088
4123
|
return {
|
|
4089
4124
|
ok: false,
|
|
4090
4125
|
phase: "preflight",
|
|
4091
4126
|
error: err(
|
|
4092
4127
|
ctx,
|
|
4093
4128
|
"/spec/install/cmd",
|
|
4094
|
-
`shell escape detected in constructed mcp-add arg '${a.slice(0, 60)}': ${
|
|
4129
|
+
`shell escape detected in constructed mcp-add arg '${a.slice(0, 60)}': ${violation.label} (${violation.hint})`,
|
|
4095
4130
|
"security-gate-bypass"
|
|
4096
4131
|
)
|
|
4097
4132
|
};
|
|
4098
4133
|
}
|
|
4099
4134
|
}
|
|
4100
|
-
const mcpFile = `${
|
|
4135
|
+
const mcpFile = `${getMcpSpawnCwd()}/.claude.json`;
|
|
4101
4136
|
const newEntry = JSON.stringify(
|
|
4102
4137
|
{ [name]: { type: "stdio", command: "npx", args: ["--yes", `${pkg}@${ver}`] } },
|
|
4103
4138
|
null,
|
|
@@ -4107,9 +4142,9 @@ var installMcpStdioAdd = async (ctx) => {
|
|
|
4107
4142
|
files: [
|
|
4108
4143
|
{
|
|
4109
4144
|
target: mcpFile,
|
|
4110
|
-
scope: "
|
|
4145
|
+
scope: "HOME",
|
|
4111
4146
|
oldText: "",
|
|
4112
|
-
newText: `// will be merged into .
|
|
4147
|
+
newText: `// will be merged into ~/.claude.json mcpServers map by \`claude mcp add --scope user\`:
|
|
4113
4148
|
${newEntry}
|
|
4114
4149
|
`
|
|
4115
4150
|
}
|
|
@@ -4124,9 +4159,10 @@ ${newEntry}
|
|
|
4124
4159
|
if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
|
|
4125
4160
|
const bk = await backup(plan, ctx);
|
|
4126
4161
|
if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
|
|
4127
|
-
const
|
|
4162
|
+
const spawnCwd = install.cwd ?? getMcpSpawnCwd();
|
|
4163
|
+
const r = await runArgs(addArgs, spawnCwd);
|
|
4128
4164
|
if (r.exitCode !== 0) {
|
|
4129
|
-
if (r.stderr.includes("already exists
|
|
4165
|
+
if (r.stderr.includes("already exists")) {
|
|
4130
4166
|
return { ok: true, alreadyInstalled: true, backupId: bk.backupId };
|
|
4131
4167
|
}
|
|
4132
4168
|
return {
|
|
@@ -4141,11 +4177,8 @@ ${newEntry}
|
|
|
4141
4177
|
)
|
|
4142
4178
|
};
|
|
4143
4179
|
}
|
|
4144
|
-
const
|
|
4145
|
-
|
|
4146
|
-
const verifyLine = `claude mcp list | grep -q ${name}`;
|
|
4147
|
-
const violation = checkCmdString(verifyLine);
|
|
4148
|
-
if (violation) {
|
|
4180
|
+
const registered = await isMcpServerRegistered(name);
|
|
4181
|
+
if (!registered) {
|
|
4149
4182
|
return {
|
|
4150
4183
|
ok: false,
|
|
4151
4184
|
phase: "verify",
|
|
@@ -4153,39 +4186,7 @@ ${newEntry}
|
|
|
4153
4186
|
error: err(
|
|
4154
4187
|
ctx,
|
|
4155
4188
|
"/spec/verify/cmd",
|
|
4156
|
-
`verify
|
|
4157
|
-
"security-gate-bypass"
|
|
4158
|
-
)
|
|
4159
|
-
};
|
|
4160
|
-
}
|
|
4161
|
-
const vr = await new Promise((resolve9) => {
|
|
4162
|
-
const child = spawn(verifyShell, [verifyFlag, verifyLine], { cwd: ctx.cwd, windowsHide: true });
|
|
4163
|
-
let stderr = "";
|
|
4164
|
-
child.stderr?.setEncoding("utf8").on("data", (c) => {
|
|
4165
|
-
stderr += c;
|
|
4166
|
-
});
|
|
4167
|
-
const timer = setTimeout(() => {
|
|
4168
|
-
child.kill("SIGKILL");
|
|
4169
|
-
resolve9({ exitCode: -1, stderr: `${stderr}[timeout]` });
|
|
4170
|
-
}, 15e3);
|
|
4171
|
-
child.on("error", (e) => {
|
|
4172
|
-
clearTimeout(timer);
|
|
4173
|
-
resolve9({ exitCode: -1, stderr: e.message });
|
|
4174
|
-
});
|
|
4175
|
-
child.on("close", (code) => {
|
|
4176
|
-
clearTimeout(timer);
|
|
4177
|
-
resolve9({ exitCode: code ?? -1, stderr });
|
|
4178
|
-
});
|
|
4179
|
-
});
|
|
4180
|
-
if (vr.exitCode !== 0) {
|
|
4181
|
-
return {
|
|
4182
|
-
ok: false,
|
|
4183
|
-
phase: "verify",
|
|
4184
|
-
backupId: bk.backupId,
|
|
4185
|
-
error: err(
|
|
4186
|
-
ctx,
|
|
4187
|
-
"/spec/verify/cmd",
|
|
4188
|
-
`verify exit ${vr.exitCode}: ${vr.stderr.slice(0, 200)}`,
|
|
4189
|
+
`verify: '${name}' not found in mcpServers map of ~/.claude.json after install spawn exit 0 (file may have been overwritten, or claude mcp add wrote to a non-default location)`,
|
|
4189
4190
|
"verify-failed"
|
|
4190
4191
|
)
|
|
4191
4192
|
};
|
|
@@ -4251,7 +4252,7 @@ var installNpmCli = async (ctx) => {
|
|
|
4251
4252
|
if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
|
|
4252
4253
|
const bk = await backup(plan, ctx);
|
|
4253
4254
|
if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
|
|
4254
|
-
const sp = await spawnCmd(ctx, cmd, []);
|
|
4255
|
+
const sp = await spawnCmd(ctx, cmd, [], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
4255
4256
|
if (!("exitCode" in sp)) return { ...sp, backupId: bk.backupId };
|
|
4256
4257
|
if (sp.exitCode !== 0) {
|
|
4257
4258
|
return {
|
|
@@ -4266,7 +4267,8 @@ var installNpmCli = async (ctx) => {
|
|
|
4266
4267
|
)
|
|
4267
4268
|
};
|
|
4268
4269
|
}
|
|
4269
|
-
const
|
|
4270
|
+
const verifyTimeoutMs = ctx.manifest.spec.verify.timeout_ms ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
4271
|
+
const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, [], verifyTimeoutMs);
|
|
4270
4272
|
if (!("exitCode" in vr)) return { ...vr, backupId: bk.backupId };
|
|
4271
4273
|
const expected = ctx.manifest.spec.verify.expected_exit_code ?? 0;
|
|
4272
4274
|
if (vr.exitCode !== expected) {
|
|
@@ -4367,7 +4369,7 @@ var installNpxSkillInstaller = async (ctx) => {
|
|
|
4367
4369
|
if (ctx.opts.dryRun) return { aborted: true, reason: "user-cancel" };
|
|
4368
4370
|
const bk = await backup(plan, ctx);
|
|
4369
4371
|
if (!bk.ok) return { ok: false, phase: "preflight", error: bk.error };
|
|
4370
|
-
const sp = await spawnCmd(ctx, install.cmd, []);
|
|
4372
|
+
const sp = await spawnCmd(ctx, install.cmd, [], DEFAULT_INSTALL_TIMEOUT_MS);
|
|
4371
4373
|
if (!("exitCode" in sp)) return { ...sp, backupId: bk.backupId };
|
|
4372
4374
|
if (sp.exitCode !== 0) {
|
|
4373
4375
|
return {
|
|
@@ -4400,7 +4402,8 @@ var installNpxSkillInstaller = async (ctx) => {
|
|
|
4400
4402
|
}
|
|
4401
4403
|
};
|
|
4402
4404
|
}
|
|
4403
|
-
const
|
|
4405
|
+
const verifyTimeoutMs = ctx.manifest.spec.verify.timeout_ms ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
4406
|
+
const vr = await spawnCmd(ctx, ctx.manifest.spec.verify.cmd, [], verifyTimeoutMs);
|
|
4404
4407
|
if (!("exitCode" in vr)) return { ...vr, backupId: bk.backupId };
|
|
4405
4408
|
const expected = ctx.manifest.spec.verify.expected_exit_code ?? 0;
|
|
4406
4409
|
if (vr.exitCode !== expected) {
|
|
@@ -4896,7 +4899,7 @@ async function warnIfAgentTeamsMissing() {
|
|
|
4896
4899
|
console.warn("\n\u26A0\uFE0F Agent Teams \u672A\u542F\u7528 \u2014 parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u4E0D\u53EF\u7528");
|
|
4897
4900
|
console.warn(" \u4FEE\u590D: claude config set env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 1");
|
|
4898
4901
|
console.warn(
|
|
4899
|
-
" \u8BF4\u660E: harnessed
|
|
4902
|
+
" \u8BF4\u660E: harnessed v3.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA parallelism-gate \u5347\u7EA7\u8DEF\u5F84\u9700 CC 2.1.133+ Agent Teams enable"
|
|
4900
4903
|
);
|
|
4901
4904
|
console.warn(
|
|
4902
4905
|
" \u4E0D\u963B\u585E setup,\u540E\u7EED parallelism-gate workflow phase \u89E6\u53D1\u65F6\u81EA\u52A8\u964D\u7EA7 subagent fan-out\n"
|
|
@@ -5052,7 +5055,7 @@ MCP servers configured. Run \`/mcp\` in Claude Code to verify each server's conn
|
|
|
5052
5055
|
);
|
|
5053
5056
|
}
|
|
5054
5057
|
console.log(
|
|
5055
|
-
"\n\u2713 harnessed
|
|
5058
|
+
"\n\u2713 harnessed v3.0 \u4E09\u5C42\u6808\u65B9\u6CD5\u8BBA bundled \u2014 23 workflows (4 master + 18 sub + 1 standalone) + 6 disciplines + 10 judgments + ~83 capabilities ready"
|
|
5056
5059
|
);
|
|
5057
5060
|
console.log(
|
|
5058
5061
|
" workflows in <packageRoot>/workflows/ (Pure bundled, NOT user-dir override per D-01)"
|
|
@@ -5060,12 +5063,15 @@ MCP servers configured. Run \`/mcp\` in Claude Code to verify each server's conn
|
|
|
5060
5063
|
process.exit(0);
|
|
5061
5064
|
});
|
|
5062
5065
|
}
|
|
5066
|
+
|
|
5067
|
+
// src/cli/status.ts
|
|
5068
|
+
init_harnessedRoot();
|
|
5063
5069
|
function registerStatus(program2) {
|
|
5064
|
-
program2.command("status").description("Show installed upstreams (from
|
|
5070
|
+
program2.command("status").description("Show installed upstreams (from <harnessed-root>/state.json)").action(async () => {
|
|
5065
5071
|
const state = await readState(process.cwd());
|
|
5066
5072
|
const names = Object.keys(state.installed).sort();
|
|
5067
5073
|
if (names.length === 0) {
|
|
5068
|
-
console.log(
|
|
5074
|
+
console.log(`no installs recorded (${harnessedFile("state.json")} absent or empty)`);
|
|
5069
5075
|
} else {
|
|
5070
5076
|
for (const n of names) {
|
|
5071
5077
|
const e = state.installed[n];
|
|
@@ -5075,18 +5081,19 @@ function registerStatus(program2) {
|
|
|
5075
5081
|
console.log(`
|
|
5076
5082
|
${names.length} install${names.length === 1 ? "" : "s"} recorded`);
|
|
5077
5083
|
}
|
|
5084
|
+
const lockPath = harnessedFile(".lock");
|
|
5078
5085
|
try {
|
|
5079
|
-
const isLocked = await lockfile.check(
|
|
5080
|
-
lockfilePath:
|
|
5086
|
+
const isLocked = await lockfile.check(getHarnessedRoot(), {
|
|
5087
|
+
lockfilePath: lockPath,
|
|
5081
5088
|
stale: 1e4
|
|
5082
5089
|
});
|
|
5083
5090
|
if (isLocked) {
|
|
5084
|
-
const s = await stat(
|
|
5091
|
+
const s = await stat(lockPath);
|
|
5085
5092
|
const ageMs = Date.now() - s.mtime.getTime();
|
|
5086
5093
|
const stale = ageMs > 1e4;
|
|
5087
5094
|
console.log(`
|
|
5088
5095
|
lock: held (since ${s.mtime.toISOString()})${stale ? " \u2014 STALE" : ""}`);
|
|
5089
|
-
console.log(
|
|
5096
|
+
console.log(` to release: wait for process to finish or delete ${lockPath}`);
|
|
5090
5097
|
} else {
|
|
5091
5098
|
console.log("\nlock: free");
|
|
5092
5099
|
}
|
|
@@ -5415,6 +5422,8 @@ function registerUninstall(program2) {
|
|
|
5415
5422
|
}
|
|
5416
5423
|
|
|
5417
5424
|
// src/cli.ts
|
|
5425
|
+
init_harnessedRoot();
|
|
5426
|
+
migrateLegacyHarnessedRoot();
|
|
5418
5427
|
var program = new Command();
|
|
5419
5428
|
program.name("harnessed").description("AI coding harness package manager + composition orchestrator").version(package_default.version);
|
|
5420
5429
|
registerInstall(program);
|