macca-method 1.0.0 → 2.0.0

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.
Files changed (56) hide show
  1. package/.agents/legacy-payloads.json +22 -0
  2. package/{skills-lock.json → .agents/macca-lock.json} +4 -2
  3. package/.agents/macca-managed-skills.txt +4 -2
  4. package/.agents/skills/_shared/references/additional-skills.md +30 -0
  5. package/.agents/skills/_shared/references/brainstorm-session.md +42 -11
  6. package/.agents/skills/_shared/references/config-mutation.md +25 -0
  7. package/.agents/skills/_shared/references/finding-format.md +25 -0
  8. package/.agents/skills/_shared/references/fix-mode.md +39 -0
  9. package/.agents/skills/_shared/references/human-loop.md +3 -1
  10. package/.agents/skills/_shared/references/implementation-principles.md +19 -0
  11. package/.agents/skills/_shared/references/invocation-policy.md +39 -0
  12. package/.agents/skills/_shared/references/language-config.md +15 -0
  13. package/.agents/skills/_shared/references/output-ownership.md +4 -2
  14. package/.agents/skills/_shared/references/runtime-config.md +7 -168
  15. package/.agents/skills/_shared/references/skill-catalog.md +34 -0
  16. package/.agents/skills/_shared/scripts/validate-skills.py +106 -4
  17. package/.agents/skills/add-feature/SKILL.md +10 -7
  18. package/.agents/skills/brainstorm-api/SKILL.md +49 -194
  19. package/.agents/skills/brainstorm-api/assets/api.template.md +147 -0
  20. package/.agents/skills/brainstorm-architecture/SKILL.md +22 -127
  21. package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +135 -0
  22. package/.agents/skills/brainstorm-prd/SKILL.md +19 -102
  23. package/.agents/skills/brainstorm-prd/assets/PRD.template.md +106 -0
  24. package/.agents/skills/brainstorm-rules/SKILL.md +17 -151
  25. package/.agents/skills/brainstorm-rules/assets/rules.template.md +127 -0
  26. package/.agents/skills/brainstorm-schema/SKILL.md +49 -115
  27. package/.agents/skills/brainstorm-schema/assets/schema.template.md +109 -0
  28. package/.agents/skills/brainstorm-styleguide/SKILL.md +19 -134
  29. package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +147 -0
  30. package/.agents/skills/brainstorm-task/SKILL.md +22 -107
  31. package/.agents/skills/brainstorm-task/assets/Task.template.md +113 -0
  32. package/.agents/skills/bug-fix/SKILL.md +45 -54
  33. package/.agents/skills/code-review/SKILL.md +26 -19
  34. package/.agents/skills/code-review/references/review-checklist.md +24 -26
  35. package/.agents/skills/developer/SKILL.md +25 -39
  36. package/.agents/skills/developer/references/close-phase.md +25 -0
  37. package/.agents/skills/developer/references/execute-task.md +69 -0
  38. package/.agents/skills/developer/references/onboarding.md +47 -0
  39. package/.agents/skills/help/SKILL.md +12 -13
  40. package/.agents/skills/meet/SKILL.md +168 -0
  41. package/.agents/skills/quick-dev/SKILL.md +209 -0
  42. package/.agents/skills/release-readiness/SKILL.md +149 -0
  43. package/.agents/skills/spec-audit/SKILL.md +37 -22
  44. package/.agents/skills/spec-compliance/SKILL.md +41 -40
  45. package/.agents/skills/spec-init/SKILL.md +29 -14
  46. package/README.md +253 -170
  47. package/bin/macca-method.js +785 -91
  48. package/flow.webp +0 -0
  49. package/image-macca-method.webp +0 -0
  50. package/package.json +13 -6
  51. package/scripts/run-skill-validator.js +24 -0
  52. package/scripts/test-install.js +398 -0
  53. package/scripts/test-upgrade-legacy.js +107 -0
  54. package/scripts/validate-skill-behavior.js +124 -0
  55. package/.agents/skills/developer/references/execution-workflow.md +0 -322
  56. package/.agents/skills/rapat/SKILL.md +0 -172
@@ -3,6 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const fs = require("node:fs");
6
+ const crypto = require("node:crypto");
6
7
  const os = require("node:os");
7
8
  const path = require("node:path");
8
9
  const readline = require("node:readline");
@@ -11,7 +12,11 @@ const PACKAGE_ROOT = path.resolve(__dirname, "..");
11
12
  const SOURCE_AGENTS_DIR = path.join(PACKAGE_ROOT, ".agents");
12
13
  const SOURCE_SKILLS_DIR = path.join(SOURCE_AGENTS_DIR, "skills");
13
14
  const SOURCE_MANAGED_SKILLS_FILE = path.join(SOURCE_AGENTS_DIR, "macca-managed-skills.txt");
14
- const SOURCE_LOCK_FILE = path.join(PACKAGE_ROOT, "skills-lock.json");
15
+ const SOURCE_LOCK_FILE = path.join(SOURCE_AGENTS_DIR, "macca-lock.json");
16
+ const SOURCE_LEGACY_PAYLOADS_FILE = path.join(SOURCE_AGENTS_DIR, "legacy-payloads.json");
17
+ const OWNERSHIP_MARKER = ".macca-owned.json";
18
+ const TRANSACTION_FILE = "macca-transaction.json";
19
+ const STATE_FILE = "macca-state.json";
15
20
 
