macca-method 2.1.1 → 2.1.2

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 (60) hide show
  1. package/.agents/macca-lock.json +1 -1
  2. package/.agents/skills/_shared/references/brainstorm-session.md +5 -5
  3. package/.agents/skills/_shared/references/invocation-policy.md +20 -20
  4. package/.agents/skills/_shared/references/output-ownership.md +11 -11
  5. package/.agents/skills/_shared/references/scope-rules.md +1 -1
  6. package/.agents/skills/_shared/references/skill-catalog.md +20 -20
  7. package/.agents/skills/_shared/scripts/validate-skills.py +37 -15
  8. package/.agents/skills/add-feature/SKILL.md +9 -3
  9. package/.agents/skills/antislop-copywriting/SKILL.md +372 -0
  10. package/.agents/skills/brainstorm-api/SKILL.md +28 -16
  11. package/.agents/skills/brainstorm-api/assets/api.template.md +35 -15
  12. package/.agents/skills/brainstorm-architecture/SKILL.md +35 -15
  13. package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +44 -25
  14. package/.agents/skills/brainstorm-prd/SKILL.md +47 -17
  15. package/.agents/skills/brainstorm-prd/assets/PRD.template.md +47 -23
  16. package/.agents/skills/brainstorm-rules/SKILL.md +36 -19
  17. package/.agents/skills/brainstorm-rules/assets/rules.template.md +32 -18
  18. package/.agents/skills/brainstorm-schema/SKILL.md +18 -8
  19. package/.agents/skills/brainstorm-schema/assets/schema.template.md +25 -10
  20. package/.agents/skills/brainstorm-styleguide/SKILL.md +37 -19
  21. package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +78 -60
  22. package/.agents/skills/brainstorm-task/SKILL.md +27 -14
  23. package/.agents/skills/brainstorm-task/assets/Task.template.md +29 -18
  24. package/.agents/skills/bug-fix/SKILL.md +25 -1
  25. package/.agents/skills/code-review/SKILL.md +7 -7
  26. package/.agents/skills/code-review/references/review-checklist.md +21 -10
  27. package/.agents/skills/developer/SKILL.md +8 -0
  28. package/.agents/skills/developer/references/execute-task.md +13 -7
  29. package/.agents/skills/help/SKILL.md +32 -20
  30. package/.agents/skills/meet/SKILL.md +9 -4
  31. package/.agents/skills/quick-dev/SKILL.md +27 -22
  32. package/.agents/skills/release-readiness/SKILL.md +17 -13
  33. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  34. package/.agents/skills/skill-creator/SKILL.md +485 -0
  35. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  36. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  37. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  38. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  39. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  40. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  41. package/.agents/skills/skill-creator/references/schemas.md +441 -0
  42. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  43. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  44. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  45. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  46. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  47. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  48. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  49. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  50. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  51. package/.agents/skills/spec-audit/SKILL.md +28 -1
  52. package/.agents/skills/spec-compliance/SKILL.md +31 -18
  53. package/.agents/skills/spec-init/SKILL.md +29 -17
  54. package/README.md +158 -122
  55. package/bin/macca-method.js +1378 -1077
  56. package/package.json +40 -40
  57. package/scripts/run-skill-validator.js +27 -9
  58. package/scripts/test-install.js +599 -357
  59. package/scripts/test-upgrade-legacy.js +119 -100
  60. package/scripts/validate-skill-behavior.js +175 -64
@@ -11,1335 +11,1636 @@ 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 SOURCE_MANAGED_SKILLS_FILE = path.join(SOURCE_AGENTS_DIR, "macca-managed-skills.txt");
14
+ const SOURCE_MANAGED_SKILLS_FILE = path.join(
15
+ SOURCE_AGENTS_DIR,
16
+ "macca-managed-skills.txt",
17
+ );
15
18
  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");
19
+ const SOURCE_LEGACY_PAYLOADS_FILE = path.join(
20
+ SOURCE_AGENTS_DIR,
21
+ "legacy-payloads.json",
22
+ );
17
23
  const OWNERSHIP_MARKER = ".macca-owned.json";
18
24
  const TRANSACTION_FILE = "macca-transaction.json";
19
25
  const STATE_FILE = "macca-state.json";
20
26
 
21
27
  const TOOL_DEFINITIONS = [
22
- {
23
- key: "copilot",
24
- aliases: ["github-copilot"],
25
- label: "GitHub Copilot",
26
- destination: (targetDir) => path.join(targetDir, ".github", "skills"),
27
- displayDestination: ".github/skills/"
28
- },
29
- {
30
- key: "cursor",
31
- aliases: [],
32
- label: "Cursor",
33
- destination: (targetDir) => path.join(targetDir, ".cursor", "skills"),
34
- displayDestination: ".cursor/skills/"
35
- },
36
- {
37
- key: "claude",
38
- aliases: ["claude-code"],
39
- label: "Claude Code",
40
- destination: (targetDir) => path.join(targetDir, ".claude", "skills"),
41
- displayDestination: ".claude/skills/"
42
- },
43
- {
44
- key: "windsurf",
45
- aliases: [],
46
- label: "Windsurf",
47
- destination: (targetDir) => path.join(targetDir, ".windsurf", "skills"),
48
- displayDestination: ".windsurf/skills/"
49
- },
50
- {
51
- key: "gemini",
52
- aliases: ["gemini-cli"],
53
- label: "Gemini CLI",
54
- destination: (targetDir) => path.join(targetDir, ".gemini", "skills"),
55
- displayDestination: ".gemini/skills/"
56
- },
57
- {
58
- key: "opencode",
59
- aliases: [],
60
- label: "OpenCode",
61
- destination: (targetDir) => path.join(targetDir, ".opencode", "skills"),
62
- displayDestination: ".opencode/skills/"
63
- },
64
- {
65
- key: "kilo",
66
- aliases: ["kilo-code"],
67
- label: "Kilo Code",
68
- destination: (targetDir) => path.join(targetDir, ".kilo", "skills"),
69
- displayDestination: ".kilo/skills/"
70
- },
71
- {
72
- key: "codex",
73
- aliases: ["openai-codex"],
74
- label: "Codex (OpenAI)",
75
- destination: (targetDir) => path.join(targetDir, ".agents", "skills"),
76
- displayDestination: ".agents/skills/"
77
- },
78
- {
79
- key: "kimi",
80
- aliases: ["kimi-cli"],
81
- label: "Kimi CLI",
82
- destination: (targetDir) => path.join(targetDir, ".agents", "skills"),
83
- displayDestination: ".agents/skills/"
84
- }
28
+ {
29
+ key: "copilot",
30
+ aliases: ["github-copilot"],
31
+ label: "GitHub Copilot",
32
+ destination: (targetDir) => path.join(targetDir, ".github", "skills"),
33
+ displayDestination: ".github/skills/",
34
+ },
35
+ {
36
+ key: "cursor",
37
+ aliases: [],
38
+ label: "Cursor",
39
+ destination: (targetDir) => path.join(targetDir, ".cursor", "skills"),
40
+ displayDestination: ".cursor/skills/",
41
+ },
42
+ {
43
+ key: "claude",
44
+ aliases: ["claude-code"],
45
+ label: "Claude Code",
46
+ destination: (targetDir) => path.join(targetDir, ".claude", "skills"),
47
+ displayDestination: ".claude/skills/",
48
+ },
49
+ {
50
+ key: "windsurf",
51
+ aliases: [],
52
+ label: "Windsurf",
53
+ destination: (targetDir) => path.join(targetDir, ".windsurf", "skills"),
54
+ displayDestination: ".windsurf/skills/",
55
+ },
56
+ {
57
+ key: "gemini",
58
+ aliases: ["gemini-cli"],
59
+ label: "Gemini CLI",
60
+ destination: (targetDir) => path.join(targetDir, ".gemini", "skills"),
61
+ displayDestination: ".gemini/skills/",
62
+ },
63
+ {
64
+ key: "opencode",
65
+ aliases: [],
66
+ label: "OpenCode",
67
+ destination: (targetDir) => path.join(targetDir, ".opencode", "skills"),
68
+ displayDestination: ".opencode/skills/",
69
+ },
70
+ {
71
+ key: "kilo",
72
+ aliases: ["kilo-code"],
73
+ label: "Kilo Code",
74
+ destination: (targetDir) => path.join(targetDir, ".kilo", "skills"),
75
+ displayDestination: ".kilo/skills/",
76
+ },
77
+ {
78
+ key: "codex",
79
+ aliases: ["openai-codex"],
80
+ label: "Codex (OpenAI)",
81
+ destination: (targetDir) => path.join(targetDir, ".agents", "skills"),
82
+ displayDestination: ".agents/skills/",
83
+ },
84
+ {
85
+ key: "kimi",
86
+ aliases: ["kimi-cli"],
87
+ label: "Kimi CLI",
88
+ destination: (targetDir) => path.join(targetDir, ".agents", "skills"),
89
+ displayDestination: ".agents/skills/",
90
+ },
85
91
  ];
86
92
 
87
93
  const TOOL_BY_KEY = new Map(TOOL_DEFINITIONS.map((tool) => [tool.key, tool]));
88
94
  const TOOL_LOOKUP = new Map();
89
95
  for (const tool of TOOL_DEFINITIONS) {
90
- TOOL_LOOKUP.set(tool.key, tool.key);
91
- for (const alias of tool.aliases) {
92
- TOOL_LOOKUP.set(alias, tool.key);
93
- }
96
+ TOOL_LOOKUP.set(tool.key, tool.key);
97
+ for (const alias of tool.aliases) {
98
+ TOOL_LOOKUP.set(alias, tool.key);
99
+ }
94
100
  }
95
101
 
