macca-method 2.1.3 → 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 +781 -277
- 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 +62 -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
|
|
|
@@ -256,6 +265,9 @@ function parseArgs(argv) {
|
|
|
256
265
|
args._.push(token);
|
|
257
266
|
}
|
|
258
267
|
|
|
268
|
+
if (args._.length > 1) {
|
|
269
|
+
throw new Error("Unexpected positional arguments. Use --directory <path> for the target project.");
|
|
270
|
+
}
|
|
259
271
|
return args;
|
|
260
272
|
}
|
|
261
273
|
|
|
@@ -291,6 +303,7 @@ function printHelp() {
|
|
|
291
303
|
"Usage:",
|
|
292
304
|
" npx macca-method@latest install [options]",
|
|
293
305
|
" npx macca-method@latest upgrade [options]",
|
|
306
|
+
" macca-method doctor [--directory <path>] Read-only installation check (no repair).",
|
|
294
307
|
" npx macca-method@latest --list-tools",
|
|
295
308
|
"",
|
|
296
309
|
"Install options:",
|
|
@@ -301,11 +314,12 @@ function printHelp() {
|
|
|
301
314
|
" --name <value> Developer name.",
|
|
302
315
|
" --project <value> Project name.",
|
|
303
316
|
" --communication-language <value>",
|
|
304
|
-
" --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.",
|
|
305
319
|
"",
|
|
306
320
|
"Examples:",
|
|
307
321
|
" npx macca-method@latest install",
|
|
308
|
-
" 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",
|
|
309
323
|
" npx macca-method@latest upgrade",
|
|
310
324
|
"",
|
|
311
325
|
].join("\n"),
|
|
@@ -332,27 +346,29 @@ function resolveTargetDirectory(rawDirectory) {
|
|
|
332
346
|
}
|
|
333
347
|
|
|
334
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
|
+
}
|
|
335
352
|
const requested = rawDirectory
|
|
336
353
|
? path.resolve(process.cwd(), rawDirectory)
|
|
337
354
|
: process.cwd();
|
|
338
355
|
const absolute = path.isAbsolute(requested)
|
|
339
356
|
? requested
|
|
340
357
|
: path.resolve(requested);
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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;
|
|
345
364
|
|
|
346
|
-
if (
|
|
365
|
+
if (isSymlink(current)) {
|
|
347
366
|
throw new Error(`Refusing symlinked target project ancestor: ${current}`);
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
for (const segment of segments) {
|
|
351
|
-
current =
|
|
352
|
-
|
|
353
|
-
? path.join(current, segment)
|
|
354
|
-
: path.join(current, segment);
|
|
355
|
-
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
370
|
+
current = path.join(current, segment);
|
|
371
|
+
if (isSymlink(current)) {
|
|
356
372
|
throw new Error(`Refusing symlinked target project ancestor: ${current}`);
|
|
357
373
|
}
|
|
358
374
|
}
|
|
@@ -442,27 +458,37 @@ function normalizeLanguage(value) {
|
|
|
442
458
|
case "bahasa inggris":
|
|
443
459
|
return "english";
|
|
444
460
|
default:
|
|
445
|
-
return
|
|
461
|
+
return "indonesian";
|
|
446
462
|
}
|
|
447
463
|
}
|
|
448
464
|
|
|
449
465
|
function ensureDirectory(directoryPath) {
|
|
466
|
+
operation.stage = "writing";
|
|
450
467
|
fs.mkdirSync(directoryPath, { recursive: true });
|
|
451
468
|
}
|
|
452
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
|
+
|
|
453
479
|
function assertSafeProjectPath(targetDir, destination) {
|
|
454
480
|
const targetRoot = path.resolve(targetDir);
|
|
455
481
|
const resolvedDestination = path.resolve(destination);
|
|
456
482
|
if (
|
|
457
483
|
resolvedDestination !== targetRoot &&
|
|
458
|
-
!resolvedDestination.startsWith(`${targetRoot}${path.sep}`)
|
|
484
|
+
!resolvedDestination.startsWith(targetRoot.endsWith(path.sep) ? targetRoot : `${targetRoot}${path.sep}`)
|
|
459
485
|
) {
|
|
460
486
|
throw new Error(
|
|
461
487
|
`Refusing destination outside target project: ${destination}`,
|
|
462
488
|
);
|
|
463
489
|
}
|
|
464
490
|
|
|
465
|
-
if (
|
|
491
|
+
if (isSymlink(targetRoot)) {
|
|
466
492
|
throw new Error(`Refusing symlinked target project: ${targetRoot}`);
|
|
467
493
|
}
|
|
468
494
|
|
|
@@ -470,7 +496,7 @@ function assertSafeProjectPath(targetDir, destination) {
|
|
|
470
496
|
let current = targetRoot;
|
|
471
497
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
472
498
|
current = path.join(current, segment);
|
|
473
|
-
if (
|
|
499
|
+
if (isSymlink(current)) {
|
|
474
500
|
throw new Error(
|
|
475
501
|
`Refusing symlinked skill destination component: ${current}`,
|
|
476
502
|
);
|
|
@@ -561,6 +587,86 @@ function hashDirectory(directoryPath) {
|
|
|
561
587
|
return hashText(JSON.stringify(getDirectoryFileHashes(directoryPath)));
|
|
562
588
|
}
|
|
563
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
|
+
|
|
564
670
|
function getLegacyPayloadHashes() {
|
|
565
671
|
if (!fs.existsSync(SOURCE_LEGACY_PAYLOADS_FILE)) return {};
|
|
566
672
|
const releases = readJsonObject(
|
|
@@ -611,46 +717,107 @@ function buildInstalledLock(managedSkills) {
|
|
|
611
717
|
}
|
|
612
718
|
|
|
613
719
|
function compareSemver(left, right) {
|
|
614
|
-
const parse = (value) =>
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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]);
|
|
624
740
|
if (diff !== 0) return diff;
|
|
625
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
|
+
}
|
|
626
758
|
return 0;
|
|
627
759
|
}
|
|
628
760
|
|
|
629
|
-
function assertPackageNotOlderThanInstalled(agentsDirectory) {
|
|
630
|
-
const installedLockPath = path.join(agentsDirectory, "macca-lock.json");
|
|
631
|
-
if (!fs.existsSync(installedLockPath)) {
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
|
|
761
|
+
function assertPackageNotOlderThanInstalled(agentsDirectory, includeRecovery = false) {
|
|
635
762
|
const packagedLock = readJsonObject(SOURCE_LOCK_FILE, "packaged MACCA lock");
|
|
636
|
-
const installedLock = readJsonObject(
|
|
637
|
-
installedLockPath,
|
|
638
|
-
"installed MACCA lock",
|
|
639
|
-
);
|
|
640
763
|
const packagedVersion = packagedLock.version;
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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;
|
|
648
801
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
+
}
|
|
654
821
|
}
|
|
655
822
|
}
|
|
656
823
|
|
|
@@ -659,12 +826,24 @@ function readOwnershipMarker(
|
|
|
659
826
|
expectedSkill = path.basename(targetPath),
|
|
660
827
|
) {
|
|
661
828
|
const markerPath = path.join(targetPath, OWNERSHIP_MARKER);
|
|
662
|
-
|
|
663
|
-
|
|
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 });
|
|
664
842
|
}
|
|
665
843
|
|
|
844
|
+
const content = fs.readFileSync(markerPath, "utf8");
|
|
666
845
|
try {
|
|
667
|
-
const marker = JSON.parse(
|
|
846
|
+
const marker = JSON.parse(content);
|
|
668
847
|
if (marker.owner !== "macca-method" || marker.skill !== expectedSkill) {
|
|
669
848
|
return null;
|
|
670
849
|
}
|
|
@@ -691,7 +870,7 @@ function assertManagedDirectoryUnchanged(targetPath, force = false) {
|
|
|
691
870
|
if (!marker.payloadHash || marker.payloadHash !== hashDirectory(targetPath)) {
|
|
692
871
|
throw new Error(
|
|
693
872
|
`Refusing to overwrite locally modified managed skill: ${targetPath}. ` +
|
|
694
|
-
"
|
|
873
|
+
"Back up and review local modifications first. --force destructively overwrites managed local modifications.",
|
|
695
874
|
);
|
|
696
875
|
}
|
|
697
876
|
}
|
|
@@ -704,7 +883,8 @@ function readJsonObject(filePath, label) {
|
|
|
704
883
|
}
|
|
705
884
|
return value;
|
|
706
885
|
} catch (error) {
|
|
707
|
-
|
|
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" });
|
|
708
888
|
}
|
|
709
889
|
}
|
|
710
890
|
|
|
@@ -722,7 +902,7 @@ function assertManagedFilesUnchanged(agentsDirectory, force = false) {
|
|
|
722
902
|
if (!fs.existsSync(filePath) || hashFile(filePath) !== expectedHash) {
|
|
723
903
|
throw new Error(
|
|
724
904
|
`Refusing to overwrite locally modified MACCA metadata: ${filePath}. ` +
|
|
725
|
-
"
|
|
905
|
+
"Back up and review local modifications first. --force destructively overwrites managed local modifications.",
|
|
726
906
|
);
|
|
727
907
|
}
|
|
728
908
|
}
|
|
@@ -737,13 +917,20 @@ function buildStateContent(files) {
|
|
|
737
917
|
}
|
|
738
918
|
|
|
739
919
|
function writeJsonAtomic(filePath, value) {
|
|
740
|
-
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
"
|
|
745
|
-
|
|
746
|
-
|
|
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
|
+
}
|
|
747
934
|
}
|
|
748
935
|
|
|
749
936
|
function removeManagedPath(targetPath, kind) {
|
|
@@ -759,24 +946,55 @@ function assertTransactionPayload(entry, transactionId, candidatePath) {
|
|
|
759
946
|
candidatePath,
|
|
760
947
|
path.basename(entry.targetPath),
|
|
761
948
|
);
|
|
762
|
-
if (
|
|
949
|
+
if (
|
|
950
|
+
!marker ||
|
|
951
|
+
marker.transactionId !== transactionId ||
|
|
952
|
+
!entry.stagedHash ||
|
|
953
|
+
hashDirectory(candidatePath) !== entry.stagedHash
|
|
954
|
+
) {
|
|
763
955
|
throw new Error(
|
|
764
956
|
`Refusing recovery of directory without matching transaction evidence: ${candidatePath}`,
|
|
765
957
|
);
|
|
766
958
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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) {
|
|
770
965
|
throw new Error(
|
|
771
966
|
`Refusing recovery of file without matching transaction hash: ${candidatePath}`,
|
|
772
967
|
);
|
|
773
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
|
+
}
|
|
774
975
|
}
|
|
775
976
|
|
|
776
|
-
function assertTransactionBackup(entry) {
|
|
977
|
+
function assertTransactionBackup(entry, phase) {
|
|
777
978
|
if (!fs.existsSync(entry.backupPath)) {
|
|
778
979
|
return;
|
|
779
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
|
+
}
|
|
780
998
|
if (entry.kind === "directory") {
|
|
781
999
|
const marker = readOwnershipMarker(
|
|
782
1000
|
entry.backupPath,
|
|
@@ -789,7 +1007,10 @@ function assertTransactionBackup(entry) {
|
|
|
789
1007
|
const validLegacy =
|
|
790
1008
|
entry.legacySkillName &&
|
|
791
1009
|
isLegacyDirectoryUnchanged(entry.backupPath, entry.legacySkillName);
|
|
792
|
-
if (
|
|
1010
|
+
if (
|
|
1011
|
+
(!validManaged && !validLegacy) ||
|
|
1012
|
+
getUnhashedEntries(entry.backupPath).length
|
|
1013
|
+
) {
|
|
793
1014
|
throw new Error(
|
|
794
1015
|
`Refusing recovery from unowned or modified backup directory: ${entry.backupPath}`,
|
|
795
1016
|
);
|
|
@@ -799,7 +1020,7 @@ function assertTransactionBackup(entry) {
|
|
|
799
1020
|
|
|
800
1021
|
function validateTransactionEntries(targetDir, journal) {
|
|
801
1022
|
if (
|
|
802
|
-
journal.version
|
|
1023
|
+
![1, 2].includes(journal.version) ||
|
|
803
1024
|
!["committing", "committed"].includes(journal.phase)
|
|
804
1025
|
) {
|
|
805
1026
|
throw new Error("Unsupported MACCA transaction journal version or phase");
|
|
@@ -866,6 +1087,19 @@ function validateTransactionEntries(targetDir, journal) {
|
|
|
866
1087
|
) {
|
|
867
1088
|
throw new Error("Transaction staged payload hash is missing or invalid");
|
|
868
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
|
+
}
|
|
869
1103
|
|
|
870
1104
|
const targetPath = path.resolve(entry.targetPath);
|
|
871
1105
|
const parent = path.dirname(targetPath);
|
|
@@ -931,7 +1165,7 @@ function recoverInterruptedTransaction(targetDir) {
|
|
|
931
1165
|
const journal = readJsonObject(journalPath, "MACCA transaction journal");
|
|
932
1166
|
const entries = validateTransactionEntries(targetDir, journal);
|
|
933
1167
|
for (const entry of entries) {
|
|
934
|
-
assertTransactionBackup(entry);
|
|
1168
|
+
assertTransactionBackup(entry, journal.phase);
|
|
935
1169
|
if (
|
|
936
1170
|
journal.phase === "committed" ||
|
|
937
1171
|
fs.existsSync(entry.backupPath) ||
|
|
@@ -942,13 +1176,38 @@ function recoverInterruptedTransaction(targetDir) {
|
|
|
942
1176
|
if (entry.stagingPath && fs.existsSync(entry.stagingPath)) {
|
|
943
1177
|
assertTransactionPayload(entry, journal.transactionId, entry.stagingPath);
|
|
944
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
|
+
}
|
|
945
1200
|
}
|
|
946
1201
|
if (journal.phase === "committed") {
|
|
1202
|
+
operation.stage = "recovery";
|
|
1203
|
+
operation.recoveryChanged = true;
|
|
947
1204
|
for (const entry of entries) {
|
|
948
1205
|
removeManagedPath(entry.backupPath, entry.kind);
|
|
949
1206
|
if (entry.stagingPath) removeManagedPath(entry.stagingPath, entry.kind);
|
|
950
1207
|
}
|
|
951
1208
|
} else {
|
|
1209
|
+
operation.stage = "recovery";
|
|
1210
|
+
operation.recoveryChanged = true;
|
|
952
1211
|
for (const entry of [...entries].reverse()) {
|
|
953
1212
|
if (fs.existsSync(entry.backupPath)) {
|
|
954
1213
|
removeManagedPath(entry.targetPath, entry.kind);
|
|
@@ -979,7 +1238,6 @@ function getUniqueDestinations(targetDir, tools) {
|
|
|
979
1238
|
function applySkillTransaction(entries, targetDir, force = false) {
|
|
980
1239
|
const suffix = `${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString("hex")}`;
|
|
981
1240
|
const prepared = [];
|
|
982
|
-
const committed = [];
|
|
983
1241
|
const journalPath = path.join(targetDir, ".agents", TRANSACTION_FILE);
|
|
984
1242
|
|
|
985
1243
|
for (const entry of entries) {
|
|
@@ -1009,7 +1267,10 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1009
1267
|
}
|
|
1010
1268
|
if (kind === "directory") {
|
|
1011
1269
|
if (isMaccaOwned(targetPath)) {
|
|
1012
|
-
assertManagedDirectoryUnchanged(
|
|
1270
|
+
assertManagedDirectoryUnchanged(
|
|
1271
|
+
targetPath,
|
|
1272
|
+
force && Boolean(sourcePath),
|
|
1273
|
+
);
|
|
1013
1274
|
} else if (
|
|
1014
1275
|
!legacySkillName ||
|
|
1015
1276
|
!isLegacyDirectoryUnchanged(targetPath, legacySkillName)
|
|
@@ -1018,12 +1279,20 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1018
1279
|
`Refusing to overwrite or remove unowned skill directory: ${targetPath}`,
|
|
1019
1280
|
);
|
|
1020
1281
|
}
|
|
1282
|
+
if (!sourcePath && getUnhashedEntries(targetPath).length) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`Refusing to remove skill with unhashed content: ${targetPath}`,
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1021
1287
|
}
|
|
1022
1288
|
}
|
|
1023
1289
|
prepared.push({
|
|
1024
1290
|
...entry,
|
|
1025
1291
|
kind,
|
|
1026
1292
|
hadTarget: fs.existsSync(targetPath),
|
|
1293
|
+
originalHash: fs.existsSync(targetPath)
|
|
1294
|
+
? hashSnapshot(targetPath, kind)
|
|
1295
|
+
: null,
|
|
1027
1296
|
stagingPath:
|
|
1028
1297
|
sourcePath || content !== undefined
|
|
1029
1298
|
? path.join(parent, `.${targetName}.macca-stage-${suffix}`)
|
|
@@ -1046,6 +1315,9 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1046
1315
|
}
|
|
1047
1316
|
} else {
|
|
1048
1317
|
fs.cpSync(entry.sourcePath, entry.stagingPath, { recursive: true });
|
|
1318
|
+
if (entry.hadTarget) {
|
|
1319
|
+
preserveUnhashedEntries(entry.targetPath, entry.stagingPath);
|
|
1320
|
+
}
|
|
1049
1321
|
const payloadHash = hashDirectory(entry.stagingPath);
|
|
1050
1322
|
writeTextFile(
|
|
1051
1323
|
path.join(entry.stagingPath, OWNERSHIP_MARKER),
|
|
@@ -1066,10 +1338,11 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1066
1338
|
entry.kind === "file"
|
|
1067
1339
|
? hashFile(entry.stagingPath)
|
|
1068
1340
|
: hashDirectory(entry.stagingPath);
|
|
1341
|
+
entry.stagedSnapshotHash = hashSnapshot(entry.stagingPath, entry.kind);
|
|
1069
1342
|
}
|
|
1070
1343
|
|
|
1071
1344
|
writeJsonAtomic(journalPath, {
|
|
1072
|
-
version:
|
|
1345
|
+
version: 2,
|
|
1073
1346
|
phase: "committing",
|
|
1074
1347
|
transactionId: suffix,
|
|
1075
1348
|
entries: prepared.map(
|
|
@@ -1080,6 +1353,8 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1080
1353
|
kind,
|
|
1081
1354
|
hadTarget,
|
|
1082
1355
|
stagedHash,
|
|
1356
|
+
stagedSnapshotHash,
|
|
1357
|
+
originalHash,
|
|
1083
1358
|
legacySkillName,
|
|
1084
1359
|
}) => ({
|
|
1085
1360
|
targetPath,
|
|
@@ -1088,6 +1363,8 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1088
1363
|
kind,
|
|
1089
1364
|
hadTarget,
|
|
1090
1365
|
stagedHash: stagingPath ? stagedHash : null,
|
|
1366
|
+
stagedSnapshotHash: stagingPath ? stagedSnapshotHash : null,
|
|
1367
|
+
originalHash,
|
|
1091
1368
|
legacySkillName,
|
|
1092
1369
|
}),
|
|
1093
1370
|
),
|
|
@@ -1095,15 +1372,19 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1095
1372
|
|
|
1096
1373
|
for (const entry of prepared) {
|
|
1097
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
|
+
}
|
|
1098
1380
|
fs.renameSync(entry.targetPath, entry.backupPath);
|
|
1099
1381
|
}
|
|
1100
|
-
committed.push(entry);
|
|
1101
1382
|
if (entry.sourcePath || entry.content !== undefined) {
|
|
1102
1383
|
fs.renameSync(entry.stagingPath, entry.targetPath);
|
|
1103
1384
|
}
|
|
1104
1385
|
}
|
|
1105
1386
|
writeJsonAtomic(journalPath, {
|
|
1106
|
-
version:
|
|
1387
|
+
version: 2,
|
|
1107
1388
|
phase: "committed",
|
|
1108
1389
|
transactionId: suffix,
|
|
1109
1390
|
entries: prepared.map(
|
|
@@ -1114,6 +1395,8 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1114
1395
|
kind,
|
|
1115
1396
|
hadTarget,
|
|
1116
1397
|
stagedHash,
|
|
1398
|
+
stagedSnapshotHash,
|
|
1399
|
+
originalHash,
|
|
1117
1400
|
legacySkillName,
|
|
1118
1401
|
}) => ({
|
|
1119
1402
|
targetPath,
|
|
@@ -1122,40 +1405,44 @@ function applySkillTransaction(entries, targetDir, force = false) {
|
|
|
1122
1405
|
kind,
|
|
1123
1406
|
hadTarget,
|
|
1124
1407
|
stagedHash: stagingPath ? stagedHash : null,
|
|
1408
|
+
stagedSnapshotHash: stagingPath ? stagedSnapshotHash : null,
|
|
1409
|
+
originalHash,
|
|
1125
1410
|
legacySkillName,
|
|
1126
1411
|
}),
|
|
1127
1412
|
),
|
|
1128
1413
|
});
|
|
1129
1414
|
} catch (error) {
|
|
1130
|
-
|
|
1131
|
-
fs.
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
+
}
|
|
1137
1437
|
}
|
|
1438
|
+
} catch (recoveryError) {
|
|
1439
|
+
error.message = `${error.message}; ${recoveryError.message}`;
|
|
1440
|
+
throw error;
|
|
1138
1441
|
}
|
|
1139
|
-
fs.rmSync(journalPath, { force: true });
|
|
1140
1442
|
throw error;
|
|
1141
|
-
} finally {
|
|
1142
|
-
for (const entry of prepared) {
|
|
1143
|
-
if (entry.stagingPath) {
|
|
1144
|
-
fs.rmSync(entry.stagingPath, {
|
|
1145
|
-
recursive: entry.kind === "directory",
|
|
1146
|
-
force: true,
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
1443
|
}
|
|
1151
1444
|
|
|
1152
|
-
|
|
1153
|
-
fs.rmSync(entry.backupPath, {
|
|
1154
|
-
recursive: entry.kind === "directory",
|
|
1155
|
-
force: true,
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
fs.rmSync(journalPath, { force: true });
|
|
1445
|
+
recoverInterruptedTransaction(targetDir);
|
|
1159
1446
|
}
|
|
1160
1447
|
|
|
1161
1448
|
function normalizeToolList(values) {
|
|
@@ -1214,11 +1501,12 @@ function mergeObjects(existing, update) {
|
|
|
1214
1501
|
|
|
1215
1502
|
const merged = { ...existing };
|
|
1216
1503
|
for (const [key, value] of Object.entries(update)) {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
+
});
|
|
1222
1510
|
}
|
|
1223
1511
|
return merged;
|
|
1224
1512
|
}
|
|
@@ -1230,15 +1518,61 @@ function readDeveloperConfig(filePath) {
|
|
|
1230
1518
|
|
|
1231
1519
|
try {
|
|
1232
1520
|
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1233
|
-
|
|
1234
|
-
throw new Error("root value must be an object");
|
|
1235
|
-
}
|
|
1521
|
+
assertValidConfig(value, filePath);
|
|
1236
1522
|
return value;
|
|
1237
1523
|
} catch (error) {
|
|
1238
|
-
|
|
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
|
+
});
|
|
1239
1531
|
}
|
|
1240
1532
|
}
|
|
1241
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
|
+
|
|
1242
1576
|
function createPrompt() {
|
|
1243
1577
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1244
1578
|
throw new Error(
|
|
@@ -1246,45 +1580,54 @@ function createPrompt() {
|
|
|
1246
1580
|
);
|
|
1247
1581
|
}
|
|
1248
1582
|
|
|
1249
|
-
|
|
1583
|
+
const prompt = readline.createInterface({
|
|
1250
1584
|
input: process.stdin,
|
|
1251
1585
|
output: process.stdout,
|
|
1252
1586
|
});
|
|
1587
|
+
preparePrompt(prompt);
|
|
1588
|
+
return prompt;
|
|
1253
1589
|
}
|
|
1254
1590
|
|
|
1255
1591
|
function askQuestion(prompt, question) {
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
+
}
|
|
1258
1607
|
});
|
|
1259
1608
|
}
|
|
1260
1609
|
|
|
1261
|
-
async function promptForTools() {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
);
|
|
1270
|
-
});
|
|
1271
|
-
process.stdout.write("\n");
|
|
1272
|
-
|
|
1273
|
-
while (true) {
|
|
1274
|
-
const answer = await askQuestion(
|
|
1275
|
-
prompt,
|
|
1276
|
-
"Masukkan nomor/nama tool (pisahkan dengan koma, 'all', kosong = codex): ",
|
|
1277
|
-
);
|
|
1278
|
-
const parsed = parseInteractiveToolSelection(answer);
|
|
1279
|
-
if (parsed.error) {
|
|
1280
|
-
process.stdout.write(` ${parsed.error}\n`);
|
|
1281
|
-
continue;
|
|
1282
|
-
}
|
|
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");
|
|
1283
1618
|
|
|
1284
|
-
|
|
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;
|
|
1285
1628
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1629
|
+
|
|
1630
|
+
return parsed.tools;
|
|
1288
1631
|
}
|
|
1289
1632
|
}
|
|
1290
1633
|
|
|
@@ -1326,44 +1669,54 @@ function parseInteractiveToolSelection(answer) {
|
|
|
1326
1669
|
return { tools: unique(tools) };
|
|
1327
1670
|
}
|
|
1328
1671
|
|
|
1329
|
-
async function promptForMetadata(seed) {
|
|
1330
|
-
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
|
+
);
|
|
1331
1687
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
);
|
|
1340
|
-
const project =
|
|
1341
|
-
seed.project !== undefined
|
|
1342
|
-
? seed.project
|
|
1343
|
-
: await askQuestion(prompt, "Nama project ini apa? (Kosong = Skip): ");
|
|
1344
|
-
const communication =
|
|
1345
|
-
seed.communicationLanguage !== undefined
|
|
1346
|
-
? seed.communicationLanguage
|
|
1347
|
-
: await askQuestion(
|
|
1348
|
-
prompt,
|
|
1349
|
-
"Bahasa komunikasi yang anda inginkan? (Kosong = Bahasa Indonesia): ",
|
|
1350
|
-
);
|
|
1351
|
-
const documents =
|
|
1352
|
-
seed.documentLanguage !== undefined
|
|
1353
|
-
? seed.documentLanguage
|
|
1354
|
-
: await askQuestion(
|
|
1355
|
-
prompt,
|
|
1356
|
-
"Bahasa dokumen yang dihasilkan? (Kosong = Bahasa Indonesia): ",
|
|
1357
|
-
);
|
|
1688
|
+
return {
|
|
1689
|
+
name: seed.name,
|
|
1690
|
+
project: seed.project,
|
|
1691
|
+
communicationLanguage: communication || "Bahasa Indonesia",
|
|
1692
|
+
documentLanguage: documents || communication || "Bahasa Indonesia",
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1358
1695
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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 });
|
|
1367
1720
|
}
|
|
1368
1721
|
}
|
|
1369
1722
|
|
|
@@ -1371,7 +1724,10 @@ function applyInstall(targetDir, tools, metadata, options = {}) {
|
|
|
1371
1724
|
const force = Boolean(options.force);
|
|
1372
1725
|
const managedSkills = getSourceManagedSkills();
|
|
1373
1726
|
const agentsDirectory = path.join(targetDir, ".agents");
|
|
1374
|
-
|
|
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);
|
|
1375
1731
|
assertSafeProjectPath(targetDir, agentsDirectory);
|
|
1376
1732
|
assertPackageNotOlderThanInstalled(agentsDirectory);
|
|
1377
1733
|
for (const fileName of [
|
|
@@ -1388,6 +1744,10 @@ function applyInstall(targetDir, tools, metadata, options = {}) {
|
|
|
1388
1744
|
readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
|
|
1389
1745
|
".agents/macca-managed-skills.txt",
|
|
1390
1746
|
);
|
|
1747
|
+
tools = unique([
|
|
1748
|
+
...readNonEmptyLines(path.join(agentsDirectory, "macca-tools.txt")),
|
|
1749
|
+
...tools,
|
|
1750
|
+
]);
|
|
1391
1751
|
const entries = [];
|
|
1392
1752
|
for (const destination of getUniqueDestinations(targetDir, tools)) {
|
|
1393
1753
|
for (const skillName of managedSkills) {
|
|
@@ -1397,12 +1757,21 @@ function applyInstall(targetDir, tools, metadata, options = {}) {
|
|
|
1397
1757
|
legacySkillName: skillName,
|
|
1398
1758
|
});
|
|
1399
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
|
+
);
|
|
1400
1774
|
}
|
|
1401
|
-
const configPath = path.join(agentsDirectory, "developer-config.json");
|
|
1402
|
-
const config = mergeObjects(
|
|
1403
|
-
readDeveloperConfig(configPath),
|
|
1404
|
-
buildDeveloperConfig(metadata),
|
|
1405
|
-
);
|
|
1406
1775
|
const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
|
|
1407
1776
|
? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
|
|
1408
1777
|
: `${managedSkills.join("\n")}\n`;
|
|
@@ -1447,6 +1816,9 @@ function applyUpgrade(targetDir, options = {}) {
|
|
|
1447
1816
|
const force = Boolean(options.force);
|
|
1448
1817
|
const agentsDirectory = path.join(targetDir, ".agents");
|
|
1449
1818
|
assertSafeProjectPath(targetDir, agentsDirectory);
|
|
1819
|
+
const configPath = path.join(agentsDirectory, "developer-config.json");
|
|
1820
|
+
assertSafeProjectPath(targetDir, configPath);
|
|
1821
|
+
readDeveloperConfig(configPath);
|
|
1450
1822
|
assertPackageNotOlderThanInstalled(agentsDirectory);
|
|
1451
1823
|
for (const fileName of [
|
|
1452
1824
|
"macca-managed-skills.txt",
|
|
@@ -1482,50 +1854,21 @@ function applyUpgrade(targetDir, options = {}) {
|
|
|
1482
1854
|
});
|
|
1483
1855
|
}
|
|
1484
1856
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
entries.push({ sourcePath: null, targetPath: oldPath });
|
|
1492
|
-
} else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
|
|
1493
|
-
entries.push({
|
|
1494
|
-
sourcePath: null,
|
|
1495
|
-
targetPath: oldPath,
|
|
1496
|
-
legacySkillName: skillName,
|
|
1497
|
-
});
|
|
1498
|
-
} else if (fs.existsSync(oldPath)) {
|
|
1499
|
-
process.stderr.write(
|
|
1500
|
-
`Preserving unmarked legacy skill directory: ${oldPath}\n`,
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1857
|
+
addRetiredSkills(
|
|
1858
|
+
entries,
|
|
1859
|
+
targetDir,
|
|
1860
|
+
destination,
|
|
1861
|
+
previousManagedSkills.filter((name) => !nextManagedSkills.includes(name)),
|
|
1862
|
+
);
|
|
1504
1863
|
}
|
|
1505
1864
|
|
|
1506
1865
|
if (tools.includes("opencode")) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
entries.push({ sourcePath: null, targetPath: oldPath });
|
|
1514
|
-
} else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
|
|
1515
|
-
entries.push({
|
|
1516
|
-
sourcePath: null,
|
|
1517
|
-
targetPath: oldPath,
|
|
1518
|
-
legacySkillName: skillName,
|
|
1519
|
-
});
|
|
1520
|
-
} else if (fs.existsSync(oldPath)) {
|
|
1521
|
-
if (!force) {
|
|
1522
|
-
throw new Error(
|
|
1523
|
-
`Legacy OpenCode skill was modified; move or back it up before upgrade: ${oldPath}. ` +
|
|
1524
|
-
"Run upgrade with --force to overwrite local modifications.",
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1866
|
+
addRetiredSkills(
|
|
1867
|
+
entries,
|
|
1868
|
+
targetDir,
|
|
1869
|
+
path.join(targetDir, ".opencode", "skill"),
|
|
1870
|
+
previousManagedSkills,
|
|
1871
|
+
);
|
|
1529
1872
|
}
|
|
1530
1873
|
const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
|
|
1531
1874
|
? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
|
|
@@ -1605,85 +1948,246 @@ function printInstallSummary(action, tools) {
|
|
|
1605
1948
|
}
|
|
1606
1949
|
|
|
1607
1950
|
async function runInstall(args) {
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1951
|
+
assertSupportedRuntime();
|
|
1952
|
+
operation.stage = "not started";
|
|
1953
|
+
operation.recoveryChanged = false;
|
|
1954
|
+
operation.targetDir = resolveTargetDirectory(args.directory);
|
|
1613
1955
|
const targetDir = assertSafeTargetDirectory(args.directory);
|
|
1614
|
-
|
|
1615
|
-
assertSafeProjectPath(targetDir,
|
|
1616
|
-
|
|
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);
|
|
1617
1960
|
const existingTools = readNonEmptyLines(
|
|
1618
1961
|
path.join(targetDir, ".agents", "macca-tools.txt"),
|
|
1619
1962
|
);
|
|
1620
|
-
tools = unique([...existingTools, ...tools]);
|
|
1621
|
-
const configPath = path.join(targetDir, ".agents", "developer-config.json");
|
|
1622
|
-
const existingConfig = readDeveloperConfig(configPath);
|
|
1623
1963
|
const hasExistingConfig = fs.existsSync(configPath);
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
documentLanguage:
|
|
1645
|
-
args.documentLanguage !== undefined
|
|
1646
|
-
? args.documentLanguage
|
|
1647
|
-
: hasExistingConfig
|
|
1648
|
-
? undefined
|
|
1649
|
-
: "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;
|
|
1650
1984
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
+
}
|
|
1665
2001
|
|
|
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
|
+
}
|
|
1666
2010
|
applyInstall(targetDir, tools, metadata, { force: Boolean(args.force) });
|
|
1667
2011
|
|
|
2012
|
+
operation.stage = "complete";
|
|
1668
2013
|
printInstallSummary("installed", tools);
|
|
1669
|
-
|
|
2014
|
+
printSetupHint(targetDir);
|
|
1670
2015
|
}
|
|
1671
2016
|
|
|
1672
2017
|
function runUpgrade(args) {
|
|
2018
|
+
assertSupportedRuntime();
|
|
2019
|
+
operation.stage = "not started";
|
|
2020
|
+
operation.recoveryChanged = false;
|
|
2021
|
+
operation.targetDir = resolveTargetDirectory(args.directory);
|
|
1673
2022
|
const targetDir = assertSafeTargetDirectory(args.directory);
|
|
1674
|
-
|
|
2023
|
+
const configPath = path.join(targetDir, ".agents", "developer-config.json");
|
|
2024
|
+
assertSafeProjectPath(targetDir, configPath);
|
|
2025
|
+
readDeveloperConfig(configPath);
|
|
2026
|
+
assertPackageNotOlderThanInstalled(path.join(targetDir, ".agents"), true);
|
|
1675
2027
|
recoverInterruptedTransaction(targetDir);
|
|
2028
|
+
assertSafeProjectPath(targetDir, path.join(targetDir, ".agents", "macca-tools.txt"));
|
|
1676
2029
|
const tools = readNonEmptyLines(
|
|
1677
2030
|
path.join(targetDir, ".agents", "macca-tools.txt"),
|
|
1678
2031
|
);
|
|
1679
2032
|
applyUpgrade(targetDir, { force: Boolean(args.force) });
|
|
2033
|
+
operation.stage = "complete";
|
|
1680
2034
|
printInstallSummary("updated", tools);
|
|
1681
|
-
|
|
2035
|
+
printSetupHint(targetDir);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
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;
|
|
1682
2165
|
}
|
|
1683
2166
|
|
|
1684
|
-
function exitWithError(
|
|
1685
|
-
|
|
1686
|
-
|
|
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;
|
|
1687
2191
|
}
|
|
1688
2192
|
|
|
1689
2193
|
main();
|