16
21
  const TOOL_DEFINITIONS = [
17
22
  {
@@ -53,8 +58,8 @@ const TOOL_DEFINITIONS = [
53
58
  key: "opencode",
54
59
  aliases: [],
55
60
  label: "OpenCode",
56
- destination: (targetDir) => path.join(targetDir, ".opencode", "skill"),
57
- displayDestination: ".opencode/skill/"
61
+ destination: (targetDir) => path.join(targetDir, ".opencode", "skills"),
62
+ displayDestination: ".opencode/skills/"
58
63
  },
59
64
  {
60
65
  key: "kilo",
@@ -74,14 +79,8 @@ const TOOL_DEFINITIONS = [
74
79
  key: "kimi",
75
80
  aliases: ["kimi-cli"],
76
81
  label: "Kimi CLI",
77
- destination: () => {
78
- if (process.platform === "win32") {
79
- return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "agents", "skills");
80
- }
81
-
82
- return path.join(os.homedir(), ".config", "agents", "skills");
83
- },
84
- displayDestination: process.platform === "win32" ? "%APPDATA%\\agents\\skills\\" : "~/.config/agents/skills/"
82
+ destination: (targetDir) => path.join(targetDir, ".agents", "skills"),
83
+ displayDestination: ".agents/skills/"
85
84
  }
86
85
  ];
87
86
 
@@ -105,13 +104,13 @@ function main() {
105
104
  return;
106
105
  }
107
106
 
108
- if (args.help || command === "help") {
109
- printHelp();
107
+ if (args.listTools) {
108
+ printToolList();
110
109
  return;
111
110
  }
112
111
 
113
- if (args.listTools) {
114
- printToolList();
112
+ if (args.help || command === "help") {
113
+ printHelp();
115
114
  return;
116
115
  }
117
116
 
@@ -262,7 +261,7 @@ function ensurePackagedFiles() {
262
261
  }
263
262
 
264
263
  if (!fs.existsSync(SOURCE_LOCK_FILE)) {
265
- throw new Error("skills-lock.json is missing from the package.");
264
+ throw new Error(".agents/macca-lock.json is missing from the package.");
266
265
  }
267
266
  }
268
267
 
@@ -272,9 +271,9 @@ function printHelp() {
272
271
  "MACCA CLI",
273
272
  "",
274
273
  "Usage:",
275
- " npx macca-method install [options]",
276
- " npx macca-method upgrade [options]",
277
- " npx macca-method --list-tools",
274
+ " npx macca-method@latest install [options]",
275
+ " npx macca-method@latest upgrade [options]",
276
+ " npx macca-method@latest --list-tools",
278
277
  "",
279
278
  "Install options:",
280
279
  " -t, --tool <name> Repeatable. Also accepts comma-separated values.",
@@ -286,9 +285,9 @@ function printHelp() {
286
285
  " --document-language <value>",
287
286
  "",
288
287
  "Examples:",
289
- " npx macca-method install",
290
- " npx macca-method install --tool github-copilot --tool codex --yes",
291
- " npx macca-method upgrade",
288
+ " npx macca-method@latest install",
289
+ " npx macca-method@latest install --tool github-copilot --tool codex --yes",
290
+ " npx macca-method@latest upgrade",
292
291
  ""
293
292
  ].join("\n")
294
293
  );
@@ -310,6 +309,26 @@ function resolveTargetDirectory(rawDirectory) {
310
309
  return path.resolve(process.cwd(), rawDirectory);
311
310
  }
312
311
 
312
+ function assertSafeTargetDirectory(rawDirectory) {
313
+ const requested = rawDirectory ? path.resolve(process.cwd(), rawDirectory) : process.cwd();
314
+ const absolute = path.isAbsolute(requested) ? requested : path.resolve(requested);
315
+ const segments = absolute.split(path.sep).filter(Boolean);
316
+ let current = absolute.startsWith(path.sep) ? path.sep : segments.shift() || absolute;
317
+
318
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
319
+ throw new Error(`Refusing symlinked target project ancestor: ${current}`);
320
+ }
321
+
322
+ for (const segment of segments) {
323
+ current = current === path.sep ? path.join(current, segment) : path.join(current, segment);
324
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
325
+ throw new Error(`Refusing symlinked target project ancestor: ${current}`);
326
+ }
327
+ }
328
+
329
+ return absolute;
330
+ }
331
+
313
332
  function readNonEmptyLines(filePath) {
314
333
  if (!fs.existsSync(filePath)) {
315
334
  return [];
@@ -329,14 +348,38 @@ function unique(values) {
329
348
  function getSourceManagedSkills() {
330
349
  const managedSkills = readNonEmptyLines(SOURCE_MANAGED_SKILLS_FILE);
331
350
  if (managedSkills.length > 0) {
332
- return managedSkills;
351
+ return validateManagedSkillNames(managedSkills, "packaged managed-skill manifest");
333
352
  }
334
353
 
335
- return fs
354
+ return validateManagedSkillNames(fs
336
355
  .readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true })
337
356
  .filter((entry) => entry.isDirectory())
338
357
  .map((entry) => entry.name)
339
- .sort();
358
+ .sort(), "packaged skills directory");
359
+ }
360
+
361
+ function validateManagedSkillNames(skillNames, sourceLabel) {
362
+ const validName = /^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/;
363
+
364
+ for (const skillName of skillNames) {
365
+ if (!validName.test(skillName) || skillName.includes("..") || path.basename(skillName) !== skillName) {
366
+ throw new Error(`Unsafe skill name in ${sourceLabel}: ${skillName}`);
367
+ }
368
+ }
369
+
370
+ return unique(skillNames);
371
+ }
372
+
373
+ function resolveOwnedSkillPath(destination, skillName) {
374
+ validateManagedSkillNames([skillName], "managed-skill manifest");
375
+ const root = path.resolve(destination);
376
+ const target = path.resolve(root, skillName);
377
+
378
+ if (!target.startsWith(`${root}${path.sep}`)) {
379
+ throw new Error(`Refusing path outside skill destination: ${skillName}`);
380
+ }
381
+
382
+ return target;
340
383
  }
341
384
 
342
385
  function normalizeLanguage(value) {
@@ -366,6 +409,27 @@ function ensureDirectory(directoryPath) {
366
409
  fs.mkdirSync(directoryPath, { recursive: true });
367
410
  }
368
411
 
412
+ function assertSafeProjectPath(targetDir, destination) {
413
+ const targetRoot = path.resolve(targetDir);
414
+ const resolvedDestination = path.resolve(destination);
415
+ if (resolvedDestination !== targetRoot && !resolvedDestination.startsWith(`${targetRoot}${path.sep}`)) {
416
+ throw new Error(`Refusing destination outside target project: ${destination}`);
417
+ }
418
+
419
+ if (fs.existsSync(targetRoot) && fs.lstatSync(targetRoot).isSymbolicLink()) {
420
+ throw new Error(`Refusing symlinked target project: ${targetRoot}`);
421
+ }
422
+
423
+ const relative = path.relative(targetRoot, resolvedDestination);
424
+ let current = targetRoot;
425
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
426
+ current = path.join(current, segment);
427
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
428
+ throw new Error(`Refusing symlinked skill destination component: ${current}`);
429
+ }
430
+ }
431
+ }
432
+
369
433
  function writeTextFile(filePath, content) {
370
434
  ensureDirectory(path.dirname(filePath));
371
435
  fs.writeFileSync(filePath, content, "utf8");
@@ -376,10 +440,492 @@ function copyFile(sourcePath, targetPath) {
376
440
  fs.copyFileSync(sourcePath, targetPath);
377
441
  }
378
442
 
379
- function copyDirectory(sourcePath, targetPath) {
380
- ensureDirectory(path.dirname(targetPath));
381
- fs.rmSync(targetPath, { recursive: true, force: true });
382
- fs.cpSync(sourcePath, targetPath, { recursive: true });
443
+ function hashFile(filePath) {
444
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
445
+ }
446
+
447
+ function hashText(content) {
448
+ return crypto.createHash("sha256").update(content).digest("hex");
449
+ }
450
+
451
+ function getDirectoryFileHashes(directoryPath) {
452
+ const hashes = {};
453
+
454
+ function walk(currentPath, relativePath = "") {
455
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true })
456
+ .filter((entry) => entry.name !== OWNERSHIP_MARKER)
457
+ .sort((left, right) => left.name.localeCompare(right.name));
458
+
459
+ for (const entry of entries) {
460
+ const entryPath = path.join(currentPath, entry.name);
461
+ const entryRelativePath = path.join(relativePath, entry.name).split(path.sep).join("/");
462
+ if (entry.isSymbolicLink()) {
463
+ throw new Error(`Refusing symlink inside managed skill: ${entryPath}`);
464
+ }
465
+ if (entry.isDirectory()) {
466
+ walk(entryPath, entryRelativePath);
467
+ } else if (entry.isFile()) {
468
+ hashes[entryRelativePath] = hashFile(entryPath);
469
+ } else {
470
+ throw new Error(`Unsupported file type inside managed skill: ${entryPath}`);
471
+ }
472
+ }
473
+ }
474
+
475
+ walk(directoryPath);
476
+ return hashes;
477
+ }
478
+
479
+ function hashDirectory(directoryPath) {
480
+ return hashText(JSON.stringify(getDirectoryFileHashes(directoryPath)));
481
+ }
482
+
483
+ function getLegacyPayloadHashes() {
484
+ if (!fs.existsSync(SOURCE_LEGACY_PAYLOADS_FILE)) return {};
485
+ const releases = readJsonObject(SOURCE_LEGACY_PAYLOADS_FILE, "legacy payload fingerprints");
486
+ const combined = {};
487
+ for (const payloads of Object.values(releases)) {
488
+ if (!payloads || typeof payloads !== "object" || Array.isArray(payloads)) continue;
489
+ for (const [encodedSkillName, payloadHash] of Object.entries(payloads)) {
490
+ if (typeof payloadHash !== "string" || !/^[a-f0-9]{64}$/.test(payloadHash)) continue;
491
+ const skillName = encodedSkillName.startsWith("base64:")
492
+ ? Buffer.from(encodedSkillName.slice("base64:".length), "base64").toString("utf8")
493
+ : encodedSkillName;
494
+ if (!combined[skillName]) combined[skillName] = new Set();
495
+ combined[skillName].add(payloadHash);
496
+ }
497
+ }
498
+ return combined;
499
+ }
500
+
501
+ function isLegacyDirectoryUnchanged(targetPath, skillName) {
502
+ if (!fs.existsSync(targetPath) || isMaccaOwned(targetPath)) return false;
503
+ if (!fs.lstatSync(targetPath).isDirectory()) return false;
504
+ const hashes = getLegacyPayloadHashes()[skillName];
505
+ const payloadHash = hashDirectory(targetPath);
506
+ return Boolean(hashes && hashes.has(payloadHash));
507
+ }
508
+
509
+ function buildInstalledLock(managedSkills) {
510
+ const lock = JSON.parse(fs.readFileSync(SOURCE_LOCK_FILE, "utf8"));
511
+ lock.hashAlgorithm = "sha256";
512
+ lock.payloadFiles = {};
513
+ for (const skillName of managedSkills) {
514
+ lock.payloadFiles[skillName] = getDirectoryFileHashes(path.join(SOURCE_SKILLS_DIR, skillName));
515
+ }
516
+ return `${JSON.stringify(lock, null, 2)}\n`;
517
+ }
518
+
519
+ function compareSemver(left, right) {
520
+ const parse = (value) => String(value || "0.0.0")
521
+ .split("-")[0]
522
+ .split(".")
523
+ .map((part) => Number.parseInt(part, 10) || 0);
524
+ const a = parse(left);
525
+ const b = parse(right);
526
+ const length = Math.max(a.length, b.length);
527
+ for (let index = 0; index < length; index += 1) {
528
+ const diff = (a[index] || 0) - (b[index] || 0);
529
+ if (diff !== 0) return diff;
530
+ }
531
+ return 0;
532
+ }
533
+
534
+ function assertPackageNotOlderThanInstalled(agentsDirectory) {
535
+ const installedLockPath = path.join(agentsDirectory, "macca-lock.json");
536
+ if (!fs.existsSync(installedLockPath)) {
537
+ return;
538
+ }
539
+
540
+ const packagedLock = readJsonObject(SOURCE_LOCK_FILE, "packaged MACCA lock");
541
+ const installedLock = readJsonObject(installedLockPath, "installed MACCA lock");
542
+ const packagedVersion = packagedLock.version;
543
+ const installedVersion = installedLock.version;
544
+ if (typeof packagedVersion !== "string" || typeof installedVersion !== "string") {
545
+ return;
546
+ }
547
+
548
+ if (compareSemver(packagedVersion, installedVersion) < 0) {
549
+ throw new Error(
550
+ `Refusing to downgrade MACCA from ${installedVersion} to ${packagedVersion}. `
551
+ + "Publish or use a newer package before running install/upgrade."
552
+ );
553
+ }
554
+ }
555
+
556
+ function readOwnershipMarker(targetPath, expectedSkill = path.basename(targetPath)) {
557
+ const markerPath = path.join(targetPath, OWNERSHIP_MARKER);
558
+ if (!fs.existsSync(markerPath)) {
559
+ return null;
560
+ }
561
+
562
+ try {
563
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf8"));
564
+ if (marker.owner !== "macca-method" || marker.skill !== expectedSkill) {
565
+ return null;
566
+ }
567
+ return marker;
568
+ } catch {
569
+ return null;
570
+ }
571
+ }
572
+
573
+ function isMaccaOwned(targetPath) {
574
+ return readOwnershipMarker(targetPath) !== null;
575
+ }
576
+
577
+ function assertManagedDirectoryUnchanged(targetPath) {
578
+ const marker = readOwnershipMarker(targetPath);
579
+ if (!marker) {
580
+ throw new Error(`Refusing to overwrite unowned skill directory: ${targetPath}`);
581
+ }
582
+ if (!marker.payloadHash || marker.payloadHash !== hashDirectory(targetPath)) {
583
+ throw new Error(`Refusing to overwrite locally modified managed skill: ${targetPath}`);
584
+ }
585
+ }
586
+
587
+ function readJsonObject(filePath, label) {
588
+ try {
589
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
590
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
591
+ throw new Error("root value must be an object");
592
+ }
593
+ return value;
594
+ } catch (error) {
595
+ throw new Error(`Invalid ${label} at ${filePath}: ${error.message}`);
596
+ }
597
+ }
598
+
599
+ function assertManagedFilesUnchanged(agentsDirectory) {
600
+ const statePath = path.join(agentsDirectory, STATE_FILE);
601
+ if (!fs.existsSync(statePath)) {
602
+ return;
603
+ }
604
+ const state = readJsonObject(statePath, "MACCA state");
605
+ for (const [fileName, expectedHash] of Object.entries(state.files || {})) {
606
+ const filePath = path.join(agentsDirectory, fileName);
607
+ if (!fs.existsSync(filePath) || hashFile(filePath) !== expectedHash) {
608
+ throw new Error(`Refusing to overwrite locally modified MACCA metadata: ${filePath}`);
609
+ }
610
+ }
611
+ }
612
+
613
+ function buildStateContent(files) {
614
+ const hashes = {};
615
+ for (const [fileName, content] of Object.entries(files)) {
616
+ hashes[fileName] = hashText(content);
617
+ }
618
+ return `${JSON.stringify({ version: 1, hashAlgorithm: "sha256", files: hashes }, null, 2)}\n`;
619
+ }
620
+
621
+ function writeJsonAtomic(filePath, value) {
622
+ const temporaryPath = `${filePath}.tmp-${process.pid}`;
623
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
624
+ fs.renameSync(temporaryPath, filePath);
625
+ }
626
+
627
+ function removeManagedPath(targetPath, kind) {
628
+ fs.rmSync(targetPath, { recursive: kind === "directory", force: true });
629
+ }
630
+
631
+ function assertTransactionPayload(entry, transactionId, candidatePath) {
632
+ if (!fs.existsSync(candidatePath)) {
633
+ return;
634
+ }
635
+ if (entry.kind === "directory") {
636
+ const marker = readOwnershipMarker(candidatePath, path.basename(entry.targetPath));
637
+ if (!marker || marker.transactionId !== transactionId) {
638
+ throw new Error(`Refusing recovery of directory without matching transaction evidence: ${candidatePath}`);
639
+ }
640
+ return;
641
+ }
642
+ if (!entry.stagedHash || hashFile(candidatePath) !== entry.stagedHash) {
643
+ throw new Error(`Refusing recovery of file without matching transaction hash: ${candidatePath}`);
644
+ }
645
+ }
646
+
647
+ function assertTransactionBackup(entry) {
648
+ if (!fs.existsSync(entry.backupPath)) {
649
+ return;
650
+ }
651
+ if (entry.kind === "directory") {
652
+ const marker = readOwnershipMarker(entry.backupPath, path.basename(entry.targetPath));
653
+ const validManaged = marker && marker.payloadHash && marker.payloadHash === hashDirectory(entry.backupPath);
654
+ const validLegacy = entry.legacySkillName
655
+ && isLegacyDirectoryUnchanged(entry.backupPath, entry.legacySkillName);
656
+ if (!validManaged && !validLegacy) {
657
+ throw new Error(`Refusing recovery from unowned or modified backup directory: ${entry.backupPath}`);
658
+ }
659
+ }
660
+ }
661
+
662
+ function validateTransactionEntries(targetDir, journal) {
663
+ if (journal.version !== 1 || !["committing", "committed"].includes(journal.phase)) {
664
+ throw new Error("Unsupported MACCA transaction journal version or phase");
665
+ }
666
+ if (typeof journal.transactionId !== "string" || !/^\d+-\d+-[a-f0-9]+$/.test(journal.transactionId)) {
667
+ throw new Error("Invalid MACCA transaction ID");
668
+ }
669
+ if (!Array.isArray(journal.entries)) {
670
+ throw new Error("Invalid MACCA transaction journal entries");
671
+ }
672
+
673
+ const metadataTargets = new Set([
674
+ "macca-managed-skills.txt",
675
+ "macca-lock.json",
676
+ "macca-tools.txt",
677
+ "developer-config.json",
678
+ STATE_FILE
679
+ ].map((fileName) => path.resolve(targetDir, ".agents", fileName)));
680
+ const skillParents = new Set([
681
+ ...TOOL_DEFINITIONS.map((tool) => path.resolve(tool.destination(targetDir))),
682
+ path.resolve(targetDir, ".opencode", "skill")
683
+ ]);
684
+ const seenPaths = new Set();
685
+
686
+ for (const entry of journal.entries) {
687
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
688
+ throw new Error("Invalid MACCA transaction journal entry");
689
+ }
690
+ if (entry.kind !== "directory" && entry.kind !== "file") {
691
+ throw new Error(`Invalid transaction entry kind: ${entry.kind}`);
692
+ }
693
+ if (typeof entry.targetPath !== "string" || typeof entry.backupPath !== "string") {
694
+ throw new Error("Transaction entry paths must be strings");
695
+ }
696
+ if (entry.stagingPath !== null && typeof entry.stagingPath !== "string") {
697
+ throw new Error("Transaction staging path must be a string or null");
698
+ }
699
+ if (typeof entry.hadTarget !== "boolean") {
700
+ throw new Error("Transaction hadTarget must be boolean");
701
+ }
702
+ if (entry.legacySkillName !== undefined && (
703
+ typeof entry.legacySkillName !== "string"
704
+ || !/^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/.test(entry.legacySkillName)
705
+ )) {
706
+ throw new Error("Transaction legacy skill name is invalid");
707
+ }
708
+ if (entry.stagingPath !== null && (typeof entry.stagedHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.stagedHash))) {
709
+ throw new Error("Transaction staged payload hash is missing or invalid");
710
+ }
711
+
712
+ const targetPath = path.resolve(entry.targetPath);
713
+ const parent = path.dirname(targetPath);
714
+ const targetName = path.basename(targetPath);
715
+ const isValidFile = entry.kind === "file" && metadataTargets.has(targetPath);
716
+ const isValidSkill = entry.kind === "directory"
717
+ && skillParents.has(parent)
718
+ && /^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/.test(targetName);
719
+ if (!isValidFile && !isValidSkill) {
720
+ throw new Error(`Transaction target is not a managed MACCA path: ${targetPath}`);
721
+ }
722
+
723
+ const backupPath = path.resolve(entry.backupPath);
724
+ const expectedBackupPath = path.join(parent, `.${targetName}.macca-backup-${journal.transactionId}`);
725
+ if (backupPath !== expectedBackupPath) {
726
+ throw new Error(`Invalid MACCA transaction backup path: ${backupPath}`);
727
+ }
728
+ if (entry.stagingPath !== null) {
729
+ const stagingPath = path.resolve(entry.stagingPath);
730
+ const expectedStagingPath = path.join(parent, `.${targetName}.macca-stage-${journal.transactionId}`);
731
+ if (stagingPath !== expectedStagingPath) {
732
+ throw new Error(`Invalid MACCA transaction staging path: ${stagingPath}`);
733
+ }
734
+ }
735
+
736
+ for (const candidate of [targetPath, backupPath, entry.stagingPath && path.resolve(entry.stagingPath)].filter(Boolean)) {
737
+ assertSafeProjectPath(targetDir, candidate);
738
+ if (seenPaths.has(candidate)) {
739
+ throw new Error(`Duplicate path in MACCA transaction journal: ${candidate}`);
740
+ }
741
+ seenPaths.add(candidate);
742
+ }
743
+ }
744
+
745
+ return journal.entries;
746
+ }
747
+
748
+ function recoverInterruptedTransaction(targetDir) {
749
+ const journalPath = path.join(targetDir, ".agents", TRANSACTION_FILE);
750
+ assertSafeProjectPath(targetDir, journalPath);
751
+ if (!fs.existsSync(journalPath)) {
752
+ return;
753
+ }
754
+
755
+ const journal = readJsonObject(journalPath, "MACCA transaction journal");
756
+ const entries = validateTransactionEntries(targetDir, journal);
757
+ for (const entry of entries) {
758
+ assertTransactionBackup(entry);
759
+ if (journal.phase === "committed" || fs.existsSync(entry.backupPath) || !entry.hadTarget) {
760
+ assertTransactionPayload(entry, journal.transactionId, entry.targetPath);
761
+ }
762
+ if (entry.stagingPath && fs.existsSync(entry.stagingPath)) {
763
+ assertTransactionPayload(entry, journal.transactionId, entry.stagingPath);
764
+ }
765
+ }
766
+ if (journal.phase === "committed") {
767
+ for (const entry of entries) {
768
+ removeManagedPath(entry.backupPath, entry.kind);
769
+ if (entry.stagingPath) removeManagedPath(entry.stagingPath, entry.kind);
770
+ }
771
+ } else {
772
+ for (const entry of [...entries].reverse()) {
773
+ if (fs.existsSync(entry.backupPath)) {
774
+ removeManagedPath(entry.targetPath, entry.kind);
775
+ fs.renameSync(entry.backupPath, entry.targetPath);
776
+ } else if (!entry.hadTarget) {
777
+ removeManagedPath(entry.targetPath, entry.kind);
778
+ }
779
+ if (entry.stagingPath) removeManagedPath(entry.stagingPath, entry.kind);
780
+ }
781
+ }
782
+ fs.rmSync(journalPath, { force: true });
783
+ }
784
+
785
+ function getUniqueDestinations(targetDir, tools) {
786
+ const destinations = new Map();
787
+ for (const toolKey of tools) {
788
+ const tool = TOOL_BY_KEY.get(toolKey);
789
+ if (!tool) {
790
+ throw new Error(`Unsupported tool: ${toolKey}`);
791
+ }
792
+ const destination = tool.destination(targetDir);
793
+ assertSafeProjectPath(targetDir, destination);
794
+ destinations.set(path.resolve(destination), destination);
795
+ }
796
+ return [...destinations.values()];
797
+ }
798
+
799
+ function applySkillTransaction(entries, targetDir) {
800
+ const suffix = `${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString("hex")}`;
801
+ const prepared = [];
802
+ const committed = [];
803
+ const journalPath = path.join(targetDir, ".agents", TRANSACTION_FILE);
804
+
805
+ for (const entry of entries) {
806
+ const { targetPath, sourcePath, content, kind = "directory", legacySkillName } = entry;
807
+ const targetName = path.basename(targetPath);
808
+ const parent = path.dirname(targetPath);
809
+ if (fs.existsSync(targetPath)) {
810
+ const stat = fs.lstatSync(targetPath);
811
+ if (stat.isSymbolicLink()) {
812
+ throw new Error(`Refusing to replace symlinked managed path: ${targetPath}`);
813
+ }
814
+ if (kind === "directory" && !stat.isDirectory()) {
815
+ throw new Error(`Skill destination is not a directory: ${targetPath}`);
816
+ }
817
+ if (kind === "file" && !stat.isFile()) {
818
+ throw new Error(`Managed file destination is not a file: ${targetPath}`);
819
+ }
820
+ if (kind === "directory") {
821
+ if (isMaccaOwned(targetPath)) {
822
+ assertManagedDirectoryUnchanged(targetPath);
823
+ } else if (!legacySkillName || !isLegacyDirectoryUnchanged(targetPath, legacySkillName)) {
824
+ throw new Error(`Refusing to overwrite or remove unowned skill directory: ${targetPath}`);
825
+ }
826
+ }
827
+ }
828
+ prepared.push({
829
+ ...entry,
830
+ kind,
831
+ hadTarget: fs.existsSync(targetPath),
832
+ stagingPath: sourcePath || content !== undefined
833
+ ? path.join(parent, `.${targetName}.macca-stage-${suffix}`)
834
+ : null,
835
+ backupPath: path.join(parent, `.${targetName}.macca-backup-${suffix}`)
836
+ });
837
+ }
838
+
839
+ try {
840
+ for (const entry of prepared) {
841
+ ensureDirectory(path.dirname(entry.targetPath));
842
+ if (!entry.sourcePath && entry.content === undefined) {
843
+ continue;
844
+ }
845
+ if (entry.kind === "file") {
846
+ if (entry.sourcePath) {
847
+ fs.copyFileSync(entry.sourcePath, entry.stagingPath);
848
+ } else {
849
+ fs.writeFileSync(entry.stagingPath, entry.content, "utf8");
850
+ }
851
+ } else {
852
+ fs.cpSync(entry.sourcePath, entry.stagingPath, { recursive: true });
853
+ const payloadHash = hashDirectory(entry.stagingPath);
854
+ writeTextFile(
855
+ path.join(entry.stagingPath, OWNERSHIP_MARKER),
856
+ `${JSON.stringify({
857
+ owner: "macca-method",
858
+ skill: path.basename(entry.targetPath),
859
+ hashAlgorithm: "sha256",
860
+ payloadHash,
861
+ transactionId: suffix
862
+ }, null, 2)}\n`
863
+ );
864
+ }
865
+ entry.stagedHash = entry.kind === "file"
866
+ ? hashFile(entry.stagingPath)
867
+ : hashDirectory(entry.stagingPath);
868
+ }
869
+
870
+ writeJsonAtomic(journalPath, {
871
+ version: 1,
872
+ phase: "committing",
873
+ transactionId: suffix,
874
+ entries: prepared.map(({ targetPath, stagingPath, backupPath, kind, hadTarget, stagedHash, legacySkillName }) => ({
875
+ targetPath,
876
+ stagingPath,
877
+ backupPath,
878
+ kind,
879
+ hadTarget,
880
+ stagedHash: stagingPath ? stagedHash : null,
881
+ legacySkillName
882
+ }))
883
+ });
884
+
885
+ for (const entry of prepared) {
886
+ if (entry.hadTarget) {
887
+ fs.renameSync(entry.targetPath, entry.backupPath);
888
+ }
889
+ committed.push(entry);
890
+ if (entry.sourcePath || entry.content !== undefined) {
891
+ fs.renameSync(entry.stagingPath, entry.targetPath);
892
+ }
893
+ }
894
+ writeJsonAtomic(journalPath, {
895
+ version: 1,
896
+ phase: "committed",
897
+ transactionId: suffix,
898
+ entries: prepared.map(({ targetPath, stagingPath, backupPath, kind, hadTarget, stagedHash, legacySkillName }) => ({
899
+ targetPath,
900
+ stagingPath,
901
+ backupPath,
902
+ kind,
903
+ hadTarget,
904
+ stagedHash: stagingPath ? stagedHash : null,
905
+ legacySkillName
906
+ }))
907
+ });
908
+ } catch (error) {
909
+ for (const entry of [...committed].reverse()) {
910
+ fs.rmSync(entry.targetPath, { recursive: entry.kind === "directory", force: true });
911
+ if (entry.hadTarget && fs.existsSync(entry.backupPath)) {
912
+ fs.renameSync(entry.backupPath, entry.targetPath);
913
+ }
914
+ }
915
+ fs.rmSync(journalPath, { force: true });
916
+ throw error;
917
+ } finally {
918
+ for (const entry of prepared) {
919
+ if (entry.stagingPath) {
920
+ fs.rmSync(entry.stagingPath, { recursive: entry.kind === "directory", force: true });
921
+ }
922
+ }
923
+ }
924
+
925
+ for (const entry of committed) {
926
+ fs.rmSync(entry.backupPath, { recursive: entry.kind === "directory", force: true });
927
+ }
928
+ fs.rmSync(journalPath, { force: true });
383
929
  }