96
102
  function main() {
97
- try {
98
- const args = parseArgs(process.argv.slice(2));
99
- const command = args._[0] || "help";
100
-
101
- if (args.version) {
102
- const packageJson = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
103
- process.stdout.write(`${packageJson.version}\n`);
104
- return;
105
- }
103
+ try {
104
+ const args = parseArgs(process.argv.slice(2));
105
+ const command = args._[0] || "help";
106
106
 
107
- if (args.listTools) {
108
- printToolList();
109
- return;
110
- }
107
+ if (args.version) {
108
+ const packageJson = JSON.parse(
109
+ fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"),
110
+ );
111
+ process.stdout.write(`${packageJson.version}\n`);
112
+ return;
113
+ }
111
114
 
112
- if (args.help || command === "help") {
113
- printHelp();
114
- return;
115
- }
115
+ if (args.listTools) {
116
+ printToolList();
117
+ return;
118
+ }
116
119
 
117
- ensurePackagedFiles();
120
+ if (args.help || command === "help") {
121
+ printHelp();
122
+ return;
123
+ }
118
124
 
119
- if (command === "install") {
120
- runInstall(args).catch((error) => exitWithError(error.message));
121
- return;
122
- }
125
+ ensurePackagedFiles();
123
126
 
124
- if (command === "upgrade") {
125
- runUpgrade(args);
126
- return;
127
- }
127
+ if (command === "install") {
128
+ runInstall(args).catch((error) => exitWithError(error.message));
129
+ return;
130
+ }
128
131
 
129
- exitWithError(`Unknown command: ${command}`);
130
- } catch (error) {
131
- exitWithError(error.message);
132
+ if (command === "upgrade") {
133
+ runUpgrade(args);
134
+ return;
132
135
  }
136
+
137
+ exitWithError(`Unknown command: ${command}`);
138
+ } catch (error) {
139
+ exitWithError(error.message);
140
+ }
133
141
  }
134
142
 
135
143
  function parseArgs(argv) {
136
- const args = {
137
- _: [],
138
- tools: []
139
- };
144
+ const args = {
145
+ _: [],
146
+ tools: [],
147
+ };
140
148
 
141
- for (let index = 0; index < argv.length; index += 1) {
142
- const token = argv[index];
143
-
144
- if (token === "-h" || token === "--help") {
145
- args.help = true;
146
- continue;
147
- }
149
+ for (let index = 0; index < argv.length; index += 1) {
150
+ const token = argv[index];
148
151
 
149
- if (token === "-v" || token === "--version") {
150
- args.version = true;
151
- continue;
152
- }
152
+ if (token === "-h" || token === "--help") {
153
+ args.help = true;
154
+ continue;
155
+ }
153
156
 
154
- if (token === "-y" || token === "--yes") {
155
- args.yes = true;
156
- continue;
157
- }
157
+ if (token === "-v" || token === "--version") {
158
+ args.version = true;
159
+ continue;
160
+ }
158
161
 
159
- if (token === "--list-tools") {
160
- args.listTools = true;
161
- continue;
162
- }
162
+ if (token === "-y" || token === "--yes") {
163
+ args.yes = true;
164
+ continue;
165
+ }
163
166
 
164
- if (token === "-t" || token === "--tool" || token === "--tools") {
165
- const result = takeValue(argv, index, token);
166
- args.tools.push(result.value);
167
- index = result.index;
168
- continue;
169
- }
167
+ if (token === "--list-tools") {
168
+ args.listTools = true;
169
+ continue;
170
+ }
170
171
 
171
- if (token.startsWith("--tool=") || token.startsWith("--tools=")) {
172
- args.tools.push(token.slice(token.indexOf("=") + 1));
173
- continue;
174
- }
172
+ if (token === "-t" || token === "--tool" || token === "--tools") {
173
+ const result = takeValue(argv, index, token);
174
+ args.tools.push(result.value);
175
+ index = result.index;
176
+ continue;
177
+ }
175
178
 
176
- if (token === "-d" || token === "--directory") {
177
- const result = takeValue(argv, index, token);
178
- args.directory = result.value;
179
- index = result.index;
180
- continue;
181
- }
179
+ if (token.startsWith("--tool=") || token.startsWith("--tools=")) {
180
+ args.tools.push(token.slice(token.indexOf("=") + 1));
181
+ continue;
182
+ }
182
183
 
183
- if (token.startsWith("--directory=")) {
184
- args.directory = token.slice(token.indexOf("=") + 1);
185
- continue;
186
- }
184
+ if (token === "-d" || token === "--directory") {
185
+ const result = takeValue(argv, index, token);
186
+ args.directory = result.value;
187
+ index = result.index;
188
+ continue;
189
+ }
187
190
 
188
- if (token === "--name") {
189
- const result = takeValue(argv, index, token);
190
- args.name = result.value;
191
- index = result.index;
192
- continue;
193
- }
191
+ if (token.startsWith("--directory=")) {
192
+ args.directory = token.slice(token.indexOf("=") + 1);
193
+ continue;
194
+ }
194
195
 
195
- if (token.startsWith("--name=")) {
196
- args.name = token.slice(token.indexOf("=") + 1);
197
- continue;
198
- }
196
+ if (token === "--name") {
197
+ const result = takeValue(argv, index, token);
198
+ args.name = result.value;
199
+ index = result.index;
200
+ continue;
201
+ }
199
202
 
200
- if (token === "--project") {
201
- const result = takeValue(argv, index, token);
202
- args.project = result.value;
203
- index = result.index;
204
- continue;
205
- }
203
+ if (token.startsWith("--name=")) {
204
+ args.name = token.slice(token.indexOf("=") + 1);
205
+ continue;
206
+ }
206
207
 
207
- if (token.startsWith("--project=")) {
208
- args.project = token.slice(token.indexOf("=") + 1);
209
- continue;
210
- }
208
+ if (token === "--project") {
209
+ const result = takeValue(argv, index, token);
210
+ args.project = result.value;
211
+ index = result.index;
212
+ continue;
213
+ }
211
214
 
212
- if (token === "--communication-language") {
213
- const result = takeValue(argv, index, token);
214
- args.communicationLanguage = result.value;
215
- index = result.index;
216
- continue;
217
- }
215
+ if (token.startsWith("--project=")) {
216
+ args.project = token.slice(token.indexOf("=") + 1);
217
+ continue;
218
+ }
218
219
 
219
- if (token.startsWith("--communication-language=")) {
220
- args.communicationLanguage = token.slice(token.indexOf("=") + 1);
221
- continue;
222
- }
220
+ if (token === "--communication-language") {
221
+ const result = takeValue(argv, index, token);
222
+ args.communicationLanguage = result.value;
223
+ index = result.index;
224
+ continue;
225
+ }
223
226
 
224
- if (token === "--document-language" || token === "--documents-language") {
225
- const result = takeValue(argv, index, token);
226
- args.documentLanguage = result.value;
227
- index = result.index;
228
- continue;
229
- }
227
+ if (token.startsWith("--communication-language=")) {
228
+ args.communicationLanguage = token.slice(token.indexOf("=") + 1);
229
+ continue;
230
+ }
230
231
 
231
- if (token.startsWith("--document-language=") || token.startsWith("--documents-language=")) {
232
- args.documentLanguage = token.slice(token.indexOf("=") + 1);
233
- continue;
234
- }
232
+ if (token === "--document-language" || token === "--documents-language") {
233
+ const result = takeValue(argv, index, token);
234
+ args.documentLanguage = result.value;
235
+ index = result.index;
236
+ continue;
237
+ }
235
238
 
236
- if (token.startsWith("-")) {
237
- throw new Error(`Unknown option: ${token}`);
238
- }
239
+ if (
240
+ token.startsWith("--document-language=") ||
241
+ token.startsWith("--documents-language=")
242
+ ) {
243
+ args.documentLanguage = token.slice(token.indexOf("=") + 1);
244
+ continue;
245
+ }
239
246
 
240
- args._.push(token);
247
+ if (token.startsWith("-")) {
248
+ throw new Error(`Unknown option: ${token}`);
241
249
  }
242
250
 
243
- return args;
251
+ args._.push(token);
252
+ }
253
+
254
+ return args;
244
255
  }
245
256
 
246
257
  function takeValue(argv, index, flag) {
247
- const value = argv[index + 1];
248
- if (value === undefined) {
249
- throw new Error(`Missing value for ${flag}`);
250
- }
251
-
252
- return {
253
- value,
254
- index: index + 1
255
- };
258
+ const value = argv[index + 1];
259
+ if (value === undefined) {
260
+ throw new Error(`Missing value for ${flag}`);
261
+ }
262
+
263
+ return {
264
+ value,
265
+ index: index + 1,
266
+ };
256
267
  }
257
268
 
258
269
  function ensurePackagedFiles() {
259
- if (!fs.existsSync(SOURCE_SKILLS_DIR)) {
260
- throw new Error("Packaged skills directory is missing. Reinstall the package or run from the repository root.");
261
- }
270
+ if (!fs.existsSync(SOURCE_SKILLS_DIR)) {
271
+ throw new Error(
272
+ "Packaged skills directory is missing. Reinstall the package or run from the repository root.",
273
+ );
274
+ }
262
275
 
263
- if (!fs.existsSync(SOURCE_LOCK_FILE)) {
264
- throw new Error(".agents/macca-lock.json is missing from the package.");
265
- }
276
+ if (!fs.existsSync(SOURCE_LOCK_FILE)) {
277
+ throw new Error(".agents/macca-lock.json is missing from the package.");
278
+ }
266
279
  }
267
280
 
268
281
  function printHelp() {
269
- process.stdout.write(
270
- [
271
- "MACCA CLI",
272
- "",
273
- "Usage:",
274
- " npx macca-method@latest install [options]",
275
- " npx macca-method@latest upgrade [options]",
276
- " npx macca-method@latest --list-tools",
277
- "",
278
- "Install options:",
279
- " -t, --tool <name> Repeatable. Also accepts comma-separated values.",
280
- " -d, --directory <path> Target project directory. Defaults to the current directory.",
281
- " -y, --yes Skip prompts and use defaults where needed.",
282
- " --name <value> Developer name.",
283
- " --project <value> Project name.",
284
- " --communication-language <value>",
285
- " --document-language <value>",
286
- "",
287
- "Examples:",
288
- " npx macca-method@latest install",
289
- " npx macca-method@latest install --tool github-copilot --tool codex --yes",
290
- " npx macca-method@latest upgrade",
291
- ""
292
- ].join("\n")
293
- );
282
+ process.stdout.write(
283
+ [
284
+ "MACCA CLI",
285
+ "",
286
+ "Usage:",
287
+ " npx macca-method@latest install [options]",
288
+ " npx macca-method@latest upgrade [options]",
289
+ " npx macca-method@latest --list-tools",
290
+ "",
291
+ "Install options:",
292
+ " -t, --tool <name> Repeatable. Also accepts comma-separated values.",
293
+ " -d, --directory <path> Target project directory. Defaults to the current directory.",
294
+ " -y, --yes Skip prompts and use defaults where needed.",
295
+ " --name <value> Developer name.",
296
+ " --project <value> Project name.",
297
+ " --communication-language <value>",
298
+ " --document-language <value>",
299
+ "",
300
+ "Examples:",
301
+ " npx macca-method@latest install",
302
+ " npx macca-method@latest install --tool github-copilot --tool codex --yes",
303
+ " npx macca-method@latest upgrade",
304
+ "",
305
+ ].join("\n"),
306
+ );
294
307
  }
295
308
 
296
309
  function printToolList() {
297
- process.stdout.write("Supported tools:\n");
298
- TOOL_DEFINITIONS.forEach((tool, index) => {
299
- const aliases = tool.aliases.length > 0 ? ` (aliases: ${tool.aliases.join(", ")})` : "";
300
- process.stdout.write(` ${index + 1}. ${tool.key} -> ${tool.label}${aliases}\n`);
301
- });
310
+ process.stdout.write("Supported tools:\n");
311
+ TOOL_DEFINITIONS.forEach((tool, index) => {
312
+ const aliases =
313
+ tool.aliases.length > 0 ? ` (aliases: ${tool.aliases.join(", ")})` : "";
314
+ process.stdout.write(
315
+ ` ${index + 1}. ${tool.key} -> ${tool.label}${aliases}\n`,
316
+ );
317
+ });
302
318
  }
303
319
 
304
320
  function resolveTargetDirectory(rawDirectory) {
305
- if (!rawDirectory) {
306
- return process.cwd();
307
- }
321
+ if (!rawDirectory) {
322
+ return process.cwd();
323
+ }
308
324
 
309
- return path.resolve(process.cwd(), rawDirectory);
325
+ return path.resolve(process.cwd(), rawDirectory);
310
326
  }
311
327
 
312
328
  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
-
329
+ const requested = rawDirectory
330
+ ? path.resolve(process.cwd(), rawDirectory)
331
+ : process.cwd();
332
+ const absolute = path.isAbsolute(requested)
333
+ ? requested
334
+ : path.resolve(requested);
335
+ const segments = absolute.split(path.sep).filter(Boolean);
336
+ let current = absolute.startsWith(path.sep)
337
+ ? path.sep
338
+ : segments.shift() || absolute;
339
+
340
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
341
+ throw new Error(`Refusing symlinked target project ancestor: ${current}`);
342
+ }
343
+
344
+ for (const segment of segments) {
345
+ current =
346
+ current === path.sep
347
+ ? path.join(current, segment)
348
+ : path.join(current, segment);
318
349
  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
- }
350
+ throw new Error(`Refusing symlinked target project ancestor: ${current}`);
327
351
  }
