argos-harness 0.1.0 → 0.2.1
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/assets/hooks/argos-guard-destructive.sh +1 -1
- package/assets/managed/disciplina-skills.md +11 -0
- package/assets/managed/formato-respuesta.md +2 -2
- package/assets/managed/identidad.md +7 -7
- package/assets/managed/operaciones-seguras.md +6 -6
- package/assets/output-styles/argos.md +12 -12
- package/dist/commands/adopt.d.ts +41 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +243 -6
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +36 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +56 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +330 -64
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/remove.d.ts +24 -0
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +94 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/workspace.d.ts +32 -0
- package/dist/commands/workspace.d.ts.map +1 -1
- package/dist/commands/workspace.js +97 -3
- package/dist/commands/workspace.js.map +1 -1
- package/dist/lib/assets.d.ts +5 -3
- package/dist/lib/assets.d.ts.map +1 -1
- package/dist/lib/assets.js +5 -2
- package/dist/lib/assets.js.map +1 -1
- package/dist/lib/managed-files.d.ts +19 -5
- package/dist/lib/managed-files.d.ts.map +1 -1
- package/dist/lib/managed-files.js +18 -6
- package/dist/lib/managed-files.js.map +1 -1
- package/dist/lib/prompter.d.ts +63 -0
- package/dist/lib/prompter.d.ts.map +1 -0
- package/dist/lib/prompter.js +30 -0
- package/dist/lib/prompter.js.map +1 -0
- package/dist/lib/settings-merge.d.ts +78 -0
- package/dist/lib/settings-merge.d.ts.map +1 -1
- package/dist/lib/settings-merge.js +204 -0
- package/dist/lib/settings-merge.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -6,9 +6,10 @@ import { listAgentIds, listSkillFiles, listSkillIds, MANAGED_BLOCK_IDS, readAsse
|
|
|
6
6
|
import { writeFileAtomic } from "../lib/atomic-write.js";
|
|
7
7
|
import { createBackup } from "../lib/backup.js";
|
|
8
8
|
import { injectBlock, listBlocks } from "../lib/markers.js";
|
|
9
|
-
import { hasArgosFileMarker, writeManagedFile, writeManagedShellFile } from "../lib/managed-files.js";
|
|
9
|
+
import { hasArgosFileMarker, hasArgosShellFileMarker, writeManagedFile, writeManagedShellFile } from "../lib/managed-files.js";
|
|
10
10
|
import { resolveArgosHome, resolveClaudeDir } from "../lib/paths.js";
|
|
11
|
-
import { mergeHooksIntoSettings } from "../lib/settings-merge.js";
|
|
11
|
+
import { applyOutputStylePolicy, isNavoriOutputStyle, mergeHooksIntoSettings } from "../lib/settings-merge.js";
|
|
12
|
+
import { isInteractive, clackPrompter } from "../lib/prompter.js";
|
|
12
13
|
import { readCliVersion } from "../lib/version.js";
|
|
13
14
|
/**
|
|
14
15
|
* Ids (basenames under assets/hooks/) of the 2 global hooks argos init
|
|
@@ -32,6 +33,7 @@ const STATUS_COUNT_ORDER = [
|
|
|
32
33
|
"updated",
|
|
33
34
|
"unchanged",
|
|
34
35
|
"skipped-foreign",
|
|
36
|
+
"overwritten-foreign",
|
|
35
37
|
"error"
|
|
36
38
|
];
|
|
37
39
|
function summarize(rows) {
|
|
@@ -40,6 +42,7 @@ function summarize(rows) {
|
|
|
40
42
|
updated: 0,
|
|
41
43
|
unchanged: 0,
|
|
42
44
|
"skipped-foreign": 0,
|
|
45
|
+
"overwritten-foreign": 0,
|
|
43
46
|
error: 0
|
|
44
47
|
};
|
|
45
48
|
for (const row of rows)counts[row.status]++;
|
|
@@ -70,6 +73,23 @@ function errorMessage(err) {
|
|
|
70
73
|
writeFileAtomic(destPath, sourceContent);
|
|
71
74
|
return "updated";
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `--force` counterpart to `writePlainFile`, used only for a supporting file
|
|
78
|
+
* (e.g. `references/core.md`) that lives under a skill directory whose
|
|
79
|
+
* `SKILL.md` was just detected as foreign (no `argos:file` marker). These
|
|
80
|
+
* plain subfiles never carry a marker of their own — the whole skill
|
|
81
|
+
* directory's ownership is decided once from `SKILL.md` (see the skills loop
|
|
82
|
+
* below) — so this always overwrites unconditionally: `overwritten-foreign`
|
|
83
|
+
* when something was already there to replace, `created` when the motor
|
|
84
|
+
* ships a subfile the foreign directory never had.
|
|
85
|
+
*/ function writeForcedPlainFile(destPath, sourceContent) {
|
|
86
|
+
const existed = existsSync(destPath);
|
|
87
|
+
if (!existed) mkdirSync(dirname(destPath), {
|
|
88
|
+
recursive: true
|
|
89
|
+
});
|
|
90
|
+
writeFileAtomic(destPath, sourceContent);
|
|
91
|
+
return existed ? "overwritten-foreign" : "created";
|
|
92
|
+
}
|
|
73
93
|
/** Inject one managed CLAUDE.md block, reporting created/updated/unchanged. */ function injectAndReport(claudeMd, id, version, content) {
|
|
74
94
|
const hadBlock = listBlocks(claudeMd).some((b)=>b.id === id);
|
|
75
95
|
const after = injectBlock(claudeMd, id, version, content);
|
|
@@ -86,6 +106,9 @@ function errorMessage(err) {
|
|
|
86
106
|
* filesystem — no process.exit, no console output.
|
|
87
107
|
*/ export function runInit(options = {}) {
|
|
88
108
|
const language = options.language ?? "es";
|
|
109
|
+
const installAgents = options.installAgents ?? true;
|
|
110
|
+
const installHooks = options.installHooks ?? true;
|
|
111
|
+
const force = options.force ?? false;
|
|
89
112
|
const version = readCliVersion();
|
|
90
113
|
const claudeDir = resolveClaudeDir();
|
|
91
114
|
const assetsDir = resolveAssetsDir();
|
|
@@ -117,7 +140,7 @@ function errorMessage(err) {
|
|
|
117
140
|
exitCode: 1
|
|
118
141
|
};
|
|
119
142
|
}
|
|
120
|
-
// 1. CLAUDE.md —
|
|
143
|
+
// 1. CLAUDE.md — MANAGED_BLOCK_IDS.length managed blocks, in order.
|
|
121
144
|
const claudeMdPath = join(claudeDir, "CLAUDE.md");
|
|
122
145
|
let claudeMd = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, "utf-8") : "";
|
|
123
146
|
const blockResults = [];
|
|
@@ -147,22 +170,27 @@ function errorMessage(err) {
|
|
|
147
170
|
detail
|
|
148
171
|
});
|
|
149
172
|
}
|
|
150
|
-
// 2. Full-file assets: output-style, agents
|
|
173
|
+
// 2. Full-file assets: output-style always, agents gated by the
|
|
174
|
+
// `installAgents` toggle (default true; the wizard's "agentes sí/no"
|
|
175
|
+
// step — spec 0004). Skills below are NEVER gated: they load on-demand
|
|
176
|
+
// and trimming the set breaks the arsenal.
|
|
151
177
|
const fullFiles = [
|
|
152
178
|
[
|
|
153
179
|
"output-styles",
|
|
154
180
|
"argos.md"
|
|
155
181
|
],
|
|
156
|
-
...listAgentIds(assetsDir).map((id)=>[
|
|
182
|
+
...installAgents ? listAgentIds(assetsDir).map((id)=>[
|
|
157
183
|
"agents",
|
|
158
184
|
`${id}.md`
|
|
159
|
-
])
|
|
185
|
+
]) : []
|
|
160
186
|
];
|
|
161
187
|
for (const relPath of fullFiles){
|
|
162
188
|
const source = readAsset(assetsDir, ...relPath);
|
|
163
189
|
const dest = join(claudeDir, ...relPath);
|
|
164
190
|
try {
|
|
165
|
-
const status = writeManagedFile(dest, source, version
|
|
191
|
+
const status = writeManagedFile(dest, source, version, {
|
|
192
|
+
force
|
|
193
|
+
});
|
|
166
194
|
rows.push({
|
|
167
195
|
path: join(...relPath),
|
|
168
196
|
status
|
|
@@ -182,14 +210,18 @@ function errorMessage(err) {
|
|
|
182
210
|
// file the skill ships.
|
|
183
211
|
// - SKILL.md present WITHOUT the marker (foreign/user-modified) → skip
|
|
184
212
|
// every file under that skill dir, untouched — same policy as a single
|
|
185
|
-
// foreign full-file asset above, extended to the whole directory.
|
|
213
|
+
// foreign full-file asset above, extended to the whole directory. Under
|
|
214
|
+
// `--force`, every file in that directory is overwritten instead
|
|
215
|
+
// (`overwritten-foreign`) — SKILL.md gets its marker stamped via
|
|
216
|
+
// `writeManagedFile`'s own `force` path, and each plain subfile is
|
|
217
|
+
// force-written via `writeForcedPlainFile` (see its own doc comment).
|
|
186
218
|
for (const skillId of listSkillIds(assetsDir)){
|
|
187
219
|
const skillMdDest = join(claudeDir, "skills", skillId, "SKILL.md");
|
|
188
220
|
const isForeignSkill = existsSync(skillMdDest) && !hasArgosFileMarker(readFileSync(skillMdDest, "utf-8"));
|
|
189
221
|
for (const relFile of listSkillFiles(assetsDir, skillId)){
|
|
190
222
|
const relParts = relFile.split("/");
|
|
191
223
|
const relPath = join("skills", skillId, ...relParts);
|
|
192
|
-
if (isForeignSkill) {
|
|
224
|
+
if (isForeignSkill && !force) {
|
|
193
225
|
rows.push({
|
|
194
226
|
path: relPath,
|
|
195
227
|
status: "skipped-foreign"
|
|
@@ -199,7 +231,9 @@ function errorMessage(err) {
|
|
|
199
231
|
const source = readAsset(assetsDir, "skills", skillId, ...relParts);
|
|
200
232
|
const dest = join(claudeDir, "skills", skillId, ...relParts);
|
|
201
233
|
try {
|
|
202
|
-
const status = relFile === "SKILL.md" ? writeManagedFile(dest, source, version
|
|
234
|
+
const status = relFile === "SKILL.md" ? writeManagedFile(dest, source, version, {
|
|
235
|
+
force
|
|
236
|
+
}) : isForeignSkill ? writeForcedPlainFile(dest, source) : writePlainFile(dest, source);
|
|
203
237
|
rows.push({
|
|
204
238
|
path: relPath,
|
|
205
239
|
status
|
|
@@ -219,62 +253,92 @@ function errorMessage(err) {
|
|
|
219
253
|
// write threw must NEVER get a settings.json entry (see 2c below): a
|
|
220
254
|
// dangling PreToolUse entry pointing at a script that isn't there hard-
|
|
221
255
|
// blocks every subsequent Bash call.
|
|
256
|
+
//
|
|
257
|
+
// Gated by the `installHooks` toggle (default true; the wizard's "hooks
|
|
258
|
+
// sí/no" step — spec 0004): when disabled, this run neither writes nor
|
|
259
|
+
// touches any pre-existing hook script/settings.json entry — a full
|
|
260
|
+
// uninstall of previously-installed hooks is `argos remove`'s job, not a
|
|
261
|
+
// side effect of toggling this flag off on a later `init` run.
|
|
222
262
|
const hookWriteFailed = new Map();
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
if (installHooks) {
|
|
264
|
+
for (const id of HOOK_IDS){
|
|
265
|
+
const relPath = [
|
|
266
|
+
"hooks",
|
|
267
|
+
`${id}.sh`
|
|
268
|
+
];
|
|
269
|
+
const source = readAsset(assetsDir, ...relPath);
|
|
270
|
+
const dest = join(claudeDir, ...relPath);
|
|
271
|
+
try {
|
|
272
|
+
const status = writeManagedShellFile(dest, source, version, {
|
|
273
|
+
force
|
|
274
|
+
});
|
|
275
|
+
rows.push({
|
|
276
|
+
path: join(...relPath),
|
|
277
|
+
status
|
|
278
|
+
});
|
|
279
|
+
hookWriteFailed.set(id, false);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
rows.push({
|
|
282
|
+
path: join(...relPath),
|
|
283
|
+
status: "error",
|
|
284
|
+
detail: errorMessage(err)
|
|
285
|
+
});
|
|
286
|
+
hookWriteFailed.set(id, true);
|
|
287
|
+
}
|
|
244
288
|
}
|
|
289
|
+
// 2c. settings.json — surgical merge of the 2 PreToolUse hook entries.
|
|
290
|
+
// Only writes/updates entries whose command targets one of the 2 scripts
|
|
291
|
+
// above; every other key and hook in the user's settings.json is left
|
|
292
|
+
// untouched (see lib/settings-merge.ts). Only hooks whose script write
|
|
293
|
+
// actually succeeded get an entry built for them at all.
|
|
294
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
295
|
+
const allHookSpecs = {
|
|
296
|
+
"argos-guard-destructive": {
|
|
297
|
+
scriptPath: join(claudeDir, "hooks", "argos-guard-destructive.sh"),
|
|
298
|
+
matcher: "Bash",
|
|
299
|
+
timeout: 10,
|
|
300
|
+
statusMessage: "argos: guard-destructive"
|
|
301
|
+
},
|
|
302
|
+
"argos-quality-gate": {
|
|
303
|
+
scriptPath: join(claudeDir, "hooks", "argos-quality-gate.sh"),
|
|
304
|
+
matcher: "Bash",
|
|
305
|
+
timeout: QUALITY_GATE_OUTER_TIMEOUT_SECONDS,
|
|
306
|
+
statusMessage: "argos: quality-gate"
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const hookSpecs = HOOK_IDS.filter((id)=>!hookWriteFailed.get(id)).map((id)=>allHookSpecs[id]);
|
|
310
|
+
// Any hook whose write just failed also gets its (possibly pre-existing,
|
|
311
|
+
// from an earlier successful run) settings.json entry stripped out — a
|
|
312
|
+
// script that's gone or broken must never be left with a live entry.
|
|
313
|
+
const failedScriptPaths = HOOK_IDS.filter((id)=>hookWriteFailed.get(id)).map((id)=>allHookSpecs[id].scriptPath);
|
|
314
|
+
const mergeResult = mergeHooksIntoSettings(settingsPath, hookSpecs, {
|
|
315
|
+
removeScriptPaths: failedScriptPaths
|
|
316
|
+
});
|
|
317
|
+
rows.push({
|
|
318
|
+
path: "settings.json",
|
|
319
|
+
status: mergeResult.status,
|
|
320
|
+
detail: mergeResult.detail
|
|
321
|
+
});
|
|
245
322
|
}
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
timeout: 10,
|
|
257
|
-
statusMessage: "argos: guard-destructive"
|
|
258
|
-
},
|
|
259
|
-
"argos-quality-gate": {
|
|
260
|
-
scriptPath: join(claudeDir, "hooks", "argos-quality-gate.sh"),
|
|
261
|
-
matcher: "Bash",
|
|
262
|
-
timeout: QUALITY_GATE_OUTER_TIMEOUT_SECONDS,
|
|
263
|
-
statusMessage: "argos: quality-gate"
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
const hookSpecs = HOOK_IDS.filter((id)=>!hookWriteFailed.get(id)).map((id)=>allHookSpecs[id]);
|
|
267
|
-
// Any hook whose write just failed also gets its (possibly pre-existing,
|
|
268
|
-
// from an earlier successful run) settings.json entry stripped out — a
|
|
269
|
-
// script that's gone or broken must never be left with a live entry.
|
|
270
|
-
const failedScriptPaths = HOOK_IDS.filter((id)=>hookWriteFailed.get(id)).map((id)=>allHookSpecs[id].scriptPath);
|
|
271
|
-
const mergeResult = mergeHooksIntoSettings(settingsPath, hookSpecs, {
|
|
272
|
-
removeScriptPaths: failedScriptPaths
|
|
323
|
+
// 2d. Voice activation (spec 0004 "Activación de la voz"):
|
|
324
|
+
// settings.json.outputStyle. Absent → set to "Argos". Matching the
|
|
325
|
+
// predecessor harness's voice (navori) → takeover per
|
|
326
|
+
// `takeoverNavoriVoice` (default true, i.e. unconditional replace under
|
|
327
|
+
// --yes/no-TTY; the interactive wizard passes `false` when the user
|
|
328
|
+
// declines). Any other value → never touched. Reported separately from
|
|
329
|
+
// the hooks settings.json row above so a takeover/foreign-voice detail is
|
|
330
|
+
// never conflated with the hooks merge outcome.
|
|
331
|
+
const outputStyleResult = applyOutputStylePolicy(join(claudeDir, "settings.json"), {
|
|
332
|
+
takeoverNavori: options.takeoverNavoriVoice ?? true
|
|
273
333
|
});
|
|
334
|
+
// "untouched" (a foreign non-navori voice, or a declined navori takeover)
|
|
335
|
+
// maps to the same "skipped-foreign" row status used elsewhere for
|
|
336
|
+
// "found something not ours, left it byte-identical".
|
|
337
|
+
const outputStyleRowStatus = outputStyleResult.status === "untouched" ? "skipped-foreign" : outputStyleResult.status;
|
|
274
338
|
rows.push({
|
|
275
|
-
path: "settings.json",
|
|
276
|
-
status:
|
|
277
|
-
detail:
|
|
339
|
+
path: "settings.json#outputStyle",
|
|
340
|
+
status: outputStyleRowStatus,
|
|
341
|
+
detail: outputStyleResult.detail
|
|
278
342
|
});
|
|
279
343
|
// 3. ~/.argos/global.json
|
|
280
344
|
const argosHome = resolveArgosHome();
|
|
@@ -309,6 +373,194 @@ function errorMessage(err) {
|
|
|
309
373
|
backupPath
|
|
310
374
|
};
|
|
311
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* A cancelled wizard's report: identical shape to a run that touched
|
|
378
|
+
* nothing. `exitCode: 1` — a cancel is neither a successful write nor
|
|
379
|
+
* silently indistinguishable from one by exit code alone (matches
|
|
380
|
+
* `runWorkspaceLinkInteractive`'s convention for its own cancel paths).
|
|
381
|
+
*/ function cancelledInitReport() {
|
|
382
|
+
return {
|
|
383
|
+
rows: [],
|
|
384
|
+
summary: "argos init: cancelado — no se tocó nada.",
|
|
385
|
+
exitCode: 1
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Read-only peek at `settings.json.outputStyle`, used only to decide whether
|
|
390
|
+
* the interactive wizard needs to ask about a navori takeover. Never throws,
|
|
391
|
+
* never writes — any missing file, unreadable file, or invalid JSON just
|
|
392
|
+
* reads as "no value" (`undefined`), which is safely a non-match for
|
|
393
|
+
* `isNavoriOutputStyle`. The actual write happens inside `runInit` via
|
|
394
|
+
* `applyOutputStylePolicy`, which re-does this read itself under its own
|
|
395
|
+
* mtime-guarded contract.
|
|
396
|
+
*/ function peekOutputStyleValue(settingsPath) {
|
|
397
|
+
if (!existsSync(settingsPath)) return undefined;
|
|
398
|
+
try {
|
|
399
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
400
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
401
|
+
return parsed.outputStyle;
|
|
402
|
+
} catch {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Read-only `--force` pre-flight: counts how many on-disk paths at motor
|
|
408
|
+
* paths (output-style, agents, skills incl. every manifest subfile, hooks)
|
|
409
|
+
* are foreign (no `argos:file`/shell marker) and would therefore be
|
|
410
|
+
* overwritten by a forced `runInit`. Used only by the interactive wizard to
|
|
411
|
+
* size its confirm prompt — the actual overwrite decision and write happen
|
|
412
|
+
* inside `runInit` itself, independently, so this can never drift into
|
|
413
|
+
* writing anything. Mirrors the same directory-ownership rule `runInit`'s
|
|
414
|
+
* own skills loop uses (SKILL.md's marker decides the whole directory), and
|
|
415
|
+
* — like `doctor.ts`'s own read-only checks — intentionally duplicates that
|
|
416
|
+
* iteration shape rather than sharing private state with the writer.
|
|
417
|
+
*/ function countForceOverwrites(claudeDir, assetsDir, installAgents, installHooks) {
|
|
418
|
+
let count = 0;
|
|
419
|
+
const fullFiles = [
|
|
420
|
+
[
|
|
421
|
+
"output-styles",
|
|
422
|
+
"argos.md"
|
|
423
|
+
],
|
|
424
|
+
...installAgents ? listAgentIds(assetsDir).map((id)=>[
|
|
425
|
+
"agents",
|
|
426
|
+
`${id}.md`
|
|
427
|
+
]) : []
|
|
428
|
+
];
|
|
429
|
+
for (const relPath of fullFiles){
|
|
430
|
+
const dest = join(claudeDir, ...relPath);
|
|
431
|
+
if (existsSync(dest) && !hasArgosFileMarker(readFileSync(dest, "utf-8"))) count++;
|
|
432
|
+
}
|
|
433
|
+
for (const skillId of listSkillIds(assetsDir)){
|
|
434
|
+
const skillMdDest = join(claudeDir, "skills", skillId, "SKILL.md");
|
|
435
|
+
const isForeignSkill = existsSync(skillMdDest) && !hasArgosFileMarker(readFileSync(skillMdDest, "utf-8"));
|
|
436
|
+
if (!isForeignSkill) continue;
|
|
437
|
+
for (const relFile of listSkillFiles(assetsDir, skillId)){
|
|
438
|
+
const dest = join(claudeDir, "skills", skillId, ...relFile.split("/"));
|
|
439
|
+
if (existsSync(dest)) count++;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (installHooks) {
|
|
443
|
+
for (const id of HOOK_IDS){
|
|
444
|
+
const dest = join(claudeDir, "hooks", `${id}.sh`);
|
|
445
|
+
if (existsSync(dest) && !hasArgosShellFileMarker(readFileSync(dest, "utf-8"))) count++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return count;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Interactive layer over `runInit` (spec 0004 F5 "argos init"). A pure
|
|
452
|
+
* additive wrapper — the core `runInit` never changes behavior or contract.
|
|
453
|
+
* Without a real TTY, or with `--yes`, this delegates to `runInit(options)`
|
|
454
|
+
* unchanged: no prompt library call is ever reached on that path. With a
|
|
455
|
+
* TTY, it runs a 3-step wizard (language, agents/hooks toggles, summary +
|
|
456
|
+
* final confirm) before calling `runInit` with the gathered choices;
|
|
457
|
+
* cancelling at any step touches nothing and returns a no-op report.
|
|
458
|
+
*/ export async function runInitInteractive(options = {}) {
|
|
459
|
+
if (!isInteractive({
|
|
460
|
+
yes: options.yes
|
|
461
|
+
})) {
|
|
462
|
+
return runInit(options);
|
|
463
|
+
}
|
|
464
|
+
const prompter = options.prompter ?? clackPrompter;
|
|
465
|
+
prompter.intro("argos init — instalación interactiva del motor");
|
|
466
|
+
const language = await prompter.select({
|
|
467
|
+
message: "Idioma del motor",
|
|
468
|
+
options: [
|
|
469
|
+
{
|
|
470
|
+
value: "es",
|
|
471
|
+
label: "es — español"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
value: "en",
|
|
475
|
+
label: "en — English"
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
initialValue: options.language ?? "es"
|
|
479
|
+
});
|
|
480
|
+
if (prompter.isCancel(language)) {
|
|
481
|
+
prompter.cancel("argos init cancelado — no se tocó nada.");
|
|
482
|
+
return cancelledInitReport();
|
|
483
|
+
}
|
|
484
|
+
const installAgents = await prompter.confirm({
|
|
485
|
+
message: "¿Instalar los agentes del motor (agents/)?",
|
|
486
|
+
initialValue: options.installAgents ?? true
|
|
487
|
+
});
|
|
488
|
+
if (prompter.isCancel(installAgents)) {
|
|
489
|
+
prompter.cancel("argos init cancelado — no se tocó nada.");
|
|
490
|
+
return cancelledInitReport();
|
|
491
|
+
}
|
|
492
|
+
const installHooks = await prompter.confirm({
|
|
493
|
+
message: "¿Instalar los hooks globales (guard-destructive + quality-gate)?",
|
|
494
|
+
initialValue: options.installHooks ?? true
|
|
495
|
+
});
|
|
496
|
+
if (prompter.isCancel(installHooks)) {
|
|
497
|
+
prompter.cancel("argos init cancelado — no se tocó nada.");
|
|
498
|
+
return cancelledInitReport();
|
|
499
|
+
}
|
|
500
|
+
const claudeDir = resolveClaudeDir();
|
|
501
|
+
// `--force` pre-flight (see `InitOptions.force`'s doc comment): flag-driven,
|
|
502
|
+
// not itself a wizard question. When active, count how many on-disk paths
|
|
503
|
+
// are foreign and about to be overwritten (read-only — see
|
|
504
|
+
// `countForceOverwrites`) and, only if that count is > 0, require an
|
|
505
|
+
// explicit confirm before proceeding. Zero foreign paths means force has
|
|
506
|
+
// nothing to do this run, so no extra prompt.
|
|
507
|
+
const force = options.force ?? false;
|
|
508
|
+
if (force) {
|
|
509
|
+
const foreignCount = countForceOverwrites(claudeDir, resolveAssetsDir(), installAgents, installHooks);
|
|
510
|
+
if (foreignCount > 0) {
|
|
511
|
+
const confirmForce = await prompter.confirm({
|
|
512
|
+
message: `${foreignCount} archivos ajenos serán sobrescritos por versiones del motor — backup previo en ~/.argos/backups. ¿Continuar?`,
|
|
513
|
+
initialValue: false
|
|
514
|
+
});
|
|
515
|
+
if (prompter.isCancel(confirmForce) || !confirmForce) {
|
|
516
|
+
prompter.cancel("argos init cancelado — no se tocó nada.");
|
|
517
|
+
return cancelledInitReport();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Voice activation (spec 0004): if the current settings.json.outputStyle
|
|
522
|
+
// matches the predecessor harness's voice, ask before taking it over —
|
|
523
|
+
// this peek is read-only and never writes; the actual takeover only
|
|
524
|
+
// happens inside runInit below, once the user has confirmed everything.
|
|
525
|
+
let takeoverNavoriVoice = options.takeoverNavoriVoice ?? true;
|
|
526
|
+
const currentOutputStyle = peekOutputStyleValue(join(claudeDir, "settings.json"));
|
|
527
|
+
if (isNavoriOutputStyle(currentOutputStyle)) {
|
|
528
|
+
const takeover = await prompter.confirm({
|
|
529
|
+
message: `settings.json.outputStyle apunta a la voz del harness predecesor ('${String(currentOutputStyle)}') — ¿reemplazarla por Argos?`,
|
|
530
|
+
initialValue: true
|
|
531
|
+
});
|
|
532
|
+
if (prompter.isCancel(takeover)) {
|
|
533
|
+
prompter.cancel("argos init cancelado — no se tocó nada.");
|
|
534
|
+
return cancelledInitReport();
|
|
535
|
+
}
|
|
536
|
+
takeoverNavoriVoice = takeover;
|
|
537
|
+
}
|
|
538
|
+
prompter.note([
|
|
539
|
+
`idioma: ${language}`,
|
|
540
|
+
`agentes: ${installAgents ? "sí" : "no"}`,
|
|
541
|
+
"hooks: " + (installHooks ? "sí" : "no"),
|
|
542
|
+
"skills: sí (siempre — cargan on-demand)",
|
|
543
|
+
`destino: ${claudeDir}`,
|
|
544
|
+
"se hace un backup antes de escribir nada"
|
|
545
|
+
].join("\n"), "Resumen");
|
|
546
|
+
const proceed = await prompter.confirm({
|
|
547
|
+
message: "¿Proceder a escribir estos cambios?",
|
|
548
|
+
initialValue: true
|
|
549
|
+
});
|
|
550
|
+
if (prompter.isCancel(proceed) || !proceed) {
|
|
551
|
+
prompter.cancel("argos init cancelado — no se tocó nada.");
|
|
552
|
+
return cancelledInitReport();
|
|
553
|
+
}
|
|
554
|
+
const report = runInit({
|
|
555
|
+
language,
|
|
556
|
+
installAgents,
|
|
557
|
+
installHooks,
|
|
558
|
+
takeoverNavoriVoice,
|
|
559
|
+
force
|
|
560
|
+
});
|
|
561
|
+
prompter.outro(report.summary);
|
|
562
|
+
return report;
|
|
563
|
+
}
|
|
312
564
|
export const initCommand = defineCommand({
|
|
313
565
|
meta: {
|
|
314
566
|
name: "init",
|
|
@@ -323,17 +575,31 @@ export const initCommand = defineCommand({
|
|
|
323
575
|
],
|
|
324
576
|
default: "es",
|
|
325
577
|
description: "Idioma del motor (global.json)."
|
|
578
|
+
},
|
|
579
|
+
yes: {
|
|
580
|
+
type: "boolean",
|
|
581
|
+
default: false,
|
|
582
|
+
description: "Fuerza modo no interactivo aunque haya una TTY real (defaults + flags, sin wizard)."
|
|
583
|
+
},
|
|
584
|
+
force: {
|
|
585
|
+
type: "boolean",
|
|
586
|
+
default: false,
|
|
587
|
+
description: "Sobrescribe con la versión del motor los archivos ajenos (sin marker argos) en agents/skills/output-styles/hooks. No toca prosa ajena de CLAUDE.md ni entradas ajenas de settings.json."
|
|
326
588
|
}
|
|
327
589
|
},
|
|
328
|
-
run ({ args }) {
|
|
329
|
-
const report =
|
|
330
|
-
language: args.language
|
|
590
|
+
async run ({ args }) {
|
|
591
|
+
const report = await runInitInteractive({
|
|
592
|
+
language: args.language,
|
|
593
|
+
yes: Boolean(args.yes),
|
|
594
|
+
force: Boolean(args.force)
|
|
331
595
|
});
|
|
332
596
|
const colorize = (status)=>{
|
|
333
597
|
const padded = status.padEnd(18);
|
|
334
598
|
switch(status){
|
|
335
599
|
case "skipped-foreign":
|
|
336
600
|
return pc.yellow(padded);
|
|
601
|
+
case "overwritten-foreign":
|
|
602
|
+
return pc.magenta(padded);
|
|
337
603
|
case "created":
|
|
338
604
|
return pc.green(padded);
|
|
339
605
|
case "updated":
|