agent-cloud-kit 1.0.0-rc.7 → 1.0.0-rc.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/README.md +14 -0
- package/dist/block.js +76 -0
- package/dist/cli.js +67 -45
- package/dist/config.js +12 -11
- package/dist/docker.js +5 -6
- package/dist/doctor.js +39 -28
- package/dist/init.js +13 -12
- package/dist/installers.js +7 -6
- package/dist/lang.js +84 -0
- package/dist/launcher.js +2 -1
- package/dist/manual.js +13 -10
- package/dist/messages/en.js +226 -0
- package/dist/messages/es.js +234 -0
- package/dist/messages/index.js +75 -0
- package/dist/prepare/banner.js +18 -18
- package/dist/prepare/detect.js +8 -6
- package/dist/prepare/index.js +101 -61
- package/dist/prepare/ui.js +0 -27
- package/dist/run.js +3 -2
- package/dist/select.js +3 -2
- package/dist/session.js +11 -14
- package/dist/setup.js +10 -9
- package/dist/skill.js +7 -6
- package/dist/steps.js +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,6 +161,20 @@ A Node version that is not exact (`22`, `22.1`, `lts/iron`, `lts/*` or `node`) i
|
|
|
161
161
|
Environment variables: `AGENT_CLOUD_KIT_DIR` overrides `/opt/agent-cloud-kit` and `AGENT_CLOUD_KIT_HOME`
|
|
162
162
|
overrides `/home/user`; both exist mainly for tests.
|
|
163
163
|
|
|
164
|
+
### Language and Kuro
|
|
165
|
+
|
|
166
|
+
The terminal speaks Spanish by default. `--lang en` (with any command) switches it to English for
|
|
167
|
+
one run, and `AGENT_CLOUD_KIT_LANG=en|es` fixes it; `--lang` wins over the variable. `LANG` and
|
|
168
|
+
`LC_ALL` are not read. Only the terminal changes: what is written in the repo (`docs/agent-cloud.md`,
|
|
169
|
+
`.claude/cloud-notes.md`, the `cloud-ready` skill, the commit) and the prompts for Claude stay in
|
|
170
|
+
Spanish. `setup` passes the language on to the version it relaunches, and `session` to the one it
|
|
171
|
+
delegates to.
|
|
172
|
+
|
|
173
|
+
In a terminal, the final messages (the end of `doctor`, `prepare` and `verify`, and the screen
|
|
174
|
+
before Claude opens) come in a block with Kuro, the mascot of issue-flow and agent-kitchen.
|
|
175
|
+
`NO_COLOR` removes the color and keeps Kuro. Outside a terminal (logs, pipes, the `SessionStart`
|
|
176
|
+
hook) the lines keep the plain `agent-cloud-kit: ` prefix.
|
|
177
|
+
|
|
164
178
|
### The `SessionStart` hook
|
|
165
179
|
|
|
166
180
|
`init` adds this hook. Outside a cloud session it does nothing, and it calls the copy of the
|
package/dist/block.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The block of the final messages, like issue-flow's: a title with a status mark, a few
|
|
3
|
+
* aligned rows and free lines, with Kuro (the mascot) beside it. It is drawn only in a terminal;
|
|
4
|
+
* logs, pipes and Claude's context keep the plain `agent-cloud-kit: ` lines.
|
|
5
|
+
*/
|
|
6
|
+
/** Kuro, the drawing of issue-flow, without trailing spaces. */
|
|
7
|
+
export const KURO = [" ▄█████▄", " ≋≋██▀▀▀▀▀██", " █ ▄ ▄ █ ╱", " ▀█▄▄▄▄█▀ ╱"];
|
|
8
|
+
/** Column where the text begins, after Kuro. */
|
|
9
|
+
const TEXT_COLUMN = 26;
|
|
10
|
+
/** Indent of the text without Kuro. */
|
|
11
|
+
const INDENT = 2;
|
|
12
|
+
/** ASCII mark of each mood. */
|
|
13
|
+
const MARKS = { ok: "[ok]", error: "[!]", question: "[?]", next: "[>]" };
|
|
14
|
+
/** SGR color of each mood. */
|
|
15
|
+
const MOOD_COLOR = { ok: "32", error: "31", question: "33", next: "36" };
|
|
16
|
+
/**
|
|
17
|
+
* Wraps a text in an SGR code when color is on.
|
|
18
|
+
*
|
|
19
|
+
* @param on - Whether color is on.
|
|
20
|
+
* @param code - SGR code.
|
|
21
|
+
* @param text - Text.
|
|
22
|
+
* @returns The text, colored or not.
|
|
23
|
+
* @example
|
|
24
|
+
* paint(false, "1", "x"); // "x"
|
|
25
|
+
*/
|
|
26
|
+
function paint(on, code, text) {
|
|
27
|
+
return on ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Visible width of a text: characters, without ANSI codes.
|
|
31
|
+
*
|
|
32
|
+
* @param text - Text.
|
|
33
|
+
* @returns The width.
|
|
34
|
+
* @example
|
|
35
|
+
* width("\x1b[1mab\x1b[0m"); // 2
|
|
36
|
+
*/
|
|
37
|
+
function width(text) {
|
|
38
|
+
return [...text.replace(/\x1b\[[0-9;]*m/g, "")].length;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Renders a block. Values are never cut or wrapped: a command to copy must stay whole.
|
|
42
|
+
*
|
|
43
|
+
* @param block - What to say.
|
|
44
|
+
* @param opts - Whether to draw Kuro and whether to color.
|
|
45
|
+
* @returns The lines, with a blank line before and after.
|
|
46
|
+
* @example
|
|
47
|
+
* renderBlock({ mood: "ok", title: "Listo" }, { art: false, color: false }); // ["", " [ok] Listo", ""]
|
|
48
|
+
*/
|
|
49
|
+
export function renderBlock(block, opts) {
|
|
50
|
+
const code = MOOD_COLOR[block.mood];
|
|
51
|
+
const text = [`${paint(opts.color, code, MARKS[block.mood])} ${paint(opts.color, `1;${code}`, block.title)}`];
|
|
52
|
+
if (block.rows?.length) {
|
|
53
|
+
const column = Math.max(...block.rows.map(([label]) => width(label))) + 2;
|
|
54
|
+
text.push("");
|
|
55
|
+
for (const [label, value] of block.rows)
|
|
56
|
+
text.push(`${paint(opts.color, "2", label)}${" ".repeat(column - width(label))}${value}`);
|
|
57
|
+
}
|
|
58
|
+
if (block.lines?.length)
|
|
59
|
+
text.push("", ...block.lines);
|
|
60
|
+
const out = [];
|
|
61
|
+
if (opts.art) {
|
|
62
|
+
// NOTE: Kuro takes rows 0-3 and the text starts on row 1, next to the head.
|
|
63
|
+
const total = Math.max(KURO.length, text.length + 1);
|
|
64
|
+
for (let row = 0; row < total; row++) {
|
|
65
|
+
const left = KURO[row] ?? "";
|
|
66
|
+
const drawn = left ? paint(opts.color, "35", left) : "";
|
|
67
|
+
const right = text[row - 1];
|
|
68
|
+
out.push(right === undefined || right === "" ? drawn : `${drawn}${" ".repeat(TEXT_COLUMN - width(left))}${right}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
for (const line of text)
|
|
73
|
+
out.push(line === "" ? "" : `${" ".repeat(INDENT)}${line}`);
|
|
74
|
+
}
|
|
75
|
+
return ["", ...out.map((line) => line.trimEnd()), ""];
|
|
76
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,46 +2,28 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @file Command line entry point of agent-cloud-kit: wires `setup`, `session`, `docker`, `doctor`,
|
|
4
4
|
* `init`, `manual`, `prepare` and `verify` to their modules, relaunching or delegating to the version each
|
|
5
|
-
* repo pins.
|
|
5
|
+
* repo pins. It picks the language of the terminal once (`--lang`, then `AGENT_CLOUD_KIT_LANG`).
|
|
6
6
|
*/
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { renderBlock } from "./block.js";
|
|
12
13
|
import { readConfig } from "./config.js";
|
|
13
14
|
import { CONFIG_FILE, DOCKERD_LOG, OUTPUT_PREFIX, RUNNING_PACKAGE_ROOT, homeDir, toolsDir } from "./constants.js";
|
|
14
|
-
import { doctor } from "./doctor.js";
|
|
15
|
+
import { doctor, doctorBlock } from "./doctor.js";
|
|
15
16
|
import { dockerResponds, realDocker } from "./docker.js";
|
|
16
17
|
import { init } from "./init.js";
|
|
18
|
+
import { LANG_ENV, LangError, resolveLang, splitLang } from "./lang.js";
|
|
17
19
|
import { findRepos, packageBin, planSetup, relaunch } from "./launcher.js";
|
|
18
20
|
import { MANUAL_FILE, writeManual } from "./manual.js";
|
|
21
|
+
import { stepLabel, t, useLang } from "./messages/index.js";
|
|
19
22
|
import { EXIT_INTERRUPTED, InterruptedError, branchDay, prepare, verifyBranch } from "./prepare/index.js";
|
|
20
23
|
import { captureProcess, runProcess } from "./run.js";
|
|
21
24
|
import { chooseTerminal } from "./select.js";
|
|
22
25
|
import { session } from "./session.js";
|
|
23
26
|
import { setupRepo } from "./setup.js";
|
|
24
|
-
/** Help text shown by `--help`. */
|
|
25
|
-
const HELP = `${OUTPUT_PREFIX}prepara repos para las sesiones cloud de Claude Code.
|
|
26
|
-
|
|
27
|
-
Uso: agent-cloud-kit <comando>
|
|
28
|
-
|
|
29
|
-
Comandos:
|
|
30
|
-
setup [--repo <path>] prepara la máquina para cada repo del directorio home (o solo <path>)
|
|
31
|
-
session informa el estado de la máquina y conecta las herramientas a la sesión
|
|
32
|
-
docker inicia el demonio de Docker y espera a que responda
|
|
33
|
-
doctor revisa los dominios permitidos, las herramientas, Docker y el estado de la máquina
|
|
34
|
-
init crea .claude/cloud.json y registra el hook SessionStart
|
|
35
|
-
manual escribe docs/agent-cloud.md a partir de .claude/cloud.json
|
|
36
|
-
prepare [--pr|--here|--resume]
|
|
37
|
-
corre en local: deja el repo listo para la nube, en un PR nuevo (--pr) o en la
|
|
38
|
-
rama actual sin commit (--here); sin opción, pregunta dónde
|
|
39
|
-
verify corre en local: lanza una sesión cloud que verifica la rama actual (ya subida)
|
|
40
|
-
|
|
41
|
-
Opciones:
|
|
42
|
-
--help muestra esta ayuda
|
|
43
|
-
--version muestra la versión del paquete
|
|
44
|
-
`;
|
|
45
27
|
/** Environment variable with the project dir that Claude Code passes to hooks. */
|
|
46
28
|
const PROJECT_DIR_VAR = "CLAUDE_PROJECT_DIR";
|
|
47
29
|
/** Environment marker set on a delegated `session`, so it never delegates again. */
|
|
@@ -93,7 +75,7 @@ async function runOne(repo, deps) {
|
|
|
93
75
|
for (const error of errors)
|
|
94
76
|
deps.print(`${OUTPUT_PREFIX}${repo}: ${error}`);
|
|
95
77
|
if (config && config.version !== deps.packageVersion) {
|
|
96
|
-
deps.print(
|
|
78
|
+
deps.print(OUTPUT_PREFIX + t().cli.versionMismatch(deps.packageVersion, CONFIG_FILE, config.version));
|
|
97
79
|
}
|
|
98
80
|
const state = await setupRepo(repo, {
|
|
99
81
|
toolsDir: toolsDir(deps.env),
|
|
@@ -103,9 +85,8 @@ async function runOne(repo, deps) {
|
|
|
103
85
|
docker: deps.docker,
|
|
104
86
|
packageRoot: deps.packageRoot,
|
|
105
87
|
});
|
|
106
|
-
const failed = state.steps.filter((s) => !s.ok).map((s) => s.name);
|
|
107
|
-
|
|
108
|
-
deps.print(`${OUTPUT_PREFIX}setup ${repo}: ${state.steps.length} pasos, ${failed.length} fallaron${detail}`);
|
|
88
|
+
const failed = state.steps.filter((s) => !s.ok).map((s) => stepLabel(s.name));
|
|
89
|
+
deps.print(OUTPUT_PREFIX + t().cli.setupSummary(repo, state.steps.length, failed.length, failed));
|
|
109
90
|
}
|
|
110
91
|
/**
|
|
111
92
|
* Carries out one item of the setup plan; may throw, the caller reports it and moves on.
|
|
@@ -115,19 +96,19 @@ async function runOne(repo, deps) {
|
|
|
115
96
|
*/
|
|
116
97
|
async function runPlanItem(item, deps) {
|
|
117
98
|
if (item.action === "skip") {
|
|
118
|
-
deps.print(
|
|
99
|
+
deps.print(OUTPUT_PREFIX + t().cli.skip(item.repo, item.reason ?? ""));
|
|
119
100
|
return;
|
|
120
101
|
}
|
|
121
102
|
if (item.action === "run" || !item.version) {
|
|
122
103
|
await runOne(item.repo, deps);
|
|
123
104
|
return;
|
|
124
105
|
}
|
|
125
|
-
deps.print(
|
|
106
|
+
deps.print(OUTPUT_PREFIX + t().cli.relaunching(item.version, item.repo));
|
|
126
107
|
try {
|
|
127
|
-
await relaunch(item.repo, item.version, deps.run, deps.env);
|
|
108
|
+
await relaunch(item.repo, item.version, deps.run, langEnv(deps.env));
|
|
128
109
|
}
|
|
129
110
|
catch (err) {
|
|
130
|
-
deps.print(
|
|
111
|
+
deps.print(OUTPUT_PREFIX + t().cli.relaunchFailed(item.version, item.repo, message(err)));
|
|
131
112
|
}
|
|
132
113
|
}
|
|
133
114
|
/**
|
|
@@ -144,25 +125,25 @@ async function setupCommand(args, deps) {
|
|
|
144
125
|
if (at >= 0) {
|
|
145
126
|
const value = args[at + 1];
|
|
146
127
|
if (!value || value.startsWith("-"))
|
|
147
|
-
deps.print(
|
|
128
|
+
deps.print(OUTPUT_PREFIX + t().cli.repoNeedsPath(REPO_OPTION));
|
|
148
129
|
else
|
|
149
130
|
await runOne(resolve(deps.cwd, value), deps);
|
|
150
131
|
return EXIT_OK;
|
|
151
132
|
}
|
|
152
133
|
const repos = findRepos(homeDir(deps.env));
|
|
153
134
|
if (!repos.length)
|
|
154
|
-
deps.print(
|
|
135
|
+
deps.print(OUTPUT_PREFIX + t().cli.noRepos(homeDir(deps.env)));
|
|
155
136
|
for (const item of planSetup(repos, deps.packageVersion, readConfig)) {
|
|
156
137
|
try {
|
|
157
138
|
await runPlanItem(item, deps);
|
|
158
139
|
}
|
|
159
140
|
catch (err) {
|
|
160
|
-
deps.print(
|
|
141
|
+
deps.print(OUTPUT_PREFIX + t().cli.setupErrorFor(item.repo, message(err)));
|
|
161
142
|
}
|
|
162
143
|
}
|
|
163
144
|
}
|
|
164
145
|
catch (err) {
|
|
165
|
-
deps.print(
|
|
146
|
+
deps.print(OUTPUT_PREFIX + t().cli.setupError(message(err)));
|
|
166
147
|
}
|
|
167
148
|
return EXIT_OK;
|
|
168
149
|
}
|
|
@@ -203,10 +184,10 @@ async function sessionCommand(deps) {
|
|
|
203
184
|
const delegated = deps.env[DELEGATED_ENV] === DELEGATED_VALUE;
|
|
204
185
|
if (!delegated && wanted && wanted !== deps.packageVersion && existsSync(packageBin(tools, wanted))) {
|
|
205
186
|
try {
|
|
206
|
-
await deps.run(packageBin(tools, wanted), ["session"], { env: { ...deps.env, [DELEGATED_ENV]: DELEGATED_VALUE } });
|
|
187
|
+
await deps.run(packageBin(tools, wanted), ["session"], { env: { ...langEnv(deps.env), [DELEGATED_ENV]: DELEGATED_VALUE } });
|
|
207
188
|
}
|
|
208
189
|
catch (err) {
|
|
209
|
-
deps.print(
|
|
190
|
+
deps.print(OUTPUT_PREFIX + t().cli.sessionDelegateFailed(wanted, message(err)));
|
|
210
191
|
}
|
|
211
192
|
return EXIT_OK;
|
|
212
193
|
}
|
|
@@ -224,7 +205,7 @@ async function sessionCommand(deps) {
|
|
|
224
205
|
async function dockerCommand(deps) {
|
|
225
206
|
if (await deps.docker.ensure().catch(() => false))
|
|
226
207
|
return EXIT_OK;
|
|
227
|
-
deps.print(
|
|
208
|
+
deps.print(OUTPUT_PREFIX + t().docker.didNotStart(DOCKERD_LOG));
|
|
228
209
|
return EXIT_FAIL;
|
|
229
210
|
}
|
|
230
211
|
/**
|
|
@@ -242,6 +223,10 @@ async function doctorCommand(deps) {
|
|
|
242
223
|
});
|
|
243
224
|
for (const line of result.lines)
|
|
244
225
|
deps.print(line);
|
|
226
|
+
const art = deps.stdoutTty ?? process.stdout.isTTY === true;
|
|
227
|
+
if (art)
|
|
228
|
+
for (const line of renderBlock(doctorBlock(result.counts), { art, color: !deps.env.NO_COLOR }))
|
|
229
|
+
deps.print(line);
|
|
245
230
|
return result.ok ? EXIT_OK : EXIT_FAIL;
|
|
246
231
|
}
|
|
247
232
|
/**
|
|
@@ -277,7 +262,7 @@ function manualCommand(deps) {
|
|
|
277
262
|
deps.print(`${OUTPUT_PREFIX}${message(err)}`);
|
|
278
263
|
return EXIT_FAIL;
|
|
279
264
|
}
|
|
280
|
-
deps.print(
|
|
265
|
+
deps.print(OUTPUT_PREFIX + t().cli.manualWritten(MANUAL_FILE));
|
|
281
266
|
return EXIT_OK;
|
|
282
267
|
}
|
|
283
268
|
/**
|
|
@@ -353,11 +338,11 @@ async function prepareCommand(args, deps) {
|
|
|
353
338
|
const known = [RESUME_OPTION, PR_OPTION, HERE_OPTION];
|
|
354
339
|
const unknown = args.filter((arg) => !known.includes(arg));
|
|
355
340
|
if (unknown.length) {
|
|
356
|
-
deps.print(
|
|
341
|
+
deps.print(OUTPUT_PREFIX + t().cli.unknownOption("prepare", unknown.join(" ")));
|
|
357
342
|
return EXIT_FAIL;
|
|
358
343
|
}
|
|
359
344
|
if (args.filter((arg) => known.includes(arg)).length > 1) {
|
|
360
|
-
deps.print(
|
|
345
|
+
deps.print(OUTPUT_PREFIX + t().cli.onlyOne(known.join(", ")));
|
|
361
346
|
return EXIT_FAIL;
|
|
362
347
|
}
|
|
363
348
|
const place = args.includes(PR_OPTION) ? "pr" : args.includes(HERE_OPTION) ? "here" : undefined;
|
|
@@ -372,7 +357,7 @@ async function prepareCommand(args, deps) {
|
|
|
372
357
|
*/
|
|
373
358
|
async function verifyCommand(args, deps) {
|
|
374
359
|
if (args.length) {
|
|
375
|
-
deps.print(
|
|
360
|
+
deps.print(OUTPUT_PREFIX + t().cli.unknownOption("verify", args.join(" ")));
|
|
376
361
|
return EXIT_FAIL;
|
|
377
362
|
}
|
|
378
363
|
return verifyBranch(deps.cwd, prepareDeps(deps));
|
|
@@ -395,10 +380,34 @@ function prepareDeps(deps) {
|
|
|
395
380
|
packageRoot: deps.packageRoot ?? RUNNING_PACKAGE_ROOT,
|
|
396
381
|
today: deps.today ?? branchDay(new Date()),
|
|
397
382
|
isTty: deps.isTty ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
|
|
398
|
-
color: deps
|
|
383
|
+
color: prepareArt(deps) && !deps.env.NO_COLOR,
|
|
384
|
+
art: prepareArt(deps),
|
|
399
385
|
onInterrupt: deps.onInterrupt ?? onProcessInterrupt,
|
|
400
386
|
};
|
|
401
387
|
}
|
|
388
|
+
/** Language of this run, set by `main`. */
|
|
389
|
+
let currentLang = "es";
|
|
390
|
+
/**
|
|
391
|
+
* The environment for another version of this package: the same, with the language of this run,
|
|
392
|
+
* so a `--lang` given here reaches the relaunched setup or the delegated session.
|
|
393
|
+
*
|
|
394
|
+
* @param env - Process environment.
|
|
395
|
+
* @returns A new environment.
|
|
396
|
+
* @example
|
|
397
|
+
* langEnv({}); // { AGENT_CLOUD_KIT_LANG: "es" }
|
|
398
|
+
*/
|
|
399
|
+
function langEnv(env) {
|
|
400
|
+
return { ...env, [LANG_ENV]: currentLang };
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Whether `prepare` and `verify` draw Kuro: stdout is a terminal (and no test said otherwise).
|
|
404
|
+
*
|
|
405
|
+
* @param deps - CLI dependencies.
|
|
406
|
+
* @returns True in a terminal.
|
|
407
|
+
*/
|
|
408
|
+
function prepareArt(deps) {
|
|
409
|
+
return deps.stdoutTty ?? (deps.isTty === undefined && process.stdout.isTTY === true);
|
|
410
|
+
}
|
|
402
411
|
/**
|
|
403
412
|
* Runs the CLI.
|
|
404
413
|
*
|
|
@@ -409,7 +418,20 @@ function prepareDeps(deps) {
|
|
|
409
418
|
* process.exitCode = await main(["setup"], realDeps());
|
|
410
419
|
*/
|
|
411
420
|
export async function main(argv, deps) {
|
|
412
|
-
const
|
|
421
|
+
const { args, flag } = splitLang(argv);
|
|
422
|
+
try {
|
|
423
|
+
currentLang = resolveLang(flag, deps.env);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
if (!(err instanceof LangError))
|
|
427
|
+
throw err;
|
|
428
|
+
currentLang = "es";
|
|
429
|
+
useLang(currentLang);
|
|
430
|
+
deps.print(OUTPUT_PREFIX + t().lang.invalid(err.source, err.value));
|
|
431
|
+
return EXIT_FAIL;
|
|
432
|
+
}
|
|
433
|
+
useLang(currentLang);
|
|
434
|
+
const [command, ...rest] = args;
|
|
413
435
|
if (command === "setup")
|
|
414
436
|
return setupCommand(rest, deps);
|
|
415
437
|
if (command === "session")
|
|
@@ -430,7 +452,7 @@ export async function main(argv, deps) {
|
|
|
430
452
|
deps.print(deps.packageVersion);
|
|
431
453
|
return EXIT_OK;
|
|
432
454
|
}
|
|
433
|
-
deps.print(
|
|
455
|
+
deps.print(t().help);
|
|
434
456
|
return command === undefined || command === "--help" ? EXIT_OK : EXIT_FAIL;
|
|
435
457
|
}
|
|
436
458
|
/**
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { CONFIG_FILE } from "./constants.js";
|
|
7
|
+
import { t } from "./messages/index.js";
|
|
7
8
|
/** Top-level key that editors use to point at the JSON schema; allowed but not part of the config. */
|
|
8
9
|
const SCHEMA_KEY = "$schema";
|
|
9
10
|
/** Exact semver: no ranges, no prefixes. */
|
|
@@ -78,12 +79,12 @@ function parseDocker(value, errors) {
|
|
|
78
79
|
if (value === undefined)
|
|
79
80
|
return { pull: PULL_AUTO };
|
|
80
81
|
if (!isRecord(value)) {
|
|
81
|
-
errors.push(fail(
|
|
82
|
+
errors.push(fail(t().config.dockerNotObject));
|
|
82
83
|
return { pull: PULL_AUTO };
|
|
83
84
|
}
|
|
84
85
|
for (const key of Object.keys(value)) {
|
|
85
86
|
if (!DOCKER_KEYS.includes(key)) {
|
|
86
|
-
errors.push(fail(`
|
|
87
|
+
errors.push(fail(t().config.unknownField(`docker.${key}`)));
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
const pull = value.pull;
|
|
@@ -91,7 +92,7 @@ function parseDocker(value, errors) {
|
|
|
91
92
|
return { pull: PULL_AUTO };
|
|
92
93
|
if (pull === false || isStringList(pull))
|
|
93
94
|
return { pull };
|
|
94
|
-
errors.push(fail(
|
|
95
|
+
errors.push(fail(t().config.badPull));
|
|
95
96
|
return { pull: PULL_AUTO };
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
@@ -108,7 +109,7 @@ function parseList(raw, key, errors) {
|
|
|
108
109
|
return [];
|
|
109
110
|
if (isStringList(value))
|
|
110
111
|
return value;
|
|
111
|
-
errors.push(fail(
|
|
112
|
+
errors.push(fail(t().config.notStringList(key)));
|
|
112
113
|
return [];
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
@@ -124,13 +125,13 @@ function parseDomains(value, errors) {
|
|
|
124
125
|
if (value === undefined)
|
|
125
126
|
return [];
|
|
126
127
|
if (!isStringList(value)) {
|
|
127
|
-
errors.push(fail(
|
|
128
|
+
errors.push(fail(t().config.notStringList("domains")));
|
|
128
129
|
return [];
|
|
129
130
|
}
|
|
130
131
|
return value.filter((host) => {
|
|
131
132
|
const valid = isHostName(host);
|
|
132
133
|
if (!valid)
|
|
133
|
-
errors.push(fail(
|
|
134
|
+
errors.push(fail(t().config.badHost(JSON.stringify(host))));
|
|
134
135
|
return valid;
|
|
135
136
|
});
|
|
136
137
|
}
|
|
@@ -145,11 +146,11 @@ function parseConfig(raw, errors) {
|
|
|
145
146
|
const known = [...CONFIG_KEYS, SCHEMA_KEY];
|
|
146
147
|
for (const key of Object.keys(raw)) {
|
|
147
148
|
if (!known.includes(key))
|
|
148
|
-
errors.push(fail(
|
|
149
|
+
errors.push(fail(t().config.unknownField(key)));
|
|
149
150
|
}
|
|
150
151
|
const version = raw.version;
|
|
151
152
|
if (typeof version !== "string" || !EXACT_SEMVER.test(version)) {
|
|
152
|
-
errors.push(fail(
|
|
153
|
+
errors.push(fail(t().config.badVersion));
|
|
153
154
|
return undefined;
|
|
154
155
|
}
|
|
155
156
|
const lists = Object.fromEntries(LIST_KEYS.map((key) => [key, parseList(raw, key, errors)]));
|
|
@@ -166,7 +167,7 @@ function parseConfig(raw, errors) {
|
|
|
166
167
|
if (typeof raw.notes === "string")
|
|
167
168
|
config.notes = raw.notes;
|
|
168
169
|
else if (raw.notes !== undefined)
|
|
169
|
-
errors.push(fail(
|
|
170
|
+
errors.push(fail(t().config.notesNotString));
|
|
170
171
|
return config;
|
|
171
172
|
}
|
|
172
173
|
/**
|
|
@@ -187,10 +188,10 @@ export function readConfig(repo) {
|
|
|
187
188
|
}
|
|
188
189
|
catch (error) {
|
|
189
190
|
const reason = error instanceof Error ? error.message : String(error);
|
|
190
|
-
return { errors: [fail(
|
|
191
|
+
return { errors: [fail(t().config.unreadable(reason))] };
|
|
191
192
|
}
|
|
192
193
|
if (!isRecord(raw))
|
|
193
|
-
return { errors: [fail(
|
|
194
|
+
return { errors: [fail(t().config.notObject)] };
|
|
194
195
|
const errors = [];
|
|
195
196
|
const config = parseConfig(raw, errors);
|
|
196
197
|
return config ? { config, errors } : { errors };
|
package/dist/docker.js
CHANGED
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
import { execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { closeSync, openSync } from "node:fs";
|
|
6
6
|
import { DOCKERD_LOG, DOCKER_WAIT_SECONDS } from "./constants.js";
|
|
7
|
+
import { t } from "./messages/index.js";
|
|
7
8
|
/** Timeout of each probe command, in milliseconds. */
|
|
8
9
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
9
10
|
/** Delay between readiness checks, in milliseconds. */
|
|
10
11
|
const POLL_MS = 1000;
|
|
11
12
|
/** Milliseconds in a second. */
|
|
12
13
|
const MS_PER_SECOND = 1000;
|
|
13
|
-
/** Message returned when there is no `dockerd` executable on PATH. */
|
|
14
|
-
export const DOCKERD_MISSING = "dockerd no está instalado";
|
|
15
14
|
/**
|
|
16
15
|
* Tells whether a command exits with code 0.
|
|
17
16
|
*
|
|
@@ -33,7 +32,7 @@ function succeeds(cmd, args) {
|
|
|
33
32
|
*
|
|
34
33
|
* @returns True when `docker info` succeeds.
|
|
35
34
|
* @example
|
|
36
|
-
* if (!dockerResponds())
|
|
35
|
+
* if (!dockerResponds()) await ensureDocker();
|
|
37
36
|
*/
|
|
38
37
|
export function dockerResponds() {
|
|
39
38
|
return succeeds("docker", ["info"]);
|
|
@@ -43,7 +42,7 @@ export function dockerResponds() {
|
|
|
43
42
|
* answers or when one is already running. Never throws.
|
|
44
43
|
*
|
|
45
44
|
* @param probe - Command probe; the real one when omitted (tests inject a fake).
|
|
46
|
-
* @returns
|
|
45
|
+
* @returns The "dockerd is not installed" message when there is no `dockerd` on PATH, another error message when the
|
|
47
46
|
* daemon could not be started, undefined otherwise.
|
|
48
47
|
* @example
|
|
49
48
|
* const problem = startDockerDetached();
|
|
@@ -52,7 +51,7 @@ export function startDockerDetached(probe = succeeds) {
|
|
|
52
51
|
if (probe("docker", ["info"]))
|
|
53
52
|
return undefined;
|
|
54
53
|
if (!probe("sh", ["-c", "command -v dockerd"]))
|
|
55
|
-
return
|
|
54
|
+
return t().docker.missing;
|
|
56
55
|
if (probe("pgrep", ["-x", "dockerd"]))
|
|
57
56
|
return undefined;
|
|
58
57
|
try {
|
|
@@ -78,7 +77,7 @@ export function startDockerDetached(probe = succeeds) {
|
|
|
78
77
|
* @param probe - Command probe; the real one when omitted (tests inject a fake).
|
|
79
78
|
* @returns True when Docker answers in time; false when it never does or cannot start.
|
|
80
79
|
* @example
|
|
81
|
-
*
|
|
80
|
+
* const up = await ensureDocker();
|
|
82
81
|
*/
|
|
83
82
|
export async function ensureDocker(probe = succeeds) {
|
|
84
83
|
if (startDockerDetached(probe))
|
package/dist/doctor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readConfig } from "./config.js";
|
|
|
8
8
|
import { CONFIG_FILE, OUTPUT_PREFIX } from "./constants.js";
|
|
9
9
|
import { TOOLS, readToolVersions } from "./detect.js";
|
|
10
10
|
import { isValidRequest, toolExecutable } from "./installers.js";
|
|
11
|
+
import { stepLabel, t } from "./messages/index.js";
|
|
11
12
|
import { expectedFingerprint } from "./session.js";
|
|
12
13
|
import { SKILL_DIR, skillVersion } from "./skill.js";
|
|
13
14
|
import { readState } from "./state.js";
|
|
@@ -49,10 +50,10 @@ async function checkDomain(host, capture) {
|
|
|
49
50
|
try {
|
|
50
51
|
const status = (await capture("curl", args)).trim();
|
|
51
52
|
const ok = status !== BLOCKED_STATUS && status !== NO_ANSWER_STATUS;
|
|
52
|
-
return { text:
|
|
53
|
+
return { text: t().doctor.domain(host, ok), level: verdict(ok) };
|
|
53
54
|
}
|
|
54
55
|
catch {
|
|
55
|
-
return { text:
|
|
56
|
+
return { text: t().doctor.domain(host, false), level: "fail" };
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
@@ -82,12 +83,12 @@ function checkTools(repo, toolsDir, state) {
|
|
|
82
83
|
if (!version)
|
|
83
84
|
return [];
|
|
84
85
|
if (!isValidRequest(tool, version))
|
|
85
|
-
return [{ text:
|
|
86
|
+
return [{ text: t().doctor.toolInvalid(tool, version), level: "fail" }];
|
|
86
87
|
const executable = toolExecutable(tool, version, toolsDir, state?.resolved ?? {});
|
|
87
88
|
if (!executable)
|
|
88
|
-
return [{ text:
|
|
89
|
+
return [{ text: t().doctor.toolUnresolved(tool, version), level: "fail" }];
|
|
89
90
|
const present = existsSync(executable);
|
|
90
|
-
return [{ text:
|
|
91
|
+
return [{ text: t().doctor.tool(tool, version, present), level: verdict(present) }];
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
@@ -99,19 +100,19 @@ function checkTools(repo, toolsDir, state) {
|
|
|
99
100
|
*/
|
|
100
101
|
function checkState(repo, ctx) {
|
|
101
102
|
if (!existsSync(join(repo, CONFIG_FILE)))
|
|
102
|
-
return [{ text:
|
|
103
|
+
return [{ text: t().doctor.fileMissing(CONFIG_FILE), level: "fail" }];
|
|
103
104
|
const { config, errors } = readConfig(repo);
|
|
104
105
|
const out = errors.map((text) => ({ text: `${CONFIG_FILE}: ${text}`, level: "fail" }));
|
|
105
106
|
if (!config)
|
|
106
107
|
return out;
|
|
107
108
|
const state = readState(ctx.toolsDir, repo);
|
|
108
109
|
if (!state)
|
|
109
|
-
return [...out, { text:
|
|
110
|
+
return [...out, { text: t().doctor.stateMissing, level: "fail" }];
|
|
110
111
|
const same = expectedFingerprint(repo, config, state) === state.fingerprint;
|
|
111
|
-
out.push({ text:
|
|
112
|
+
out.push({ text: t().doctor.fingerprint(same), level: verdict(same) });
|
|
112
113
|
for (const s of state.steps) {
|
|
113
114
|
const seconds = (s.ms / MS_PER_SECOND).toFixed(DURATION_DECIMALS);
|
|
114
|
-
out.push({ text:
|
|
115
|
+
out.push({ text: t().doctor.step(stepLabel(s.name), s.ok, seconds), level: verdict(s.ok) });
|
|
115
116
|
}
|
|
116
117
|
return out;
|
|
117
118
|
}
|
|
@@ -132,10 +133,7 @@ function checkPlugins(repo) {
|
|
|
132
133
|
return [];
|
|
133
134
|
return Object.entries(plugins)
|
|
134
135
|
.filter(([, enabled]) => enabled === true)
|
|
135
|
-
.map(([name]) => ({
|
|
136
|
-
text: `aviso: el plugin ${name} está activado pero no está en el repo; no llegará a la nube (copia sus skills con skills-manager)`,
|
|
137
|
-
level: "warn",
|
|
138
|
-
}));
|
|
136
|
+
.map(([name]) => ({ text: t().doctor.plugin(name), level: "warn" }));
|
|
139
137
|
}
|
|
140
138
|
catch {
|
|
141
139
|
return [];
|
|
@@ -154,12 +152,7 @@ function checkSkill(repo, packageVersion) {
|
|
|
154
152
|
const installed = skillVersion(repo);
|
|
155
153
|
if (installed === undefined || installed === packageVersion)
|
|
156
154
|
return [];
|
|
157
|
-
return [
|
|
158
|
-
{
|
|
159
|
-
text: `aviso: ${SKILL_DIR} está en ${installed} y el paquete en ${packageVersion}; corre npx agent-cloud-kit@${packageVersion} init`,
|
|
160
|
-
level: "warn",
|
|
161
|
-
},
|
|
162
|
-
];
|
|
155
|
+
return [{ text: t().doctor.skill(SKILL_DIR, installed, packageVersion), level: "warn" }];
|
|
163
156
|
}
|
|
164
157
|
/**
|
|
165
158
|
* Runs one group of checks, turning anything it throws into a failed check.
|
|
@@ -173,7 +166,7 @@ async function guarded(name, fn) {
|
|
|
173
166
|
return await fn();
|
|
174
167
|
}
|
|
175
168
|
catch (err) {
|
|
176
|
-
return [{ text:
|
|
169
|
+
return [{ text: t().doctor.groupError(name, err instanceof Error ? err.message : String(err)), level: "fail" }];
|
|
177
170
|
}
|
|
178
171
|
}
|
|
179
172
|
/**
|
|
@@ -187,14 +180,32 @@ async function guarded(name, fn) {
|
|
|
187
180
|
*/
|
|
188
181
|
export async function doctor(repo, ctx) {
|
|
189
182
|
const checks = [];
|
|
190
|
-
|
|
191
|
-
checks.push(...(await guarded(
|
|
192
|
-
checks.push(...(await guarded(
|
|
183
|
+
const groups = t().doctor.groups;
|
|
184
|
+
checks.push(...(await guarded(groups.network, async () => Promise.all(domainsToProbe(repo).map((host) => checkDomain(host, ctx.capture))))));
|
|
185
|
+
checks.push(...(await guarded(groups.tools, () => checkTools(repo, ctx.toolsDir, readState(ctx.toolsDir, repo)))));
|
|
186
|
+
checks.push(...(await guarded(groups.docker, () => {
|
|
193
187
|
const docker = ctx.dockerInfo();
|
|
194
|
-
return [{ text:
|
|
188
|
+
return [{ text: t().doctor.docker(docker), level: verdict(docker) }];
|
|
195
189
|
})));
|
|
196
|
-
checks.push(...(await guarded(
|
|
197
|
-
checks.push(...(await guarded(
|
|
198
|
-
checks.push(...(await guarded(
|
|
199
|
-
|
|
190
|
+
checks.push(...(await guarded(groups.state, () => checkState(repo, ctx))));
|
|
191
|
+
checks.push(...(await guarded(groups.plugins, () => checkPlugins(repo))));
|
|
192
|
+
checks.push(...(await guarded(groups.skill, () => checkSkill(repo, ctx.packageVersion))));
|
|
193
|
+
const failed = checks.filter((c) => c.level === "fail").length;
|
|
194
|
+
const warnings = checks.filter((c) => c.level === "warn").length;
|
|
195
|
+
return { lines: checks.map((c) => OUTPUT_PREFIX + c.text), ok: failed === 0, counts: { total: checks.length, failed, warnings } };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The final block of `doctor`, shown with Kuro in a terminal.
|
|
199
|
+
*
|
|
200
|
+
* @param counts - Checks that ran, failed and warned.
|
|
201
|
+
* @returns What the block says.
|
|
202
|
+
* @example
|
|
203
|
+
* doctorBlock({ total: 9, failed: 0, warnings: 0 }); // { mood: "ok", title: "Todo en orden" }
|
|
204
|
+
*/
|
|
205
|
+
export function doctorBlock(counts) {
|
|
206
|
+
const m = t().doctor;
|
|
207
|
+
const lines = counts.warnings ? [m.warnings(counts.warnings)] : [];
|
|
208
|
+
if (!counts.failed)
|
|
209
|
+
return { mood: "ok", title: m.allOk, lines };
|
|
210
|
+
return { mood: "error", title: m.someFailed(counts.failed, counts.total), lines: [...lines, m.failedHint] };
|
|
200
211
|
}
|