352
+ }
328
353
 
329
- return absolute;
354
+ return absolute;
330
355
  }
331
356
 
332
357
  function readNonEmptyLines(filePath) {
333
- if (!fs.existsSync(filePath)) {
334
- return [];
335
- }
336
-
337
- return fs
338
- .readFileSync(filePath, "utf8")
339
- .split(/\r?\n/)
340
- .map((line) => line.trim())
341
- .filter(Boolean);
358
+ if (!fs.existsSync(filePath)) {
359
+ return [];
360
+ }
361
+
362
+ return fs
363
+ .readFileSync(filePath, "utf8")
364
+ .split(/\r?\n/)
365
+ .map((line) => line.trim())
366
+ .filter(Boolean);
342
367
  }
343
368
 
344
369
  function unique(values) {
345
- return [...new Set(values)];
370
+ return [...new Set(values)];
346
371
  }
347
372
 
348
373
  function getSourceManagedSkills() {
349
- const managedSkills = readNonEmptyLines(SOURCE_MANAGED_SKILLS_FILE);
350
- if (managedSkills.length > 0) {
351
- return validateManagedSkillNames(managedSkills, "packaged managed-skill manifest");
352
- }
353
-
354
- return validateManagedSkillNames(fs
355
- .readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true })
356
- .filter((entry) => entry.isDirectory())
357
- .map((entry) => entry.name)
358
- .sort(), "packaged skills directory");
374
+ const managedSkills = readNonEmptyLines(SOURCE_MANAGED_SKILLS_FILE);
375
+ if (managedSkills.length > 0) {
376
+ return validateManagedSkillNames(
377
+ managedSkills,
378
+ "packaged managed-skill manifest",
379
+ );
380
+ }
381
+
382
+ return validateManagedSkillNames(
383
+ fs
384
+ .readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true })
385
+ .filter((entry) => entry.isDirectory())
386
+ .map((entry) => entry.name)
387
+ .sort(),
388
+ "packaged skills directory",
389
+ );
359
390
  }
360
391
 
361
392
  function validateManagedSkillNames(skillNames, sourceLabel) {
362
- const validName = /^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/;
393
+ const validName = /^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/;
363
394
 
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
- }
395
+ for (const skillName of skillNames) {
396
+ if (
397
+ !validName.test(skillName) ||
398
+ skillName.includes("..") ||
399
+ path.basename(skillName) !== skillName
400
+ ) {
401
+ throw new Error(`Unsafe skill name in ${sourceLabel}: ${skillName}`);
368
402
  }
403
+ }
369
404
 
370
- return unique(skillNames);
405
+ return unique(skillNames);
371
406
  }
372
407
 
373
408
  function resolveOwnedSkillPath(destination, skillName) {
374
- validateManagedSkillNames([skillName], "managed-skill manifest");
375
- const root = path.resolve(destination);
376
- const target = path.resolve(root, skillName);
409
+ validateManagedSkillNames([skillName], "managed-skill manifest");
410
+ const root = path.resolve(destination);
411
+ const target = path.resolve(root, skillName);
377
412
 
378
- if (!target.startsWith(`${root}${path.sep}`)) {
379
- throw new Error(`Refusing path outside skill destination: ${skillName}`);
380
- }
413
+ if (!target.startsWith(`${root}${path.sep}`)) {
414
+ throw new Error(`Refusing path outside skill destination: ${skillName}`);
415
+ }
381
416
 
382
- return target;
417
+ return target;
383
418
  }
384
419
 
385
420
  function normalizeLanguage(value) {
386
- const trimmed = String(value || "").trim();
387
- const lowered = trimmed.toLowerCase();
388
-
389
- switch (lowered) {
390
- case "":
391
- case "id":
392
- case "indo":
393
- case "indonesia":
394
- case "indonesian":
395
- case "bahasa indonesia":
396
- return "indonesian";
397
- case "en":
398
- case "eng":
399
- case "english":
400
- case "inggris":
401
- case "bahasa inggris":
402
- return "english";
403
- default:
404
- return lowered;
405
- }
421
+ const trimmed = String(value || "").trim();
422
+ const lowered = trimmed.toLowerCase();
423
+
424
+ switch (lowered) {
425
+ case "":
426
+ case "id":
427
+ case "indo":
428
+ case "indonesia":
429
+ case "indonesian":
430
+ case "bahasa indonesia":
431
+ return "indonesian";
432
+ case "en":
433
+ case "eng":
434
+ case "english":
435
+ case "inggris":
436
+ case "bahasa inggris":
437
+ return "english";
438
+ default:
439
+ return lowered;
440
+ }
406
441
  }
407
442
 
408
443
  function ensureDirectory(directoryPath) {
409
- fs.mkdirSync(directoryPath, { recursive: true });
444
+ fs.mkdirSync(directoryPath, { recursive: true });
410
445
  }
411
446
 
412
447
  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
- }
448
+ const targetRoot = path.resolve(targetDir);
449
+ const resolvedDestination = path.resolve(destination);
450
+ if (
451
+ resolvedDestination !== targetRoot &&
452
+ !resolvedDestination.startsWith(`${targetRoot}${path.sep}`)
453
+ ) {
454
+ throw new Error(
455
+ `Refusing destination outside target project: ${destination}`,
456
+ );
457
+ }
418
458
 
419
- if (fs.existsSync(targetRoot) && fs.lstatSync(targetRoot).isSymbolicLink()) {
420
- throw new Error(`Refusing symlinked target project: ${targetRoot}`);
421
- }
459
+ if (fs.existsSync(targetRoot) && fs.lstatSync(targetRoot).isSymbolicLink()) {
460
+ throw new Error(`Refusing symlinked target project: ${targetRoot}`);
461
+ }
422
462
 
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
- }
463
+ const relative = path.relative(targetRoot, resolvedDestination);
464
+ let current = targetRoot;
465
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
466
+ current = path.join(current, segment);
467
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
468
+ throw new Error(
469
+ `Refusing symlinked skill destination component: ${current}`,
470
+ );
430
471
  }
472
+ }
431
473
  }
432
474
 
433
475
  function writeTextFile(filePath, content) {
434
- ensureDirectory(path.dirname(filePath));
435
- fs.writeFileSync(filePath, content, "utf8");
476
+ ensureDirectory(path.dirname(filePath));
477
+ fs.writeFileSync(filePath, content, "utf8");
436
478
  }
437
479
 
438
480
  function copyFile(sourcePath, targetPath) {
439
- ensureDirectory(path.dirname(targetPath));
440
- fs.copyFileSync(sourcePath, targetPath);
481
+ ensureDirectory(path.dirname(targetPath));
482
+ fs.copyFileSync(sourcePath, targetPath);
441
483
  }
442
484
 
443
485
  function hashFile(filePath) {
444
- return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
486
+ return crypto
487
+ .createHash("sha256")
488
+ .update(fs.readFileSync(filePath))
489
+ .digest("hex");
445
490
  }
446
491
 
447
492
  function hashText(content) {
448
- return crypto.createHash("sha256").update(content).digest("hex");
493
+ return crypto.createHash("sha256").update(content).digest("hex");
449
494
  }
450
495
 
451
496
  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
- }
497
+ const hashes = {};
498
+
499
+ function walk(currentPath, relativePath = "") {
500
+ const entries = fs
501
+ .readdirSync(currentPath, { withFileTypes: true })
502
+ .filter((entry) => entry.name !== OWNERSHIP_MARKER)
503
+ .sort((left, right) => left.name.localeCompare(right.name));
504
+
505
+ for (const entry of entries) {
506
+ const entryPath = path.join(currentPath, entry.name);
507
+ const entryRelativePath = path
508
+ .join(relativePath, entry.name)
509
+ .split(path.sep)
510
+ .join("/");
511
+ if (entry.isSymbolicLink()) {
512
+ throw new Error(`Refusing symlink inside managed skill: ${entryPath}`);
513
+ }
514
+ if (entry.isDirectory()) {
515
+ walk(entryPath, entryRelativePath);
516
+ } else if (entry.isFile()) {
517
+ hashes[entryRelativePath] = hashFile(entryPath);
518
+ } else {
519
+ throw new Error(
520
+ `Unsupported file type inside managed skill: ${entryPath}`,
521
+ );
522
+ }
473
523
  }
524
+ }
474
525
 
