macca-method 2.1.1 → 2.1.3

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