384
930
 
385
931
  function normalizeToolList(values) {
@@ -407,20 +953,57 @@ function normalizeToolList(values) {
407
953
  }
408
954
 
409
955
  function buildDeveloperConfig(options) {
410
- return {
411
- name: options.name,
412
- project: options.project,
413
- languagePreferences: {
414
- communication: {
956
+ const config = {};
957
+ if (options.name !== undefined) config.name = options.name;
958
+ if (options.project !== undefined) config.project = options.project;
959
+ if (options.communicationLanguage !== undefined || options.documentLanguage !== undefined) {
960
+ config.languagePreferences = {};
961
+ if (options.communicationLanguage !== undefined) {
962
+ config.languagePreferences.communication = {
415
963
  raw: options.communicationLanguage,
416
964
  normalized: normalizeLanguage(options.communicationLanguage)
417
- },
418
- documents: {
965
+ };
966
+ }
967
+ if (options.documentLanguage !== undefined) {
968
+ config.languagePreferences.documents = {
419
969
  raw: options.documentLanguage,
420
970
  normalized: normalizeLanguage(options.documentLanguage)
421
- }
971
+ };
422
972
  }
423
- };
973
+ }
974
+ return config;
975
+ }
976
+
977
+ function mergeObjects(existing, update) {
978
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
979
+ return update;
980
+ }
981
+
982
+ const merged = { ...existing };
983
+ for (const [key, value] of Object.entries(update)) {
984
+ if (value && typeof value === "object" && !Array.isArray(value)) {
985
+ merged[key] = mergeObjects(existing[key], value);
986
+ } else {
987
+ merged[key] = value;
988
+ }
989
+ }
990
+ return merged;
991
+ }
992
+
993
+ function readDeveloperConfig(filePath) {
994
+ if (!fs.existsSync(filePath)) {
995
+ return {};
996
+ }
997
+
998
+ try {
999
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
1000
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1001
+ throw new Error("root value must be an object");
1002
+ }
1003
+ return value;
1004
+ } catch (error) {
1005
+ throw new Error(`Cannot preserve malformed ${filePath}: ${error.message}`);
1006
+ }
424
1007
  }