475
- walk(directoryPath);
476
- return hashes;
526
+ walk(directoryPath);
527
+ return hashes;
477
528
  }
478
529
 
479
530
  function hashDirectory(directoryPath) {
480
- return hashText(JSON.stringify(getDirectoryFileHashes(directoryPath)));
531
+ return hashText(JSON.stringify(getDirectoryFileHashes(directoryPath)));
481
532
  }
482
533
 
483
534
  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;
535
+ if (!fs.existsSync(SOURCE_LEGACY_PAYLOADS_FILE)) return {};
536
+ const releases = readJsonObject(
537
+ SOURCE_LEGACY_PAYLOADS_FILE,
538
+ "legacy payload fingerprints",
539
+ );
540
+ const combined = {};
541
+ for (const payloads of Object.values(releases)) {
542
+ if (!payloads || typeof payloads !== "object" || Array.isArray(payloads))
543
+ continue;
544
+ for (const [encodedSkillName, payloadHash] of Object.entries(payloads)) {
545
+ if (
546
+ typeof payloadHash !== "string" ||
547
+ !/^[a-f0-9]{64}$/.test(payloadHash)
548
+ )
549
+ continue;
550
+ const skillName = encodedSkillName.startsWith("base64:")
551
+ ? Buffer.from(
552
+ encodedSkillName.slice("base64:".length),
553
+ "base64",
554
+ ).toString("utf8")
555
+ : encodedSkillName;
556
+ if (!combined[skillName]) combined[skillName] = new Set();
557
+ combined[skillName].add(payloadHash);
558
+ }
559
+ }
560
+ return combined;
499
561
  }
500
562
 
501
563
  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));
564
+ if (!fs.existsSync(targetPath) || isMaccaOwned(targetPath)) return false;
565
+ if (!fs.lstatSync(targetPath).isDirectory()) return false;
566
+ const hashes = getLegacyPayloadHashes()[skillName];
567
+ const payloadHash = hashDirectory(targetPath);
568
+ return Boolean(hashes && hashes.has(payloadHash));
507
569
  }
508
570
 
509
571
  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`;
572
+ const lock = JSON.parse(fs.readFileSync(SOURCE_LOCK_FILE, "utf8"));
573
+ lock.hashAlgorithm = "sha256";
574
+ lock.payloadFiles = {};
575
+ for (const skillName of managedSkills) {
576
+ lock.payloadFiles[skillName] = getDirectoryFileHashes(
577
+ path.join(SOURCE_SKILLS_DIR, skillName),
578
+ );
579
+ }
580
+ return `${JSON.stringify(lock, null, 2)}\n`;
517
581
  }
518
582
 
519
583
  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;
584
+ const parse = (value) =>
585
+ String(value || "0.0.0")
586
+ .split("-")[0]
587
+ .split(".")
588
+ .map((part) => Number.parseInt(part, 10) || 0);
589
+ const a = parse(left);
590
+ const b = parse(right);
591
+ const length = Math.max(a.length, b.length);
592
+ for (let index = 0; index < length; index += 1) {
593
+ const diff = (a[index] || 0) - (b[index] || 0);
594
+ if (diff !== 0) return diff;
595
+ }
596
+ return 0;
532
597
  }
533
598
 
534
599
  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
- }
600
+ const installedLockPath = path.join(agentsDirectory, "macca-lock.json");
601
+ if (!fs.existsSync(installedLockPath)) {
602
+ return;
603
+ }
604
+
605
+ const packagedLock = readJsonObject(SOURCE_LOCK_FILE, "packaged MACCA lock");
606
+ const installedLock = readJsonObject(
607
+ installedLockPath,
608
+ "installed MACCA lock",
609
+ );
610
+ const packagedVersion = packagedLock.version;
611
+ const installedVersion = installedLock.version;
612
+ if (
613
+ typeof packagedVersion !== "string" ||
614
+ typeof installedVersion !== "string"
615
+ ) {
616
+ return;
617
+ }
618
+
619
+ if (compareSemver(packagedVersion, installedVersion) < 0) {
620
+ throw new Error(
621
+ `Refusing to downgrade MACCA from ${installedVersion} to ${packagedVersion}. ` +
622
+ "Publish or use a newer package before running install/upgrade.",
623
+ );
624
+ }
554
625
  }
555
626
 
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
- }
627
+ function readOwnershipMarker(
628
+ targetPath,
629
+ expectedSkill = path.basename(targetPath),
630
+ ) {
631
+ const markerPath = path.join(targetPath, OWNERSHIP_MARKER);
632
+ if (!fs.existsSync(markerPath)) {
633
+ return null;
634
+ }
635
+
636
+ try {
637
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf8"));
638
+ if (marker.owner !== "macca-method" || marker.skill !== expectedSkill) {
639
+ return null;
640
+ }
641
+ return marker;
642
+ } catch {
643
+ return null;
644
+ }
571
645
  }
572
646
 
573
647
  function isMaccaOwned(targetPath) {
574
- return readOwnershipMarker(targetPath) !== null;
648
+ return readOwnershipMarker(targetPath) !== null;
575
649
  }
576
650
 
577
651
  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
- }
652
+ const marker = readOwnershipMarker(targetPath);
653
+ if (!marker) {
654
+ throw new Error(
655
+ `Refusing to overwrite unowned skill directory: ${targetPath}`,
656
+ );
657
+ }
658
+ if (!marker.payloadHash || marker.payloadHash !== hashDirectory(targetPath)) {
659
+ throw new Error(
660
+ `Refusing to overwrite locally modified managed skill: ${targetPath}`,
661
+ );
662
+ }
585
663
  }
586
664
 
587
665
  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
- }
666
+ try {
667
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
668
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
669
+ throw new Error("root value must be an object");
670
+ }
671
+ return value;
672
+ } catch (error) {
673
+ throw new Error(`Invalid ${label} at ${filePath}: ${error.message}`);
674
+ }
597
675
  }
598
676
 
599
677
  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
- }
678
+ const statePath = path.join(agentsDirectory, STATE_FILE);
679
+ if (!fs.existsSync(statePath)) {
680
+ return;
681
+ }
682
+ const state = readJsonObject(statePath, "MACCA state");
683
+ for (const [fileName, expectedHash] of Object.entries(state.files || {})) {
684
+ const filePath = path.join(agentsDirectory, fileName);
685
+ if (!fs.existsSync(filePath) || hashFile(filePath) !== expectedHash) {
686
+ throw new Error(
687
+ `Refusing to overwrite locally modified MACCA metadata: ${filePath}`,
688
+ );
689
+ }
690
+ }
611
691
  }
612
692
 
613
693
  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`;
694
+ const hashes = {};
695
+ for (const [fileName, content] of Object.entries(files)) {
696
+ hashes[fileName] = hashText(content);
697
+ }
698
+ return `${JSON.stringify({ version: 1, hashAlgorithm: "sha256", files: hashes }, null, 2)}\n`;
619
699
  }
620
700
 
621
701
  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);
702
+ const temporaryPath = `${filePath}.tmp-${process.pid}`;
703
+ fs.writeFileSync(
704
+ temporaryPath,
705
+ `${JSON.stringify(value, null, 2)}\n`,
706
+ "utf8",
707
+ );
708
+ fs.renameSync(temporaryPath, filePath);
625
709
  }
626
710
 
627
711
  function removeManagedPath(targetPath, kind) {
628
- fs.rmSync(targetPath, { recursive: kind === "directory", force: true });
712
+ fs.rmSync(targetPath, { recursive: kind === "directory", force: true });
629
713
  }
630
714
 
631
715
  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
- }
716
+ if (!fs.existsSync(candidatePath)) {
717
+ return;
718
+ }
719
+ if (entry.kind === "directory") {
720
+ const marker = readOwnershipMarker(
721
+ candidatePath,
722
+ path.basename(entry.targetPath),
723
+ );
724
+ if (!marker || marker.transactionId !== transactionId) {
725
+ throw new Error(
726
+ `Refusing recovery of directory without matching transaction evidence: ${candidatePath}`,
727
+ );
728
+ }
729
+ return;
730
+ }
731
+ if (!entry.stagedHash || hashFile(candidatePath) !== entry.stagedHash) {
732
+ throw new Error(
733
+ `Refusing recovery of file without matching transaction hash: ${candidatePath}`,
734
+ );
735
+ }
645
736
  }
646
737
 
647
738
  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
- }
739
+ if (!fs.existsSync(entry.backupPath)) {
740
+ return;
741
+ }
742
+ if (entry.kind === "directory") {
743
+ const marker = readOwnershipMarker(
744
+ entry.backupPath,
745
+ path.basename(entry.targetPath),
746
+ );
747
+ const validManaged =
748
+ marker &&
749
+ marker.payloadHash &&
750
+ marker.payloadHash === hashDirectory(entry.backupPath);
751
+ const validLegacy =
752
+ entry.legacySkillName &&
753
+ isLegacyDirectoryUnchanged(entry.backupPath, entry.legacySkillName);
754
+ if (!validManaged && !validLegacy) {
755
+ throw new Error(
756
+ `Refusing recovery from unowned or modified backup directory: ${entry.backupPath}`,
757
+ );
758
+ }
759
+ }
660
760
  }
661
761
 
662
762
  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
