patchwarden 0.6.0 → 0.6.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 +3 -3
- package/dist/doctor.js +1 -1
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/changeCapture.d.ts +41 -0
- package/dist/runner/changeCapture.js +171 -1
- package/dist/runner/runTask.js +204 -24
- package/dist/smoke-test.js +8 -8
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/auditTask.js +11 -5
- package/dist/tools/getTaskSummary.d.ts +3 -0
- package/dist/tools/getTaskSummary.js +15 -1
- package/dist/tools/healthCheck.d.ts +5 -0
- package/dist/tools/healthCheck.js +21 -0
- package/dist/tools/registry.js +53 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +2 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/performance-notes.md +55 -0
- package/docs/release-v0.6.1.md +75 -0
- package/package.json +3 -2
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/lifecycle-smoke.js +336 -2
- package/scripts/mcp-manifest-check.js +30 -7
- package/scripts/mcp-smoke.js +11 -8
- package/scripts/pack-clean.js +157 -1
- package/scripts/unit-tests.js +36 -0
- package/src/doctor.ts +1 -1
- package/src/logging.ts +152 -0
- package/src/runner/changeCapture.ts +212 -1
- package/src/runner/runTask.ts +220 -22
- package/src/smoke-test.ts +5 -5
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/auditTask.ts +11 -5
- package/src/tools/getTaskSummary.ts +22 -1
- package/src/tools/healthCheck.ts +22 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/toolCatalog.ts +2 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
|
@@ -39,8 +39,8 @@ async function test(name, fn) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function git(args) {
|
|
43
|
-
const result = spawnSync("git", args, { cwd
|
|
42
|
+
function git(args, cwd = repoPath) {
|
|
43
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
44
44
|
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -159,6 +159,7 @@ try {
|
|
|
159
159
|
const { auditTask } = await import("../dist/tools/auditTask.js");
|
|
160
160
|
const { waitForTask } = await import("../dist/tools/waitForTask.js");
|
|
161
161
|
const { runTask } = await import("../dist/runner/runTask.js");
|
|
162
|
+
const { reloadConfig } = await import("../dist/config.js");
|
|
162
163
|
|
|
163
164
|
await test("list_agents reports configured executables", async () => {
|
|
164
165
|
const result = listAgents();
|
|
@@ -454,6 +455,339 @@ try {
|
|
|
454
455
|
}
|
|
455
456
|
});
|
|
456
457
|
|
|
458
|
+
// ═══════════════════════════════════════════════════════════
|
|
459
|
+
// Phase 4: Tracked external dirty tests (separate workspace)
|
|
460
|
+
// ═══════════════════════════════════════════════════════════
|
|
461
|
+
|
|
462
|
+
await test("pre-existing tracked external dirty unchanged → warning only, done", async () => {
|
|
463
|
+
// Create a separate workspace with git at the root
|
|
464
|
+
const ws2 = join(tempRoot, "workspace-tracked-dirty");
|
|
465
|
+
const repo2 = join(ws2, "repo");
|
|
466
|
+
const ext2 = join(ws2, "external");
|
|
467
|
+
mkdirSync(repo2, { recursive: true });
|
|
468
|
+
mkdirSync(ext2, { recursive: true });
|
|
469
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
470
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
471
|
+
status: "running", pid: process.pid, instance_id: "td-watcher", launcher_pid: process.pid,
|
|
472
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
473
|
+
}), "utf-8");
|
|
474
|
+
|
|
475
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
476
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
477
|
+
|
|
478
|
+
git(["init"], ws2);
|
|
479
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
480
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
481
|
+
|
|
482
|
+
// Now modify external file to create pre-existing dirty state
|
|
483
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "pre-existing dirty content\n", "utf-8");
|
|
484
|
+
|
|
485
|
+
const cfg2 = join(tempRoot, "config-tracked-dirty.json");
|
|
486
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
487
|
+
workspaceRoot: ws2,
|
|
488
|
+
plansDir: ".patchwarden/plans",
|
|
489
|
+
tasksDir: ".patchwarden/tasks",
|
|
490
|
+
agents: {
|
|
491
|
+
noop: { command: process.execPath, args: ["-e", "console.log('noop')"] },
|
|
492
|
+
},
|
|
493
|
+
allowedTestCommands: ["node --check main.js"],
|
|
494
|
+
maxReadFileBytes: 200000,
|
|
495
|
+
defaultTaskTimeoutSeconds: 10,
|
|
496
|
+
watcherStaleSeconds: 3600,
|
|
497
|
+
}, null, 2), "utf-8");
|
|
498
|
+
|
|
499
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
500
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
501
|
+
reloadConfig(cfg2);
|
|
502
|
+
|
|
503
|
+
const plan = savePlan({ title: "Noop with external dirty", content: "Do nothing." });
|
|
504
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
505
|
+
const result = await runTask(task.task_id);
|
|
506
|
+
|
|
507
|
+
if (result.status !== "done") {
|
|
508
|
+
throw new Error(`Expected done, got: ${JSON.stringify(result)}`);
|
|
509
|
+
}
|
|
510
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
511
|
+
|
|
512
|
+
// Must have pre-existing external dirty warning
|
|
513
|
+
if (!structured.warnings?.some((w) => w.includes("Pre-existing external dirty"))) {
|
|
514
|
+
throw new Error(`Missing pre-existing dirty warning: ${JSON.stringify(structured.warnings)}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// new_out_of_scope_changes must be empty (file didn't change during task)
|
|
518
|
+
if (structured.new_out_of_scope_changes?.length > 0) {
|
|
519
|
+
throw new Error(`Should have no new out-of-scope changes: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// verify_status should NOT be failed
|
|
523
|
+
if (structured.verify_status === "failed") {
|
|
524
|
+
throw new Error(`verify_status should not be failed: ${structured.verify_status}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// get_task_summary acceptance_status should NOT be failed
|
|
528
|
+
const summary = getTaskSummary(task.task_id);
|
|
529
|
+
if (summary.acceptance_status === "failed") {
|
|
530
|
+
throw new Error(`acceptance_status should not be failed: ${summary.acceptance_status}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// audit_task should NOT fail scope_changes
|
|
534
|
+
const audit = auditTask(task.task_id);
|
|
535
|
+
const scopeCheck = audit.checks.find((c) => c.name === "scope_changes");
|
|
536
|
+
if (!scopeCheck || scopeCheck.result !== "pass") {
|
|
537
|
+
throw new Error(`audit scope_changes should pass: ${JSON.stringify(scopeCheck)}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Restore config AFTER all assertions
|
|
541
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
542
|
+
reloadConfig(oldConfig);
|
|
543
|
+
|
|
544
|
+
// Print result.json key fields for reporting
|
|
545
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await test("pre-existing tracked external dirty changed during task → failed_scope_violation", async () => {
|
|
549
|
+
const ws2 = join(tempRoot, "workspace-tracked-dirty2");
|
|
550
|
+
const repo2 = join(ws2, "repo");
|
|
551
|
+
const ext2 = join(ws2, "external");
|
|
552
|
+
mkdirSync(repo2, { recursive: true });
|
|
553
|
+
mkdirSync(ext2, { recursive: true });
|
|
554
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
555
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
556
|
+
status: "running", pid: process.pid, instance_id: "td-watcher2", launcher_pid: process.pid,
|
|
557
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
558
|
+
}), "utf-8");
|
|
559
|
+
|
|
560
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
561
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
562
|
+
|
|
563
|
+
git(["init"], ws2);
|
|
564
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
565
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
566
|
+
|
|
567
|
+
// Modify external file BEFORE task (pre-existing dirty)
|
|
568
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "pre-existing dirty content\n", "utf-8");
|
|
569
|
+
|
|
570
|
+
const cfg2 = join(tempRoot, "config-tracked-dirty2.json");
|
|
571
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
572
|
+
workspaceRoot: ws2,
|
|
573
|
+
plansDir: ".patchwarden/plans",
|
|
574
|
+
tasksDir: ".patchwarden/tasks",
|
|
575
|
+
agents: {
|
|
576
|
+
extmod: { command: process.execPath, args: ["-e", "require('fs').appendFileSync('../external/external-tracked.txt',' modified by task\\n')"] },
|
|
577
|
+
},
|
|
578
|
+
allowedTestCommands: ["node --check main.js"],
|
|
579
|
+
maxReadFileBytes: 200000,
|
|
580
|
+
defaultTaskTimeoutSeconds: 10,
|
|
581
|
+
watcherStaleSeconds: 3600,
|
|
582
|
+
}, null, 2), "utf-8");
|
|
583
|
+
|
|
584
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
585
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
586
|
+
reloadConfig(cfg2);
|
|
587
|
+
|
|
588
|
+
const plan = savePlan({ title: "External dirty modifier", content: "Modify external tracked file." });
|
|
589
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
590
|
+
const result = await runTask(task.task_id);
|
|
591
|
+
|
|
592
|
+
if (result.status !== "failed_scope_violation") {
|
|
593
|
+
throw new Error(`Expected failed_scope_violation, got: ${JSON.stringify(result)}`);
|
|
594
|
+
}
|
|
595
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
596
|
+
|
|
597
|
+
// new_out_of_scope_changes must contain the external tracked file
|
|
598
|
+
if (!structured.new_out_of_scope_changes?.some((f) => f.path.includes("external-tracked.txt"))) {
|
|
599
|
+
throw new Error(`Missing new out-of-scope change: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// verify_status must be failed
|
|
603
|
+
if (structured.verify_status !== "failed") {
|
|
604
|
+
throw new Error(`verify_status must be failed: ${structured.verify_status}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// rollback plan must contain the file
|
|
608
|
+
const rollbackPath = join(task.path, "rollback_scope_violation_plan.md");
|
|
609
|
+
if (!existsSync(rollbackPath)) throw new Error("rollback_scope_violation_plan.md missing");
|
|
610
|
+
const rollback = readFileSync(rollbackPath, "utf-8");
|
|
611
|
+
if (!rollback.includes("external-tracked.txt")) {
|
|
612
|
+
throw new Error(`Rollback plan missing external-tracked.txt: ${rollback}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// get_task_summary acceptance_status must be failed
|
|
616
|
+
const summary = getTaskSummary(task.task_id);
|
|
617
|
+
if (summary.acceptance_status !== "failed") {
|
|
618
|
+
throw new Error(`acceptance_status must be failed: ${summary.acceptance_status}`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// audit_task verdict must be fail
|
|
622
|
+
const audit = auditTask(task.task_id);
|
|
623
|
+
if (audit.verdict !== "fail") {
|
|
624
|
+
throw new Error(`audit verdict must be fail: ${audit.verdict}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Restore config AFTER all assertions
|
|
628
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
629
|
+
reloadConfig(oldConfig);
|
|
630
|
+
|
|
631
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
await test("clean tracked external file changed during task → failed_scope_violation", async () => {
|
|
635
|
+
const ws2 = join(tempRoot, "workspace-tracked-dirty3");
|
|
636
|
+
const repo2 = join(ws2, "repo");
|
|
637
|
+
const ext2 = join(ws2, "external");
|
|
638
|
+
mkdirSync(repo2, { recursive: true });
|
|
639
|
+
mkdirSync(ext2, { recursive: true });
|
|
640
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
641
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
642
|
+
status: "running", pid: process.pid, instance_id: "td-watcher3", launcher_pid: process.pid,
|
|
643
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
644
|
+
}), "utf-8");
|
|
645
|
+
|
|
646
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
647
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
648
|
+
|
|
649
|
+
git(["init"], ws2);
|
|
650
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
651
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
652
|
+
|
|
653
|
+
// External file is clean (committed state) — no pre-existing dirty
|
|
654
|
+
|
|
655
|
+
const cfg2 = join(tempRoot, "config-tracked-dirty3.json");
|
|
656
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
657
|
+
workspaceRoot: ws2,
|
|
658
|
+
plansDir: ".patchwarden/plans",
|
|
659
|
+
tasksDir: ".patchwarden/tasks",
|
|
660
|
+
agents: {
|
|
661
|
+
extmod: { command: process.execPath, args: ["-e", "require('fs').appendFileSync('../external/external-tracked.txt',' modified by task\\n')"] },
|
|
662
|
+
},
|
|
663
|
+
allowedTestCommands: ["node --check main.js"],
|
|
664
|
+
maxReadFileBytes: 200000,
|
|
665
|
+
defaultTaskTimeoutSeconds: 10,
|
|
666
|
+
watcherStaleSeconds: 3600,
|
|
667
|
+
}, null, 2), "utf-8");
|
|
668
|
+
|
|
669
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
670
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
671
|
+
reloadConfig(cfg2);
|
|
672
|
+
|
|
673
|
+
const plan = savePlan({ title: "Clean external modifier", content: "Modify clean external tracked file." });
|
|
674
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
675
|
+
const result = await runTask(task.task_id);
|
|
676
|
+
|
|
677
|
+
if (result.status !== "failed_scope_violation") {
|
|
678
|
+
throw new Error(`Expected failed_scope_violation, got: ${JSON.stringify(result)}`);
|
|
679
|
+
}
|
|
680
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
681
|
+
|
|
682
|
+
// new_out_of_scope_changes must contain the external tracked file (clean→dirty)
|
|
683
|
+
if (!structured.new_out_of_scope_changes?.some((f) => f.path.includes("external-tracked.txt"))) {
|
|
684
|
+
throw new Error(`Missing new out-of-scope change: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// verify_status must be failed
|
|
688
|
+
if (structured.verify_status !== "failed") {
|
|
689
|
+
throw new Error(`verify_status must be failed: ${structured.verify_status}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// get_task_summary acceptance_status must be failed
|
|
693
|
+
const summary = getTaskSummary(task.task_id);
|
|
694
|
+
if (summary.acceptance_status !== "failed") {
|
|
695
|
+
throw new Error(`acceptance_status must be failed: ${summary.acceptance_status}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// audit_task verdict must be fail
|
|
699
|
+
const audit = auditTask(task.task_id);
|
|
700
|
+
if (audit.verdict !== "fail") {
|
|
701
|
+
throw new Error(`audit verdict must be fail: ${audit.verdict}`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Restore config AFTER all assertions
|
|
705
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
706
|
+
reloadConfig(oldConfig);
|
|
707
|
+
|
|
708
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
await test("tracked external file rename → failed_scope_violation", async () => {
|
|
712
|
+
const ws2 = join(tempRoot, "workspace-tracked-rename");
|
|
713
|
+
const repo2 = join(ws2, "repo");
|
|
714
|
+
const ext2 = join(ws2, "external");
|
|
715
|
+
mkdirSync(repo2, { recursive: true });
|
|
716
|
+
mkdirSync(ext2, { recursive: true });
|
|
717
|
+
mkdirSync(join(ws2, ".patchwarden"), { recursive: true });
|
|
718
|
+
writeFileSync(join(ws2, ".patchwarden", "watcher-heartbeat.json"), JSON.stringify({
|
|
719
|
+
status: "running", pid: process.pid, instance_id: "td-watcher-rename", launcher_pid: process.pid,
|
|
720
|
+
started_at: new Date().toISOString(), last_heartbeat_at: new Date().toISOString(),
|
|
721
|
+
}), "utf-8");
|
|
722
|
+
|
|
723
|
+
writeFileSync(join(repo2, "main.js"), "console.log('main');\n", "utf-8");
|
|
724
|
+
writeFileSync(join(ext2, "external-tracked.txt"), "external tracked content\n", "utf-8");
|
|
725
|
+
|
|
726
|
+
git(["init"], ws2);
|
|
727
|
+
git(["add", "repo/main.js", "external/external-tracked.txt"], ws2);
|
|
728
|
+
git(["-c", "user.name=T", "-c", "user.email=t@e", "commit", "-m", "init"], ws2);
|
|
729
|
+
|
|
730
|
+
// External file is clean (committed state) — no pre-existing dirty
|
|
731
|
+
|
|
732
|
+
const cfg2 = join(tempRoot, "config-tracked-rename.json");
|
|
733
|
+
writeFileSync(cfg2, JSON.stringify({
|
|
734
|
+
workspaceRoot: ws2,
|
|
735
|
+
plansDir: ".patchwarden/plans",
|
|
736
|
+
tasksDir: ".patchwarden/tasks",
|
|
737
|
+
agents: {
|
|
738
|
+
extrenamer: { command: process.execPath, args: ["-e", "require('fs').renameSync('../external/external-tracked.txt','../external/external-renamed.txt')"] },
|
|
739
|
+
},
|
|
740
|
+
allowedTestCommands: ["node --check main.js"],
|
|
741
|
+
maxReadFileBytes: 200000,
|
|
742
|
+
defaultTaskTimeoutSeconds: 10,
|
|
743
|
+
watcherStaleSeconds: 3600,
|
|
744
|
+
}, null, 2), "utf-8");
|
|
745
|
+
|
|
746
|
+
const oldConfig = process.env.PATCHWARDEN_CONFIG;
|
|
747
|
+
process.env.PATCHWARDEN_CONFIG = cfg2;
|
|
748
|
+
reloadConfig(cfg2);
|
|
749
|
+
|
|
750
|
+
const plan = savePlan({ title: "External rename", content: "Rename external tracked file." });
|
|
751
|
+
const task = createTask({ plan_id: plan.plan_id, agent: "extrenamer", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
752
|
+
const result = await runTask(task.task_id);
|
|
753
|
+
|
|
754
|
+
if (result.status !== "failed_scope_violation") {
|
|
755
|
+
throw new Error(`Expected failed_scope_violation, got: ${JSON.stringify(result)}`);
|
|
756
|
+
}
|
|
757
|
+
const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
|
|
758
|
+
|
|
759
|
+
// new_out_of_scope_changes must contain rename evidence (old or new path)
|
|
760
|
+
const hasRenameEvidence = structured.new_out_of_scope_changes?.some(
|
|
761
|
+
(f) => f.path.includes("external-renamed.txt") || f.path.includes("external-tracked.txt") || f.change === "renamed"
|
|
762
|
+
);
|
|
763
|
+
if (!hasRenameEvidence) {
|
|
764
|
+
throw new Error(`Missing rename evidence in new_out_of_scope_changes: ${JSON.stringify(structured.new_out_of_scope_changes)}`);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// verify_status must be failed
|
|
768
|
+
if (structured.verify_status !== "failed") {
|
|
769
|
+
throw new Error(`verify_status must be failed: ${structured.verify_status}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// get_task_summary acceptance_status must be failed
|
|
773
|
+
const summary = getTaskSummary(task.task_id);
|
|
774
|
+
if (summary.acceptance_status !== "failed") {
|
|
775
|
+
throw new Error(`acceptance_status must be failed: ${summary.acceptance_status}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// audit_task verdict must be fail
|
|
779
|
+
const audit = auditTask(task.task_id);
|
|
780
|
+
if (audit.verdict !== "fail") {
|
|
781
|
+
throw new Error(`audit verdict must be fail: ${audit.verdict}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Restore config AFTER all assertions
|
|
785
|
+
process.env.PATCHWARDEN_CONFIG = oldConfig;
|
|
786
|
+
reloadConfig(oldConfig);
|
|
787
|
+
|
|
788
|
+
console.log(` [result.json] status=${structured.status}, verify_status=${structured.verify_status}, new_out_of_scope=${JSON.stringify(structured.new_out_of_scope_changes)}, preexisting=${JSON.stringify(structured.preexisting_external_dirty_files)}, warnings=${JSON.stringify(structured.warnings)}`);
|
|
789
|
+
});
|
|
790
|
+
|
|
457
791
|
await test("timeout terminates a long-running agent", async () => {
|
|
458
792
|
const plan = savePlan({ title: "Timeout", content: "Wait." });
|
|
459
793
|
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 1 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -21,12 +21,32 @@ const transportEnv = {
|
|
|
21
21
|
...process.env,
|
|
22
22
|
PATCHWARDEN_TOOL_PROFILE: profile,
|
|
23
23
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
|
|
25
|
+
// When checking chatgpt_direct profile, ensure enableDirectProfile is true
|
|
26
|
+
// by creating a temporary config if needed.
|
|
27
|
+
let tempConfigPath = null;
|
|
28
|
+
if (profile === "chatgpt_direct") {
|
|
29
|
+
const baseConfig = existsSync(defaultConfigPath)
|
|
30
|
+
? JSON.parse(readFileSync(defaultConfigPath, "utf-8"))
|
|
31
|
+
: {};
|
|
32
|
+
if (!baseConfig.enableDirectProfile) {
|
|
33
|
+
const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
|
|
34
|
+
const { tmpdir } = await import("node:os");
|
|
35
|
+
const { join } = await import("node:path");
|
|
36
|
+
tempConfigPath = join(mkdtempSync(join(tmpdir(), "pw-direct-")), "config.json");
|
|
37
|
+
writeFileSync(tempConfigPath, JSON.stringify({ ...baseConfig, enableDirectProfile: true }, null, 2), "utf-8");
|
|
38
|
+
transportEnv.PATCHWARDEN_CONFIG = tempConfigPath;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!transportEnv.PATCHWARDEN_CONFIG) {
|
|
43
|
+
if (process.env.PATCHWARDEN_CONFIG) {
|
|
44
|
+
transportEnv.PATCHWARDEN_CONFIG = process.env.PATCHWARDEN_CONFIG;
|
|
45
|
+
} else if (existsSync(defaultConfigPath)) {
|
|
46
|
+
transportEnv.PATCHWARDEN_CONFIG = defaultConfigPath;
|
|
47
|
+
} else {
|
|
48
|
+
delete transportEnv.PATCHWARDEN_CONFIG;
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
const transport = new StdioClientTransport({
|
|
@@ -159,4 +179,7 @@ try {
|
|
|
159
179
|
process.exitCode = 1;
|
|
160
180
|
} finally {
|
|
161
181
|
await client.close().catch(() => {});
|
|
182
|
+
if (tempConfigPath) {
|
|
183
|
+
try { rmSync(tempConfigPath, { force: true }); } catch {}
|
|
184
|
+
}
|
|
162
185
|
}
|
package/scripts/mcp-smoke.js
CHANGED
|
@@ -126,8 +126,10 @@ try {
|
|
|
126
126
|
"read_workspace_file",
|
|
127
127
|
"retry_task",
|
|
128
128
|
"run_verification",
|
|
129
|
+
"safe_status",
|
|
129
130
|
"save_plan",
|
|
130
131
|
"search_workspace",
|
|
132
|
+
"sync_file",
|
|
131
133
|
"wait_for_task",
|
|
132
134
|
];
|
|
133
135
|
if (JSON.stringify(names) !== JSON.stringify(expected)) {
|
|
@@ -139,7 +141,7 @@ try {
|
|
|
139
141
|
if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
|
|
140
142
|
throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
|
|
141
143
|
}
|
|
142
|
-
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !==
|
|
144
|
+
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 30) {
|
|
143
145
|
throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
|
|
144
146
|
}
|
|
145
147
|
if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
|
|
@@ -210,7 +212,7 @@ try {
|
|
|
210
212
|
test_command: "npm test",
|
|
211
213
|
});
|
|
212
214
|
if (
|
|
213
|
-
task.server_version !== "0.6.
|
|
215
|
+
task.server_version !== "0.6.1" ||
|
|
214
216
|
!/^[a-f0-9]{64}$/.test(task.tool_manifest_sha256 || "") ||
|
|
215
217
|
task.next_tool_call?.name !== "wait_for_task" ||
|
|
216
218
|
task.next_tool_call?.arguments?.timeout_seconds !== 25
|
|
@@ -389,7 +391,7 @@ try {
|
|
|
389
391
|
await disabledClient.close();
|
|
390
392
|
ok("chatgpt_direct disabled exposes only health_check with diagnostic");
|
|
391
393
|
|
|
392
|
-
// 2. chatgpt_direct enabled:
|
|
394
|
+
// 2. chatgpt_direct enabled: 10 tools + minimal create_direct_session
|
|
393
395
|
const enabledConfigPath = join(tempRoot, "direct-enabled.json");
|
|
394
396
|
const directRepo = join(workspaceRoot, "direct-fixture");
|
|
395
397
|
mkdirSync(join(directRepo, "src"), { recursive: true });
|
|
@@ -447,12 +449,13 @@ try {
|
|
|
447
449
|
"read_workspace_file",
|
|
448
450
|
"run_verification",
|
|
449
451
|
"search_workspace",
|
|
452
|
+
"sync_file",
|
|
450
453
|
];
|
|
451
454
|
if (JSON.stringify(enabledNames) !== JSON.stringify(expectedDirect)) {
|
|
452
455
|
throw new Error(`chatgpt_direct enabled tools mismatch: ${enabledNames.join(", ")}`);
|
|
453
456
|
}
|
|
454
|
-
if (enabledTools._meta.tool_count !==
|
|
455
|
-
throw new Error(`chatgpt_direct enabled tool_count should be
|
|
457
|
+
if (enabledTools._meta.tool_count !== 10) {
|
|
458
|
+
throw new Error(`chatgpt_direct enabled tool_count should be 10, got ${enabledTools._meta.tool_count}`);
|
|
456
459
|
}
|
|
457
460
|
|
|
458
461
|
// Minimal create_direct_session
|
|
@@ -472,12 +475,12 @@ try {
|
|
|
472
475
|
if (enabledHealth.direct_profile_enabled !== true) {
|
|
473
476
|
throw new Error(`direct_profile_enabled should be true, got ${enabledHealth.direct_profile_enabled}`);
|
|
474
477
|
}
|
|
475
|
-
if (enabledHealth.direct_tool_count !==
|
|
476
|
-
throw new Error(`direct_tool_count should be
|
|
478
|
+
if (enabledHealth.direct_tool_count !== 10) {
|
|
479
|
+
throw new Error(`direct_tool_count should be 10, got ${enabledHealth.direct_tool_count}`);
|
|
477
480
|
}
|
|
478
481
|
|
|
479
482
|
await enabledClient.close();
|
|
480
|
-
ok("chatgpt_direct enabled exposes
|
|
483
|
+
ok("chatgpt_direct enabled exposes 10 tools and create_direct_session works");
|
|
481
484
|
} catch (error) {
|
|
482
485
|
fail("MCP smoke test", error);
|
|
483
486
|
} finally {
|
package/scripts/pack-clean.js
CHANGED
|
@@ -3,11 +3,14 @@ import {
|
|
|
3
3
|
cpSync,
|
|
4
4
|
existsSync,
|
|
5
5
|
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
6
7
|
readdirSync,
|
|
7
8
|
rmSync,
|
|
8
9
|
statSync,
|
|
10
|
+
writeFileSync,
|
|
9
11
|
} from "node:fs";
|
|
10
12
|
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { deflateRawSync } from "node:zlib";
|
|
11
14
|
import { join, relative, resolve } from "node:path";
|
|
12
15
|
import { fileURLToPath } from "node:url";
|
|
13
16
|
|
|
@@ -16,6 +19,10 @@ const root = resolve(scriptDir, "..");
|
|
|
16
19
|
const releaseDir = resolve(root, "release");
|
|
17
20
|
const archivePath = resolve(root, "patchwarden-release.tar.gz");
|
|
18
21
|
|
|
22
|
+
// Read version from package.json for zip naming
|
|
23
|
+
const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8"));
|
|
24
|
+
const zipArchivePath = resolve(root, `PatchWarden-v${pkg.version}.zip`);
|
|
25
|
+
|
|
19
26
|
const include = [
|
|
20
27
|
"dist",
|
|
21
28
|
"docs",
|
|
@@ -45,6 +52,119 @@ const forbidden = [
|
|
|
45
52
|
/(^|[\\/])kill-patchwarden\.(cmd|ps1)$/i,
|
|
46
53
|
];
|
|
47
54
|
|
|
55
|
+
// ── Minimal zip writer helpers (POSIX paths, DEFLATE via zlib) ────
|
|
56
|
+
// Defined before main execution so const CRC_TABLE is initialized.
|
|
57
|
+
|
|
58
|
+
const CRC_TABLE = new Uint32Array(256);
|
|
59
|
+
for (let i = 0; i < 256; i++) {
|
|
60
|
+
let c = i;
|
|
61
|
+
for (let j = 0; j < 8; j++) {
|
|
62
|
+
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
63
|
+
}
|
|
64
|
+
CRC_TABLE[i] = c;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function crc32(buf) {
|
|
68
|
+
let c = 0xFFFFFFFF;
|
|
69
|
+
for (let i = 0; i < buf.length; i++) {
|
|
70
|
+
c = CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
|
|
71
|
+
}
|
|
72
|
+
return (c ^ 0xFFFFFFFF) >>> 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function dosDateTime(date) {
|
|
76
|
+
const dosTime = (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1);
|
|
77
|
+
const dosDate = (((date.getFullYear() - 1980) & 0x7F) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
|
|
78
|
+
return { dosTime, dosDate };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createZip(files, outputPath) {
|
|
82
|
+
const localParts = [];
|
|
83
|
+
const centralParts = [];
|
|
84
|
+
let offset = 0;
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const { dosTime, dosDate } = dosDateTime(now);
|
|
87
|
+
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const posixPath = file.path.replace(/\\/g, "/");
|
|
90
|
+
const nameBuf = Buffer.from(posixPath, "utf-8");
|
|
91
|
+
const content = file.content;
|
|
92
|
+
const compressed = deflateRawSync(content);
|
|
93
|
+
const useDeflate = compressed.length < content.length;
|
|
94
|
+
const method = useDeflate ? 8 : 0;
|
|
95
|
+
const data = useDeflate ? compressed : content;
|
|
96
|
+
const crc = crc32(content);
|
|
97
|
+
|
|
98
|
+
const lh = Buffer.alloc(30);
|
|
99
|
+
lh.writeUInt32LE(0x04034b50, 0);
|
|
100
|
+
lh.writeUInt16LE(20, 4);
|
|
101
|
+
lh.writeUInt16LE(0, 6);
|
|
102
|
+
lh.writeUInt16LE(method, 8);
|
|
103
|
+
lh.writeUInt16LE(dosTime, 10);
|
|
104
|
+
lh.writeUInt16LE(dosDate, 12);
|
|
105
|
+
lh.writeUInt32LE(crc, 14);
|
|
106
|
+
lh.writeUInt32LE(data.length, 18);
|
|
107
|
+
lh.writeUInt32LE(content.length, 22);
|
|
108
|
+
lh.writeUInt16LE(nameBuf.length, 26);
|
|
109
|
+
lh.writeUInt16LE(0, 28);
|
|
110
|
+
|
|
111
|
+
const localHeaderOffset = offset;
|
|
112
|
+
localParts.push(Buffer.concat([lh, nameBuf, data]));
|
|
113
|
+
offset += lh.length + nameBuf.length + data.length;
|
|
114
|
+
|
|
115
|
+
const cd = Buffer.alloc(46);
|
|
116
|
+
cd.writeUInt32LE(0x02014b50, 0);
|
|
117
|
+
cd.writeUInt16LE(20, 4);
|
|
118
|
+
cd.writeUInt16LE(20, 6);
|
|
119
|
+
cd.writeUInt16LE(0, 8);
|
|
120
|
+
cd.writeUInt16LE(method, 10);
|
|
121
|
+
cd.writeUInt16LE(dosTime, 12);
|
|
122
|
+
cd.writeUInt16LE(dosDate, 14);
|
|
123
|
+
cd.writeUInt32LE(crc, 16);
|
|
124
|
+
cd.writeUInt32LE(data.length, 20);
|
|
125
|
+
cd.writeUInt32LE(content.length, 24);
|
|
126
|
+
cd.writeUInt16LE(nameBuf.length, 28);
|
|
127
|
+
cd.writeUInt16LE(0, 30);
|
|
128
|
+
cd.writeUInt16LE(0, 32);
|
|
129
|
+
cd.writeUInt16LE(0, 34);
|
|
130
|
+
cd.writeUInt16LE(0, 36);
|
|
131
|
+
cd.writeUInt32LE(0, 38);
|
|
132
|
+
cd.writeUInt32LE(localHeaderOffset, 42);
|
|
133
|
+
centralParts.push(Buffer.concat([cd, nameBuf]));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const centralDir = Buffer.concat(centralParts);
|
|
137
|
+
const centralDirOffset = offset;
|
|
138
|
+
|
|
139
|
+
const eocd = Buffer.alloc(22);
|
|
140
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
141
|
+
eocd.writeUInt16LE(0, 4);
|
|
142
|
+
eocd.writeUInt16LE(0, 6);
|
|
143
|
+
eocd.writeUInt16LE(files.length, 8);
|
|
144
|
+
eocd.writeUInt16LE(files.length, 10);
|
|
145
|
+
eocd.writeUInt32LE(centralDir.length, 12);
|
|
146
|
+
eocd.writeUInt32LE(centralDirOffset, 16);
|
|
147
|
+
eocd.writeUInt16LE(0, 20);
|
|
148
|
+
|
|
149
|
+
writeFileSync(outputPath, Buffer.concat([...localParts, centralDir, eocd]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readZipEntryNames(zipPath) {
|
|
153
|
+
const buf = readFileSync(zipPath);
|
|
154
|
+
const names = [];
|
|
155
|
+
for (let i = 0; i < buf.length - 4; i++) {
|
|
156
|
+
if (buf.readUInt32LE(i) === 0x02014b50) {
|
|
157
|
+
const nameLen = buf.readUInt16LE(i + 28);
|
|
158
|
+
const extraLen = buf.readUInt16LE(i + 30);
|
|
159
|
+
const commentLen = buf.readUInt16LE(i + 32);
|
|
160
|
+
const name = buf.toString("utf-8", i + 46, i + 46 + nameLen);
|
|
161
|
+
names.push(name);
|
|
162
|
+
i += 45 + nameLen + extraLen + commentLen;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return names;
|
|
166
|
+
}
|
|
167
|
+
|
|
48
168
|
console.log("[pack-clean] Preparing clean release directory...");
|
|
49
169
|
rmSync(releaseDir, { recursive: true, force: true });
|
|
50
170
|
rmSync(archivePath, { force: true });
|
|
@@ -115,7 +235,43 @@ if (badArchiveEntries.length > 0) {
|
|
|
115
235
|
|
|
116
236
|
const sizeKb = (statSync(archivePath).size / 1024).toFixed(1);
|
|
117
237
|
console.log(`[pack-clean] Release directory: ${releaseFiles.length} files`);
|
|
118
|
-
console.log(`[pack-clean] Archive: ${archivePath} (${sizeKb} KB)`);
|
|
238
|
+
console.log(`[pack-clean] tar.gz Archive: ${archivePath} (${sizeKb} KB)`);
|
|
239
|
+
|
|
240
|
+
// ── Create zip archive with POSIX path separators ─────────────────
|
|
241
|
+
console.log("[pack-clean] Creating zip archive with POSIX paths...");
|
|
242
|
+
rmSync(zipArchivePath, { force: true });
|
|
243
|
+
|
|
244
|
+
const zipFiles = releaseFiles.map((file) => {
|
|
245
|
+
const rel = toPosix(relative(releaseDir, file));
|
|
246
|
+
return { path: rel, content: readFileSync(file) };
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
createZip(zipFiles, zipArchivePath);
|
|
250
|
+
|
|
251
|
+
// Verify zip entries use POSIX separators
|
|
252
|
+
const zipEntryNames = readZipEntryNames(zipArchivePath);
|
|
253
|
+
const badZipEntries = zipEntryNames.filter((name) => name.includes("\\"));
|
|
254
|
+
if (badZipEntries.length > 0) {
|
|
255
|
+
console.error("[pack-clean] Backslash path separators found in zip entries:");
|
|
256
|
+
for (const name of badZipEntries.slice(0, 20)) {
|
|
257
|
+
console.error(` ${name}`);
|
|
258
|
+
}
|
|
259
|
+
rmSync(zipArchivePath, { force: true });
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const badZipForbidden = zipEntryNames.filter(isForbidden);
|
|
264
|
+
if (badZipForbidden.length > 0) {
|
|
265
|
+
console.error("[pack-clean] Forbidden files in zip archive:");
|
|
266
|
+
for (const entry of badZipForbidden) {
|
|
267
|
+
console.error(` ${entry}`);
|
|
268
|
+
}
|
|
269
|
+
rmSync(zipArchivePath, { force: true });
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const zipSizeKb = (statSync(zipArchivePath).size / 1024).toFixed(1);
|
|
274
|
+
console.log(`[pack-clean] zip Archive: ${zipArchivePath} (${zipSizeKb} KB, ${zipEntryNames.length} entries)`);
|
|
119
275
|
console.log("[pack-clean] OK");
|
|
120
276
|
|
|
121
277
|
function listFiles(dir) {
|