macca-method 2.1.2 → 3.0.0-rc.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/.agents/macca-lock.json +2 -1
- package/.agents/macca-managed-skills.txt +1 -0
- package/.agents/skills/_shared/references/additional-skills.md +17 -1
- package/.agents/skills/_shared/references/brainstorm-session.md +61 -21
- package/.agents/skills/_shared/references/config-mutation.md +58 -18
- package/.agents/skills/_shared/references/fix-mode.md +21 -7
- package/.agents/skills/_shared/references/human-loop.md +2 -0
- package/.agents/skills/_shared/references/implementation-principles.md +4 -0
- package/.agents/skills/_shared/references/interaction-contract.md +33 -0
- package/.agents/skills/_shared/references/invocation-policy.md +14 -3
- package/.agents/skills/_shared/references/language-config.md +24 -4
- package/.agents/skills/_shared/references/output-ownership.md +12 -5
- package/.agents/skills/_shared/references/planning-principles.md +35 -0
- package/.agents/skills/_shared/references/scope-delta.md +21 -0
- package/.agents/skills/_shared/references/scope-rules.md +1 -1
- package/.agents/skills/_shared/references/skill-catalog.md +30 -13
- package/.agents/skills/_shared/references/workspace-safety.md +11 -0
- package/.agents/skills/_shared/scripts/config-file.js +132 -0
- package/.agents/skills/_shared/scripts/config-validator.js +164 -0
- package/.agents/skills/_shared/scripts/read-preferences.js +128 -0
- package/.agents/skills/add-feature/SKILL.md +24 -18
- package/.agents/skills/brainstorm-api/SKILL.md +24 -23
- package/.agents/skills/brainstorm-api/assets/api.template.md +11 -23
- package/.agents/skills/brainstorm-architecture/SKILL.md +41 -48
- package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +24 -16
- package/.agents/skills/brainstorm-prd/SKILL.md +26 -28
- package/.agents/skills/brainstorm-prd/assets/PRD.template.md +10 -9
- package/.agents/skills/brainstorm-rules/SKILL.md +42 -46
- package/.agents/skills/brainstorm-rules/assets/rules.template.md +8 -8
- package/.agents/skills/brainstorm-schema/SKILL.md +17 -17
- package/.agents/skills/brainstorm-schema/assets/schema.template.md +9 -8
- package/.agents/skills/brainstorm-styleguide/SKILL.md +27 -26
- package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +27 -51
- package/.agents/skills/brainstorm-task/SKILL.md +53 -37
- package/.agents/skills/brainstorm-task/assets/Task.template.md +11 -12
- package/.agents/skills/bug-fix/SKILL.md +78 -175
- package/.agents/skills/bug-fix/assets/bug-log.template.md +61 -0
- package/.agents/skills/code-review/SKILL.md +14 -6
- package/.agents/skills/code-review/references/review-checklist.md +24 -44
- package/.agents/skills/developer/SKILL.md +23 -25
- package/.agents/skills/developer/references/close-phase.md +11 -6
- package/.agents/skills/developer/references/execute-task.md +15 -8
- package/.agents/skills/developer/references/onboarding.md +16 -19
- package/.agents/skills/help/SKILL.md +49 -49
- package/.agents/skills/meet/SKILL.md +37 -85
- package/.agents/skills/quick-dev/SKILL.md +40 -75
- package/.agents/skills/release-readiness/SKILL.md +1 -1
- package/.agents/skills/setup-macca-method/SKILL.md +79 -0
- package/.agents/skills/spec-audit/SKILL.md +21 -17
- package/.agents/skills/spec-compliance/SKILL.md +36 -18
- package/.agents/skills/spec-init/SKILL.md +53 -22
- package/CHANGELOG.md +31 -0
- package/README.md +92 -1195
- package/bin/macca-method.js +833 -286
- package/docs/configuration.md +174 -0
- package/docs/troubleshooting.md +180 -0
- package/docs/workflows.md +175 -0
- package/package.json +48 -4
- package/scripts/lib/npm-command.js +76 -0
- package/scripts/test-cli-setup.js +869 -0
- package/scripts/test-config.js +320 -0
- package/scripts/test-install.js +63 -28
- package/scripts/test-installer-safety.js +545 -0
- package/scripts/test-npm-command.js +143 -0
- package/scripts/test-package-safety.js +252 -0
- package/scripts/test-preferences.js +475 -0
- package/scripts/test-upgrade-legacy.js +11 -21
- package/scripts/validate-docs.js +166 -0
- package/scripts/validate-package.js +285 -0
- package/scripts/validate-skill-behavior.js +914 -12
- package/.agents/skills/antislop-copywriting/SKILL.md +0 -372
- package/.agents/skills/skill-creator/LICENSE.txt +0 -202
- package/.agents/skills/skill-creator/SKILL.md +0 -485
- package/.agents/skills/skill-creator/agents/analyzer.md +0 -274
- package/.agents/skills/skill-creator/agents/comparator.md +0 -202
- package/.agents/skills/skill-creator/agents/grader.md +0 -223
- package/.agents/skills/skill-creator/assets/eval_review.html +0 -146
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.agents/skills/skill-creator/references/schemas.md +0 -441
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
- package/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
- package/.agents/skills/skill-creator/scripts/utils.py +0 -47
package/bin/macca-method.js
CHANGED
|
@@ -11,6 +11,10 @@ const readline = require("node:readline");
|
|
|
11
11
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
12
12
|
const SOURCE_AGENTS_DIR = path.join(PACKAGE_ROOT, ".agents");
|
|
13
13
|
const SOURCE_SKILLS_DIR = path.join(SOURCE_AGENTS_DIR, "skills");
|
|
14
|
+
const { assertValidConfig } = require(path.join(
|
|
15
|
+
SOURCE_SKILLS_DIR, "_shared", "scripts", "config-validator.js",
|
|
16
|
+
));
|
|
17
|
+
const operation = { stage: "not started", targetDir: null, recoveryChanged: false };
|
|
14
18
|
const SOURCE_MANAGED_SKILLS_FILE = path.join(
|
|
15
19
|
SOURCE_AGENTS_DIR,
|
|
16
20
|
"macca-managed-skills.txt",
|
|
@@ -122,10 +126,15 @@ function main() {
|
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
if (command === "doctor") {
|
|
130
|
+
runDoctor(args);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
ensurePackagedFiles();
|
|
126
135
|
|
|
127
136
|
if (command === "install") {
|
|
128
|
-
runInstall(args).catch(
|
|
137
|
+
runInstall(args).catch(exitWithError);
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
131
140
|
|
|
@@ -136,7 +145,7 @@ function main() {
|
|
|
136
145
|
|
|
137
146
|
exitWithError(`Unknown command: ${command}`);
|
|
138
147
|
} catch (error) {
|
|
139
|
-
exitWithError(error
|
|
148
|
+
exitWithError(error);
|
|
140
149
|
}
|
|
141
150
|
}
|
|
142
151
|
|
|
@@ -164,6 +173,11 @@ function parseArgs(argv) {
|
|
|
164
173
|
continue;
|
|
165
174
|
}
|
|
166
175
|
|
|
176
|
+
if (token === "-f" || token === "--force") {
|
|
177
|
+
args.force = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
167
181
|
if (token === "--list-tools") {
|
|
168
182
|
args.listTools = true;
|
|
169
183
|
continue;
|
|
@@ -251,6 +265,9 @@ function parseArgs(argv) {
|
|
|
251
265
|
args._.push(token);
|
|
252
266
|
}
|
|
253
267
|
|
|
268
|
+
if (args._.length > 1) {
|
|
269
|
+
throw new Error("Unexpected positional arguments. Use --directory <path> for the target project.");
|
|
270
|
+
}
|
|
254
271
|
return args;
|
|
255
272
|
}
|
|
256
273
|
|
|
@@ -286,20 +303,23 @@ function printHelp() {
|
|
|
286
303
|
"Usage:",
|
|
287
304
|
" npx macca-method@latest install [options]",
|
|
288
305
|
" npx macca-method@latest upgrade [options]",
|
|
306
|
+
" macca-method doctor [--directory <path>] Read-only installation check (no repair).",
|
|
289
307
|
" npx macca-method@latest --list-tools",
|
|
290
308
|
"",
|
|
291
309
|
"Install options:",
|
|
292
310
|
" -t, --tool <name> Repeatable. Also accepts comma-separated values.",
|
|
293
311
|
" -d, --directory <path> Target project directory. Defaults to the current directory.",
|
|
294
312
|
" -y, --yes Skip prompts and use defaults where needed.",
|
|
313
|
+
" -f, --force Overwrite locally modified managed skills.",
|
|
295
314
|
" --name <value> Developer name.",
|
|
296
315
|
" --project <value> Project name.",
|
|
297
316
|
" --communication-language <value>",
|
|
298
|
-
" --document-language <value>",
|
|
317
|
+
" --document-language <value> Defaults to the communication language on first install.",
|
|
318
|
+
" Requires Node.js >=22; Node.js 22 and 24 are supported runtimes in the configured CI matrix.",
|
|
299
319
|
"",
|
|
300
320
|
"Examples:",
|
|
301
321
|
" npx macca-method@latest install",
|
|
302
|
-
" npx macca-method@latest install --tool github-copilot --tool codex --yes",
|
|
322
|
+
" npx macca-method@latest install --tool github-copilot --tool codex --name \"Your Name\" --project \"Your Project\" --yes",
|
|
303
323
|
" npx macca-method@latest upgrade",
|
|
304
324
|
"",
|
|
305
325
|
].join("\n"),
|
|
@@ -326,27 +346,29 @@ function resolveTargetDirectory(rawDirectory) {
|
|
|
326
346
|
}
|
|
327
347
|
|
|
328
348
|
function assertSafeTargetDirectory(rawDirectory) {
|
|
349
|
+
if (path.sep === "\\" && /^[\\/]{2}/.test(rawDirectory || process.cwd())) {
|
|
350
|
+
throw Object.assign(new Error("Windows UNC/device paths are unsupported. Use a local drive path such as C:\\projects\\app."), { code: "MACCA_UNSUPPORTED_PATH", path: rawDirectory });
|
|
351
|
+
}
|
|
329
352
|
const requested = rawDirectory
|
|
330
353
|
? path.resolve(process.cwd(), rawDirectory)
|
|
331
354
|
: process.cwd();
|
|
332
355
|
const absolute = path.isAbsolute(requested)
|
|
333
356
|
? requested
|
|
334
357
|
: path.resolve(requested);
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
358
|
+
const root = path.parse(absolute).root;
|
|
359
|
+
if (path.sep === "\\" && /^[\\/]{2}/.test(root)) {
|
|
360
|
+
throw Object.assign(new Error("Windows UNC/device paths are unsupported. Use a local drive path."), { code: "MACCA_UNSUPPORTED_PATH", path: absolute });
|
|
361
|
+
}
|
|
362
|
+
const segments = absolute.slice(root.length).split(path.sep).filter(Boolean);
|
|
363
|
+
let current = root;
|
|
339
364
|
|
|
340
|
-
if (
|
|
365
|
+
if (isSymlink(current)) {
|
|
341
366
|
throw new Error(`Refusing symlinked target project ancestor: ${current}`);
|
|
342
367
|
}
|
|
343
368
|
|
|
344
369
|
for (const segment of segments) {
|
|
345
|
-
current =
|
|
346
|
-
|
|
347
|
-
? path.join(current, segment)
|
|
348
|
-
: path.join(current, segment);
|
|
349
|
-
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
370
|
+
current = path.join(current, segment);
|
|
371
|
+
if (isSymlink(current)) {
|
|
350
372
|
throw new Error(`Refusing symlinked target project ancestor: ${current}`);
|
|
351
373
|
}
|
|
352
374
|
}
|
|
@@ -436,27 +458,37 @@ function normalizeLanguage(value) {
|
|
|
436
458
|
case "bahasa inggris":
|
|
437
459
|
return "english";
|
|
438
460
|
default:
|
|
439
|
-
return
|
|
461
|
+
return "indonesian";
|
|
440
462
|
}
|
|
441
463
|
}
|
|
442
464
|
|
|
443
465
|
function ensureDirectory(directoryPath) {
|
|
466
|
+
operation.stage = "writing";
|
|
444
467
|
fs.mkdirSync(directoryPath, { recursive: true });
|
|
445
468
|
}
|
|
446
469
|
|
|
470
|
+
function isSymlink(candidate) {
|
|
471
|
+
try {
|
|
472
|
+
return fs.lstatSync(candidate).isSymbolicLink();
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error.code === "ENOENT") return false;
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
447
479
|
function assertSafeProjectPath(targetDir, destination) {
|
|
448
480
|
const targetRoot = path.resolve(targetDir);
|
|
449
481
|
const resolvedDestination = path.resolve(destination);
|
|
450
482
|
if (
|
|
451
483
|
resolvedDestination !== targetRoot &&
|
|
452
|
-
!resolvedDestination.startsWith(`${targetRoot}${path.sep}`)
|
|
484
|
+
!resolvedDestination.startsWith(targetRoot.endsWith(path.sep) ? targetRoot : `${targetRoot}${path.sep}`)
|
|
453
485
|
) {
|
|
454
486
|
throw new Error(
|
|
455
487
|
`Refusing destination outside target project: ${destination}`,
|
|
456
488
|
);
|
|
457
489
|
}
|
|
458
490
|
|
|
459
|
-
if (
|
|
491
|
+
if (isSymlink(targetRoot)) {
|
|
460
492
|
throw new Error(`Refusing symlinked target project: ${targetRoot}`);
|
|
461
493
|
}
|
|
462
494
|
|
|
@@ -464,7 +496,7 @@ function assertSafeProjectPath(targetDir, destination) {
|
|
|
464
496
|
let current = targetRoot;
|
|
465
497
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
466
498
|
current = path.join(current, segment);
|
|
467
|
-
if (
|
|
499
|
+
if (isSymlink(current)) {
|
|
468
500
|
throw new Error(
|
|
469
501
|
`Refusing symlinked skill destination component: ${current}`,
|
|
470
502
|
);
|
|
@@ -493,13 +525,37 @@ function hashText(content) {
|
|
|
493
525
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
494
526
|
}
|
|
495
527
|
|
|
528
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
529
|
+
OWNERSHIP_MARKER,
|
|
530
|
+
".DS_Store",
|
|
531
|
+
"Thumbs.db",
|
|
532
|
+
"__pycache__",
|
|
533
|
+
".pytest_cache",
|
|
534
|
+
".ruff_cache",
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
function shouldIgnoreDirectoryEntry(name) {
|
|
538
|
+
if (IGNORED_DIRECTORY_NAMES.has(name)) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
if (
|
|
542
|
+
name.endsWith(".pyc") ||
|
|
543
|
+
name.endsWith(".pyo") ||
|
|
544
|
+
name.endsWith(".tmp") ||
|
|
545
|
+
name.startsWith(".tmp-")
|
|
546
|
+
) {
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
496
552
|
function getDirectoryFileHashes(directoryPath) {
|
|
497
553
|
const hashes = {};
|
|
498
554
|
|
|
499
555
|
function walk(currentPath, relativePath = "") {
|
|
500
556
|
const entries = fs
|
|
501
557
|
.readdirSync(currentPath, { withFileTypes: true })
|
|
502
|
-
.filter((entry) => entry.name
|
|
558
|
+
.filter((entry) => !shouldIgnoreDirectoryEntry(entry.name))
|
|
503
559
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
504
560
|
|
|
505
561
|
for (const entry of entries) {
|
|
@@ -531,6 +587,86 @@ function hashDirectory(directoryPath) {
|
|
|
531
587
|
return hashText(JSON.stringify(getDirectoryFileHashes(directoryPath)));
|
|
532
588
|
}
|
|
533
589
|
|
|
590
|
+
// Payload fingerprints intentionally omit caches. Destructive operations need
|
|
591
|
+
// a separate snapshot that includes every entry, including empty directories.
|
|
592
|
+
function hashSnapshot(targetPath, kind) {
|
|
593
|
+
const stat = fs.lstatSync(targetPath);
|
|
594
|
+
if (
|
|
595
|
+
stat.isSymbolicLink() ||
|
|
596
|
+
(kind === "directory" ? !stat.isDirectory() : !stat.isFile())
|
|
597
|
+
) {
|
|
598
|
+
throw new Error(`Refusing unexpected managed path type: ${targetPath}`);
|
|
599
|
+
}
|
|
600
|
+
if (kind === "file") return hashFile(targetPath);
|
|
601
|
+
|
|
602
|
+
const entries = [];
|
|
603
|
+
function walk(directory, relative = "") {
|
|
604
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
605
|
+
const candidate = path.join(directory, name);
|
|
606
|
+
const entryPath = relative ? `${relative}/${name}` : name;
|
|
607
|
+
const entryStat = fs.lstatSync(candidate);
|
|
608
|
+
if (entryStat.isSymbolicLink()) {
|
|
609
|
+
throw new Error(`Refusing symlink inside managed skill: ${candidate}`);
|
|
610
|
+
}
|
|
611
|
+
if (entryStat.isDirectory()) {
|
|
612
|
+
entries.push([entryPath, "directory"]);
|
|
613
|
+
walk(candidate, entryPath);
|
|
614
|
+
} else if (entryStat.isFile()) {
|
|
615
|
+
entries.push([entryPath, "file", hashFile(candidate)]);
|
|
616
|
+
} else {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`Unsupported file type inside managed skill: ${candidate}`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
walk(targetPath);
|
|
624
|
+
return hashText(JSON.stringify(entries));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function getUnhashedEntries(directoryPath) {
|
|
628
|
+
const omitted = [];
|
|
629
|
+
function walk(directory, relative = "") {
|
|
630
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
631
|
+
if (!relative && entry.name === OWNERSHIP_MARKER) continue;
|
|
632
|
+
const entryPath = path.join(relative, entry.name);
|
|
633
|
+
const candidate = path.join(directory, entry.name);
|
|
634
|
+
if (
|
|
635
|
+
shouldIgnoreDirectoryEntry(entry.name) ||
|
|
636
|
+
(entry.isDirectory() && fs.readdirSync(candidate).length === 0)
|
|
637
|
+
) {
|
|
638
|
+
omitted.push(entryPath);
|
|
639
|
+
} else if (entry.isDirectory()) {
|
|
640
|
+
walk(candidate, entryPath);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
walk(directoryPath);
|
|
645
|
+
return omitted;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function preserveUnhashedEntries(targetPath, stagingPath) {
|
|
649
|
+
for (const relative of getUnhashedEntries(targetPath)) {
|
|
650
|
+
const source = path.join(targetPath, relative);
|
|
651
|
+
const destination = path.join(stagingPath, relative);
|
|
652
|
+
if (fs.existsSync(destination)) {
|
|
653
|
+
const kind = fs.lstatSync(source).isDirectory() ? "directory" : "file";
|
|
654
|
+
if (hashSnapshot(source, kind) !== hashSnapshot(destination, kind)) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`Refusing to overwrite preserved skill content: ${source}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
ensureDirectory(path.dirname(destination));
|
|
661
|
+
fs.cpSync(source, destination, {
|
|
662
|
+
recursive: true,
|
|
663
|
+
errorOnExist: true,
|
|
664
|
+
force: false,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
534
670
|
function getLegacyPayloadHashes() {
|
|
535
671
|
if (!fs.existsSync(SOURCE_LEGACY_PAYLOADS_FILE)) return {};
|
|
536
672
|
const releases = readJsonObject(
|
|
@@ -581,46 +717,107 @@ function buildInstalledLock(managedSkills) {
|
|
|
581
717
|
}
|
|
582
718
|
|
|
583
719
|
function compareSemver(left, right) {
|
|
584
|
-
const parse = (value) =>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
720
|
+
const parse = (value, label) => {
|
|
721
|
+
const match = typeof value === "string" && value.match(
|
|
722
|
+
/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/,
|
|
723
|
+
);
|
|
724
|
+
const prerelease = match && match[4] ? match[4].split(".") : [];
|
|
725
|
+
if (!match || match[0] !== value || prerelease.some((part) => /^0[0-9]+$/.test(part))) {
|
|
726
|
+
throw Object.assign(new Error(
|
|
727
|
+
`Invalid semantic version for ${label}: expected MAJOR.MINOR.PATCH with optional prerelease/build metadata; numeric version and prerelease identifiers must not have leading zeros.`,
|
|
728
|
+
), { code: "MACCA_VERSION_INVALID" });
|
|
729
|
+
}
|
|
730
|
+
return { core: match.slice(1, 4), prerelease };
|
|
731
|
+
};
|
|
732
|
+
// Keep numeric identifiers as decimal strings: SemVer does not bound their size.
|
|
733
|
+
const compareNumeric = (a, b) => a.length !== b.length
|
|
734
|
+
? (a.length < b.length ? -1 : 1)
|
|
735
|
+
: (a === b ? 0 : a < b ? -1 : 1);
|
|
736
|
+
const a = parse(left, "left version");
|
|
737
|
+
const b = parse(right, "right version");
|
|
738
|
+
for (let index = 0; index < 3; index += 1) {
|
|
739
|
+
const diff = compareNumeric(a.core[index], b.core[index]);
|
|
594
740
|
if (diff !== 0) return diff;
|
|
595
741
|
}
|
|
742
|
+
if (!a.prerelease.length || !b.prerelease.length) {
|
|
743
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length ? -1 : 1;
|
|
744
|
+
}
|
|
745
|
+
for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index += 1) {
|
|
746
|
+
const x = a.prerelease[index];
|
|
747
|
+
const y = b.prerelease[index];
|
|
748
|
+
if (x === y) continue;
|
|
749
|
+
const xNumeric = /^[0-9]+$/.test(x);
|
|
750
|
+
const yNumeric = /^[0-9]+$/.test(y);
|
|
751
|
+
if (xNumeric && yNumeric) return compareNumeric(x, y);
|
|
752
|
+
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
|
753
|
+
return x < y ? -1 : 1;
|
|
754
|
+
}
|
|
755
|
+
if (a.prerelease.length !== b.prerelease.length) {
|
|
756
|
+
return a.prerelease.length < b.prerelease.length ? -1 : 1;
|
|
757
|
+
}
|
|
596
758
|
return 0;
|
|
597
759
|
}
|
|
598
760
|
|
|
599
|
-
function assertPackageNotOlderThanInstalled(agentsDirectory) {
|
|
600
|
-
const installedLockPath = path.join(agentsDirectory, "macca-lock.json");
|
|
601
|
-
if (!fs.existsSync(installedLockPath)) {
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
|
|
761
|
+
function assertPackageNotOlderThanInstalled(agentsDirectory, includeRecovery = false) {
|
|
605
762
|
const packagedLock = readJsonObject(SOURCE_LOCK_FILE, "packaged MACCA lock");
|
|
606
|
-
const installedLock = readJsonObject(
|
|
607
|
-
installedLockPath,
|
|
608
|
-
"installed MACCA lock",
|
|
609
|
-
);
|
|
610
763
|
const packagedVersion = packagedLock.version;
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
764
|
+
const targetDir = path.dirname(agentsDirectory);
|
|
765
|
+
const installedLockPath = path.join(agentsDirectory, "macca-lock.json");
|
|
766
|
+
const assertRegularLock = (lockPath) => {
|
|
767
|
+
assertSafeProjectPath(targetDir, lockPath);
|
|
768
|
+
if (!fs.lstatSync(lockPath).isFile()) {
|
|
769
|
+
throw new Error(`Invalid MACCA lock at ${lockPath}: expected a regular file.`);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const checkVersion = (lockPath) => {
|
|
773
|
+
let installedVersion = packagedVersion;
|
|
774
|
+
if (lockPath) {
|
|
775
|
+
assertRegularLock(lockPath);
|
|
776
|
+
installedVersion = readJsonObject(lockPath, "installed MACCA lock").version;
|
|
777
|
+
}
|
|
778
|
+
let comparison;
|
|
779
|
+
try {
|
|
780
|
+
comparison = compareSemver(packagedVersion, installedVersion);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if (error.code !== "MACCA_VERSION_INVALID") throw error;
|
|
783
|
+
throw Object.assign(new Error(
|
|
784
|
+
`Cannot verify MACCA downgrade safety: ${error.message} ` +
|
|
785
|
+
`Check the version fields in the packaged MACCA lock (${SOURCE_LOCK_FILE}, left) and installed MACCA lock (${lockPath || installedLockPath}, right) before retrying.`,
|
|
786
|
+
), { code: error.code });
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (comparison < 0) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`Refusing to downgrade MACCA from ${installedVersion} to ${packagedVersion}. ` +
|
|
792
|
+
"Publish or use a newer package before running install/upgrade.",
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
// Validate the package even on first install, before a target can be created.
|
|
797
|
+
checkVersion(null);
|
|
798
|
+
assertSafeProjectPath(targetDir, installedLockPath);
|
|
799
|
+
if (fs.existsSync(installedLockPath)) checkVersion(installedLockPath);
|
|
800
|
+
if (!includeRecovery) return;
|
|
618
801
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
802
|
+
const journalPath = path.join(agentsDirectory, TRANSACTION_FILE);
|
|
803
|
+
assertSafeProjectPath(targetDir, journalPath);
|
|
804
|
+
if (!fs.existsSync(journalPath)) return;
|
|
805
|
+
const journal = readJsonObject(journalPath, "MACCA transaction journal");
|
|
806
|
+
const entries = validateTransactionEntries(targetDir, journal);
|
|
807
|
+
// Recovery may restore a newer lock or delete version evidence. Inspect only
|
|
808
|
+
// the whitelisted lock entry, after validating its paths and recovery hashes.
|
|
809
|
+
for (const entry of entries) {
|
|
810
|
+
if (path.resolve(entry.targetPath) !== path.resolve(installedLockPath)) continue;
|
|
811
|
+
if (fs.existsSync(entry.backupPath)) {
|
|
812
|
+
assertRegularLock(entry.backupPath);
|
|
813
|
+
assertTransactionBackup(entry, journal.phase);
|
|
814
|
+
checkVersion(entry.backupPath);
|
|
815
|
+
}
|
|
816
|
+
if (entry.stagingPath && fs.existsSync(entry.stagingPath)) {
|
|
817
|
+
assertRegularLock(entry.stagingPath);
|
|
818
|
+
assertTransactionPayload(entry, journal.transactionId, entry.stagingPath);
|
|
819
|
+
checkVersion(entry.stagingPath);
|
|
820
|
+
}
|
|
624
821
|
}
|
|
625
822
|
}
|
|
626
823
|
|
|
@@ -629,12 +826,24 @@ function readOwnershipMarker(
|
|
|
629
826
|
expectedSkill = path.basename(targetPath),
|
|
630
827
|
) {
|
|
631
828
|
const markerPath = path.join(targetPath, OWNERSHIP_MARKER);
|
|
632
|
-
|
|
633
|
-
|
|
829
|
+
let stat;
|
|
830
|
+
try {
|
|
831
|
+
stat = fs.lstatSync(markerPath);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (error.code === "ENOENT") return null;
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
// Payload hashing omits this metadata file. Check its type independently so
|
|
837
|
+
// ownership checks never follow a symlink or try to read a pipe/device.
|
|
838
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
839
|
+
throw Object.assign(new Error(
|
|
840
|
+
`Unsafe ownership marker at ${JSON.stringify(markerPath)}: expected a regular non-symlink file. Back up and review this path before retrying.`,
|
|
841
|
+
), { code: "MACCA_UNSAFE_MARKER", path: markerPath });
|
|
634
842
|
}
|
|
635
843
|
|
|
844
|
+
const content = fs.readFileSync(markerPath, "utf8");
|
|
636
845
|
try {
|
|
637
|
-
const marker = JSON.parse(
|
|
846
|
+
const marker = JSON.parse(content);
|
|
638
847
|
if (marker.owner !== "macca-method" || marker.skill !== expectedSkill) {
|
|
639
848
|
return null;
|
|
640
849
|
}
|
|
@@ -648,7 +857,10 @@ function isMaccaOwned(targetPath) {
|
|
|
648
857
|
return readOwnershipMarker(targetPath) !== null;
|
|
649
858
|
}
|
|
650
859
|
|
|
651
|
-
function assertManagedDirectoryUnchanged(targetPath) {
|
|
860
|
+
function assertManagedDirectoryUnchanged(targetPath, force = false) {
|
|
861
|
+
if (force) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
652
864
|
const marker = readOwnershipMarker(targetPath);
|
|
653
865
|
if (!marker) {
|
|
654
866
|
throw new Error(
|
|
@@ -657,7 +869,8 @@ function assertManagedDirectoryUnchanged(targetPath) {
|
|
|
657
869
|
}
|
|
658
870
|
if (!marker.payloadHash || marker.payloadHash !== hashDirectory(targetPath)) {
|
|
659
871
|
throw new Error(
|
|
660
|
-
`Refusing to overwrite locally modified managed skill: ${targetPath}
|
|
872
|
+
`Refusing to overwrite locally modified managed skill: ${targetPath}. ` +
|
|
873
|
+
"Back up and review local modifications first. --force destructively overwrites managed local modifications.",
|
|
661
874
|
);
|
|
662
875
|
}
|
|
663
876
|
}
|
|
@@ -670,11 +883,15 @@ function readJsonObject(filePath, label) {
|
|
|
670
883
|
}
|
|
671
884
|
return value;
|
|
672
885
|
} catch (error) {
|
|
673
|
-
|
|
886
|
+
if (error.code) throw error;
|
|
887
|
+
throw Object.assign(new Error(`Invalid ${label} at ${filePath}: expected a JSON object.`), { path: filePath, code: "MACCA_METADATA_INVALID" });
|
|
674
888
|
}
|
|
675
889
|
}
|
|
676
890
|
|
|
677
|
-
function assertManagedFilesUnchanged(agentsDirectory) {
|
|
891
|
+
function assertManagedFilesUnchanged(agentsDirectory, force = false) {
|
|
892
|
+
if (force) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
678
895
|
const statePath = path.join(agentsDirectory, STATE_FILE);
|
|
679
896
|
if (!fs.existsSync(statePath)) {
|
|
680
897
|
return;
|
|
@@ -684,7 +901,8 @@ function assertManagedFilesUnchanged(agentsDirectory) {
|
|
|
684
901
|
const filePath = path.join(agentsDirectory, fileName);
|
|
685
902
|
if (!fs.existsSync(filePath) || hashFile(filePath) !== expectedHash) {
|
|
686
903
|
throw new Error(
|
|
687
|
-
`Refusing to overwrite locally modified MACCA metadata: ${filePath}
|
|
904
|
+
`Refusing to overwrite locally modified MACCA metadata: ${filePath}. ` +
|
|
905
|
+
"Back up and review local modifications first. --force destructively overwrites managed local modifications.",
|
|
688
906
|
);
|
|
689
907
|
}
|
|
690
908
|
}
|
|
@@ -699,13 +917,20 @@ function buildStateContent(files) {
|
|
|
699
917
|
}
|
|
700
918
|
|
|
701
919
|
function writeJsonAtomic(filePath, value) {
|
|
702
|
-
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
"
|
|
707
|
-
|
|
708
|
-
|
|
920
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(16).toString("hex")}`;
|
|
921
|
+
let descriptor;
|
|
922
|
+
let created = false;
|
|
923
|
+
try {
|
|
924
|
+
descriptor = fs.openSync(temporaryPath, "wx", 0o600);
|
|
925
|
+
created = true;
|
|
926
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
927
|
+
fs.closeSync(descriptor);
|
|
928
|
+
descriptor = undefined;
|
|
929
|
+
fs.renameSync(temporaryPath, filePath);
|
|
930
|
+
} finally {
|
|
931
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
932
|
+
if (created) fs.rmSync(temporaryPath, { force: true });
|
|
933
|
+
}
|
|
709
934
|
}
|
|
710
935
|
|
|
711
936
|
function removeManagedPath(targetPath, kind) {
|
|
@@ -721,24 +946,55 @@ function assertTransactionPayload(entry, transactionId, candidatePath) {
|
|
|
721
946
|
candidatePath,
|
|
722
947
|
path.basename(entry.targetPath),
|
|
723
948
|
);
|
|
724
|
-
if (
|
|
949
|
+
if (
|
|
950
|
+
!marker ||
|
|
951
|
+
marker.transactionId !== transactionId ||
|
|
952
|
+
!entry.stagedHash ||
|
|
953
|
+
hashDirectory(candidatePath) !== entry.stagedHash
|
|
954
|
+
) {
|
|
725
955
|
throw new Error(
|
|
726
956
|
`Refusing recovery of directory without matching transaction evidence: ${candidatePath}`,
|
|
727
957
|
);
|
|
728
958
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
959
|
+
if (!entry.stagedSnapshotHash && getUnhashedEntries(candidatePath).length) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
`Refusing legacy recovery with unhashed directory content: ${candidatePath}`,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
} else if (!entry.stagedHash || hashFile(candidatePath) !== entry.stagedHash) {
|
|
732
965
|
throw new Error(
|
|
733
966
|
`Refusing recovery of file without matching transaction hash: ${candidatePath}`,
|
|
734
967
|
);
|
|
735
968
|
}
|
|
969
|
+
const snapshot = hashSnapshot(candidatePath, entry.kind);
|
|
970
|
+
if (entry.stagedSnapshotHash && snapshot !== entry.stagedSnapshotHash) {
|
|
971
|
+
throw new Error(
|
|
972
|
+
`Refusing recovery of modified transaction payload: ${candidatePath}`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
736
975
|
}
|
|
737
976
|
|
|
738
|
-
function assertTransactionBackup(entry) {
|
|
977
|
+
function assertTransactionBackup(entry, phase) {
|
|
739
978
|
if (!fs.existsSync(entry.backupPath)) {
|
|
740
979
|
return;
|
|
741
980
|
}
|
|
981
|
+
const snapshot = hashSnapshot(entry.backupPath, entry.kind);
|
|
982
|
+
if (
|
|
983
|
+
!entry.hadTarget ||
|
|
984
|
+
(entry.originalHash && snapshot !== entry.originalHash)
|
|
985
|
+
) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
`Refusing recovery from modified transaction backup: ${entry.backupPath}`,
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
if (entry.originalHash) return;
|
|
991
|
+
// Old journals lack a pre-transaction snapshot. A file backup can be restored
|
|
992
|
+
// without discarding its contents, but cannot safely be deleted on commit.
|
|
993
|
+
if (entry.kind === "file" && phase === "committed") {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`Refusing cleanup of legacy backup without a snapshot: ${entry.backupPath}`,
|
|
996
|
+
);
|
|
997
|
+
}
|
|
742
998
|
if (entry.kind === "directory") {
|
|
743
999
|
const marker = readOwnershipMarker(
|
|
744
1000
|
entry.backupPath,
|
|
@@ -751,7 +1007,10 @@ function assertTransactionBackup(entry) {
|
|
|
751
1007
|
const validLegacy =
|
|
752
1008
|
entry.legacySkillName &&
|
|
753
1009
|
isLegacyDirectoryUnchanged(entry.backupPath, entry.legacySkillName);
|
|
754
|
-
if (
|
|
1010
|
+
if (
|
|
1011
|
+
(!validManaged && !validLegacy) ||
|
|
1012
|
+
getUnhashedEntries(entry.backupPath).length
|
|
1013
|
+
) {
|
|
755
1014
|
throw new Error(
|
|
756
1015
|
`Refusing recovery from unowned or modified backup directory: ${entry.backupPath}`,
|
|
757
1016
|
);
|
|
@@ -761,7 +1020,7 @@ function assertTransactionBackup(entry) {
|
|
|
761
1020
|
|
|
762
1021
|
function validateTransactionEntries(targetDir, journal) {
|
|
763
1022
|
if (
|
|
764
|
-
journal.version
|
|
1023
|
+
![1, 2].includes(journal.version) ||
|
|
765
1024
|
!["committing", "committed"].includes(journal.phase)
|
|
766
1025
|
) {
|
|
767
1026
|
throw new Error("Unsupported MACCA transaction journal version or phase");
|
|
@@ -828,6 +1087,19 @@ function validateTransactionEntries(targetDir, journal) {
|
|
|
828
1087
|
) {
|
|
829
1088
|
throw new Error("Transaction staged payload hash is missing or invalid");
|
|
830
1089
|
}
|
|
1090
|
+
for (const [field, required] of [
|
|
1091
|
+
["originalHash", entry.hadTarget],
|
|
1092
|
+
["stagedSnapshotHash", entry.stagingPath !== null],
|
|
1093
|
+
]) {
|
|
1094
|
+
if ((journal.version === 2 && required) || entry[field] != null) {
|
|
1095
|
+
if (
|
|
1096
|
+
typeof entry[field] !== "string" ||
|
|
1097
|
+
!/^[a-f0-9]{64}$/.test(entry[field])
|
|
1098
|
+
) {
|
|
1099
|
+
throw new Error(`Transaction ${field} is missing or invalid`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
831
1103
|
|
|
832
1104
|
const targetPath = path.resolve(entry.targetPath);
|
|
833
1105
|
const parent = path.dirname(targetPath);
|
|
@@ -893,7 +1165,7 @@ function recoverInterruptedTransaction(targetDir) {
|
|
|
893
1165
|
const journal = readJsonObject(journalPath, "MACCA transaction journal");
|
|
894
1166
|
const entries = validateTransactionEntries(targetDir, journal);
|
|
895
1167
|
for (const entry of entries) {
|
|
896
|
-
assertTransactionBackup(entry);
|
|
1168
|
+
assertTransactionBackup(entry, journal.phase);
|
|
897
1169
|
if (
|
|
898
1170
|
journal.phase === "committed" ||
|
|
899
1171
|
fs.existsSync(entry.backupPath) ||
|
|
@@ -904,13 +1176,38 @@ function recoverInterruptedTransaction(targetDir) {
|
|
|
904
1176
|
if (entry.stagingPath && fs.existsSync(entry.stagingPath)) {
|
|
905
1177
|
assertTransactionPayload(entry, journal.transactionId, entry.stagingPath);
|
|
906
1178
|
}
|
|
1179
|
+
if (
|
|
1180
|
+
journal.phase === "committing" &&
|
|
1181
|
+
entry.hadTarget &&
|
|
1182
|
+
!fs.existsSync(entry.backupPath) &&
|
|
1183
|
+
entry.originalHash &&
|
|
1184
|
+
(!fs.existsSync(entry.targetPath) ||
|
|
1185
|
+
hashSnapshot(entry.targetPath, entry.kind) !== entry.originalHash)
|
|
1186
|
+
) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Refusing recovery of modified original target: ${entry.targetPath}`,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
// Recovery can restore a config different from the visible one. Validate the
|
|
1193
|
+
// candidate after evidence checks, but before the first rollback/cleanup.
|
|
1194
|
+
for (const entry of entries) {
|
|
1195
|
+
if (entry.targetPath === path.join(targetDir, ".agents", "developer-config.json")) {
|
|
1196
|
+
const candidate = journal.phase === "committing" && fs.existsSync(entry.backupPath)
|
|
1197
|
+
? entry.backupPath : entry.targetPath;
|
|
1198
|
+
readDeveloperConfig(candidate);
|
|
1199
|
+
}
|
|
907
1200
|
}
|
|
908
1201
|
if (journal.phase === "committed") {
|
|
1202
|
+
operation.stage = "recovery";
|
|
1203
|
+
operation.recoveryChanged = true;
|
|
909
1204
|
for (const entry of entries) {
|
|
910
1205
|
removeManagedPath(entry.backupPath, entry.kind);
|
|
911
1206
|
if (entry.stagingPath) removeManagedPath(entry.stagingPath, entry.kind);
|
|
912
1207
|
}
|
|
913
1208
|
} else {
|
|
1209
|
+
operation.stage = "recovery";
|
|
1210
|
+
operation.recoveryChanged = true;
|
|
914
1211
|
for (const entry of [...entries].reverse()) {
|
|
915
1212
|
if (fs.existsSync(entry.backupPath)) {
|
|
916
1213
|
removeManagedPath(entry.targetPath, entry.kind);
|
|
@@ -938,10 +1235,9 @@ function getUniqueDestinations(targetDir, tools) {
|
|
|
938
1235
|
return [...destinations.values()];
|
|
939
1236
|
}
|
|
940
1237
|
|
|
941
|
-
function applySkillTransaction(entries, targetDir) {
|
|
1238
|
+
function applySkillTransaction(entries, targetDir, force = false) {
|
|
942
1239
|
const suffix = `${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString("hex")}`;
|
|
943
1240
|
const prepared = [];
|
|
944
|
-
const committed = [];
|
|
945
1241
|
const journalPath = path.join(targetDir, ".agents", TRANSACTION_FILE);
|
|
946
1242
|
|
|
947
1243
|
for (const entry of entries) {
|
|
@@ -971,7 +1267,10 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
971
1267
|
}
|
|
972
1268
|
if (kind === "directory") {
|
|
973
1269
|
if (isMaccaOwned(targetPath)) {
|
|
974
|
-
assertManagedDirectoryUnchanged(
|
|
1270
|
+
assertManagedDirectoryUnchanged(
|
|
1271
|
+
targetPath,
|
|
1272
|
+
force && Boolean(sourcePath),
|
|
1273
|
+
);
|
|
975
1274
|
} else if (
|
|
976
1275
|
!legacySkillName ||
|
|
977
1276
|
!isLegacyDirectoryUnchanged(targetPath, legacySkillName)
|
|
@@ -980,12 +1279,20 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
980
1279
|
`Refusing to overwrite or remove unowned skill directory: ${targetPath}`,
|
|
981
1280
|
);
|
|
982
1281
|
}
|
|
1282
|
+
if (!sourcePath && getUnhashedEntries(targetPath).length) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`Refusing to remove skill with unhashed content: ${targetPath}`,
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
983
1287
|
}
|
|
984
1288
|
}
|
|
985
1289
|
prepared.push({
|
|
986
1290
|
...entry,
|
|
987
1291
|
kind,
|
|
988
1292
|
hadTarget: fs.existsSync(targetPath),
|
|
1293
|
+
originalHash: fs.existsSync(targetPath)
|
|
1294
|
+
? hashSnapshot(targetPath, kind)
|
|
1295
|
+
: null,
|
|
989
1296
|
stagingPath:
|
|
990
1297
|
sourcePath || content !== undefined
|
|
991
1298
|
? path.join(parent, `.${targetName}.macca-stage-${suffix}`)
|
|
@@ -1008,6 +1315,9 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1008
1315
|
}
|
|
1009
1316
|
} else {
|
|
1010
1317
|
fs.cpSync(entry.sourcePath, entry.stagingPath, { recursive: true });
|
|
1318
|
+
if (entry.hadTarget) {
|
|
1319
|
+
preserveUnhashedEntries(entry.targetPath, entry.stagingPath);
|
|
1320
|
+
}
|
|
1011
1321
|
const payloadHash = hashDirectory(entry.stagingPath);
|
|
1012
1322
|
writeTextFile(
|
|
1013
1323
|
path.join(entry.stagingPath, OWNERSHIP_MARKER),
|
|
@@ -1028,10 +1338,11 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1028
1338
|
entry.kind === "file"
|
|
1029
1339
|
? hashFile(entry.stagingPath)
|
|
1030
1340
|
: hashDirectory(entry.stagingPath);
|
|
1341
|
+
entry.stagedSnapshotHash = hashSnapshot(entry.stagingPath, entry.kind);
|
|
1031
1342
|
}
|
|
1032
1343
|
|
|
1033
1344
|
writeJsonAtomic(journalPath, {
|
|
1034
|
-
version:
|
|
1345
|
+
version: 2,
|
|
1035
1346
|
phase: "committing",
|
|
1036
1347
|
transactionId: suffix,
|
|
1037
1348
|
entries: prepared.map(
|
|
@@ -1042,6 +1353,8 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1042
1353
|
kind,
|
|
1043
1354
|
hadTarget,
|
|
1044
1355
|
stagedHash,
|
|
1356
|
+
stagedSnapshotHash,
|
|
1357
|
+
originalHash,
|
|
1045
1358
|
legacySkillName,
|
|
1046
1359
|
}) => ({
|
|
1047
1360
|
targetPath,
|
|
@@ -1050,6 +1363,8 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1050
1363
|
kind,
|
|
1051
1364
|
hadTarget,
|
|
1052
1365
|
stagedHash: stagingPath ? stagedHash : null,
|
|
1366
|
+
stagedSnapshotHash: stagingPath ? stagedSnapshotHash : null,
|
|
1367
|
+
originalHash,
|
|
1053
1368
|
legacySkillName,
|
|
1054
1369
|
}),
|
|
1055
1370
|
),
|
|
@@ -1057,15 +1372,19 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1057
1372
|
|
|
1058
1373
|
for (const entry of prepared) {
|
|
1059
1374
|
if (entry.hadTarget) {
|
|
1375
|
+
if (hashSnapshot(entry.targetPath, entry.kind) !== entry.originalHash) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
`Managed path changed during transaction: ${entry.targetPath}`,
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1060
1380
|
fs.renameSync(entry.targetPath, entry.backupPath);
|
|
1061
1381
|
}
|
|
1062
|
-
committed.push(entry);
|
|
1063
1382
|
if (entry.sourcePath || entry.content !== undefined) {
|
|
1064
1383
|
fs.renameSync(entry.stagingPath, entry.targetPath);
|
|
1065
1384
|
}
|
|
1066
1385
|
}
|
|
1067
1386
|
writeJsonAtomic(journalPath, {
|
|
1068
|
-
version:
|
|
1387
|
+
version: 2,
|
|
1069
1388
|
phase: "committed",
|
|
1070
1389
|
transactionId: suffix,
|
|
1071
1390
|
entries: prepared.map(
|
|
@@ -1076,6 +1395,8 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1076
1395
|
kind,
|
|
1077
1396
|
hadTarget,
|
|
1078
1397
|
stagedHash,
|
|
1398
|
+
stagedSnapshotHash,
|
|
1399
|
+
originalHash,
|
|
1079
1400
|
legacySkillName,
|
|
1080
1401
|
}) => ({
|
|
1081
1402
|
targetPath,
|
|
@@ -1084,40 +1405,44 @@ function applySkillTransaction(entries, targetDir) {
|
|
|
1084
1405
|
kind,
|
|
1085
1406
|
hadTarget,
|
|
1086
1407
|
stagedHash: stagingPath ? stagedHash : null,
|
|
1408
|
+
stagedSnapshotHash: stagingPath ? stagedSnapshotHash : null,
|
|
1409
|
+
originalHash,
|
|
1087
1410
|
legacySkillName,
|
|
1088
1411
|
}),
|
|
1089
1412
|
),
|
|
1090
1413
|
});
|
|
1091
1414
|
} catch (error) {
|
|
1092
|
-
|
|
1093
|
-
fs.
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1415
|
+
try {
|
|
1416
|
+
if (fs.existsSync(journalPath)) {
|
|
1417
|
+
// Live failures and crash recovery must use the same preflight checks.
|
|
1418
|
+
// A conflict leaves the journal and every remaining path for recovery.
|
|
1419
|
+
recoverInterruptedTransaction(targetDir);
|
|
1420
|
+
} else {
|
|
1421
|
+
const staged = prepared.filter(
|
|
1422
|
+
(entry) => entry.stagingPath && fs.existsSync(entry.stagingPath),
|
|
1423
|
+
);
|
|
1424
|
+
// A partially written stage has no trustworthy snapshot. Validate all
|
|
1425
|
+
// completed stages before deleting any; retain partial/conflicting work.
|
|
1426
|
+
for (const entry of staged) {
|
|
1427
|
+
if (!entry.stagedSnapshotHash) {
|
|
1428
|
+
throw new Error(
|
|
1429
|
+
`Preserving incomplete transaction staging: ${entry.stagingPath}`,
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
assertTransactionPayload(entry, suffix, entry.stagingPath);
|
|
1433
|
+
}
|
|
1434
|
+
for (const entry of staged) {
|
|
1435
|
+
removeManagedPath(entry.stagingPath, entry.kind);
|
|
1436
|
+
}
|
|
1099
1437
|
}
|
|
1438
|
+
} catch (recoveryError) {
|
|
1439
|
+
error.message = `${error.message}; ${recoveryError.message}`;
|
|
1440
|
+
throw error;
|
|
1100
1441
|
}
|
|
1101
|
-
fs.rmSync(journalPath, { force: true });
|
|
1102
1442
|
throw error;
|
|
1103
|
-
} finally {
|
|
1104
|
-
for (const entry of prepared) {
|
|
1105
|
-
if (entry.stagingPath) {
|
|
1106
|
-
fs.rmSync(entry.stagingPath, {
|
|
1107
|
-
recursive: entry.kind === "directory",
|
|
1108
|
-
force: true,
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
1443
|
}
|
|
1113
1444
|
|
|
1114
|
-
|
|
1115
|
-
fs.rmSync(entry.backupPath, {
|
|
1116
|
-
recursive: entry.kind === "directory",
|
|
1117
|
-
force: true,
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
fs.rmSync(journalPath, { force: true });
|
|
1445
|
+
recoverInterruptedTransaction(targetDir);
|
|
1121
1446
|
}
|
|
1122
1447
|
|
|
1123
1448
|
function normalizeToolList(values) {
|
|
@@ -1176,11 +1501,12 @@ function mergeObjects(existing, update) {
|
|
|
1176
1501
|
|
|
1177
1502
|
const merged = { ...existing };
|
|
1178
1503
|
for (const [key, value] of Object.entries(update)) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1504
|
+
Object.defineProperty(merged, key, {
|
|
1505
|
+
value: value && typeof value === "object" && !Array.isArray(value)
|
|
1506
|
+
? mergeObjects(Object.hasOwn(existing, key) ? existing[key] : undefined, value)
|
|
1507
|
+
: value,
|
|
1508
|
+
enumerable: true, writable: true, configurable: true,
|
|
1509
|
+
});
|
|
1184
1510
|
}
|
|
1185
1511
|
return merged;
|
|
1186
1512
|
}
|
|
@@ -1192,15 +1518,61 @@ function readDeveloperConfig(filePath) {
|
|
|
1192
1518
|
|
|
1193
1519
|
try {
|
|
1194
1520
|
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1195
|
-
|
|
1196
|
-
throw new Error("root value must be an object");
|
|
1197
|
-
}
|
|
1521
|
+
assertValidConfig(value, filePath);
|
|
1198
1522
|
return value;
|
|
1199
1523
|
} catch (error) {
|
|
1200
|
-
|
|
1524
|
+
if (error.code) {
|
|
1525
|
+
error.path = filePath;
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1528
|
+
throw Object.assign(new Error(`Cannot preserve malformed ${filePath}: $ must contain valid JSON.`), {
|
|
1529
|
+
code: "MACCA_CONFIG_INVALID", field: "$", fields: ["$"], path: filePath,
|
|
1530
|
+
});
|
|
1201
1531
|
}
|
|
1202
1532
|
}
|
|
1203
1533
|
|
|
1534
|
+
const promptStates = new WeakMap();
|
|
1535
|
+
|
|
1536
|
+
function cancellation() {
|
|
1537
|
+
return Object.assign(new Error("Setup cancelled."), { code: "MACCA_CANCELLED" });
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function preparePrompt(prompt) {
|
|
1541
|
+
if (promptStates.has(prompt)) return promptStates.get(prompt);
|
|
1542
|
+
const state = { lines: [], pending: null, error: null };
|
|
1543
|
+
const fail = (error) => {
|
|
1544
|
+
state.error = error;
|
|
1545
|
+
if (state.pending) {
|
|
1546
|
+
state.pending.reject(error);
|
|
1547
|
+
state.pending = null;
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
const onLine = (line) => {
|
|
1551
|
+
if (state.pending) {
|
|
1552
|
+
state.pending.resolve(line);
|
|
1553
|
+
state.pending = null;
|
|
1554
|
+
} else state.lines.push(line);
|
|
1555
|
+
};
|
|
1556
|
+
const onCancel = () => fail(cancellation());
|
|
1557
|
+
const onError = (error) => fail(Object.assign(new Error("Interactive input failed. Check your terminal and retry."), { code: error.code || "MACCA_INPUT_ERROR" }));
|
|
1558
|
+
prompt.on("line", onLine);
|
|
1559
|
+
prompt.on("SIGINT", onCancel);
|
|
1560
|
+
prompt.on("close", onCancel);
|
|
1561
|
+
prompt.on("error", onError);
|
|
1562
|
+
prompt.input?.on("error", onError);
|
|
1563
|
+
state.dispose = () => {
|
|
1564
|
+
prompt.close();
|
|
1565
|
+
prompt.removeListener("line", onLine);
|
|
1566
|
+
prompt.removeListener("SIGINT", onCancel);
|
|
1567
|
+
prompt.removeListener("close", onCancel);
|
|
1568
|
+
prompt.removeListener("error", onError);
|
|
1569
|
+
prompt.input?.removeListener("error", onError);
|
|
1570
|
+
promptStates.delete(prompt);
|
|
1571
|
+
};
|
|
1572
|
+
promptStates.set(prompt, state);
|
|
1573
|
+
return state;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1204
1576
|
function createPrompt() {
|
|
1205
1577
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1206
1578
|
throw new Error(
|
|
@@ -1208,45 +1580,54 @@ function createPrompt() {
|
|
|
1208
1580
|
);
|
|
1209
1581
|
}
|
|
1210
1582
|
|
|
1211
|
-
|
|
1583
|
+
const prompt = readline.createInterface({
|
|
1212
1584
|
input: process.stdin,
|
|
1213
1585
|
output: process.stdout,
|
|
1214
1586
|
});
|
|
1587
|
+
preparePrompt(prompt);
|
|
1588
|
+
return prompt;
|
|
1215
1589
|
}
|
|
1216
1590
|
|
|
1217
1591
|
function askQuestion(prompt, question) {
|
|
1218
|
-
|
|
1219
|
-
|
|
1592
|
+
const state = preparePrompt(prompt);
|
|
1593
|
+
return new Promise((resolve, reject) => {
|
|
1594
|
+
if (state.error) return reject(state.error);
|
|
1595
|
+
state.pending = { resolve, reject };
|
|
1596
|
+
try {
|
|
1597
|
+
prompt.setPrompt(question);
|
|
1598
|
+
prompt.prompt();
|
|
1599
|
+
if (state.lines.length && state.pending) {
|
|
1600
|
+
state.pending = null;
|
|
1601
|
+
resolve(state.lines.shift());
|
|
1602
|
+
}
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
state.pending = null;
|
|
1605
|
+
reject(error);
|
|
1606
|
+
}
|
|
1220
1607
|
});
|
|
1221
1608
|
}
|
|
1222
1609
|
|
|
1223
|
-
async function promptForTools() {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
);
|
|
1232
|
-
});
|
|
1233
|
-
process.stdout.write("\n");
|
|
1234
|
-
|
|
1235
|
-
while (true) {
|
|
1236
|
-
const answer = await askQuestion(
|
|
1237
|
-
prompt,
|
|
1238
|
-
"Masukkan nomor/nama tool (pisahkan dengan koma, 'all', kosong = codex): ",
|
|
1239
|
-
);
|
|
1240
|
-
const parsed = parseInteractiveToolSelection(answer);
|
|
1241
|
-
if (parsed.error) {
|
|
1242
|
-
process.stdout.write(` ${parsed.error}\n`);
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1610
|
+
async function promptForTools(prompt) {
|
|
1611
|
+
process.stdout.write("\nPilih AI tool yang mau dipasang:\n");
|
|
1612
|
+
TOOL_DEFINITIONS.forEach((tool, index) => {
|
|
1613
|
+
process.stdout.write(
|
|
1614
|
+
` ${index + 1}. ${tool.label} -> ${tool.displayDestination}\n`,
|
|
1615
|
+
);
|
|
1616
|
+
});
|
|
1617
|
+
process.stdout.write("\n");
|
|
1245
1618
|
|
|
1246
|
-
|
|
1619
|
+
while (true) {
|
|
1620
|
+
const answer = await askQuestion(
|
|
1621
|
+
prompt,
|
|
1622
|
+
"Masukkan nomor/nama tool (pisahkan dengan koma, 'all', kosong = codex): ",
|
|
1623
|
+
);
|
|
1624
|
+
const parsed = parseInteractiveToolSelection(answer);
|
|
1625
|
+
if (parsed.error) {
|
|
1626
|
+
process.stdout.write(` ${parsed.error}\n`);
|
|
1627
|
+
continue;
|
|
1247
1628
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1629
|
+
|
|
1630
|
+
return parsed.tools;
|
|
1250
1631
|
}
|
|
1251
1632
|
}
|
|
1252
1633
|
|
|
@@ -1288,51 +1669,65 @@ function parseInteractiveToolSelection(answer) {
|
|
|
1288
1669
|
return { tools: unique(tools) };
|
|
1289
1670
|
}
|
|
1290
1671
|
|
|
1291
|
-
async function promptForMetadata(seed) {
|
|
1292
|
-
const
|
|
1672
|
+
async function promptForMetadata(seed, prompt) {
|
|
1673
|
+
const communication =
|
|
1674
|
+
seed.communicationLanguage !== undefined
|
|
1675
|
+
? seed.communicationLanguage
|
|
1676
|
+
: await askQuestion(
|
|
1677
|
+
prompt,
|
|
1678
|
+
"Bahasa komunikasi yang anda inginkan? (Kosong = Bahasa Indonesia): ",
|
|
1679
|
+
);
|
|
1680
|
+
const documents =
|
|
1681
|
+
seed.documentLanguage !== undefined
|
|
1682
|
+
? seed.documentLanguage
|
|
1683
|
+
: await askQuestion(
|
|
1684
|
+
prompt,
|
|
1685
|
+
"Bahasa dokumen yang dihasilkan? (Kosong = sama dengan komunikasi): ",
|
|
1686
|
+
);
|
|
1293
1687
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
);
|
|
1302
|
-
const project =
|
|
1303
|
-
seed.project !== undefined
|
|
1304
|
-
? seed.project
|
|
1305
|
-
: await askQuestion(prompt, "Nama project ini apa? (Kosong = Skip): ");
|
|
1306
|
-
const communication =
|
|
1307
|
-
seed.communicationLanguage !== undefined
|
|
1308
|
-
? seed.communicationLanguage
|
|
1309
|
-
: await askQuestion(
|
|
1310
|
-
prompt,
|
|
1311
|
-
"Bahasa komunikasi yang anda inginkan? (Kosong = Bahasa Indonesia): ",
|
|
1312
|
-
);
|
|
1313
|
-
const documents =
|
|
1314
|
-
seed.documentLanguage !== undefined
|
|
1315
|
-
? seed.documentLanguage
|
|
1316
|
-
: await askQuestion(
|
|
1317
|
-
prompt,
|
|
1318
|
-
"Bahasa dokumen yang dihasilkan? (Kosong = Bahasa Indonesia): ",
|
|
1319
|
-
);
|
|
1688
|
+
return {
|
|
1689
|
+
name: seed.name,
|
|
1690
|
+
project: seed.project,
|
|
1691
|
+
communicationLanguage: communication || "Bahasa Indonesia",
|
|
1692
|
+
documentLanguage: documents || communication || "Bahasa Indonesia",
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1320
1695
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1696
|
+
function addRetiredSkills(entries, targetDir, destination, skillNames) {
|
|
1697
|
+
assertSafeProjectPath(targetDir, destination);
|
|
1698
|
+
for (const skillName of skillNames) {
|
|
1699
|
+
const targetPath = resolveOwnedSkillPath(destination, skillName);
|
|
1700
|
+
if (!fs.existsSync(targetPath)) continue;
|
|
1701
|
+
try {
|
|
1702
|
+
assertSafeProjectPath(targetDir, targetPath);
|
|
1703
|
+
hashSnapshot(targetPath, "directory");
|
|
1704
|
+
if (isMaccaOwned(targetPath)) {
|
|
1705
|
+
// Retirement never needs to discard local edits, even with --force.
|
|
1706
|
+
assertManagedDirectoryUnchanged(targetPath);
|
|
1707
|
+
} else if (!isLegacyDirectoryUnchanged(targetPath, skillName)) {
|
|
1708
|
+
throw new Error("ownership could not be verified");
|
|
1709
|
+
}
|
|
1710
|
+
if (getUnhashedEntries(targetPath).length) {
|
|
1711
|
+
throw new Error("directory contains unhashed content");
|
|
1712
|
+
}
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
process.stderr.write(
|
|
1715
|
+
`Preserving obsolete skill directory: ${targetPath} (${error.message})\n`,
|
|
1716
|
+
);
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
entries.push({ sourcePath: null, targetPath, legacySkillName: skillName });
|
|
1329
1720
|
}
|
|
1330
1721
|
}
|
|
1331
1722
|
|
|
1332
|
-
function applyInstall(targetDir, tools, metadata) {
|
|
1723
|
+
function applyInstall(targetDir, tools, metadata, options = {}) {
|
|
1724
|
+
const force = Boolean(options.force);
|
|
1333
1725
|
const managedSkills = getSourceManagedSkills();
|
|
1334
1726
|
const agentsDirectory = path.join(targetDir, ".agents");
|
|
1335
|
-
|
|
1727
|
+
const configPath = path.join(agentsDirectory, "developer-config.json");
|
|
1728
|
+
assertSafeProjectPath(targetDir, configPath);
|
|
1729
|
+
const config = mergeObjects(readDeveloperConfig(configPath), buildDeveloperConfig(metadata));
|
|
1730
|
+
assertValidConfig(config, configPath);
|
|
1336
1731
|
assertSafeProjectPath(targetDir, agentsDirectory);
|
|
1337
1732
|
assertPackageNotOlderThanInstalled(agentsDirectory);
|
|
1338
1733
|
for (const fileName of [
|
|
@@ -1344,11 +1739,15 @@ function applyInstall(targetDir, tools, metadata) {
|
|
|
1344
1739
|
]) {
|
|
1345
1740
|
assertSafeProjectPath(targetDir, path.join(agentsDirectory, fileName));
|
|
1346
1741
|
}
|
|
1347
|
-
assertManagedFilesUnchanged(agentsDirectory);
|
|
1742
|
+
assertManagedFilesUnchanged(agentsDirectory, force);
|
|
1348
1743
|
const previousManagedSkills = validateManagedSkillNames(
|
|
1349
1744
|
readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
|
|
1350
1745
|
".agents/macca-managed-skills.txt",
|
|
1351
1746
|
);
|
|
1747
|
+
tools = unique([
|
|
1748
|
+
...readNonEmptyLines(path.join(agentsDirectory, "macca-tools.txt")),
|
|
1749
|
+
...tools,
|
|
1750
|
+
]);
|
|
1352
1751
|
const entries = [];
|
|
1353
1752
|
for (const destination of getUniqueDestinations(targetDir, tools)) {
|
|
1354
1753
|
for (const skillName of managedSkills) {
|
|
@@ -1358,12 +1757,21 @@ function applyInstall(targetDir, tools, metadata) {
|
|
|
1358
1757
|
legacySkillName: skillName,
|
|
1359
1758
|
});
|
|
1360
1759
|
}
|
|
1760
|
+
addRetiredSkills(
|
|
1761
|
+
entries,
|
|
1762
|
+
targetDir,
|
|
1763
|
+
destination,
|
|
1764
|
+
previousManagedSkills.filter((name) => !managedSkills.includes(name)),
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
if (tools.includes("opencode")) {
|
|
1768
|
+
addRetiredSkills(
|
|
1769
|
+
entries,
|
|
1770
|
+
targetDir,
|
|
1771
|
+
path.join(targetDir, ".opencode", "skill"),
|
|
1772
|
+
previousManagedSkills,
|
|
1773
|
+
);
|
|
1361
1774
|
}
|
|
1362
|
-
const configPath = path.join(agentsDirectory, "developer-config.json");
|
|
1363
|
-
const config = mergeObjects(
|
|
1364
|
-
readDeveloperConfig(configPath),
|
|
1365
|
-
buildDeveloperConfig(metadata),
|
|
1366
|
-
);
|
|
1367
1775
|
const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
|
|
1368
1776
|
? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
|
|
1369
1777
|
: `${managedSkills.join("\n")}\n`;
|
|
@@ -1401,12 +1809,16 @@ function applyInstall(targetDir, tools, metadata) {
|
|
|
1401
1809
|
targetPath: path.join(agentsDirectory, STATE_FILE),
|
|
1402
1810
|
},
|
|
1403
1811
|
);
|
|
1404
|
-
applySkillTransaction(entries, targetDir);
|
|
1812
|
+
applySkillTransaction(entries, targetDir, force);
|
|
1405
1813
|
}
|
|
1406
1814
|
|
|
1407
|
-
function applyUpgrade(targetDir) {
|
|
1815
|
+
function applyUpgrade(targetDir, options = {}) {
|
|
1816
|
+
const force = Boolean(options.force);
|
|
1408
1817
|
const agentsDirectory = path.join(targetDir, ".agents");
|
|
1409
1818
|
assertSafeProjectPath(targetDir, agentsDirectory);
|
|
1819
|
+
const configPath = path.join(agentsDirectory, "developer-config.json");
|
|
1820
|
+
assertSafeProjectPath(targetDir, configPath);
|
|
1821
|
+
readDeveloperConfig(configPath);
|
|
1410
1822
|
assertPackageNotOlderThanInstalled(agentsDirectory);
|
|
1411
1823
|
for (const fileName of [
|
|
1412
1824
|
"macca-managed-skills.txt",
|
|
@@ -1417,7 +1829,7 @@ function applyUpgrade(targetDir) {
|
|
|
1417
1829
|
]) {
|
|
1418
1830
|
assertSafeProjectPath(targetDir, path.join(agentsDirectory, fileName));
|
|
1419
1831
|
}
|
|
1420
|
-
assertManagedFilesUnchanged(agentsDirectory);
|
|
1832
|
+
assertManagedFilesUnchanged(agentsDirectory, force);
|
|
1421
1833
|
const tools = readNonEmptyLines(
|
|
1422
1834
|
path.join(agentsDirectory, "macca-tools.txt"),
|
|
1423
1835
|
);
|
|
@@ -1442,47 +1854,21 @@ function applyUpgrade(targetDir) {
|
|
|
1442
1854
|
});
|
|
1443
1855
|
}
|
|
1444
1856
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
entries.push({ sourcePath: null, targetPath: oldPath });
|
|
1452
|
-
} else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
|
|
1453
|
-
entries.push({
|
|
1454
|
-
sourcePath: null,
|
|
1455
|
-
targetPath: oldPath,
|
|
1456
|
-
legacySkillName: skillName,
|
|
1457
|
-
});
|
|
1458
|
-
} else if (fs.existsSync(oldPath)) {
|
|
1459
|
-
process.stderr.write(
|
|
1460
|
-
`Preserving unmarked legacy skill directory: ${oldPath}\n`,
|
|
1461
|
-
);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1857
|
+
addRetiredSkills(
|
|
1858
|
+
entries,
|
|
1859
|
+
targetDir,
|
|
1860
|
+
destination,
|
|
1861
|
+
previousManagedSkills.filter((name) => !nextManagedSkills.includes(name)),
|
|
1862
|
+
);
|
|
1464
1863
|
}
|
|
1465
1864
|
|
|
1466
1865
|
if (tools.includes("opencode")) {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
entries.push({ sourcePath: null, targetPath: oldPath });
|
|
1474
|
-
} else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
|
|
1475
|
-
entries.push({
|
|
1476
|
-
sourcePath: null,
|
|
1477
|
-
targetPath: oldPath,
|
|
1478
|
-
legacySkillName: skillName,
|
|
1479
|
-
});
|
|
1480
|
-
} else if (fs.existsSync(oldPath)) {
|
|
1481
|
-
throw new Error(
|
|
1482
|
-
`Legacy OpenCode skill was modified; move or back it up before upgrade: ${oldPath}`,
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1866
|
+
addRetiredSkills(
|
|
1867
|
+
entries,
|
|
1868
|
+
targetDir,
|
|
1869
|
+
path.join(targetDir, ".opencode", "skill"),
|
|
1870
|
+
previousManagedSkills,
|
|
1871
|
+
);
|
|
1486
1872
|
}
|
|
1487
1873
|
const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
|
|
1488
1874
|
? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
|
|
@@ -1511,7 +1897,7 @@ function applyUpgrade(targetDir) {
|
|
|
1511
1897
|
targetPath: path.join(agentsDirectory, STATE_FILE),
|
|
1512
1898
|
},
|
|
1513
1899
|
);
|
|
1514
|
-
applySkillTransaction(entries, targetDir);
|
|
1900
|
+
applySkillTransaction(entries, targetDir, force);
|
|
1515
1901
|
|
|
1516
1902
|
if (tools.includes("kimi")) reportLegacyKimiInstall();
|
|
1517
1903
|
}
|
|
@@ -1562,85 +1948,246 @@ function printInstallSummary(action, tools) {
|
|
|
1562
1948
|
}
|
|
1563
1949
|
|
|
1564
1950
|
async function runInstall(args) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1951
|
+
assertSupportedRuntime();
|
|
1952
|
+
operation.stage = "not started";
|
|
1953
|
+
operation.recoveryChanged = false;
|
|
1954
|
+
operation.targetDir = resolveTargetDirectory(args.directory);
|
|
1570
1955
|
const targetDir = assertSafeTargetDirectory(args.directory);
|
|
1571
|
-
|
|
1572
|
-
assertSafeProjectPath(targetDir,
|
|
1573
|
-
|
|
1956
|
+
const configPath = path.join(targetDir, ".agents", "developer-config.json");
|
|
1957
|
+
assertSafeProjectPath(targetDir, configPath);
|
|
1958
|
+
assertSafeProjectPath(targetDir, path.join(targetDir, ".agents", "macca-tools.txt"));
|
|
1959
|
+
const existingConfig = readDeveloperConfig(configPath);
|
|
1574
1960
|
const existingTools = readNonEmptyLines(
|
|
1575
1961
|
path.join(targetDir, ".agents", "macca-tools.txt"),
|
|
1576
1962
|
);
|
|
1577
|
-
tools = unique([...existingTools, ...tools]);
|
|
1578
|
-
const configPath = path.join(targetDir, ".agents", "developer-config.json");
|
|
1579
|
-
const existingConfig = readDeveloperConfig(configPath);
|
|
1580
1963
|
const hasExistingConfig = fs.existsSync(configPath);
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
documentLanguage:
|
|
1602
|
-
args.documentLanguage !== undefined
|
|
1603
|
-
? args.documentLanguage
|
|
1604
|
-
: hasExistingConfig
|
|
1605
|
-
? undefined
|
|
1606
|
-
: "Bahasa Indonesia",
|
|
1964
|
+
let tools = normalizeToolList(args.tools);
|
|
1965
|
+
let metadata;
|
|
1966
|
+
let prompt;
|
|
1967
|
+
try {
|
|
1968
|
+
const getPrompt = () => (prompt ||= createPrompt());
|
|
1969
|
+
if (!tools.length) {
|
|
1970
|
+
tools = args.yes ? (existingTools.length ? existingTools : ["codex"]) : await promptForTools(getPrompt());
|
|
1971
|
+
}
|
|
1972
|
+
tools = unique([...existingTools, ...tools]);
|
|
1973
|
+
const seed = {
|
|
1974
|
+
name: args.name,
|
|
1975
|
+
project: args.project,
|
|
1976
|
+
communicationLanguage: args.communicationLanguage,
|
|
1977
|
+
documentLanguage: args.documentLanguage,
|
|
1978
|
+
};
|
|
1979
|
+
if (args.yes) {
|
|
1980
|
+
metadata = { ...seed };
|
|
1981
|
+
if (!hasExistingConfig) {
|
|
1982
|
+
metadata.communicationLanguage ??= "Bahasa Indonesia";
|
|
1983
|
+
metadata.documentLanguage ??= metadata.communicationLanguage;
|
|
1607
1984
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1985
|
+
} else {
|
|
1986
|
+
// Reusing a saved section must not rewrite its raw/normalized pair.
|
|
1987
|
+
const saved = existingConfig.languagePreferences || {};
|
|
1988
|
+
const communication = seed.communicationLanguage ?? saved.communication?.raw ?? saved.communication?.normalized;
|
|
1989
|
+
const documents = seed.documentLanguage ?? saved.documents?.raw ?? saved.documents?.normalized;
|
|
1990
|
+
metadata = await promptForMetadata({ ...seed, communicationLanguage: communication, documentLanguage: documents },
|
|
1991
|
+
communication === undefined || documents === undefined ? getPrompt() : prompt);
|
|
1992
|
+
if (seed.communicationLanguage === undefined && saved.communication) delete metadata.communicationLanguage;
|
|
1993
|
+
if (seed.documentLanguage === undefined && saved.documents) delete metadata.documentLanguage;
|
|
1994
|
+
}
|
|
1995
|
+
assertValidConfig(mergeObjects(existingConfig, buildDeveloperConfig(metadata)), configPath);
|
|
1996
|
+
getUniqueDestinations(targetDir, tools);
|
|
1997
|
+
if (prompt && promptStates.get(prompt).error) throw promptStates.get(prompt).error;
|
|
1998
|
+
} finally {
|
|
1999
|
+
if (prompt) promptStates.get(prompt).dispose();
|
|
2000
|
+
}
|
|
1622
2001
|
|
|
1623
|
-
|
|
2002
|
+
assertPackageNotOlderThanInstalled(path.join(targetDir, ".agents"), true);
|
|
2003
|
+
recoverInterruptedTransaction(targetDir);
|
|
2004
|
+
// A crash can leave the config only in its backup. First-install defaults
|
|
2005
|
+
// must not overwrite settings restored by recovery on an unattended retry.
|
|
2006
|
+
if (args.yes && !hasExistingConfig && fs.existsSync(configPath)) {
|
|
2007
|
+
if (args.communicationLanguage === undefined) delete metadata.communicationLanguage;
|
|
2008
|
+
if (args.documentLanguage === undefined) delete metadata.documentLanguage;
|
|
2009
|
+
}
|
|
2010
|
+
applyInstall(targetDir, tools, metadata, { force: Boolean(args.force) });
|
|
1624
2011
|
|
|
2012
|
+
operation.stage = "complete";
|
|
1625
2013
|
printInstallSummary("installed", tools);
|
|
1626
|
-
|
|
2014
|
+
printSetupHint(targetDir);
|
|
1627
2015
|
}
|
|
1628
2016
|
|
|
1629
2017
|
function runUpgrade(args) {
|
|
2018
|
+
assertSupportedRuntime();
|
|
2019
|
+
operation.stage = "not started";
|
|
2020
|
+
operation.recoveryChanged = false;
|
|
2021
|
+
operation.targetDir = resolveTargetDirectory(args.directory);
|
|
1630
2022
|
const targetDir = assertSafeTargetDirectory(args.directory);
|
|
1631
|
-
|
|
2023
|
+
const configPath = path.join(targetDir, ".agents", "developer-config.json");
|
|
2024
|
+
assertSafeProjectPath(targetDir, configPath);
|
|
2025
|
+
readDeveloperConfig(configPath);
|
|
2026
|
+
assertPackageNotOlderThanInstalled(path.join(targetDir, ".agents"), true);
|
|
1632
2027
|
recoverInterruptedTransaction(targetDir);
|
|
2028
|
+
assertSafeProjectPath(targetDir, path.join(targetDir, ".agents", "macca-tools.txt"));
|
|
1633
2029
|
const tools = readNonEmptyLines(
|
|
1634
2030
|
path.join(targetDir, ".agents", "macca-tools.txt"),
|
|
1635
2031
|
);
|
|
1636
|
-
applyUpgrade(targetDir);
|
|
2032
|
+
applyUpgrade(targetDir, { force: Boolean(args.force) });
|
|
2033
|
+
operation.stage = "complete";
|
|
1637
2034
|
printInstallSummary("updated", tools);
|
|
1638
|
-
|
|
2035
|
+
printSetupHint(targetDir);
|
|
1639
2036
|
}
|
|
1640
2037
|
|
|
1641
|
-
function
|
|
1642
|
-
process.
|
|
1643
|
-
|
|
2038
|
+
function printSetupHint(targetDir) {
|
|
2039
|
+
process.stdout.write(` Target project: ${targetDir}\n Restart your AI host to load the skills. Run the setup-macca-method skill for setup; name/project are optional.\n\n`);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function assertSupportedRuntime() {
|
|
2043
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
2044
|
+
if (major < 22) {
|
|
2045
|
+
throw Object.assign(new Error("Unsupported Node.js runtime. Install Node.js 22 or 24, then retry."), { code: "MACCA_NODE_UNSUPPORTED" });
|
|
2046
|
+
}
|
|
2047
|
+
if (![22, 24].includes(major)) process.stderr.write("Notice: this Node.js runtime is unverified; CI verifies Node.js 22 and 24.\n");
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function runDoctor(args) {
|
|
2051
|
+
operation.stage = "read-only diagnosis";
|
|
2052
|
+
operation.targetDir = resolveTargetDirectory(args.directory);
|
|
2053
|
+
let failures = 0;
|
|
2054
|
+
let warnings = 0;
|
|
2055
|
+
const report = (level, message, filePath) => {
|
|
2056
|
+
if (level === "FAIL") failures += 1;
|
|
2057
|
+
if (level === "WARN") warnings += 1;
|
|
2058
|
+
process.stdout.write(`${level}: ${message}${filePath ? ` Path: ${JSON.stringify(filePath)}` : ""}\n`);
|
|
2059
|
+
};
|
|
2060
|
+
const inspect = (filePath, check) => {
|
|
2061
|
+
try { return check(); } catch (error) {
|
|
2062
|
+
report("FAIL", error.message, filePath);
|
|
2063
|
+
return undefined;
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
2067
|
+
report(major < 22 ? "FAIL" : [22, 24].includes(major) ? "OK" : "WARN",
|
|
2068
|
+
major < 22 ? "Unsupported Node.js runtime; use Node.js 22 or 24." :
|
|
2069
|
+
[22, 24].includes(major) ? "Node.js runtime is supported." : "Node.js runtime is unverified; CI verifies 22 and 24.");
|
|
2070
|
+
const targetDir = inspect(operation.targetDir, () => assertSafeTargetDirectory(args.directory));
|
|
2071
|
+
if (targetDir) {
|
|
2072
|
+
const metadata = (name) => path.join(targetDir, ".agents", name);
|
|
2073
|
+
const required = (filePath) => {
|
|
2074
|
+
assertSafeProjectPath(targetDir, filePath);
|
|
2075
|
+
const stat = fs.lstatSync(filePath);
|
|
2076
|
+
if (!stat.isFile()) throw new Error("Required installed file is not a regular file.");
|
|
2077
|
+
};
|
|
2078
|
+
inspect(targetDir, () => {
|
|
2079
|
+
if (!fs.statSync(targetDir).isDirectory()) throw new Error("Target project is not a directory.");
|
|
2080
|
+
});
|
|
2081
|
+
const journalPath = metadata(TRANSACTION_FILE);
|
|
2082
|
+
inspect(journalPath, () => {
|
|
2083
|
+
assertSafeProjectPath(targetDir, journalPath);
|
|
2084
|
+
if (fs.existsSync(journalPath)) report("FAIL", "Interrupted transaction retained. Back up the project and journal, then rerun install/upgrade for validated recovery; do not delete the journal.", journalPath);
|
|
2085
|
+
});
|
|
2086
|
+
inspect(metadata("developer-config.json"), () => {
|
|
2087
|
+
required(metadata("developer-config.json"));
|
|
2088
|
+
readDeveloperConfig(metadata("developer-config.json"));
|
|
2089
|
+
report("OK", "Developer config is valid (values hidden).");
|
|
2090
|
+
});
|
|
2091
|
+
const tools = inspect(metadata("macca-tools.txt"), () => {
|
|
2092
|
+
required(metadata("macca-tools.txt"));
|
|
2093
|
+
const selected = readNonEmptyLines(metadata("macca-tools.txt"));
|
|
2094
|
+
if (!selected.length || selected.some((key) => !TOOL_BY_KEY.has(key))) throw new Error("Selected tools are missing or unsupported; review macca-tools.txt.");
|
|
2095
|
+
report("OK", `Selected tools: ${unique(selected).join(", ")}.`);
|
|
2096
|
+
return unique(selected);
|
|
2097
|
+
});
|
|
2098
|
+
const skills = inspect(metadata("macca-managed-skills.txt"), () => {
|
|
2099
|
+
required(metadata("macca-managed-skills.txt"));
|
|
2100
|
+
const names = validateManagedSkillNames(readNonEmptyLines(metadata("macca-managed-skills.txt")), "installed manifest");
|
|
2101
|
+
if (!names.length || !names.includes("_shared")) throw new Error("Installed managed-skill manifest is empty or missing _shared.");
|
|
2102
|
+
return names;
|
|
2103
|
+
});
|
|
2104
|
+
const lock = inspect(metadata("macca-lock.json"), () => {
|
|
2105
|
+
required(metadata("macca-lock.json"));
|
|
2106
|
+
const value = readJsonObject(metadata("macca-lock.json"), "installed lock");
|
|
2107
|
+
if (typeof value.version !== "string" || !value.version) throw new Error("Installed lock version is missing or invalid.");
|
|
2108
|
+
if (!Array.isArray(value.skills) || !value.skills.length || value.skills.some((name) => typeof name !== "string")) throw new Error("Installed lock skill list is missing or invalid.");
|
|
2109
|
+
validateManagedSkillNames(value.skills, "installed lock");
|
|
2110
|
+
if (skills && (value.skills.some((name) => !skills.includes(name)) || skills.some((name) => !value.skills.includes(name)))) throw new Error("Installed lock and managed-skill manifest disagree.");
|
|
2111
|
+
if (value.payloadFiles !== undefined && (!value.payloadFiles || typeof value.payloadFiles !== "object" || Array.isArray(value.payloadFiles))) throw new Error("Installed lock payloadFiles must be an object.");
|
|
2112
|
+
return value;
|
|
2113
|
+
});
|
|
2114
|
+
inspect(metadata(STATE_FILE), () => {
|
|
2115
|
+
assertSafeProjectPath(targetDir, metadata(STATE_FILE));
|
|
2116
|
+
if (!fs.existsSync(metadata(STATE_FILE))) {
|
|
2117
|
+
report("WARN", "Installation state is absent; metadata integrity cannot be verified.", metadata(STATE_FILE));
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
required(metadata(STATE_FILE));
|
|
2121
|
+
const state = readJsonObject(metadata(STATE_FILE), "MACCA state");
|
|
2122
|
+
for (const name of ["macca-tools.txt", "macca-managed-skills.txt", "macca-lock.json"]) {
|
|
2123
|
+
required(metadata(name));
|
|
2124
|
+
if (!/^[a-f0-9]{64}$/.test(state.files?.[name] || "")) throw new Error("Installation state is missing a valid metadata fingerprint.");
|
|
2125
|
+
if (hashFile(metadata(name)) !== state.files[name]) report("WARN", "Local metadata drift; back up and review before upgrade.", metadata(name));
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
if (tools && skills && lock) {
|
|
2129
|
+
if (!lock.payloadFiles) report("WARN", "Installed lock has no payloadFiles; checking current packaged references/assets for existence only. Upgrade after backup to establish fingerprints.", metadata("macca-lock.json"));
|
|
2130
|
+
const destinations = inspect(targetDir, () => getUniqueDestinations(targetDir, tools));
|
|
2131
|
+
for (const destination of destinations || []) {
|
|
2132
|
+
for (const skill of skills) {
|
|
2133
|
+
const skillPath = resolveOwnedSkillPath(destination, skill);
|
|
2134
|
+
inspect(skillPath, () => {
|
|
2135
|
+
assertSafeProjectPath(targetDir, skillPath);
|
|
2136
|
+
const actual = getDirectoryFileHashes(skillPath);
|
|
2137
|
+
if (skill !== "_shared") required(path.join(skillPath, "SKILL.md"));
|
|
2138
|
+
const expected = lock.payloadFiles ? lock.payloadFiles[skill] : getDirectoryFileHashes(path.join(SOURCE_SKILLS_DIR, skill));
|
|
2139
|
+
if (!expected || typeof expected !== "object" || Array.isArray(expected) || !Object.keys(expected).length) throw new Error("Installed lock is missing valid payload fingerprints for this skill.");
|
|
2140
|
+
if (skill === "_shared" && !Object.keys(expected).some((name) => name.startsWith("references/"))) throw new Error("Shared references are missing from installed lock.");
|
|
2141
|
+
for (const [relative, hash] of Object.entries(expected)) {
|
|
2142
|
+
if (!relative || relative.includes("\\") || relative.split("/").some((part) => !part || part === "." || part === "..") || path.isAbsolute(relative) || /^[A-Za-z]:/.test(relative) || typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)) throw new Error("Invalid payload path or hash in installed lock.");
|
|
2143
|
+
const filePath = path.join(skillPath, relative);
|
|
2144
|
+
if (!Object.hasOwn(actual, relative)) report("FAIL", "Required installed payload file is missing.", filePath);
|
|
2145
|
+
else if (lock.payloadFiles && actual[relative] !== hash) report("WARN", "Local skill drift; back up and review before upgrade.", filePath);
|
|
2146
|
+
}
|
|
2147
|
+
if (lock.payloadFiles && Object.keys(actual).some((relative) => !Object.hasOwn(expected, relative))) report("WARN", "Additional local skill files; back up and review before upgrade.", skillPath);
|
|
2148
|
+
const marker = readOwnershipMarker(skillPath, skill);
|
|
2149
|
+
const markerPath = path.join(skillPath, OWNERSHIP_MARKER);
|
|
2150
|
+
if (!marker) {
|
|
2151
|
+
report("WARN", "Ownership marker is absent or invalid; upgrade may require review.", markerPath);
|
|
2152
|
+
} else if (typeof marker.payloadHash !== "string" ||
|
|
2153
|
+
!/^[a-f0-9]{64}$/.test(marker.payloadHash) ||
|
|
2154
|
+
marker.payloadHash !== hashText(JSON.stringify(actual))) {
|
|
2155
|
+
report("WARN", "Ownership marker payload fingerprint is missing, malformed, or mismatched; ordinary upgrade will refuse this skill. Back up and review the skill and marker; doctor does not repair them.", markerPath);
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
process.stdout.write(`Doctor: ${failures} failure(s), ${warnings} warning(s). Read-only; no files changed.\n`);
|
|
2163
|
+
if (failures) process.stdout.write("Next: review the reported paths and back up existing files before install/upgrade.\n");
|
|
2164
|
+
process.exitCode = failures ? 1 : 0;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function exitWithError(error) {
|
|
2168
|
+
if (typeof error === "string") error = new Error(error);
|
|
2169
|
+
const nextSteps = {
|
|
2170
|
+
EACCES: "Check permissions and ownership of the reported path; choose a writable project directory.",
|
|
2171
|
+
EPERM: "Check permissions or file locks at the reported path; close programs holding it and retry.",
|
|
2172
|
+
EBUSY: "Close programs holding the reported path, then retry.",
|
|
2173
|
+
ENOSPC: "Free space on the target volume, then retry; preserve transaction files for recovery.",
|
|
2174
|
+
ENOENT: "Check the reported path and parent directory. Restore missing package files or run install for a missing installation.",
|
|
2175
|
+
MACCA_CONFIG_INVALID: "Back up developer-config.json, correct the listed fields without sharing secret values, then retry. --force cannot bypass config validation.",
|
|
2176
|
+
MACCA_CANCELLED: "Rerun install when ready, or use --yes with explicit flags.",
|
|
2177
|
+
MACCA_INPUT_ERROR: "Open a working terminal, or use --yes with explicit flags.",
|
|
2178
|
+
MACCA_NODE_UNSUPPORTED: "Use Node.js 22 or 24, then retry.",
|
|
2179
|
+
};
|
|
2180
|
+
let next = nextSteps[error.code] || "Review the reported path and back up local files before retrying. Use doctor for a read-only check.";
|
|
2181
|
+
if (/symlink/i.test(error.message)) next = "Symlinked paths are rejected conservatively. Resolve the real path (including macOS aliases such as /var or /tmp), review it, and pass that real directory with --directory.";
|
|
2182
|
+
let pending = false;
|
|
2183
|
+
if (operation.targetDir) {
|
|
2184
|
+
try { pending = fs.existsSync(path.join(operation.targetDir, ".agents", TRANSACTION_FILE)); } catch { /* Error reporting must not mask the original error. */ }
|
|
2185
|
+
}
|
|
2186
|
+
const stage = operation.stage === "not started" ? "not started; no files changed by this attempt" :
|
|
2187
|
+
operation.stage === "read-only diagnosis" ? "read-only diagnosis; no files changed" :
|
|
2188
|
+
`${operation.stage}; files may have changed${operation.recoveryChanged ? "; recovery may have changed files" : ""}`;
|
|
2189
|
+
process.stderr.write(`\nError [${error.code || "MACCA_ERROR"}]: ${error.message}\nPath: ${JSON.stringify(error.path || operation.targetDir || process.cwd())}\nChange stage: ${stage}.\n${pending ? "Pending transaction journal retained; back up and review it before retrying recovery.\n" : ""}Next: ${next}\n`);
|
|
2190
|
+
process.exitCode = error.code === "MACCA_CANCELLED" ? 130 : 1;
|
|
1644
2191
|
}
|
|
1645
2192
|
|
|
1646
2193
|
main();
|