- }
763
+ if (
764
+ journal.version !== 1 ||
765
+ !["committing", "committed"].includes(journal.phase)
766
+ ) {
767
+ throw new Error("Unsupported MACCA transaction journal version or phase");
768
+ }
769
+ if (
770
+ typeof journal.transactionId !== "string" ||
771
+ !/^\d+-\d+-[a-f0-9]+$/.test(journal.transactionId)
772
+ ) {
773
+ throw new Error("Invalid MACCA transaction ID");
774
+ }
775
+ if (!Array.isArray(journal.entries)) {
776
+ throw new Error("Invalid MACCA transaction journal entries");
777
+ }
778
+
779
+ const metadataTargets = new Set(
780
+ [
781
+ "macca-managed-skills.txt",
782
+ "macca-lock.json",
783
+ "macca-tools.txt",
784
+ "developer-config.json",
785
+ STATE_FILE,
786
+ ].map((fileName) => path.resolve(targetDir, ".agents", fileName)),
787
+ );
788
+ const skillParents = new Set([
789
+ ...TOOL_DEFINITIONS.map((tool) =>
790
+ path.resolve(tool.destination(targetDir)),
791
+ ),
792
+ path.resolve(targetDir, ".opencode", "skill"),
793
+ ]);
794
+ const seenPaths = new Set();
795
+
796
+ for (const entry of journal.entries) {
797
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
798
+ throw new Error("Invalid MACCA transaction journal entry");
799
+ }
800
+ if (entry.kind !== "directory" && entry.kind !== "file") {
801
+ throw new Error(`Invalid transaction entry kind: ${entry.kind}`);
802
+ }
803
+ if (
804
+ typeof entry.targetPath !== "string" ||
805
+ typeof entry.backupPath !== "string"
806
+ ) {
807
+ throw new Error("Transaction entry paths must be strings");
808
+ }
809
+ if (entry.stagingPath !== null && typeof entry.stagingPath !== "string") {
810
+ throw new Error("Transaction staging path must be a string or null");
811
+ }
812
+ if (typeof entry.hadTarget !== "boolean") {
813
+ throw new Error("Transaction hadTarget must be boolean");
814
+ }
815
+ if (
816
+ entry.legacySkillName !== undefined &&
817
+ (typeof entry.legacySkillName !== "string" ||
818
+ !/^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/.test(
819
+ entry.legacySkillName,
820
+ ))
821
+ ) {
822
+ throw new Error("Transaction legacy skill name is invalid");
823
+ }
824
+ if (
825
+ entry.stagingPath !== null &&
826
+ (typeof entry.stagedHash !== "string" ||
827
+ !/^[a-f0-9]{64}$/.test(entry.stagedHash))
828
+ ) {
829
+ throw new Error("Transaction staged payload hash is missing or invalid");
830
+ }
831
+
832
+ const targetPath = path.resolve(entry.targetPath);
833
+ const parent = path.dirname(targetPath);
834
+ const targetName = path.basename(targetPath);
835
+ const isValidFile =
836
+ entry.kind === "file" && metadataTargets.has(targetPath);
837
+ const isValidSkill =
838
+ entry.kind === "directory" &&
839
+ skillParents.has(parent) &&
840
+ /^(?:_[a-z0-9]+|[a-z0-9]+(?:-[a-z0-9]+)*)$/.test(targetName);
841
+ if (!isValidFile && !isValidSkill) {
842
+ throw new Error(
843
+ `Transaction target is not a managed MACCA path: ${targetPath}`,
844
+ );
845
+ }
846
+
847
+ const backupPath = path.resolve(entry.backupPath);
848
+ const expectedBackupPath = path.join(
849
+ parent,
850
+ `.${targetName}.macca-backup-${journal.transactionId}`,
851
+ );
852
+ if (backupPath !== expectedBackupPath) {
853
+ throw new Error(`Invalid MACCA transaction backup path: ${backupPath}`);
854
+ }
855
+ if (entry.stagingPath !== null) {
856
+ const stagingPath = path.resolve(entry.stagingPath);
857
+ const expectedStagingPath = path.join(
858
+ parent,
859
+ `.${targetName}.macca-stage-${journal.transactionId}`,
860
+ );
861
+ if (stagingPath !== expectedStagingPath) {
862
+ throw new Error(
863
+ `Invalid MACCA transaction staging path: ${stagingPath}`,
864
+ );
865
+ }
866
+ }
735
867
 
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
- }
868
+ for (const candidate of [
869
+ targetPath,
870
+ backupPath,
871
+ entry.stagingPath && path.resolve(entry.stagingPath),
872
+ ].filter(Boolean)) {
873
+ assertSafeProjectPath(targetDir, candidate);
874
+ if (seenPaths.has(candidate)) {
875
+ throw new Error(
876
+ `Duplicate path in MACCA transaction journal: ${candidate}`,
877
+ );
878
+ }
879
+ seenPaths.add(candidate);
743
880
  }
881
+ }
744
882
 
745
- return journal.entries;
883
+ return journal.entries;
746
884
  }
747
885
 
748
886
  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);
887
+ const journalPath = path.join(targetDir, ".agents", TRANSACTION_FILE);
888
+ assertSafeProjectPath(targetDir, journalPath);
889
+ if (!fs.existsSync(journalPath)) {
890
+ return;
891
+ }
892
+
893
+ const journal = readJsonObject(journalPath, "MACCA transaction journal");
894
+ const entries = validateTransactionEntries(targetDir, journal);
895
+ for (const entry of entries) {
896
+ assertTransactionBackup(entry);
897
+ if (
898
+ journal.phase === "committed" ||
899
+ fs.existsSync(entry.backupPath) ||
900
+ !entry.hadTarget
901
+ ) {
902
+ assertTransactionPayload(entry, journal.transactionId, entry.targetPath);
903
+ }
904
+ if (entry.stagingPath && fs.existsSync(entry.stagingPath)) {
905
+ assertTransactionPayload(entry, journal.transactionId, entry.stagingPath);
906
+ }
907
+ }
908
+ if (journal.phase === "committed") {
757
909
  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 });
910
+ removeManagedPath(entry.backupPath, entry.kind);
911
+ if (entry.stagingPath) removeManagedPath(entry.stagingPath, entry.kind);
912
+ }
913
+ } else {
914
+ for (const entry of [...entries].reverse()) {
915
+ if (fs.existsSync(entry.backupPath)) {
916
+ removeManagedPath(entry.targetPath, entry.kind);
917
+ fs.renameSync(entry.backupPath, entry.targetPath);
918
+ } else if (!entry.hadTarget) {
919
+ removeManagedPath(entry.targetPath, entry.kind);
920
+ }
921
+ if (entry.stagingPath) removeManagedPath(entry.stagingPath, entry.kind);
922
+ }
923
+ }
924
+ fs.rmSync(journalPath, { force: true });
783
925
  }
784
926
 
785
927
  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()];
928
+ const destinations = new Map();
929
+ for (const toolKey of tools) {
930
+ const tool = TOOL_BY_KEY.get(toolKey);
931
+ if (!tool) {
932
+ throw new Error(`Unsupported tool: ${toolKey}`);
933
+ }
934
+ const destination = tool.destination(targetDir);
935
+ assertSafeProjectPath(targetDir, destination);
936
+ destinations.set(path.resolve(destination), destination);
937
+ }
938
+ return [...destinations.values()];
797
939
  }
798
940
 
799
941
  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);
942
+ const suffix = `${process.pid}-${Date.now()}-${crypto.randomBytes(8).toString("hex")}`;
943
+ const prepared = [];
944
+ const committed = [];
945
+ const journalPath = path.join(targetDir, ".agents", TRANSACTION_FILE);
946
+
947
+ for (const entry of entries) {
948
+ const {
949
+ targetPath,
950
+ sourcePath,
951
+ content,
952
+ kind = "directory",
953
+ legacySkillName,
954
+ } = entry;
955
+ const targetName = path.basename(targetPath);
956
+ const parent = path.dirname(targetPath);
957
+ if (fs.existsSync(targetPath)) {
958
+ const stat = fs.lstatSync(targetPath);
959
+ if (stat.isSymbolicLink()) {
960
+ throw new Error(
961
+ `Refusing to replace symlinked managed path: ${targetPath}`,
962
+ );
963
+ }
964
+ if (kind === "directory" && !stat.isDirectory()) {
965
+ throw new Error(`Skill destination is not a directory: ${targetPath}`);
966
+ }
967
+ if (kind === "file" && !stat.isFile()) {
968
+ throw new Error(
969
+ `Managed file destination is not a file: ${targetPath}`,
970
+ );
971
+ }
972
+ if (kind === "directory") {
973
+ if (isMaccaOwned(targetPath)) {
974
+ assertManagedDirectoryUnchanged(targetPath);
975
+ } else if (
976
+ !legacySkillName ||
977
+ !isLegacyDirectoryUnchanged(targetPath, legacySkillName)
978
+ ) {
979
+ throw new Error(
980
+ `Refusing to overwrite or remove unowned skill directory: ${targetPath}`,
981
+ );
982
+ }
983
+ }
984
+ }
985
+ prepared.push({
986
+ ...entry,
987
+ kind,
988
+ hadTarget: fs.existsSync(targetPath),
989
+ stagingPath:
990
+ sourcePath || content !== undefined
991
+ ? path.join(parent, `.${targetName}.macca-stage-${suffix}`)
992
+ : null,
993
+ backupPath: path.join(parent, `.${targetName}.macca-backup-${suffix}`),
994
+ });
995
+ }
996
+
997
+ try {
998
+ for (const entry of prepared) {
999
+ ensureDirectory(path.dirname(entry.targetPath));
1000
+ if (!entry.sourcePath && entry.content === undefined) {
1001
+ continue;
1002
+ }
1003
+ if (entry.kind === "file") {
1004
+ if (entry.sourcePath) {
1005
+ fs.copyFileSync(entry.sourcePath, entry.stagingPath);
1006
+ } else {
1007
+ fs.writeFileSync(entry.stagingPath, entry.content, "utf8");
1008
+ }
1009
+ } else {
1010
+ fs.cpSync(entry.sourcePath, entry.stagingPath, { recursive: true });
1011
+ const payloadHash = hashDirectory(entry.stagingPath);
1012
+ writeTextFile(
1013
+ path.join(entry.stagingPath, OWNERSHIP_MARKER),
1014
+ `${JSON.stringify(
1015
+ {
1016
+ owner: "macca-method",
1017
+ skill: path.basename(entry.targetPath),
1018
+ hashAlgorithm: "sha256",
1019
+ payloadHash,
1020
+ transactionId: suffix,
1021
+ },
1022
+ null,
1023
+ 2,
1024
+ )}\n`,
1025
+ );
1026
+ }
1027
+ entry.stagedHash =
1028
+ entry.kind === "file"
1029
+ ? hashFile(entry.stagingPath)
1030
+ : hashDirectory(entry.stagingPath);
1031
+ }
1032
+
1033
+ writeJsonAtomic(journalPath, {
1034
+ version: 1,
1035
+ phase: "committing",
1036
+ transactionId: suffix,
1037
+ entries: prepared.map(
1038
+ ({
1039
+ targetPath,
1040
+ stagingPath,
1041
+ backupPath,
1042
+ kind,
1043
+ hadTarget,
1044
+ stagedHash,
1045
+ legacySkillName,
1046
+ }) => ({
1047
+ targetPath,
1048
+ stagingPath,
1049
+ backupPath,
1050
+ kind,
1051
+ hadTarget,
1052
+ stagedHash: stagingPath ? stagedHash : null,
1053
+ legacySkillName,
1054
+ }),
1055
+ ),
1056
+ });
804
1057
 
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
- });
1058
+ for (const entry of prepared) {
1059
+ if (entry.hadTarget) {
1060
+ fs.renameSync(entry.targetPath, entry.backupPath);
1061
+ }
1062
+ committed.push(entry);
1063
+ if (entry.sourcePath || entry.content !== undefined) {
1064
+ fs.renameSync(entry.stagingPath, entry.targetPath);
1065
+ }
1066
+ }
1067
+ writeJsonAtomic(journalPath, {
1068
+ version: 1,
1069
+ phase: "committed",
1070
+ transactionId: suffix,
1071
+ entries: prepared.map(
1072
+ ({
1073
+ targetPath,
1074
+ stagingPath,
1075
+ backupPath,
1076
+ kind,
1077
+ hadTarget,
1078
+ stagedHash,
1079
+ legacySkillName,
1080
+ }) => ({
1081
+ targetPath,
1082
+ stagingPath,
1083
+ backupPath,
1084
+ kind,
1085
+ hadTarget,
1086
+ stagedHash: stagingPath ? stagedHash : null,
1087
+ legacySkillName,
1088
+ }),
1089
+ ),
1090
+ });
1091
+ } catch (error) {
1092
+ for (const entry of [...committed].reverse()) {
1093
+ fs.rmSync(entry.targetPath, {
1094
+ recursive: entry.kind === "directory",
1095
+ force: true,
1096
+ });
1097
+ if (entry.hadTarget && fs.existsSync(entry.backupPath)) {
1098
+ fs.renameSync(entry.backupPath, entry.targetPath);
1099
+ }
837
1100
  }
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
- }))
1101
+ fs.rmSync(journalPath, { force: true });
1102
+ throw error;
1103
+ } finally {
1104
+ for (const entry of prepared) {
1105
+ if (entry.stagingPath) {
1106
+ fs.rmSync(entry.stagingPath, {
1107
+ recursive: entry.kind === "directory",
1108
+ force: true,
907
1109
  });
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
- }
1110
+ }
923
1111
  }
