terminal-pilot 0.0.6 → 0.0.8
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/dist/cli.js +5663 -1269
- package/dist/cli.js.map +4 -4
- package/dist/commands/close-session.js +268 -87
- package/dist/commands/close-session.js.map +4 -4
- package/dist/commands/create-session.js +268 -87
- package/dist/commands/create-session.js.map +4 -4
- package/dist/commands/fill.js +268 -87
- package/dist/commands/fill.js.map +4 -4
- package/dist/commands/get-session.js +268 -87
- package/dist/commands/get-session.js.map +4 -4
- package/dist/commands/index.js +2404 -279
- package/dist/commands/index.js.map +4 -4
- package/dist/commands/install.js +1372 -14
- package/dist/commands/install.js.map +4 -4
- package/dist/commands/installer.js +178 -15
- package/dist/commands/installer.js.map +4 -4
- package/dist/commands/list-sessions.js +268 -87
- package/dist/commands/list-sessions.js.map +4 -4
- package/dist/commands/press-key.js +268 -87
- package/dist/commands/press-key.js.map +4 -4
- package/dist/commands/read-history.js +268 -87
- package/dist/commands/read-history.js.map +4 -4
- package/dist/commands/read-screen.js +268 -87
- package/dist/commands/read-screen.js.map +4 -4
- package/dist/commands/resize.js +268 -87
- package/dist/commands/resize.js.map +4 -4
- package/dist/commands/runtime.js +27 -85
- package/dist/commands/runtime.js.map +4 -4
- package/dist/commands/screenshot.js +1033 -91
- package/dist/commands/screenshot.js.map +4 -4
- package/dist/commands/send-signal.js +268 -87
- package/dist/commands/send-signal.js.map +4 -4
- package/dist/commands/type.js +268 -87
- package/dist/commands/type.js.map +4 -4
- package/dist/commands/uninstall.js +419 -17
- package/dist/commands/uninstall.js.map +4 -4
- package/dist/commands/wait-for-exit.js +268 -87
- package/dist/commands/wait-for-exit.js.map +4 -4
- package/dist/commands/wait-for.js +268 -87
- package/dist/commands/wait-for.js.map +4 -4
- package/dist/index.js +19 -83
- package/dist/index.js.map +3 -3
- package/dist/terminal-pilot.js +19 -83
- package/dist/terminal-pilot.js.map +3 -3
- package/dist/terminal-session.js +19 -83
- package/dist/terminal-session.js.map +3 -3
- package/dist/testing/cli-repl.js +5598 -1203
- package/dist/testing/cli-repl.js.map +4 -4
- package/dist/testing/qa-cli.js +5601 -1206
- package/dist/testing/qa-cli.js.map +4 -4
- package/package.json +17 -8
package/dist/commands/install.js
CHANGED
|
@@ -1,22 +1,1380 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import { installSkill } from "@poe-code/agent-skill-config";
|
|
3
|
-
import { defineCommand, S } from "@poe-code/cmdkit";
|
|
4
|
-
|
|
5
|
-
// src/commands/installer.ts
|
|
1
|
+
// ../agent-skill-config/src/configs.ts
|
|
6
2
|
import os from "node:os";
|
|
7
3
|
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// ../agent-defs/src/agents/claude-code.ts
|
|
6
|
+
var claudeCodeAgent = {
|
|
7
|
+
id: "claude-code",
|
|
8
|
+
name: "claude-code",
|
|
9
|
+
label: "Claude Code",
|
|
10
|
+
summary: "Configure Claude Code to route through Poe.",
|
|
11
|
+
aliases: ["claude"],
|
|
12
|
+
binaryName: "claude",
|
|
13
|
+
configPath: "~/.claude/settings.json",
|
|
14
|
+
branding: {
|
|
15
|
+
colors: {
|
|
16
|
+
dark: "#C15F3C",
|
|
17
|
+
light: "#C15F3C"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ../agent-defs/src/agents/claude-desktop.ts
|
|
23
|
+
var claudeDesktopAgent = {
|
|
24
|
+
id: "claude-desktop",
|
|
25
|
+
name: "claude-desktop",
|
|
26
|
+
label: "Claude Desktop",
|
|
27
|
+
summary: "Anthropic's official desktop application for Claude",
|
|
28
|
+
configPath: "~/.claude/settings.json",
|
|
29
|
+
branding: {
|
|
30
|
+
colors: {
|
|
31
|
+
dark: "#D97757",
|
|
32
|
+
light: "#D97757"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ../agent-defs/src/agents/codex.ts
|
|
38
|
+
var codexAgent = {
|
|
39
|
+
id: "codex",
|
|
40
|
+
name: "codex",
|
|
41
|
+
label: "Codex",
|
|
42
|
+
summary: "Configure Codex to use Poe as the model provider.",
|
|
43
|
+
binaryName: "codex",
|
|
44
|
+
configPath: "~/.codex/config.toml",
|
|
45
|
+
branding: {
|
|
46
|
+
colors: {
|
|
47
|
+
dark: "#D5D9DF",
|
|
48
|
+
light: "#7A7F86"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ../agent-defs/src/agents/opencode.ts
|
|
54
|
+
var openCodeAgent = {
|
|
55
|
+
id: "opencode",
|
|
56
|
+
name: "opencode",
|
|
57
|
+
label: "OpenCode CLI",
|
|
58
|
+
summary: "Configure OpenCode CLI to use the Poe API.",
|
|
59
|
+
binaryName: "opencode",
|
|
60
|
+
configPath: "~/.config/opencode/config.json",
|
|
61
|
+
branding: {
|
|
62
|
+
colors: {
|
|
63
|
+
dark: "#4A4F55",
|
|
64
|
+
light: "#2F3338"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ../agent-defs/src/agents/kimi.ts
|
|
70
|
+
var kimiAgent = {
|
|
71
|
+
id: "kimi",
|
|
72
|
+
name: "kimi",
|
|
73
|
+
label: "Kimi",
|
|
74
|
+
summary: "Configure Kimi CLI to use Poe API",
|
|
75
|
+
aliases: ["kimi-cli"],
|
|
76
|
+
binaryName: "kimi",
|
|
77
|
+
configPath: "~/.kimi/config.toml",
|
|
78
|
+
branding: {
|
|
79
|
+
colors: {
|
|
80
|
+
dark: "#7B68EE",
|
|
81
|
+
light: "#6A5ACD"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ../agent-defs/src/registry.ts
|
|
87
|
+
var allAgents = [
|
|
88
|
+
claudeCodeAgent,
|
|
89
|
+
claudeDesktopAgent,
|
|
90
|
+
codexAgent,
|
|
91
|
+
openCodeAgent,
|
|
92
|
+
kimiAgent
|
|
93
|
+
];
|
|
94
|
+
var lookup = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const agent of allAgents) {
|
|
96
|
+
const values = [agent.id, agent.name, ...agent.aliases ?? []];
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
const normalized = value.toLowerCase();
|
|
99
|
+
if (!lookup.has(normalized)) {
|
|
100
|
+
lookup.set(normalized, agent.id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function resolveAgentId(input) {
|
|
105
|
+
if (!input) {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
return lookup.get(input.toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ../agent-skill-config/src/configs.ts
|
|
112
|
+
var agentSkillConfigs = {
|
|
113
|
+
"claude-code": {
|
|
114
|
+
globalSkillDir: "~/.claude/skills",
|
|
115
|
+
localSkillDir: ".claude/skills"
|
|
116
|
+
},
|
|
117
|
+
codex: {
|
|
118
|
+
globalSkillDir: "~/.codex/skills",
|
|
119
|
+
localSkillDir: ".codex/skills"
|
|
120
|
+
},
|
|
121
|
+
opencode: {
|
|
122
|
+
globalSkillDir: "~/.config/opencode/skills",
|
|
123
|
+
localSkillDir: ".opencode/skills"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var supportedAgents = Object.keys(agentSkillConfigs);
|
|
127
|
+
function resolveAgentSupport(input, registry = agentSkillConfigs) {
|
|
128
|
+
const resolvedId = resolveAgentId(input);
|
|
129
|
+
if (!resolvedId) {
|
|
130
|
+
return { status: "unknown", input };
|
|
131
|
+
}
|
|
132
|
+
const config = registry[resolvedId];
|
|
133
|
+
if (!config) {
|
|
134
|
+
return { status: "unsupported", input, id: resolvedId };
|
|
135
|
+
}
|
|
136
|
+
return { status: "supported", input, id: resolvedId, config };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ../config-mutations/src/mutations/file-mutation.ts
|
|
140
|
+
function ensureDirectory(options) {
|
|
141
|
+
return {
|
|
142
|
+
kind: "ensureDirectory",
|
|
143
|
+
path: options.path,
|
|
144
|
+
label: options.label
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function remove(options) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "removeFile",
|
|
150
|
+
target: options.target,
|
|
151
|
+
whenEmpty: options.whenEmpty,
|
|
152
|
+
whenContentMatches: options.whenContentMatches,
|
|
153
|
+
label: options.label
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function removeDirectory(options) {
|
|
157
|
+
return {
|
|
158
|
+
kind: "removeDirectory",
|
|
159
|
+
path: options.path,
|
|
160
|
+
force: options.force,
|
|
161
|
+
label: options.label
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function chmod(options) {
|
|
165
|
+
return {
|
|
166
|
+
kind: "chmod",
|
|
167
|
+
target: options.target,
|
|
168
|
+
mode: options.mode,
|
|
169
|
+
label: options.label
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function backup(options) {
|
|
173
|
+
return {
|
|
174
|
+
kind: "backup",
|
|
175
|
+
target: options.target,
|
|
176
|
+
label: options.label
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
var fileMutation = {
|
|
180
|
+
ensureDirectory,
|
|
181
|
+
remove,
|
|
182
|
+
removeDirectory,
|
|
183
|
+
chmod,
|
|
184
|
+
backup
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ../config-mutations/src/mutations/template-mutation.ts
|
|
188
|
+
function write(options) {
|
|
189
|
+
return {
|
|
190
|
+
kind: "templateWrite",
|
|
191
|
+
target: options.target,
|
|
192
|
+
templateId: options.templateId,
|
|
193
|
+
context: options.context,
|
|
194
|
+
label: options.label
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function mergeToml(options) {
|
|
198
|
+
return {
|
|
199
|
+
kind: "templateMergeToml",
|
|
200
|
+
target: options.target,
|
|
201
|
+
templateId: options.templateId,
|
|
202
|
+
context: options.context,
|
|
203
|
+
label: options.label
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function mergeJson(options) {
|
|
207
|
+
return {
|
|
208
|
+
kind: "templateMergeJson",
|
|
209
|
+
target: options.target,
|
|
210
|
+
templateId: options.templateId,
|
|
211
|
+
context: options.context,
|
|
212
|
+
label: options.label
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
var templateMutation = {
|
|
216
|
+
write,
|
|
217
|
+
mergeToml,
|
|
218
|
+
mergeJson
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// ../config-mutations/src/execution/apply-mutation.ts
|
|
222
|
+
import Mustache from "mustache";
|
|
223
|
+
|
|
224
|
+
// ../config-mutations/src/formats/json.ts
|
|
225
|
+
import * as jsonc from "jsonc-parser";
|
|
226
|
+
function isConfigObject(value) {
|
|
227
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
|
+
}
|
|
229
|
+
function parse2(content) {
|
|
230
|
+
if (!content || content.trim() === "") {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
const errors = [];
|
|
234
|
+
const parsed = jsonc.parse(content, errors, {
|
|
235
|
+
allowTrailingComma: true,
|
|
236
|
+
disallowComments: false
|
|
237
|
+
});
|
|
238
|
+
if (errors.length > 0) {
|
|
239
|
+
throw new Error(`JSON parse error: ${jsonc.printParseErrorCode(errors[0].error)}`);
|
|
240
|
+
}
|
|
241
|
+
if (parsed === null || parsed === void 0) {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
if (!isConfigObject(parsed)) {
|
|
245
|
+
throw new Error("Expected JSON object.");
|
|
246
|
+
}
|
|
247
|
+
return parsed;
|
|
248
|
+
}
|
|
249
|
+
function serialize(obj) {
|
|
250
|
+
return `${JSON.stringify(obj, null, 2)}
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
function merge(base, patch) {
|
|
254
|
+
const result = { ...base };
|
|
255
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
256
|
+
if (value === void 0) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const existing = result[key];
|
|
260
|
+
if (isConfigObject(existing) && isConfigObject(value)) {
|
|
261
|
+
result[key] = merge(existing, value);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
result[key] = value;
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function prune(obj, shape) {
|
|
269
|
+
let changed = false;
|
|
270
|
+
const result = { ...obj };
|
|
271
|
+
for (const [key, pattern] of Object.entries(shape)) {
|
|
272
|
+
if (!(key in result)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const current = result[key];
|
|
276
|
+
if (isConfigObject(pattern) && Object.keys(pattern).length === 0) {
|
|
277
|
+
delete result[key];
|
|
278
|
+
changed = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (isConfigObject(pattern) && isConfigObject(current)) {
|
|
282
|
+
const { changed: childChanged, result: childResult } = prune(
|
|
283
|
+
current,
|
|
284
|
+
pattern
|
|
285
|
+
);
|
|
286
|
+
if (childChanged) {
|
|
287
|
+
changed = true;
|
|
288
|
+
}
|
|
289
|
+
if (Object.keys(childResult).length === 0) {
|
|
290
|
+
delete result[key];
|
|
291
|
+
} else {
|
|
292
|
+
result[key] = childResult;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
delete result[key];
|
|
297
|
+
changed = true;
|
|
298
|
+
}
|
|
299
|
+
return { changed, result };
|
|
300
|
+
}
|
|
301
|
+
var jsonFormat = {
|
|
302
|
+
parse: parse2,
|
|
303
|
+
serialize,
|
|
304
|
+
merge,
|
|
305
|
+
prune
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// ../config-mutations/src/formats/toml.ts
|
|
309
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
310
|
+
function isConfigObject2(value) {
|
|
311
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
312
|
+
}
|
|
313
|
+
function parse3(content) {
|
|
314
|
+
if (!content || content.trim() === "") {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
const parsed = parseToml(content);
|
|
318
|
+
if (!isConfigObject2(parsed)) {
|
|
319
|
+
throw new Error("Expected TOML document to be a table.");
|
|
320
|
+
}
|
|
321
|
+
return parsed;
|
|
322
|
+
}
|
|
323
|
+
function serialize2(obj) {
|
|
324
|
+
const serialized = stringifyToml(obj);
|
|
325
|
+
return serialized.endsWith("\n") ? serialized : `${serialized}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
function merge2(base, patch) {
|
|
329
|
+
const result = { ...base };
|
|
330
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
331
|
+
if (value === void 0) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const existing = result[key];
|
|
335
|
+
if (isConfigObject2(existing) && isConfigObject2(value)) {
|
|
336
|
+
result[key] = merge2(existing, value);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
result[key] = value;
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
function prune2(obj, shape) {
|
|
344
|
+
let changed = false;
|
|
345
|
+
const result = { ...obj };
|
|
346
|
+
for (const [key, pattern] of Object.entries(shape)) {
|
|
347
|
+
if (!(key in result)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const current = result[key];
|
|
351
|
+
if (isConfigObject2(pattern) && Object.keys(pattern).length === 0) {
|
|
352
|
+
delete result[key];
|
|
353
|
+
changed = true;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (isConfigObject2(pattern) && isConfigObject2(current)) {
|
|
357
|
+
const { changed: childChanged, result: childResult } = prune2(
|
|
358
|
+
current,
|
|
359
|
+
pattern
|
|
360
|
+
);
|
|
361
|
+
if (childChanged) {
|
|
362
|
+
changed = true;
|
|
363
|
+
}
|
|
364
|
+
if (Object.keys(childResult).length === 0) {
|
|
365
|
+
delete result[key];
|
|
366
|
+
} else {
|
|
367
|
+
result[key] = childResult;
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
delete result[key];
|
|
372
|
+
changed = true;
|
|
373
|
+
}
|
|
374
|
+
return { changed, result };
|
|
375
|
+
}
|
|
376
|
+
var tomlFormat = {
|
|
377
|
+
parse: parse3,
|
|
378
|
+
serialize: serialize2,
|
|
379
|
+
merge: merge2,
|
|
380
|
+
prune: prune2
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// ../config-mutations/src/formats/index.ts
|
|
384
|
+
var formatRegistry = {
|
|
385
|
+
json: jsonFormat,
|
|
386
|
+
toml: tomlFormat
|
|
387
|
+
};
|
|
388
|
+
var extensionMap = {
|
|
389
|
+
".json": "json",
|
|
390
|
+
".toml": "toml"
|
|
391
|
+
};
|
|
392
|
+
function getConfigFormat(pathOrFormat) {
|
|
393
|
+
if (pathOrFormat in formatRegistry) {
|
|
394
|
+
return formatRegistry[pathOrFormat];
|
|
395
|
+
}
|
|
396
|
+
const ext = getExtension(pathOrFormat);
|
|
397
|
+
const formatName = extensionMap[ext];
|
|
398
|
+
if (!formatName) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Unsupported config format. Cannot detect format from "${pathOrFormat}". Supported extensions: ${Object.keys(extensionMap).join(", ")}. Supported format names: ${Object.keys(formatRegistry).join(", ")}.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return formatRegistry[formatName];
|
|
404
|
+
}
|
|
405
|
+
function detectFormat(path4) {
|
|
406
|
+
const ext = getExtension(path4);
|
|
407
|
+
return extensionMap[ext];
|
|
408
|
+
}
|
|
409
|
+
function getExtension(path4) {
|
|
410
|
+
const lastDot = path4.lastIndexOf(".");
|
|
411
|
+
if (lastDot === -1) {
|
|
412
|
+
return "";
|
|
413
|
+
}
|
|
414
|
+
return path4.slice(lastDot).toLowerCase();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ../config-mutations/src/execution/path-utils.ts
|
|
418
|
+
import path2 from "node:path";
|
|
419
|
+
function expandHome(targetPath, homeDir) {
|
|
420
|
+
if (!targetPath?.startsWith("~")) {
|
|
421
|
+
return targetPath;
|
|
422
|
+
}
|
|
423
|
+
if (targetPath.startsWith("~./")) {
|
|
424
|
+
targetPath = `~/.${targetPath.slice(3)}`;
|
|
425
|
+
}
|
|
426
|
+
let remainder = targetPath.slice(1);
|
|
427
|
+
if (remainder.startsWith("/") || remainder.startsWith("\\")) {
|
|
428
|
+
remainder = remainder.slice(1);
|
|
429
|
+
} else if (remainder.startsWith(".")) {
|
|
430
|
+
remainder = remainder.slice(1);
|
|
431
|
+
if (remainder.startsWith("/") || remainder.startsWith("\\")) {
|
|
432
|
+
remainder = remainder.slice(1);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return remainder.length === 0 ? homeDir : path2.join(homeDir, remainder);
|
|
436
|
+
}
|
|
437
|
+
function validateHomePath(targetPath) {
|
|
438
|
+
if (typeof targetPath !== "string" || targetPath.length === 0) {
|
|
439
|
+
throw new Error("Target path must be a non-empty string.");
|
|
440
|
+
}
|
|
441
|
+
if (!targetPath.startsWith("~")) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`All target paths must be home-relative (start with ~). Received: "${targetPath}"`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function resolvePath(rawPath, homeDir, pathMapper) {
|
|
448
|
+
validateHomePath(rawPath);
|
|
449
|
+
const expanded = expandHome(rawPath, homeDir);
|
|
450
|
+
if (!pathMapper) {
|
|
451
|
+
return expanded;
|
|
452
|
+
}
|
|
453
|
+
const rawDirectory = path2.dirname(expanded);
|
|
454
|
+
const mappedDirectory = pathMapper.mapTargetDirectory({
|
|
455
|
+
targetDirectory: rawDirectory
|
|
456
|
+
});
|
|
457
|
+
const filename = path2.basename(expanded);
|
|
458
|
+
return filename.length === 0 ? mappedDirectory : path2.join(mappedDirectory, filename);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ../config-mutations/src/fs-utils.ts
|
|
462
|
+
function isNotFound(error) {
|
|
463
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
464
|
+
}
|
|
465
|
+
async function readFileIfExists(fs, target) {
|
|
466
|
+
try {
|
|
467
|
+
return await fs.readFile(target, "utf8");
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (isNotFound(error)) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function pathExists(fs, target) {
|
|
476
|
+
try {
|
|
477
|
+
await fs.stat(target);
|
|
478
|
+
return true;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (isNotFound(error)) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function createTimestamp() {
|
|
487
|
+
return (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ../config-mutations/src/execution/apply-mutation.ts
|
|
491
|
+
function resolveValue(resolver, options) {
|
|
492
|
+
if (typeof resolver === "function") {
|
|
493
|
+
return resolver(options);
|
|
494
|
+
}
|
|
495
|
+
return resolver;
|
|
496
|
+
}
|
|
497
|
+
function createInvalidDocumentBackupPath(targetPath) {
|
|
498
|
+
const ext = targetPath.includes(".") ? targetPath.split(".").pop() : "bak";
|
|
499
|
+
return `${targetPath}.invalid-${createTimestamp()}.${ext}`;
|
|
500
|
+
}
|
|
501
|
+
async function backupInvalidDocument(fs, targetPath, content) {
|
|
502
|
+
const backupPath = createInvalidDocumentBackupPath(targetPath);
|
|
503
|
+
await fs.writeFile(backupPath, content, { encoding: "utf8" });
|
|
504
|
+
}
|
|
505
|
+
function describeMutation(kind, targetPath) {
|
|
506
|
+
const displayPath = targetPath ?? "target";
|
|
507
|
+
switch (kind) {
|
|
508
|
+
case "ensureDirectory":
|
|
509
|
+
return `Create ${displayPath}`;
|
|
510
|
+
case "removeDirectory":
|
|
511
|
+
return `Remove directory ${displayPath}`;
|
|
512
|
+
case "backup":
|
|
513
|
+
return `Backup ${displayPath}`;
|
|
514
|
+
case "templateWrite":
|
|
515
|
+
return `Write ${displayPath}`;
|
|
516
|
+
case "chmod":
|
|
517
|
+
return `Set permissions on ${displayPath}`;
|
|
518
|
+
case "removeFile":
|
|
519
|
+
return `Remove ${displayPath}`;
|
|
520
|
+
case "configMerge":
|
|
521
|
+
case "configPrune":
|
|
522
|
+
case "configTransform":
|
|
523
|
+
case "templateMergeToml":
|
|
524
|
+
case "templateMergeJson":
|
|
525
|
+
return `Update ${displayPath}`;
|
|
526
|
+
default:
|
|
527
|
+
return "Operation";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function pruneKeysByPrefix(table, prefix) {
|
|
531
|
+
const result = {};
|
|
532
|
+
for (const [key, value] of Object.entries(table)) {
|
|
533
|
+
if (!key.startsWith(prefix)) {
|
|
534
|
+
result[key] = value;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
function isConfigObject3(value) {
|
|
540
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
541
|
+
}
|
|
542
|
+
function mergeWithPruneByPrefix(base, patch, pruneByPrefix) {
|
|
543
|
+
const result = { ...base };
|
|
544
|
+
const prefixMap = pruneByPrefix ?? {};
|
|
545
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
546
|
+
const current = result[key];
|
|
547
|
+
const prefix = prefixMap[key];
|
|
548
|
+
if (isConfigObject3(current) && isConfigObject3(value)) {
|
|
549
|
+
if (prefix) {
|
|
550
|
+
const pruned = pruneKeysByPrefix(current, prefix);
|
|
551
|
+
result[key] = { ...pruned, ...value };
|
|
552
|
+
} else {
|
|
553
|
+
result[key] = mergeWithPruneByPrefix(
|
|
554
|
+
current,
|
|
555
|
+
value,
|
|
556
|
+
prefixMap
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
result[key] = value;
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
async function applyMutation(mutation, context, options) {
|
|
566
|
+
switch (mutation.kind) {
|
|
567
|
+
case "ensureDirectory":
|
|
568
|
+
return applyEnsureDirectory(mutation, context, options);
|
|
569
|
+
case "removeDirectory":
|
|
570
|
+
return applyRemoveDirectory(mutation, context, options);
|
|
571
|
+
case "removeFile":
|
|
572
|
+
return applyRemoveFile(mutation, context, options);
|
|
573
|
+
case "chmod":
|
|
574
|
+
return applyChmod(mutation, context, options);
|
|
575
|
+
case "backup":
|
|
576
|
+
return applyBackup(mutation, context, options);
|
|
577
|
+
case "configMerge":
|
|
578
|
+
return applyConfigMerge(mutation, context, options);
|
|
579
|
+
case "configPrune":
|
|
580
|
+
return applyConfigPrune(mutation, context, options);
|
|
581
|
+
case "configTransform":
|
|
582
|
+
return applyConfigTransform(mutation, context, options);
|
|
583
|
+
case "templateWrite":
|
|
584
|
+
return applyTemplateWrite(mutation, context, options);
|
|
585
|
+
case "templateMergeToml":
|
|
586
|
+
return applyTemplateMerge(mutation, context, options, "toml");
|
|
587
|
+
case "templateMergeJson":
|
|
588
|
+
return applyTemplateMerge(mutation, context, options, "json");
|
|
589
|
+
default: {
|
|
590
|
+
const never = mutation;
|
|
591
|
+
throw new Error(`Unknown mutation kind: ${never.kind}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function applyEnsureDirectory(mutation, context, options) {
|
|
596
|
+
const rawPath = resolveValue(mutation.path, options);
|
|
597
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
598
|
+
const details = {
|
|
599
|
+
kind: mutation.kind,
|
|
600
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
601
|
+
targetPath
|
|
602
|
+
};
|
|
603
|
+
const existed = await pathExists(context.fs, targetPath);
|
|
604
|
+
if (!context.dryRun) {
|
|
605
|
+
await context.fs.mkdir(targetPath, { recursive: true });
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
outcome: {
|
|
609
|
+
changed: !existed,
|
|
610
|
+
effect: "mkdir",
|
|
611
|
+
detail: existed ? "noop" : "create"
|
|
612
|
+
},
|
|
613
|
+
details
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function applyRemoveDirectory(mutation, context, options) {
|
|
617
|
+
const rawPath = resolveValue(mutation.path, options);
|
|
618
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
619
|
+
const details = {
|
|
620
|
+
kind: mutation.kind,
|
|
621
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
622
|
+
targetPath
|
|
623
|
+
};
|
|
624
|
+
const existed = await pathExists(context.fs, targetPath);
|
|
625
|
+
if (!existed) {
|
|
626
|
+
return {
|
|
627
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
628
|
+
details
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (typeof context.fs.rm !== "function") {
|
|
632
|
+
return {
|
|
633
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
634
|
+
details
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (mutation.force) {
|
|
638
|
+
if (!context.dryRun) {
|
|
639
|
+
await context.fs.rm(targetPath, { recursive: true, force: true });
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
643
|
+
details
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const entries = await context.fs.readdir(targetPath);
|
|
647
|
+
if (entries.length > 0) {
|
|
648
|
+
return {
|
|
649
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
650
|
+
details
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (!context.dryRun) {
|
|
654
|
+
await context.fs.rm(targetPath, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
658
|
+
details
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async function applyRemoveFile(mutation, context, options) {
|
|
662
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
663
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
664
|
+
const details = {
|
|
665
|
+
kind: mutation.kind,
|
|
666
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
667
|
+
targetPath
|
|
668
|
+
};
|
|
669
|
+
try {
|
|
670
|
+
const content = await context.fs.readFile(targetPath, "utf8");
|
|
671
|
+
const trimmed = content.trim();
|
|
672
|
+
if (mutation.whenContentMatches && !mutation.whenContentMatches.test(trimmed)) {
|
|
673
|
+
return {
|
|
674
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
675
|
+
details
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (mutation.whenEmpty && trimmed.length > 0) {
|
|
679
|
+
return {
|
|
680
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
681
|
+
details
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (!context.dryRun) {
|
|
685
|
+
await context.fs.unlink(targetPath);
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
689
|
+
details
|
|
690
|
+
};
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (isNotFound(error)) {
|
|
693
|
+
return {
|
|
694
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
695
|
+
details
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function applyChmod(mutation, context, options) {
|
|
702
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
703
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
704
|
+
const details = {
|
|
705
|
+
kind: mutation.kind,
|
|
706
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
707
|
+
targetPath
|
|
708
|
+
};
|
|
709
|
+
if (typeof context.fs.chmod !== "function") {
|
|
710
|
+
return {
|
|
711
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
712
|
+
details
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const stat = await context.fs.stat(targetPath);
|
|
717
|
+
const currentMode = typeof stat.mode === "number" ? stat.mode & 511 : null;
|
|
718
|
+
if (currentMode === mutation.mode) {
|
|
719
|
+
return {
|
|
720
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
721
|
+
details
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (!context.dryRun) {
|
|
725
|
+
await context.fs.chmod(targetPath, mutation.mode);
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
outcome: { changed: true, effect: "chmod", detail: "update" },
|
|
729
|
+
details
|
|
730
|
+
};
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (isNotFound(error)) {
|
|
733
|
+
return {
|
|
734
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
735
|
+
details
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function applyBackup(mutation, context, options) {
|
|
742
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
743
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
744
|
+
const details = {
|
|
745
|
+
kind: mutation.kind,
|
|
746
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
747
|
+
targetPath
|
|
748
|
+
};
|
|
749
|
+
const content = await readFileIfExists(context.fs, targetPath);
|
|
750
|
+
if (content === null) {
|
|
751
|
+
return {
|
|
752
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
753
|
+
details
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (!context.dryRun) {
|
|
757
|
+
const backupPath = `${targetPath}.backup-${createTimestamp()}`;
|
|
758
|
+
await context.fs.writeFile(backupPath, content, { encoding: "utf8" });
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
outcome: { changed: true, effect: "copy", detail: "backup" },
|
|
762
|
+
details
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async function applyConfigMerge(mutation, context, options) {
|
|
766
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
767
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
768
|
+
const details = {
|
|
769
|
+
kind: mutation.kind,
|
|
770
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
771
|
+
targetPath
|
|
772
|
+
};
|
|
773
|
+
const formatName = mutation.format ?? detectFormat(rawPath);
|
|
774
|
+
if (!formatName) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`Cannot detect config format for "${rawPath}". Provide explicit format option.`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
const format = getConfigFormat(formatName);
|
|
780
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
781
|
+
let current;
|
|
782
|
+
try {
|
|
783
|
+
current = rawContent === null ? {} : format.parse(rawContent);
|
|
784
|
+
} catch {
|
|
785
|
+
if (rawContent !== null) {
|
|
786
|
+
await backupInvalidDocument(context.fs, targetPath, rawContent);
|
|
787
|
+
}
|
|
788
|
+
current = {};
|
|
789
|
+
}
|
|
790
|
+
const value = resolveValue(mutation.value, options);
|
|
791
|
+
let merged;
|
|
792
|
+
if (mutation.pruneByPrefix) {
|
|
793
|
+
merged = mergeWithPruneByPrefix(current, value, mutation.pruneByPrefix);
|
|
794
|
+
} else {
|
|
795
|
+
merged = format.merge(current, value);
|
|
796
|
+
}
|
|
797
|
+
const serialized = format.serialize(merged);
|
|
798
|
+
const changed = serialized !== rawContent;
|
|
799
|
+
if (changed && !context.dryRun) {
|
|
800
|
+
await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
outcome: {
|
|
804
|
+
changed,
|
|
805
|
+
effect: changed ? "write" : "none",
|
|
806
|
+
detail: changed ? rawContent === null ? "create" : "update" : "noop"
|
|
807
|
+
},
|
|
808
|
+
details
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
async function applyConfigPrune(mutation, context, options) {
|
|
812
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
813
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
814
|
+
const details = {
|
|
815
|
+
kind: mutation.kind,
|
|
816
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
817
|
+
targetPath
|
|
818
|
+
};
|
|
819
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
820
|
+
if (rawContent === null) {
|
|
821
|
+
return {
|
|
822
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
823
|
+
details
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const formatName = mutation.format ?? detectFormat(rawPath);
|
|
827
|
+
if (!formatName) {
|
|
828
|
+
throw new Error(
|
|
829
|
+
`Cannot detect config format for "${rawPath}". Provide explicit format option.`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
const format = getConfigFormat(formatName);
|
|
833
|
+
let current;
|
|
834
|
+
try {
|
|
835
|
+
current = format.parse(rawContent);
|
|
836
|
+
} catch {
|
|
837
|
+
return {
|
|
838
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
839
|
+
details
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (mutation.onlyIf && !mutation.onlyIf(current, options)) {
|
|
843
|
+
return {
|
|
844
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
845
|
+
details
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
const shape = resolveValue(mutation.shape, options);
|
|
849
|
+
const { changed, result } = format.prune(current, shape);
|
|
850
|
+
if (!changed) {
|
|
851
|
+
return {
|
|
852
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
853
|
+
details
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (Object.keys(result).length === 0) {
|
|
857
|
+
if (!context.dryRun) {
|
|
858
|
+
await context.fs.unlink(targetPath);
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
862
|
+
details
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const serialized = format.serialize(result);
|
|
866
|
+
if (!context.dryRun) {
|
|
867
|
+
await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
outcome: { changed: true, effect: "write", detail: "update" },
|
|
871
|
+
details
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
async function applyConfigTransform(mutation, context, options) {
|
|
875
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
876
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
877
|
+
const details = {
|
|
878
|
+
kind: mutation.kind,
|
|
879
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
880
|
+
targetPath
|
|
881
|
+
};
|
|
882
|
+
const formatName = mutation.format ?? detectFormat(rawPath);
|
|
883
|
+
if (!formatName) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
`Cannot detect config format for "${rawPath}". Provide explicit format option.`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const format = getConfigFormat(formatName);
|
|
889
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
890
|
+
let current;
|
|
891
|
+
try {
|
|
892
|
+
current = rawContent === null ? {} : format.parse(rawContent);
|
|
893
|
+
} catch {
|
|
894
|
+
if (rawContent !== null) {
|
|
895
|
+
await backupInvalidDocument(context.fs, targetPath, rawContent);
|
|
896
|
+
}
|
|
897
|
+
current = {};
|
|
898
|
+
}
|
|
899
|
+
const { content: transformed, changed } = mutation.transform(current, options);
|
|
900
|
+
if (!changed) {
|
|
901
|
+
return {
|
|
902
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
903
|
+
details
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
if (transformed === null) {
|
|
907
|
+
if (rawContent === null) {
|
|
908
|
+
return {
|
|
909
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
910
|
+
details
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
if (!context.dryRun) {
|
|
914
|
+
await context.fs.unlink(targetPath);
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
outcome: { changed: true, effect: "delete", detail: "delete" },
|
|
918
|
+
details
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const serialized = format.serialize(transformed);
|
|
922
|
+
if (!context.dryRun) {
|
|
923
|
+
await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
outcome: {
|
|
927
|
+
changed: true,
|
|
928
|
+
effect: "write",
|
|
929
|
+
detail: rawContent === null ? "create" : "update"
|
|
930
|
+
},
|
|
931
|
+
details
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
async function applyTemplateWrite(mutation, context, options) {
|
|
935
|
+
if (!context.templates) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
"Template mutations require a templates loader. Provide templates function to runMutations context."
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
941
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
942
|
+
const details = {
|
|
943
|
+
kind: mutation.kind,
|
|
944
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
945
|
+
targetPath
|
|
946
|
+
};
|
|
947
|
+
const template = await context.templates(mutation.templateId);
|
|
948
|
+
const templateContext = mutation.context ? resolveValue(mutation.context, options) : {};
|
|
949
|
+
const rendered = Mustache.render(template, templateContext);
|
|
950
|
+
const existed = await pathExists(context.fs, targetPath);
|
|
951
|
+
if (!context.dryRun) {
|
|
952
|
+
await context.fs.writeFile(targetPath, rendered, { encoding: "utf8" });
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
outcome: {
|
|
956
|
+
changed: true,
|
|
957
|
+
effect: "write",
|
|
958
|
+
detail: existed ? "update" : "create"
|
|
959
|
+
},
|
|
960
|
+
details
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async function applyTemplateMerge(mutation, context, options, formatName) {
|
|
964
|
+
if (!context.templates) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
"Template mutations require a templates loader. Provide templates function to runMutations context."
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const rawPath = resolveValue(mutation.target, options);
|
|
970
|
+
const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
|
|
971
|
+
const details = {
|
|
972
|
+
kind: mutation.kind,
|
|
973
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
974
|
+
targetPath
|
|
975
|
+
};
|
|
976
|
+
const format = getConfigFormat(formatName);
|
|
977
|
+
const template = await context.templates(mutation.templateId);
|
|
978
|
+
const templateContext = mutation.context ? resolveValue(mutation.context, options) : {};
|
|
979
|
+
const rendered = Mustache.render(template, templateContext);
|
|
980
|
+
let templateDoc;
|
|
981
|
+
try {
|
|
982
|
+
templateDoc = format.parse(rendered);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
throw new Error(
|
|
985
|
+
`Failed to parse rendered template "${mutation.templateId}" as ${formatName.toUpperCase()}: ${error}`,
|
|
986
|
+
{ cause: error }
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
990
|
+
let current;
|
|
991
|
+
try {
|
|
992
|
+
current = rawContent === null ? {} : format.parse(rawContent);
|
|
993
|
+
} catch {
|
|
994
|
+
if (rawContent !== null) {
|
|
995
|
+
await backupInvalidDocument(context.fs, targetPath, rawContent);
|
|
996
|
+
}
|
|
997
|
+
current = {};
|
|
998
|
+
}
|
|
999
|
+
const merged = format.merge(current, templateDoc);
|
|
1000
|
+
const serialized = format.serialize(merged);
|
|
1001
|
+
const changed = serialized !== rawContent;
|
|
1002
|
+
if (changed && !context.dryRun) {
|
|
1003
|
+
await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
outcome: {
|
|
1007
|
+
changed,
|
|
1008
|
+
effect: changed ? "write" : "none",
|
|
1009
|
+
detail: changed ? rawContent === null ? "create" : "update" : "noop"
|
|
1010
|
+
},
|
|
1011
|
+
details
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ../config-mutations/src/execution/run-mutations.ts
|
|
1016
|
+
async function runMutations(mutations, context, options) {
|
|
1017
|
+
const effects = [];
|
|
1018
|
+
let anyChanged = false;
|
|
1019
|
+
const resolverOptions = options ?? {};
|
|
1020
|
+
for (const mutation of mutations) {
|
|
1021
|
+
const { outcome } = await executeMutation(
|
|
1022
|
+
mutation,
|
|
1023
|
+
context,
|
|
1024
|
+
resolverOptions
|
|
1025
|
+
);
|
|
1026
|
+
effects.push(outcome);
|
|
1027
|
+
if (outcome.changed) {
|
|
1028
|
+
anyChanged = true;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
changed: anyChanged,
|
|
1033
|
+
effects
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
async function executeMutation(mutation, context, options) {
|
|
1037
|
+
context.observers?.onStart?.({
|
|
1038
|
+
kind: mutation.kind,
|
|
1039
|
+
label: mutation.label ?? mutation.kind,
|
|
1040
|
+
targetPath: void 0
|
|
1041
|
+
// Will be resolved during apply
|
|
1042
|
+
});
|
|
1043
|
+
try {
|
|
1044
|
+
const { outcome, details } = await applyMutation(mutation, context, options);
|
|
1045
|
+
context.observers?.onComplete?.(details, outcome);
|
|
1046
|
+
return { outcome, details };
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
context.observers?.onError?.(
|
|
1049
|
+
{
|
|
1050
|
+
kind: mutation.kind,
|
|
1051
|
+
label: mutation.label ?? mutation.kind,
|
|
1052
|
+
targetPath: void 0
|
|
1053
|
+
},
|
|
1054
|
+
error
|
|
1055
|
+
);
|
|
1056
|
+
throw error;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ../config-mutations/src/template/render.ts
|
|
1061
|
+
import Mustache2 from "mustache";
|
|
1062
|
+
var originalEscape = Mustache2.escape;
|
|
1063
|
+
|
|
1064
|
+
// ../agent-skill-config/src/apply.ts
|
|
1065
|
+
var UnsupportedAgentError = class extends Error {
|
|
1066
|
+
constructor(agentId) {
|
|
1067
|
+
super(`Unsupported agent: ${agentId}`);
|
|
1068
|
+
this.name = "UnsupportedAgentError";
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
function toHomeRelative(localSkillDir) {
|
|
1072
|
+
if (localSkillDir.startsWith("~/") || localSkillDir === "~") {
|
|
1073
|
+
return localSkillDir;
|
|
1074
|
+
}
|
|
1075
|
+
const normalized = localSkillDir.startsWith("./") ? localSkillDir.slice(2) : localSkillDir;
|
|
1076
|
+
return `~/${normalized}`;
|
|
1077
|
+
}
|
|
1078
|
+
var SKILL_TEMPLATE_ID = "__skill_content__";
|
|
1079
|
+
async function installSkill(agentId, skill, options) {
|
|
1080
|
+
const support = resolveAgentSupport(agentId);
|
|
1081
|
+
if (support.status !== "supported") {
|
|
1082
|
+
throw new UnsupportedAgentError(agentId);
|
|
1083
|
+
}
|
|
1084
|
+
const scope = options.scope ?? "local";
|
|
1085
|
+
const config = support.config;
|
|
1086
|
+
const skillDir = scope === "global" ? config.globalSkillDir : toHomeRelative(config.localSkillDir);
|
|
1087
|
+
const skillFolderPath = `${skillDir}/${skill.name}`;
|
|
1088
|
+
const skillFilePath = `${skillFolderPath}/SKILL.md`;
|
|
1089
|
+
const displayPath = `${scope === "global" ? config.globalSkillDir : config.localSkillDir}/${skill.name}/SKILL.md`;
|
|
1090
|
+
await runMutations(
|
|
1091
|
+
[
|
|
1092
|
+
fileMutation.ensureDirectory({
|
|
1093
|
+
path: skillFolderPath,
|
|
1094
|
+
label: `Ensure skill directory ${skill.name}`
|
|
1095
|
+
}),
|
|
1096
|
+
templateMutation.write({
|
|
1097
|
+
target: skillFilePath,
|
|
1098
|
+
templateId: SKILL_TEMPLATE_ID,
|
|
1099
|
+
label: `Write skill ${skill.name}`
|
|
1100
|
+
})
|
|
1101
|
+
],
|
|
1102
|
+
{
|
|
1103
|
+
fs: options.fs,
|
|
1104
|
+
homeDir: scope === "global" ? options.homeDir : options.cwd,
|
|
1105
|
+
dryRun: options.dryRun,
|
|
1106
|
+
observers: options.observers,
|
|
1107
|
+
templates: async (templateId) => {
|
|
1108
|
+
if (templateId === SKILL_TEMPLATE_ID) {
|
|
1109
|
+
return skill.content;
|
|
1110
|
+
}
|
|
1111
|
+
throw new Error(`Unknown template: ${templateId}`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1115
|
+
return { skillPath: skillFilePath, displayPath };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ../cmdkit/src/index.ts
|
|
1119
|
+
import { fileURLToPath } from "node:url";
|
|
1120
|
+
|
|
1121
|
+
// ../cmdkit-schema/src/index.ts
|
|
1122
|
+
function assertValidEnumValues(values) {
|
|
1123
|
+
if (values.length === 0) {
|
|
1124
|
+
throw new Error("Enum schema requires at least one value");
|
|
1125
|
+
}
|
|
1126
|
+
const uniqueValues = new Set(values);
|
|
1127
|
+
if (uniqueValues.size !== values.length) {
|
|
1128
|
+
throw new Error("Enum schema values must be unique");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
var S = {
|
|
1132
|
+
String(options = {}) {
|
|
1133
|
+
return {
|
|
1134
|
+
kind: "string",
|
|
1135
|
+
...options
|
|
1136
|
+
};
|
|
1137
|
+
},
|
|
1138
|
+
Number(options = {}) {
|
|
1139
|
+
return {
|
|
1140
|
+
kind: "number",
|
|
1141
|
+
...options
|
|
1142
|
+
};
|
|
1143
|
+
},
|
|
1144
|
+
Boolean(options = {}) {
|
|
1145
|
+
return {
|
|
1146
|
+
kind: "boolean",
|
|
1147
|
+
...options
|
|
1148
|
+
};
|
|
1149
|
+
},
|
|
1150
|
+
Enum(values, options = {}) {
|
|
1151
|
+
assertValidEnumValues(values);
|
|
1152
|
+
return {
|
|
1153
|
+
kind: "enum",
|
|
1154
|
+
values,
|
|
1155
|
+
...options
|
|
1156
|
+
};
|
|
1157
|
+
},
|
|
1158
|
+
Array(item, options = {}) {
|
|
1159
|
+
return {
|
|
1160
|
+
kind: "array",
|
|
1161
|
+
item,
|
|
1162
|
+
...options
|
|
1163
|
+
};
|
|
1164
|
+
},
|
|
1165
|
+
Object(shape) {
|
|
1166
|
+
return {
|
|
1167
|
+
kind: "object",
|
|
1168
|
+
shape
|
|
1169
|
+
};
|
|
1170
|
+
},
|
|
1171
|
+
Optional(inner) {
|
|
1172
|
+
return {
|
|
1173
|
+
kind: "optional",
|
|
1174
|
+
inner
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// ../cmdkit/src/index.ts
|
|
1180
|
+
var commandConfigSymbol = /* @__PURE__ */ Symbol("cmdkit.command.config");
|
|
1181
|
+
var commandSourcePathSymbol = /* @__PURE__ */ Symbol("cmdkit.command.sourcePath");
|
|
1182
|
+
var UserError = class extends Error {
|
|
1183
|
+
constructor(message) {
|
|
1184
|
+
super(message);
|
|
1185
|
+
this.name = "UserError";
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
function cloneScope(scope) {
|
|
1189
|
+
return scope === void 0 ? void 0 : [...scope];
|
|
1190
|
+
}
|
|
1191
|
+
function cloneSecretDefinition(secret) {
|
|
1192
|
+
return {
|
|
1193
|
+
env: secret.env,
|
|
1194
|
+
description: secret.description,
|
|
1195
|
+
optional: secret.optional
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function cloneSecrets(secrets) {
|
|
1199
|
+
if (secrets === void 0) {
|
|
1200
|
+
return {};
|
|
1201
|
+
}
|
|
1202
|
+
return Object.fromEntries(
|
|
1203
|
+
Object.entries(secrets).map(([key, secret]) => [key, cloneSecretDefinition(secret)])
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
function cloneRequires(requires) {
|
|
1207
|
+
if (requires === void 0) {
|
|
1208
|
+
return void 0;
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
auth: requires.auth,
|
|
1212
|
+
apiVersion: requires.apiVersion,
|
|
1213
|
+
check: requires.check
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function parseStackPath(candidate) {
|
|
1217
|
+
if (candidate.startsWith("file://")) {
|
|
1218
|
+
try {
|
|
1219
|
+
return fileURLToPath(candidate);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return void 0;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (candidate.startsWith("/")) {
|
|
1225
|
+
return candidate;
|
|
1226
|
+
}
|
|
1227
|
+
return void 0;
|
|
1228
|
+
}
|
|
1229
|
+
function extractStackPath(line) {
|
|
1230
|
+
const trimmed = line.trim();
|
|
1231
|
+
const fileIndex = trimmed.indexOf("file://");
|
|
1232
|
+
if (fileIndex >= 0) {
|
|
1233
|
+
const location2 = trimmed.slice(fileIndex);
|
|
1234
|
+
const separatorIndex2 = location2.lastIndexOf(":");
|
|
1235
|
+
const previousSeparatorIndex2 = separatorIndex2 >= 0 ? location2.lastIndexOf(":", separatorIndex2 - 1) : -1;
|
|
1236
|
+
const candidate2 = separatorIndex2 >= 0 && previousSeparatorIndex2 >= 0 ? location2.slice(0, previousSeparatorIndex2) : location2;
|
|
1237
|
+
return parseStackPath(candidate2);
|
|
1238
|
+
}
|
|
1239
|
+
const slashIndex = trimmed.indexOf("/");
|
|
1240
|
+
if (slashIndex < 0) {
|
|
1241
|
+
return void 0;
|
|
1242
|
+
}
|
|
1243
|
+
const location = trimmed.slice(slashIndex);
|
|
1244
|
+
const separatorIndex = location.lastIndexOf(":");
|
|
1245
|
+
const previousSeparatorIndex = separatorIndex >= 0 ? location.lastIndexOf(":", separatorIndex - 1) : -1;
|
|
1246
|
+
const candidate = separatorIndex >= 0 && previousSeparatorIndex >= 0 ? location.slice(0, previousSeparatorIndex) : location;
|
|
1247
|
+
return parseStackPath(candidate);
|
|
1248
|
+
}
|
|
1249
|
+
function inferCommandSourcePath() {
|
|
1250
|
+
const stack = new Error().stack;
|
|
1251
|
+
if (typeof stack !== "string") {
|
|
1252
|
+
return void 0;
|
|
1253
|
+
}
|
|
1254
|
+
for (const line of stack.split("\n").slice(1)) {
|
|
1255
|
+
const candidate = extractStackPath(line);
|
|
1256
|
+
if (candidate === void 0) {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (candidate.includes("/packages/cmdkit/src/index.ts") || candidate.includes("/packages/cmdkit/dist/index.js")) {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
return candidate;
|
|
1263
|
+
}
|
|
1264
|
+
return void 0;
|
|
1265
|
+
}
|
|
1266
|
+
function composeChecks(parentCheck, childCheck) {
|
|
1267
|
+
if (parentCheck === void 0) {
|
|
1268
|
+
return childCheck;
|
|
1269
|
+
}
|
|
1270
|
+
if (childCheck === void 0) {
|
|
1271
|
+
return parentCheck;
|
|
1272
|
+
}
|
|
1273
|
+
return async (ctx) => {
|
|
1274
|
+
const parentResult = await parentCheck(ctx);
|
|
1275
|
+
if (!parentResult.ok) {
|
|
1276
|
+
return parentResult;
|
|
1277
|
+
}
|
|
1278
|
+
return childCheck(ctx);
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
function mergeRequires(parent, child) {
|
|
1282
|
+
if (parent === void 0 && child === void 0) {
|
|
1283
|
+
return void 0;
|
|
1284
|
+
}
|
|
1285
|
+
const merged = {
|
|
1286
|
+
auth: child?.auth ?? parent?.auth,
|
|
1287
|
+
apiVersion: child?.apiVersion ?? parent?.apiVersion,
|
|
1288
|
+
check: composeChecks(parent?.check, child?.check)
|
|
1289
|
+
};
|
|
1290
|
+
if (merged.auth === void 0 && merged.apiVersion === void 0 && merged.check === void 0) {
|
|
1291
|
+
return void 0;
|
|
1292
|
+
}
|
|
1293
|
+
return merged;
|
|
1294
|
+
}
|
|
1295
|
+
function mergeSecrets(parent, child) {
|
|
1296
|
+
return cloneSecrets({
|
|
1297
|
+
...parent,
|
|
1298
|
+
...child
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
function resolveCommandScope(ownScope, inheritedScope) {
|
|
1302
|
+
return cloneScope(ownScope ?? inheritedScope) ?? ["cli", "sdk"];
|
|
1303
|
+
}
|
|
1304
|
+
function createBaseCommand(config) {
|
|
1305
|
+
const command = {
|
|
1306
|
+
kind: "command",
|
|
1307
|
+
name: config.name,
|
|
1308
|
+
description: config.description,
|
|
1309
|
+
aliases: [...config.aliases ?? []],
|
|
1310
|
+
positional: [...config.positional ?? []],
|
|
1311
|
+
params: config.params,
|
|
1312
|
+
secrets: cloneSecrets(config.secrets),
|
|
1313
|
+
scope: resolveCommandScope(config.scope, void 0),
|
|
1314
|
+
confirm: config.confirm ?? false,
|
|
1315
|
+
requires: cloneRequires(config.requires),
|
|
1316
|
+
handler: config.handler,
|
|
1317
|
+
render: config.render
|
|
1318
|
+
};
|
|
1319
|
+
Object.defineProperty(command, commandConfigSymbol, {
|
|
1320
|
+
value: {
|
|
1321
|
+
scope: cloneScope(config.scope),
|
|
1322
|
+
secrets: cloneSecrets(config.secrets),
|
|
1323
|
+
requires: cloneRequires(config.requires),
|
|
1324
|
+
sourcePath: inferCommandSourcePath()
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
return command;
|
|
1328
|
+
}
|
|
1329
|
+
function getInternalCommandConfig(command) {
|
|
1330
|
+
return command[commandConfigSymbol];
|
|
1331
|
+
}
|
|
1332
|
+
function materializeCommand(command, inherited) {
|
|
1333
|
+
const internal = getInternalCommandConfig(command);
|
|
1334
|
+
const materialized = {
|
|
1335
|
+
kind: "command",
|
|
1336
|
+
name: command.name,
|
|
1337
|
+
description: command.description,
|
|
1338
|
+
aliases: [...command.aliases],
|
|
1339
|
+
positional: [...command.positional],
|
|
1340
|
+
params: command.params,
|
|
1341
|
+
secrets: mergeSecrets(inherited.secrets, internal.secrets),
|
|
1342
|
+
scope: resolveCommandScope(internal.scope, inherited.scope),
|
|
1343
|
+
confirm: command.confirm,
|
|
1344
|
+
requires: mergeRequires(inherited.requires, internal.requires),
|
|
1345
|
+
handler: command.handler,
|
|
1346
|
+
render: command.render
|
|
1347
|
+
};
|
|
1348
|
+
Object.defineProperty(materialized, commandConfigSymbol, {
|
|
1349
|
+
value: {
|
|
1350
|
+
scope: cloneScope(internal.scope),
|
|
1351
|
+
secrets: cloneSecrets(internal.secrets),
|
|
1352
|
+
requires: cloneRequires(internal.requires),
|
|
1353
|
+
sourcePath: internal.sourcePath
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
Object.defineProperty(materialized, commandSourcePathSymbol, {
|
|
1357
|
+
value: internal.sourcePath
|
|
1358
|
+
});
|
|
1359
|
+
return materialized;
|
|
1360
|
+
}
|
|
1361
|
+
function defineCommand(config) {
|
|
1362
|
+
return materializeCommand(createBaseCommand(config), {
|
|
1363
|
+
scope: void 0,
|
|
1364
|
+
secrets: {},
|
|
1365
|
+
requires: void 0
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/commands/installer.ts
|
|
1370
|
+
import os2 from "node:os";
|
|
1371
|
+
import path3 from "node:path";
|
|
8
1372
|
import * as nodeFs from "node:fs/promises";
|
|
9
1373
|
import { readFile } from "node:fs/promises";
|
|
10
|
-
import { UserError } from "@poe-code/cmdkit";
|
|
11
|
-
import {
|
|
12
|
-
getAgentConfig,
|
|
13
|
-
resolveAgentSupport as resolveSkillAgentSupport,
|
|
14
|
-
supportedAgents as skillSupportedAgents
|
|
15
|
-
} from "@poe-code/agent-skill-config";
|
|
16
1374
|
var DEFAULT_INSTALL_AGENT = "claude-code";
|
|
17
1375
|
var DEFAULT_INSTALL_SCOPE = "local";
|
|
18
1376
|
var TERMINAL_PILOT_SKILL_NAME = "terminal-pilot";
|
|
19
|
-
var installableAgents =
|
|
1377
|
+
var installableAgents = supportedAgents;
|
|
20
1378
|
function isNotFoundError(error) {
|
|
21
1379
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
22
1380
|
}
|
|
@@ -24,7 +1382,7 @@ function resolveInstallerServices(installer) {
|
|
|
24
1382
|
return {
|
|
25
1383
|
fs: installer?.fs ?? nodeFs,
|
|
26
1384
|
cwd: installer?.cwd ?? process.cwd(),
|
|
27
|
-
homeDir: installer?.homeDir ??
|
|
1385
|
+
homeDir: installer?.homeDir ?? os2.homedir(),
|
|
28
1386
|
platform: installer?.platform ?? process.platform
|
|
29
1387
|
};
|
|
30
1388
|
}
|
|
@@ -32,7 +1390,7 @@ function throwUnsupportedAgent(agent) {
|
|
|
32
1390
|
throw new UserError(`Unsupported agent: ${agent}`);
|
|
33
1391
|
}
|
|
34
1392
|
function resolveInstallableAgent(agent) {
|
|
35
|
-
const skillSupport =
|
|
1393
|
+
const skillSupport = resolveAgentSupport(agent);
|
|
36
1394
|
if (skillSupport.status !== "supported" || !skillSupport.id) {
|
|
37
1395
|
throwUnsupportedAgent(agent);
|
|
38
1396
|
}
|