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