1112
+ }
924
1113
 
925
- for (const entry of committed) {
926
- fs.rmSync(entry.backupPath, { recursive: entry.kind === "directory", force: true });
927
- }
928
- fs.rmSync(journalPath, { force: true });
1114
+ for (const entry of committed) {
1115
+ fs.rmSync(entry.backupPath, {
1116
+ recursive: entry.kind === "directory",
1117
+ force: true,
1118
+ });
1119
+ }
1120
+ fs.rmSync(journalPath, { force: true });
929
1121
  }
930
1122
 
931
1123
  function normalizeToolList(values) {
932
- const rawTokens = [];
933
- for (const value of values) {
934
- rawTokens.push(...String(value).split(","));
1124
+ const rawTokens = [];
1125
+ for (const value of values) {
1126
+ rawTokens.push(...String(value).split(","));
1127
+ }
1128
+
1129
+ const resolved = [];
1130
+ for (const token of rawTokens) {
1131
+ const trimmed = token.trim().toLowerCase();
1132
+ if (!trimmed) {
1133
+ continue;
935
1134
  }
936
1135
 
937
- const resolved = [];
938
- for (const token of rawTokens) {
939
- const trimmed = token.trim().toLowerCase();
940
- if (!trimmed) {
941
- continue;
942
- }
943
-
944
- const key = TOOL_LOOKUP.get(trimmed);
945
- if (!key) {
946
- throw new Error(`Unknown tool: ${token}`);
947
- }
948
-
949
- resolved.push(key);
1136
+ const key = TOOL_LOOKUP.get(trimmed);
1137
+ if (!key) {
1138
+ throw new Error(`Unknown tool: ${token}`);
950
1139
  }
951
1140
 
952
- return unique(resolved);
1141
+ resolved.push(key);
1142
+ }
1143
+
1144
+ return unique(resolved);
953
1145
  }
954
1146
 
955
1147
  function buildDeveloperConfig(options) {
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 = {
963
- raw: options.communicationLanguage,
964
- normalized: normalizeLanguage(options.communicationLanguage)
965
- };
966
- }
967
- if (options.documentLanguage !== undefined) {
968
- config.languagePreferences.documents = {
969
- raw: options.documentLanguage,
970
- normalized: normalizeLanguage(options.documentLanguage)
971
- };
972
- }
973
- }
974
- return config;
1148
+ const config = {};
1149
+ if (options.name !== undefined) config.name = options.name;
1150
+ if (options.project !== undefined) config.project = options.project;
1151
+ if (
1152
+ options.communicationLanguage !== undefined ||
1153
+ options.documentLanguage !== undefined
1154
+ ) {
1155
+ config.languagePreferences = {};
1156
+ if (options.communicationLanguage !== undefined) {
1157
+ config.languagePreferences.communication = {
1158
+ raw: options.communicationLanguage,
1159
+ normalized: normalizeLanguage(options.communicationLanguage),
1160
+ };
1161
+ }
1162
+ if (options.documentLanguage !== undefined) {
1163
+ config.languagePreferences.documents = {
1164
+ raw: options.documentLanguage,
1165
+ normalized: normalizeLanguage(options.documentLanguage),
1166
+ };
1167
+ }
1168
+ }
1169
+ return config;
975
1170
  }
976
1171
 
977
1172
  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
- }
1173
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
1174
+ return update;
1175
+ }
1176
+
1177
+ const merged = { ...existing };
1178
+ for (const [key, value] of Object.entries(update)) {
1179
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1180
+ merged[key] = mergeObjects(existing[key], value);
1181
+ } else {
1182
+ merged[key] = value;
989
1183
  }
990
- return merged;
1184
+ }
1185
+ return merged;
991
1186
  }
992
1187
 
993
1188
  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
- }
1189
+ if (!fs.existsSync(filePath)) {
1190
+ return {};
1191
+ }
1192
+
1193
+ try {
1194
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
1195
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1196
+ throw new Error("root value must be an object");
1197
+ }
1198
+ return value;
1199
+ } catch (error) {
1200
+ throw new Error(`Cannot preserve malformed ${filePath}: ${error.message}`);
1201
+ }
1007
1202
  }
1008
1203
 
1009
1204
  function createPrompt() {
1010
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
1011
- throw new Error("Interactive install requires a TTY. Use --yes and explicit flags in non-interactive environments.");
1012
- }
1205
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1206
+ throw new Error(
1207
+ "Interactive install requires a TTY. Use --yes and explicit flags in non-interactive environments.",
1208
+ );
1209
+ }
1013
1210
 
1014
- return readline.createInterface({
1015
- input: process.stdin,
1016
- output: process.stdout
1017
- });
1211
+ return readline.createInterface({
1212
+ input: process.stdin,
1213
+ output: process.stdout,
1214
+ });
1018
1215
  }
1019
1216
 
1020
1217
  function askQuestion(prompt, question) {
1021
- return new Promise((resolve) => {
1022
- prompt.question(question, resolve);
1023
- });
1218
+ return new Promise((resolve) => {
1219
+ prompt.question(question, resolve);
1220
+ });
1024
1221
  }
1025
1222
 
1026
1223
  async function promptForTools() {
1027
- const prompt = createPrompt();
1224
+ const prompt = createPrompt();
1028
1225
 
1029
- try {
1030
- process.stdout.write("\nPilih AI tool yang mau dipasang:\n");
1031
- TOOL_DEFINITIONS.forEach((tool, index) => {
1032
- process.stdout.write(` ${index + 1}. ${tool.label} -> ${tool.displayDestination}\n`);
1033
- });
1034
- process.stdout.write("\n");
1035
-
1036
- while (true) {
1037
- const answer = await askQuestion(prompt, "Masukkan nomor/nama tool (pisahkan dengan koma, 'all', kosong = codex): ");
1038
- const parsed = parseInteractiveToolSelection(answer);
1039
- if (parsed.error) {
1040
- process.stdout.write(` ${parsed.error}\n`);
1041
- continue;
1042
- }
1226
+ try {
1227
+ process.stdout.write("\nPilih AI tool yang mau dipasang:\n");
1228
+ TOOL_DEFINITIONS.forEach((tool, index) => {
1229
+ process.stdout.write(
1230
+ ` ${index + 1}. ${tool.label} -> ${tool.displayDestination}\n`,
1231
+ );
1232
+ });
1233
+ process.stdout.write("\n");
1043
1234
 
1044
- return parsed.tools;
1045
- }
1046
- } finally {
1047
- prompt.close();
1048
- }
1235
+ while (true) {
1236
+ const answer = await askQuestion(
1237
+ prompt,
1238
+ "Masukkan nomor/nama tool (pisahkan dengan koma, 'all', kosong = codex): ",
1239
+ );
1240
+ const parsed = parseInteractiveToolSelection(answer);
1241
+ if (parsed.error) {
1242
+ process.stdout.write(` ${parsed.error}\n`);
1243
+ continue;
1244
+ }
1245
+
1246
+ return parsed.tools;
1247
+ }
1248
+ } finally {
1249
+ prompt.close();
1250
+ }
1049
1251
  }
1050
1252
 