425
1008
 
426
1009
  function createPrompt() {
@@ -531,77 +1114,164 @@ function applyInstall(targetDir, tools, metadata) {
531
1114
  const managedSkills = getSourceManagedSkills();
532
1115
  const agentsDirectory = path.join(targetDir, ".agents");
533
1116
  ensureDirectory(targetDir);
534
- ensureDirectory(agentsDirectory);
535
-
536
- if (fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)) {
537
- copyFile(SOURCE_MANAGED_SKILLS_FILE, path.join(agentsDirectory, "macca-managed-skills.txt"));
538
- } else {
539
- writeTextFile(path.join(agentsDirectory, "macca-managed-skills.txt"), `${managedSkills.join("\n")}\n`);
1117
+ assertSafeProjectPath(targetDir, agentsDirectory);
1118
+ assertPackageNotOlderThanInstalled(agentsDirectory);
1119
+ for (const fileName of ["macca-managed-skills.txt", "macca-tools.txt", "developer-config.json", "macca-lock.json", STATE_FILE]) {
1120
+ assertSafeProjectPath(targetDir, path.join(agentsDirectory, fileName));
540
1121
  }
541
-
542
- copyFile(SOURCE_LOCK_FILE, path.join(targetDir, "skills-lock.json"));
543
-
544
- for (const toolKey of tools) {
545
- const tool = TOOL_BY_KEY.get(toolKey);
546
- const destination = tool.destination(targetDir);
547
- ensureDirectory(destination);
548
-
1122
+ assertManagedFilesUnchanged(agentsDirectory);
1123
+ const previousManagedSkills = validateManagedSkillNames(
1124
+ readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
1125
+ ".agents/macca-managed-skills.txt"
1126
+ );
1127
+ const entries = [];
1128
+ for (const destination of getUniqueDestinations(targetDir, tools)) {
549
1129
  for (const skillName of managedSkills) {
550
- copyDirectory(path.join(SOURCE_SKILLS_DIR, skillName), path.join(destination, skillName));
1130
+ entries.push({
1131
+ sourcePath: path.join(SOURCE_SKILLS_DIR, skillName),
1132
+ targetPath: resolveOwnedSkillPath(destination, skillName),
1133
+ legacySkillName: skillName
1134
+ });
551
1135
  }
552
1136
  }
553
-
554
- if (!tools.includes("codex")) {
555
- fs.rmSync(path.join(agentsDirectory, "skills"), { recursive: true, force: true });
556
- }
557
-
558
- writeTextFile(path.join(agentsDirectory, "macca-tools.txt"), `${tools.join("\n")}\n`);
559
- writeTextFile(
560
- path.join(agentsDirectory, "developer-config.json"),
561
- `${JSON.stringify(buildDeveloperConfig(metadata), null, 2)}\n`
1137
+ const configPath = path.join(agentsDirectory, "developer-config.json");
1138
+ const config = mergeObjects(readDeveloperConfig(configPath), buildDeveloperConfig(metadata));
1139
+ const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
1140
+ ? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
1141
+ : `${managedSkills.join("\n")}\n`;
1142
+ const lockContent = buildInstalledLock(managedSkills);
1143
+ const toolsContent = `${tools.join("\n")}\n`;
1144
+ const stateContent = buildStateContent({
1145
+ "macca-managed-skills.txt": managedContent,
1146
+ "macca-lock.json": lockContent,
1147
+ "macca-tools.txt": toolsContent
1148
+ });
1149
+ entries.push(
1150
+ {
1151
+ kind: "file",
1152
+ content: managedContent,
1153
+ targetPath: path.join(agentsDirectory, "macca-managed-skills.txt")
1154
+ },
1155
+ { kind: "file", content: lockContent, targetPath: path.join(agentsDirectory, "macca-lock.json") },
1156
+ { kind: "file", content: toolsContent, targetPath: path.join(agentsDirectory, "macca-tools.txt") },
1157
+ { kind: "file", content: `${JSON.stringify(config, null, 2)}\n`, targetPath: configPath },
1158
+ { kind: "file", content: stateContent, targetPath: path.join(agentsDirectory, STATE_FILE) }
562
1159
  );
1160
+ applySkillTransaction(entries, targetDir);
563
1161
  }
564
1162
 
565
1163
  function applyUpgrade(targetDir) {
566
1164
  const agentsDirectory = path.join(targetDir, ".agents");
1165
+ assertSafeProjectPath(targetDir, agentsDirectory);
1166
+ assertPackageNotOlderThanInstalled(agentsDirectory);
1167
+ for (const fileName of ["macca-managed-skills.txt", "macca-tools.txt", "developer-config.json", "macca-lock.json", STATE_FILE]) {
1168
+ assertSafeProjectPath(targetDir, path.join(agentsDirectory, fileName));
1169
+ }
1170
+ assertManagedFilesUnchanged(agentsDirectory);
567
1171
  const tools = readNonEmptyLines(path.join(agentsDirectory, "macca-tools.txt"));
568
1172
  if (tools.length === 0) {
569
1173
  throw new Error(".agents/macca-tools.txt was not found. Run install first so MACCA knows which tools to update.");
570
1174
  }
571
1175
 
572
- const previousManagedSkills = readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt"));
1176
+ const previousManagedSkills = validateManagedSkillNames(
1177
+ readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
1178
+ ".agents/macca-managed-skills.txt"
1179
+ );
573
1180
  const nextManagedSkills = getSourceManagedSkills();
574
- const skillsToClean = unique([...previousManagedSkills, ...nextManagedSkills]);
575
-
576
- for (const toolKey of tools) {
577
- const tool = TOOL_BY_KEY.get(toolKey);
578
- if (!tool) {
579
- throw new Error(`Unsupported tool in .agents/macca-tools.txt: ${toolKey}`);
1181
+ const entries = [];
1182
+ for (const destination of getUniqueDestinations(targetDir, tools)) {
1183
+ for (const skillName of nextManagedSkills) {
1184
+ entries.push({
1185
+ sourcePath: path.join(SOURCE_SKILLS_DIR, skillName),
1186
+ targetPath: resolveOwnedSkillPath(destination, skillName),
1187
+ legacySkillName: skillName
1188
+ });
580
1189
  }
581
1190
 
582
- const destination = tool.destination(targetDir);
583
- ensureDirectory(destination);
584
-
585
- for (const skillName of skillsToClean) {
586
- fs.rmSync(path.join(destination, skillName), { recursive: true, force: true });
1191
+ for (const skillName of previousManagedSkills.filter((name) => !nextManagedSkills.includes(name))) {
1192
+ const oldPath = resolveOwnedSkillPath(destination, skillName);
1193
+ if (fs.existsSync(oldPath) && isMaccaOwned(oldPath)) {
1194
+ assertManagedDirectoryUnchanged(oldPath);
1195
+ entries.push({ sourcePath: null, targetPath: oldPath });
1196
+ } else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
1197
+ entries.push({ sourcePath: null, targetPath: oldPath, legacySkillName: skillName });
1198
+ } else if (fs.existsSync(oldPath)) {
1199
+ process.stderr.write(`Preserving unmarked legacy skill directory: ${oldPath}\n`);
1200
+ }
587
1201
  }
1202
+ }
588
1203
 
589
- for (const skillName of nextManagedSkills) {
590
- copyDirectory(path.join(SOURCE_SKILLS_DIR, skillName), path.join(destination, skillName));
1204
+ if (tools.includes("opencode")) {
1205
+ const legacyOpenCodeRoot = path.join(targetDir, ".opencode", "skill");
1206
+ assertSafeProjectPath(targetDir, legacyOpenCodeRoot);
1207
+ for (const skillName of previousManagedSkills) {
1208
+ const oldPath = resolveOwnedSkillPath(legacyOpenCodeRoot, skillName);
1209
+ if (fs.existsSync(oldPath) && isMaccaOwned(oldPath)) {
1210
+ assertManagedDirectoryUnchanged(oldPath);
1211
+ entries.push({ sourcePath: null, targetPath: oldPath });
1212
+ } else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
1213
+ entries.push({ sourcePath: null, targetPath: oldPath, legacySkillName: skillName });
1214
+ } else if (fs.existsSync(oldPath)) {
1215
+ throw new Error(`Legacy OpenCode skill was modified; move or back it up before upgrade: ${oldPath}`);
1216
+ }
591
1217
  }
592
1218
  }
1219
+ const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
1220
+ ? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
1221
+ : `${nextManagedSkills.join("\n")}\n`;
1222
+ const lockContent = buildInstalledLock(nextManagedSkills);
1223
+ const toolsContent = `${tools.join("\n")}\n`;
1224
+ const stateContent = buildStateContent({
1225
+ "macca-managed-skills.txt": managedContent,
1226
+ "macca-lock.json": lockContent,
1227
+ "macca-tools.txt": toolsContent
1228
+ });
1229
+ entries.push(
1230
+ {
1231
+ kind: "file",
1232
+ content: managedContent,
1233
+ targetPath: path.join(agentsDirectory, "macca-managed-skills.txt")
1234
+ },
1235
+ { kind: "file", content: lockContent, targetPath: path.join(agentsDirectory, "macca-lock.json") },
1236
+ { kind: "file", content: stateContent, targetPath: path.join(agentsDirectory, STATE_FILE) }
1237
+ );
1238
+ applySkillTransaction(entries, targetDir);
593
1239
 
594
- if (!tools.includes("codex")) {
595
- fs.rmSync(path.join(agentsDirectory, "skills"), { recursive: true, force: true });
1240
+ if (tools.includes("kimi")) reportLegacyKimiInstall();
1241
+ }
1242
+
1243
+ function getLegacyKimiRoot() {
1244
+ if (process.platform === "win32") {
1245
+ return path.join(
1246
+ process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"),
1247
+ "agents",
1248
+ "skills"
1249
+ );
596
1250
  }
1251
+ return path.join(os.homedir(), ".config", "agents", "skills");
1252
+ }
597
1253
 
598
- if (fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)) {
599
- copyFile(SOURCE_MANAGED_SKILLS_FILE, path.join(agentsDirectory, "macca-managed-skills.txt"));
600
- } else {
601
- writeTextFile(path.join(agentsDirectory, "macca-managed-skills.txt"), `${nextManagedSkills.join("\n")}\n`);
1254
+ function reportLegacyKimiInstall() {
1255
+ const legacyRoot = getLegacyKimiRoot();
1256
+ if (!fs.existsSync(legacyRoot)) return;
1257
+
1258
+ const legacyHashes = getLegacyPayloadHashes();
1259
+ const detected = [];
1260
+ for (const [skillName, acceptedHashes] of Object.entries(legacyHashes)) {
1261
+ const skillPath = path.join(legacyRoot, skillName);
1262
+ if (!fs.existsSync(skillPath)) continue;
1263
+ const stat = fs.lstatSync(skillPath);
1264
+ if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
1265
+ const payloadHash = hashDirectory(skillPath);
1266
+ detected.push({ skillPath, unchanged: acceptedHashes.has(payloadHash) });
602
1267
  }
603
1268
 
604
- copyFile(SOURCE_LOCK_FILE, path.join(targetDir, "skills-lock.json"));
1269
+ if (!detected.length) return;
1270
+ process.stderr.write(
1271
+ `Legacy Kimi copies remain outside this project at ${legacyRoot}. `
1272
+ + "MACCA did not delete global files. Verify the new project-local .agents/skills installation, "
1273
+ + "then remove old copies manually if your Kimi version still discovers them.\n"
1274
+ );
605
1275
  }
606
1276
 
607
1277
  function printInstallSummary(action, tools) {
@@ -619,16 +1289,38 @@ async function runInstall(args) {
619
1289
  tools = args.yes ? ["codex"] : await promptForTools();
620
1290
  }
621
1291
 
1292
+ const targetDir = assertSafeTargetDirectory(args.directory);
1293
+ ensureDirectory(targetDir);
1294
+ assertSafeProjectPath(targetDir, path.join(targetDir, ".agents"));
1295
+ recoverInterruptedTransaction(targetDir);
1296
+ const existingTools = readNonEmptyLines(path.join(targetDir, ".agents", "macca-tools.txt"));
1297
+ tools = unique([...existingTools, ...tools]);
1298
+ const configPath = path.join(targetDir, ".agents", "developer-config.json");
1299
+ const existingConfig = readDeveloperConfig(configPath);
1300
+ const hasExistingConfig = fs.existsSync(configPath);
622
1301
  const metadata = args.yes
623
1302
  ? {
624
- name: args.name || "",
625
- project: args.project || "",
626
- communicationLanguage: args.communicationLanguage || "Bahasa Indonesia",
627
- documentLanguage: args.documentLanguage || "Bahasa Indonesia"
1303
+ name: args.name !== undefined ? args.name : (hasExistingConfig ? undefined : ""),
1304
+ project: args.project !== undefined ? args.project : (hasExistingConfig ? undefined : ""),
1305
+ communicationLanguage: args.communicationLanguage !== undefined
1306
+ ? args.communicationLanguage
1307
+ : (hasExistingConfig ? undefined : "Bahasa Indonesia"),
1308
+ documentLanguage: args.documentLanguage !== undefined
1309
+ ? args.documentLanguage
1310
+ : (hasExistingConfig ? undefined : "Bahasa Indonesia")
628
1311
  }
629
- : await promptForMetadata(args);
1312
+ : await promptForMetadata({
1313
+ ...args,
1314
+ name: args.name !== undefined ? args.name : existingConfig.name,
1315
+ project: args.project !== undefined ? args.project : existingConfig.project,
1316
+ communicationLanguage: args.communicationLanguage !== undefined
1317
+ ? args.communicationLanguage
1318
+ : existingConfig.languagePreferences?.communication?.raw,
1319
+ documentLanguage: args.documentLanguage !== undefined
1320
+ ? args.documentLanguage
1321
+ : existingConfig.languagePreferences?.documents?.raw
1322
+ });
630
1323
 
631
- const targetDir = resolveTargetDirectory(args.directory);
632
1324
  applyInstall(targetDir, tools, metadata);
633
1325
 
634
1326
  printInstallSummary("installed", tools);
@@ -636,7 +1328,9 @@ async function runInstall(args) {
636
1328
  }
637
1329
 
638
1330
  function runUpgrade(args) {
639
- const targetDir = resolveTargetDirectory(args.directory);
1331
+ const targetDir = assertSafeTargetDirectory(args.directory);
1332
+ assertSafeProjectPath(targetDir, path.join(targetDir, ".agents"));
1333
+ recoverInterruptedTransaction(targetDir);
640
1334
  const tools = readNonEmptyLines(path.join(targetDir, ".agents", "macca-tools.txt"));
641
1335
  applyUpgrade(targetDir);
642
1336
  printInstallSummary("updated", tools);
@@ -648,4 +1342,4 @@ function exitWithError(message) {
648
1342
  process.exit(1);
649
1343
  }
650
1344
 
651
- main();
1345
+ main();