1051
1253
  function parseInteractiveToolSelection(answer) {
1052
- const trimmed = String(answer || "").trim();
1053
- if (!trimmed) {
1054
- return { tools: ["codex"] };
1055
- }
1056
-
1057
- if (trimmed.toLowerCase() === "all") {
1058
- return { tools: TOOL_DEFINITIONS.map((tool) => tool.key) };
1059
- }
1254
+ const trimmed = String(answer || "").trim();
1255
+ if (!trimmed) {
1256
+ return { tools: ["codex"] };
1257
+ }
1060
1258
 
1061
- const tools = [];
1062
- for (const part of trimmed.split(",")) {
1063
- const token = part.trim();
1064
- if (!token) {
1065
- continue;
1066
- }
1259
+ if (trimmed.toLowerCase() === "all") {
1260
+ return { tools: TOOL_DEFINITIONS.map((tool) => tool.key) };
1261
+ }
1067
1262
 
1068
- if (/^\d+$/.test(token)) {
1069
- const index = Number(token) - 1;
1070
- if (index < 0 || index >= TOOL_DEFINITIONS.length) {
1071
- return { error: `Pilihan nomor tidak valid: ${token}` };
1072
- }
1263
+ const tools = [];
1264
+ for (const part of trimmed.split(",")) {
1265
+ const token = part.trim();
1266
+ if (!token) {
1267
+ continue;
1268
+ }
1073
1269
 
1074
- tools.push(TOOL_DEFINITIONS[index].key);
1075
- continue;
1076
- }
1270
+ if (/^\d+$/.test(token)) {
1271
+ const index = Number(token) - 1;
1272
+ if (index < 0 || index >= TOOL_DEFINITIONS.length) {
1273
+ return { error: `Pilihan nomor tidak valid: ${token}` };
1274
+ }
1077
1275
 
1078
- const key = TOOL_LOOKUP.get(token.toLowerCase());
1079
- if (!key) {
1080
- return { error: `Nama tool tidak dikenal: ${token}` };
1081
- }
1276
+ tools.push(TOOL_DEFINITIONS[index].key);
1277
+ continue;
1278
+ }
1082
1279
 
1083
- tools.push(key);
1280
+ const key = TOOL_LOOKUP.get(token.toLowerCase());
1281
+ if (!key) {
1282
+ return { error: `Nama tool tidak dikenal: ${token}` };
1084
1283
  }
1085
1284
 
1086
- return { tools: unique(tools) };
1285
+ tools.push(key);
1286
+ }
1287
+
1288
+ return { tools: unique(tools) };
1087
1289
  }
1088
1290
 
1089
1291
  async function promptForMetadata(seed) {
1090
- const prompt = createPrompt();
1091
-
1092
- try {
1093
- const name = seed.name !== undefined ? seed.name : await askQuestion(prompt, "Kamu mau dipanggil apa? (Kosong = Skip): ");
1094
- const project = seed.project !== undefined ? seed.project : await askQuestion(prompt, "Nama project ini apa? (Kosong = Skip): ");
1095
- const communication = seed.communicationLanguage !== undefined
1096
- ? seed.communicationLanguage
1097
- : await askQuestion(prompt, "Bahasa komunikasi yang anda inginkan? (Kosong = Bahasa Indonesia): ");
1098
- const documents = seed.documentLanguage !== undefined
1099
- ? seed.documentLanguage
1100
- : await askQuestion(prompt, "Bahasa dokumen yang dihasilkan? (Kosong = Bahasa Indonesia): ");
1101
-
1102
- return {
1103
- name: name || "",
1104
- project: project || "",
1105
- communicationLanguage: communication || "Bahasa Indonesia",
1106
- documentLanguage: documents || "Bahasa Indonesia"
1107
- };
1108
- } finally {
1109
- prompt.close();
1110
- }
1292
+ const prompt = createPrompt();
1293
+
1294
+ try {
1295
+ const name =
1296
+ seed.name !== undefined
1297
+ ? seed.name
1298
+ : await askQuestion(
1299
+ prompt,
1300
+ "Kamu mau dipanggil apa? (Kosong = Skip): ",
1301
+ );
1302
+ const project =
1303
+ seed.project !== undefined
1304
+ ? seed.project
1305
+ : await askQuestion(prompt, "Nama project ini apa? (Kosong = Skip): ");
1306
+ const communication =
1307
+ seed.communicationLanguage !== undefined
1308
+ ? seed.communicationLanguage
1309
+ : await askQuestion(
1310
+ prompt,
1311
+ "Bahasa komunikasi yang anda inginkan? (Kosong = Bahasa Indonesia): ",
1312
+ );
1313
+ const documents =
1314
+ seed.documentLanguage !== undefined
1315
+ ? seed.documentLanguage
1316
+ : await askQuestion(
1317
+ prompt,
1318
+ "Bahasa dokumen yang dihasilkan? (Kosong = Bahasa Indonesia): ",
1319
+ );
1320
+
1321
+ return {
1322
+ name: name || "",
1323
+ project: project || "",
1324
+ communicationLanguage: communication || "Bahasa Indonesia",
1325
+ documentLanguage: documents || "Bahasa Indonesia",
1326
+ };
1327
+ } finally {
1328
+ prompt.close();
1329
+ }
1111
1330
  }
1112
1331
 
1113
1332
  function applyInstall(targetDir, tools, metadata) {
1114
- const managedSkills = getSourceManagedSkills();
1115
- const agentsDirectory = path.join(targetDir, ".agents");
1116
- ensureDirectory(targetDir);
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));
1121
- }
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)) {
1129
- for (const skillName of managedSkills) {
1130
- entries.push({
1131
- sourcePath: path.join(SOURCE_SKILLS_DIR, skillName),
1132
- targetPath: resolveOwnedSkillPath(destination, skillName),
1133
- legacySkillName: skillName
1134
- });
1135
- }
1136
- }
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) }
1159
- );
1160
- applySkillTransaction(entries, targetDir);
1333
+ const managedSkills = getSourceManagedSkills();
1334
+ const agentsDirectory = path.join(targetDir, ".agents");
1335
+ ensureDirectory(targetDir);
1336
+ assertSafeProjectPath(targetDir, agentsDirectory);
1337
+ assertPackageNotOlderThanInstalled(agentsDirectory);
1338
+ for (const fileName of [
1339
+ "macca-managed-skills.txt",
1340
+ "macca-tools.txt",
1341
+ "developer-config.json",
1342
+ "macca-lock.json",
1343
+ STATE_FILE,
1344
+ ]) {
1345
+ assertSafeProjectPath(targetDir, path.join(agentsDirectory, fileName));
1346
+ }
1347
+ assertManagedFilesUnchanged(agentsDirectory);
1348
+ const previousManagedSkills = validateManagedSkillNames(
1349
+ readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
1350
+ ".agents/macca-managed-skills.txt",
1351
+ );
1352
+ const entries = [];
1353
+ for (const destination of getUniqueDestinations(targetDir, tools)) {
1354
+ for (const skillName of managedSkills) {
1355
+ entries.push({
1356
+ sourcePath: path.join(SOURCE_SKILLS_DIR, skillName),
1357
+ targetPath: resolveOwnedSkillPath(destination, skillName),
1358
+ legacySkillName: skillName,
1359
+ });
1360
+ }
1361
+ }
1362
+ const configPath = path.join(agentsDirectory, "developer-config.json");
1363
+ const config = mergeObjects(
1364
+ readDeveloperConfig(configPath),
1365
+ buildDeveloperConfig(metadata),
1366
+ );
1367
+ const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
1368
+ ? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
1369
+ : `${managedSkills.join("\n")}\n`;
1370
+ const lockContent = buildInstalledLock(managedSkills);
1371
+ const toolsContent = `${tools.join("\n")}\n`;
1372
+ const stateContent = buildStateContent({
1373
+ "macca-managed-skills.txt": managedContent,
1374
+ "macca-lock.json": lockContent,
1375
+ "macca-tools.txt": toolsContent,
1376
+ });
1377
+ entries.push(
1378
+ {
1379
+ kind: "file",
1380
+ content: managedContent,
1381
+ targetPath: path.join(agentsDirectory, "macca-managed-skills.txt"),
1382
+ },
1383
+ {
1384
+ kind: "file",
1385
+ content: lockContent,
1386
+ targetPath: path.join(agentsDirectory, "macca-lock.json"),
1387
+ },
1388
+ {
1389
+ kind: "file",
1390
+ content: toolsContent,
1391
+ targetPath: path.join(agentsDirectory, "macca-tools.txt"),
1392
+ },
1393
+ {
1394
+ kind: "file",
1395
+ content: `${JSON.stringify(config, null, 2)}\n`,
1396
+ targetPath: configPath,
1397
+ },
1398
+ {
1399
+ kind: "file",
1400
+ content: stateContent,
1401
+ targetPath: path.join(agentsDirectory, STATE_FILE),
1402
+ },
1403
+ );
1404
+ applySkillTransaction(entries, targetDir);
1161
1405
  }
1162
1406
 
1163
1407
  function applyUpgrade(targetDir) {
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);
1171
- const tools = readNonEmptyLines(path.join(agentsDirectory, "macca-tools.txt"));
1172
- if (tools.length === 0) {
1173
- throw new Error(".agents/macca-tools.txt was not found. Run install first so MACCA knows which tools to update.");
1174
- }
1175
-
1176
- const previousManagedSkills = validateManagedSkillNames(
1177
- readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
1178
- ".agents/macca-managed-skills.txt"
1179
- );
1180
- const nextManagedSkills = getSourceManagedSkills();
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
- });
1189
- }
1190
-
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
- }
1201
- }
1202
- }
1203
-
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
- }
1217
- }
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) }
1408
+ const agentsDirectory = path.join(targetDir, ".agents");
1409
+ assertSafeProjectPath(targetDir, agentsDirectory);
1410
+ assertPackageNotOlderThanInstalled(agentsDirectory);
1411
+ for (const fileName of [
1412
+ "macca-managed-skills.txt",
1413
+ "macca-tools.txt",
1414
+ "developer-config.json",
1415
+ "macca-lock.json",
1416
+ STATE_FILE,
1417
+ ]) {
1418
+ assertSafeProjectPath(targetDir, path.join(agentsDirectory, fileName));
1419
+ }
1420
+ assertManagedFilesUnchanged(agentsDirectory);
1421
+ const tools = readNonEmptyLines(
1422
+ path.join(agentsDirectory, "macca-tools.txt"),
1423
+ );
1424
+ if (tools.length === 0) {
1425
+ throw new Error(
1426
+ ".agents/macca-tools.txt was not found. Run install first so MACCA knows which tools to update.",
1237
1427
  );
1238
- applySkillTransaction(entries, targetDir);
1428
+ }
1429
+
1430
+ const previousManagedSkills = validateManagedSkillNames(
1431
+ readNonEmptyLines(path.join(agentsDirectory, "macca-managed-skills.txt")),
1432
+ ".agents/macca-managed-skills.txt",
1433
+ );
1434
+ const nextManagedSkills = getSourceManagedSkills();
1435
+ const entries = [];
1436
+ for (const destination of getUniqueDestinations(targetDir, tools)) {
1437
+ for (const skillName of nextManagedSkills) {
1438
+ entries.push({
1439
+ sourcePath: path.join(SOURCE_SKILLS_DIR, skillName),
1440
+ targetPath: resolveOwnedSkillPath(destination, skillName),
1441
+ legacySkillName: skillName,
1442
+ });
1443
+ }
1444
+
1445
+ for (const skillName of previousManagedSkills.filter(
1446
+ (name) => !nextManagedSkills.includes(name),
1447
+ )) {
1448
+ const oldPath = resolveOwnedSkillPath(destination, skillName);
1449
+ if (fs.existsSync(oldPath) && isMaccaOwned(oldPath)) {
1450
+ assertManagedDirectoryUnchanged(oldPath);
1451
+ entries.push({ sourcePath: null, targetPath: oldPath });
1452
+ } else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
1453
+ entries.push({
1454
+ sourcePath: null,
1455
+ targetPath: oldPath,
1456
+ legacySkillName: skillName,
1457
+ });
1458
+ } else if (fs.existsSync(oldPath)) {
1459
+ process.stderr.write(
1460
+ `Preserving unmarked legacy skill directory: ${oldPath}\n`,
1461
+ );
1462
+ }
1463
+ }
1464
+ }
1465
+
1466
+ if (tools.includes("opencode")) {
1467
+ const legacyOpenCodeRoot = path.join(targetDir, ".opencode", "skill");
1468
+ assertSafeProjectPath(targetDir, legacyOpenCodeRoot);
1469
+ for (const skillName of previousManagedSkills) {
1470
+ const oldPath = resolveOwnedSkillPath(legacyOpenCodeRoot, skillName);
1471
+ if (fs.existsSync(oldPath) && isMaccaOwned(oldPath)) {
1472
+ assertManagedDirectoryUnchanged(oldPath);
1473
+ entries.push({ sourcePath: null, targetPath: oldPath });
1474
+ } else if (isLegacyDirectoryUnchanged(oldPath, skillName)) {
1475
+ entries.push({
1476
+ sourcePath: null,
1477
+ targetPath: oldPath,
1478
+ legacySkillName: skillName,
1479
+ });
1480
+ } else if (fs.existsSync(oldPath)) {
1481
+ throw new Error(
1482
+ `Legacy OpenCode skill was modified; move or back it up before upgrade: ${oldPath}`,
1483
+ );
1484
+ }
1485
+ }
1486
+ }
1487
+ const managedContent = fs.existsSync(SOURCE_MANAGED_SKILLS_FILE)
1488
+ ? fs.readFileSync(SOURCE_MANAGED_SKILLS_FILE, "utf8")
1489
+ : `${nextManagedSkills.join("\n")}\n`;
1490
+ const lockContent = buildInstalledLock(nextManagedSkills);
1491
+ const toolsContent = `${tools.join("\n")}\n`;
1492
+ const stateContent = buildStateContent({
1493
+ "macca-managed-skills.txt": managedContent,
1494
+ "macca-lock.json": lockContent,
1495
+ "macca-tools.txt": toolsContent,
1496
+ });
1497
+ entries.push(
1498
+ {
1499
+ kind: "file",
1500
+ content: managedContent,
1501
+ targetPath: path.join(agentsDirectory, "macca-managed-skills.txt"),
1502
+ },
1503
+ {
1504
+ kind: "file",
1505
+ content: lockContent,
1506
+ targetPath: path.join(agentsDirectory, "macca-lock.json"),
1507
+ },
1508
+ {
1509
+ kind: "file",
1510
+ content: stateContent,
1511
+ targetPath: path.join(agentsDirectory, STATE_FILE),
1512
+ },
1513
+ );
1514
+ applySkillTransaction(entries, targetDir);
1239
1515
 
1240
- if (tools.includes("kimi")) reportLegacyKimiInstall();
1516
+ if (tools.includes("kimi")) reportLegacyKimiInstall();
1241
1517
  }
1242
1518
 
1243
1519
  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
- );
1250
- }
1251
- return path.join(os.homedir(), ".config", "agents", "skills");
1520
+ if (process.platform === "win32") {
1521
+ return path.join(
1522
+ process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"),
1523
+ "agents",
1524
+ "skills",
1525
+ );
1526
+ }
1527
+ return path.join(os.homedir(), ".config", "agents", "skills");
1252
1528
  }
1253
1529
 
1254
1530
  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) });
1267
- }
1268
-
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
- );
1531
+ const legacyRoot = getLegacyKimiRoot();
1532
+ if (!fs.existsSync(legacyRoot)) return;
1533
+
1534
+ const legacyHashes = getLegacyPayloadHashes();
1535
+ const detected = [];
1536
+ for (const [skillName, acceptedHashes] of Object.entries(legacyHashes)) {
1537
+ const skillPath = path.join(legacyRoot, skillName);
1538
+ if (!fs.existsSync(skillPath)) continue;
1539
+ const stat = fs.lstatSync(skillPath);
1540
+ if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
1541
+ const payloadHash = hashDirectory(skillPath);
1542
+ detected.push({ skillPath, unchanged: acceptedHashes.has(payloadHash) });
1543
+ }
1544
+
1545
+ if (!detected.length) return;
1546
+ process.stderr.write(
1547
+ `Legacy Kimi copies remain outside this project at ${legacyRoot}. ` +
1548
+ "MACCA did not delete global files. Verify the new project-local .agents/skills installation, " +
1549
+ "then remove old copies manually if your Kimi version still discovers them.\n",
1550
+ );
1275
1551
  }
1276
1552
 
1277
1553
  function printInstallSummary(action, tools) {
1278
- process.stdout.write(`\n MACCA ${action} untuk:\n`);
1279
- for (const toolKey of tools) {
1280
- const tool = TOOL_BY_KEY.get(toolKey);
1281
- process.stdout.write(` ✓ ${tool.label.padEnd(16)} -> ${tool.displayDestination}\n`);
1282
- }
1283
- process.stdout.write("\n");
1554
+ process.stdout.write(`\n MACCA ${action} untuk:\n`);
1555
+ for (const toolKey of tools) {
1556
+ const tool = TOOL_BY_KEY.get(toolKey);
1557
+ process.stdout.write(
1558
+ ` ✓ ${tool.label.padEnd(16)} -> ${tool.displayDestination}\n`,
1559
+ );
1560
+ }
1561
+ process.stdout.write("\n");
1284
1562
  }
1285
1563
 
1286
1564
  async function runInstall(args) {
1287
- let tools = normalizeToolList(args.tools);
1288
- if (tools.length === 0) {
1289
- tools = args.yes ? ["codex"] : await promptForTools();
1290
- }
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);
1301
- const metadata = args.yes
1302
- ? {
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")
1311
- }
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
- });
1323
-
1324
- applyInstall(targetDir, tools, metadata);
1325
-
1326
- printInstallSummary("installed", tools);
1327
- process.stdout.write(` Target project: ${targetDir}\n\n`);
1565
+ let tools = normalizeToolList(args.tools);
1566
+ if (tools.length === 0) {
1567
+ tools = args.yes ? ["codex"] : await promptForTools();
1568
+ }
1569
+
1570
+ const targetDir = assertSafeTargetDirectory(args.directory);
1571
+ ensureDirectory(targetDir);
1572
+ assertSafeProjectPath(targetDir, path.join(targetDir, ".agents"));
1573
+ recoverInterruptedTransaction(targetDir);
1574
+ const existingTools = readNonEmptyLines(
1575
+ path.join(targetDir, ".agents", "macca-tools.txt"),
1576
+ );
1577
+ tools = unique([...existingTools, ...tools]);
1578
+ const configPath = path.join(targetDir, ".agents", "developer-config.json");
1579
+ const existingConfig = readDeveloperConfig(configPath);
1580
+ const hasExistingConfig = fs.existsSync(configPath);
1581
+ const metadata = args.yes
1582
+ ? {
1583
+ name:
1584
+ args.name !== undefined
1585
+ ? args.name
1586
+ : hasExistingConfig
1587
+ ? undefined
1588
+ : "",
1589
+ project:
1590
+ args.project !== undefined
1591
+ ? args.project
1592
+ : hasExistingConfig
1593
+ ? undefined
1594
+ : "",
1595
+ communicationLanguage:
1596
+ args.communicationLanguage !== undefined
1597
+ ? args.communicationLanguage
1598
+ : hasExistingConfig
1599
+ ? undefined
1600
+ : "Bahasa Indonesia",
1601
+ documentLanguage:
1602
+ args.documentLanguage !== undefined
1603
+ ? args.documentLanguage
1604
+ : hasExistingConfig
1605
+ ? undefined
1606
+ : "Bahasa Indonesia",
1607
+ }
1608
+ : await promptForMetadata({
1609
+ ...args,
1610
+ name: args.name !== undefined ? args.name : existingConfig.name,
1611
+ project:
1612
+ args.project !== undefined ? args.project : existingConfig.project,
1613
+ communicationLanguage:
1614
+ args.communicationLanguage !== undefined
1615
+ ? args.communicationLanguage
1616
+ : existingConfig.languagePreferences?.communication?.raw,
1617
+ documentLanguage:
1618
+ args.documentLanguage !== undefined
1619
+ ? args.documentLanguage
1620
+ : existingConfig.languagePreferences?.documents?.raw,
1621
+ });
1622
+
1623
+ applyInstall(targetDir, tools, metadata);
1624
+
1625
+ printInstallSummary("installed", tools);
1626
+ process.stdout.write(` Target project: ${targetDir}\n\n`);
1328
1627
  }
1329
1628
 
1330
1629
  function runUpgrade(args) {
1331
- const targetDir = assertSafeTargetDirectory(args.directory);
1332
- assertSafeProjectPath(targetDir, path.join(targetDir, ".agents"));
1333
- recoverInterruptedTransaction(targetDir);
1334
- const tools = readNonEmptyLines(path.join(targetDir, ".agents", "macca-tools.txt"));
1335
- applyUpgrade(targetDir);
1336
- printInstallSummary("updated", tools);
1337
- process.stdout.write(` Target project: ${targetDir}\n\n`);
1630
+ const targetDir = assertSafeTargetDirectory(args.directory);
1631
+ assertSafeProjectPath(targetDir, path.join(targetDir, ".agents"));
1632
+ recoverInterruptedTransaction(targetDir);
1633
+ const tools = readNonEmptyLines(
1634
+ path.join(targetDir, ".agents", "macca-tools.txt"),
1635
+ );
1636
+ applyUpgrade(targetDir);
1637
+ printInstallSummary("updated", tools);
1638
+ process.stdout.write(` Target project: ${targetDir}\n\n`);
1338
1639
  }
1339
1640
 
1340
1641
  function exitWithError(message) {
1341
- process.stderr.write(`\nError: ${message}\n`);
1342
- process.exit(1);
1642
+ process.stderr.write(`\nError: ${message}\n`);
1643
+ process.exit(1);
1343
1644
  }
1344
1645
 
1345
1646
  main();