karajan-code 4.18.0 → 4.19.0
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/package.json +1 -1
- package/scripts/dual-publish.mjs +67 -0
- package/scripts/verify-pack.mjs +8 -5
- package/src/checks/guarantee-level.js +38 -0
- package/src/cli/register-meta.js +1 -1
- package/src/cli/register-roles-skills.js +17 -1
- package/src/commands/agents.js +4 -2
- package/src/commands/doctor.js +16 -0
- package/src/commands/review-gate.js +23 -0
- package/src/environment/briefs.js +3 -0
- package/src/environment/playbook.js +3 -1
- package/src/harden/sentinel-hooks.js +57 -2
- package/src/harden/workflow-engine.js +9 -9
- package/src/harden/workflow-templates.js +2 -2
- package/src/mutate/diff-scope.js +6 -2
- package/src/review/mutation-pregate.js +48 -0
- package/src/start/sweep.js +46 -4
- package/src/utils/update-check.js +25 -6
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dual-publish (MIG-A, KJC-TSK-0751, épica KJC-PCS-0077, ADR 0004) — el
|
|
4
|
+
* MISMO contenido sale bajo dos nombres npm durante la migración al scope:
|
|
5
|
+
* karajan-code (legacy, publish normal) y @karajan-family/code (scoped).
|
|
6
|
+
*
|
|
7
|
+
* Uso:
|
|
8
|
+
* node scripts/dual-publish.mjs --pack-only # genera+verifica el tarball scoped
|
|
9
|
+
* node scripts/dual-publish.mjs --publish --otp=XXX # publica el scoped (tras publicar el legacy)
|
|
10
|
+
*
|
|
11
|
+
* El swap del name es EN SITIO con restauración en finally: un crash a mitad
|
|
12
|
+
* no deja el manifest renombrado. La verificación es el MISMO verify-pack
|
|
13
|
+
* (name-agnóstico desde la parte 1): empaqueta, instala aislado y corre kj.
|
|
14
|
+
* El PRIMER publish del scoped lo hace el usuario (cuenta karajan-family,
|
|
15
|
+
* security key); los siguientes van con la sesión normal.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { join, dirname, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
export const LEGACY_NAME = "karajan-code";
|
|
23
|
+
export const SCOPED_NAME = "@karajan-family/code";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ejecuta `fn` con el manifest renombrado al scoped (+ publishConfig public,
|
|
27
|
+
* obligatorio para scoped) y lo RESTAURA byte a byte pase lo que pase.
|
|
28
|
+
*/
|
|
29
|
+
export async function withScopedName(repoRoot, fn) {
|
|
30
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
31
|
+
const original = readFileSync(pkgPath, "utf8");
|
|
32
|
+
const pkg = JSON.parse(original);
|
|
33
|
+
if (pkg.name !== LEGACY_NAME) {
|
|
34
|
+
throw new Error(`dual-publish: el manifest dice "${pkg.name}" y solo se renombra desde ${LEGACY_NAME} — jamás a ciegas`);
|
|
35
|
+
}
|
|
36
|
+
// publishConfig se MERGEA, no se pisa (catch de codex): un registry/tag
|
|
37
|
+
// preexistente debe sobrevivir al publish scoped.
|
|
38
|
+
writeFileSync(pkgPath, `${JSON.stringify({ ...pkg, name: SCOPED_NAME, publishConfig: { ...pkg.publishConfig, access: "public" } }, null, 2)}\n`, "utf8");
|
|
39
|
+
try {
|
|
40
|
+
return await fn();
|
|
41
|
+
} finally {
|
|
42
|
+
writeFileSync(pkgPath, original, "utf8");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// argv[1] normalizado (catch de codex): node lo absolutiza hoy, pero el
|
|
47
|
+
// guard no debe depender de ese detalle de implementación.
|
|
48
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
49
|
+
if (isMain) {
|
|
50
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
const publish = args.includes("--publish");
|
|
53
|
+
const otp = args.find((a) => a.startsWith("--otp="))?.slice(6);
|
|
54
|
+
await withScopedName(repoRoot, async () => {
|
|
55
|
+
console.log(`dual-publish: manifest → ${SCOPED_NAME} (temporal)`);
|
|
56
|
+
// La E2E real: empaqueta el scoped, instálalo aislado y corre kj.
|
|
57
|
+
execFileSync("node", [join(repoRoot, "scripts", "verify-pack.mjs")], { cwd: repoRoot, stdio: "inherit" });
|
|
58
|
+
if (publish) {
|
|
59
|
+
const pubArgs = ["publish", "--ignore-scripts", ...(otp ? [`--otp=${otp}`] : [])];
|
|
60
|
+
console.log(`dual-publish: npm ${pubArgs.join(" ").replace(/--otp=\S+/, "--otp=***")}`);
|
|
61
|
+
execFileSync("npm", pubArgs, { cwd: repoRoot, stdio: "inherit" });
|
|
62
|
+
} else {
|
|
63
|
+
console.log("dual-publish: --pack-only — verificado y SIN publicar (usa --publish en la release)");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
console.log(`dual-publish: manifest restaurado a ${LEGACY_NAME} ✓`);
|
|
67
|
+
}
|
package/scripts/verify-pack.mjs
CHANGED
|
@@ -27,6 +27,9 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
28
|
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
|
|
29
29
|
const expectedVersion = pkg.version;
|
|
30
|
+
// MIG-A (KJC-TSK-0751): el nombre sale del manifest — el MISMO verify-pack
|
|
31
|
+
// valida el tarball unscoped y el @karajan-family/* del dual-publish.
|
|
32
|
+
const pkgName = pkg.name;
|
|
30
33
|
|
|
31
34
|
// Subprocess env: strip CLAUDECODE (Claude Code blocks nested non-interactive
|
|
32
35
|
// runs otherwise) and force a non-interactive, quiet npm.
|
|
@@ -54,13 +57,13 @@ let gTmp = null;
|
|
|
54
57
|
let pnpmTmp = null;
|
|
55
58
|
let qsTmp = null;
|
|
56
59
|
try {
|
|
57
|
-
console.log(`verify-pack: packing
|
|
60
|
+
console.log(`verify-pack: packing ${pkgName}@${expectedVersion}…`);
|
|
58
61
|
// --json gives us the exact filename without parsing human output.
|
|
59
62
|
const packOut = run("npm", ["pack", "--json", "--silent"], { cwd: repoRoot });
|
|
60
63
|
const filename = JSON.parse(packOut)[0]?.filename;
|
|
61
64
|
if (!filename) fail("npm pack did not report a filename", packOut);
|
|
62
|
-
// npm
|
|
63
|
-
// the file lands in cwd under
|
|
65
|
+
// npm --json reports the exact filename (scoped names normalized to
|
|
66
|
+
// scope-name-x.y.z.tgz); the file lands in cwd under that name.
|
|
64
67
|
tgzPath = path.join(repoRoot, filename);
|
|
65
68
|
if (!fs.existsSync(tgzPath)) fail(`packed tarball not found at ${tgzPath}`);
|
|
66
69
|
|
|
@@ -93,7 +96,7 @@ try {
|
|
|
93
96
|
const { scanPaths, loadPrivacyList } = await import(
|
|
94
97
|
path.join(repoRoot, "src", "privacy", "scan.js")
|
|
95
98
|
);
|
|
96
|
-
const shippedDir = path.join(tmpDir, "node_modules", "
|
|
99
|
+
const shippedDir = path.join(tmpDir, "node_modules", ...pkgName.split("/"));
|
|
97
100
|
const pFindings = scanPaths([shippedDir], { list: loadPrivacyList() });
|
|
98
101
|
const pBlocks = pFindings.filter((f) => f.severity === "block");
|
|
99
102
|
if (pBlocks.length > 0) {
|
|
@@ -265,7 +268,7 @@ try {
|
|
|
265
268
|
);
|
|
266
269
|
}
|
|
267
270
|
|
|
268
|
-
console.log(`\n✓ verify-pack:
|
|
271
|
+
console.log(`\n✓ verify-pack: ${pkgName}@${expectedVersion} installs clean and runs.`);
|
|
269
272
|
} finally {
|
|
270
273
|
if (tgzPath && fs.existsSync(tgzPath)) fs.rmSync(tgzPath, { force: true });
|
|
271
274
|
if (tmpDir && fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarantee level en runtime (KJC-TSK-0756, hallazgo de campo de Pedro):
|
|
3
|
+
* la doctrina "kj jamás finge una supervisión que no puede aplicar" vivía
|
|
4
|
+
* solo en el README — ahora kj doctor la DECLARA para el host actual, con
|
|
5
|
+
* el porqué. El acoplamiento a Claude es UNA capa (tier A: hooks síncronos
|
|
6
|
+
* bloqueantes, que hoy solo ese harness expone) y es decisión de ADR
|
|
7
|
+
* (v4.13), no accidente; el tier B (git gates) es el suelo agnóstico.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { detectHostAgent } from "../utils/agent-detect.js";
|
|
12
|
+
|
|
13
|
+
/** Pure: computes the three tiers from observable facts. */
|
|
14
|
+
export function guaranteeLevel({ host, harnessWired, policyDeclared, policyWorkflow }) {
|
|
15
|
+
const tierA = host === "claude" && harnessWired
|
|
16
|
+
? { active: true, reason: "Claude Code expone hooks síncronos bloqueantes y el harness del Sentinel está instalado (kj harden)" }
|
|
17
|
+
: host === "claude"
|
|
18
|
+
? { active: false, reason: "host Claude sin harness instalado — corre kj harden para activar la supervisión en el turno" }
|
|
19
|
+
: { active: false, reason: `el host ${host ?? "desconocido"} no expone hooks síncronos bloqueantes — no se finge la supervisión; el tier B sigue siendo el suelo de garantía` };
|
|
20
|
+
const tierB = { active: true, reason: "los gates viven en el repositorio, no en el harness: cualquier host, el diff violador no entra" };
|
|
21
|
+
const tierC = policyDeclared && policyWorkflow
|
|
22
|
+
? { active: true, reason: "kj-policy.yml re-verifica el PR contra la policy declarada (merge-blocking)" }
|
|
23
|
+
: policyDeclared
|
|
24
|
+
? { active: false, reason: "hay policy declarada pero sin workflow de CI — corre kj harden para sembrar kj-policy.yml" }
|
|
25
|
+
: { active: false, reason: "sin .karajan/policy.yml declarada — el tier C solo existe donde hay policy que verificar" };
|
|
26
|
+
return { host: host ?? null, tierA, tierB, tierC };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Facts from disk + env; injectable for tests. */
|
|
30
|
+
export function collectGuaranteeFacts({ projectDir = process.cwd(), deps = {} } = {}) {
|
|
31
|
+
const { detectHost = detectHostAgent, exists = existsSync } = deps;
|
|
32
|
+
return guaranteeLevel({
|
|
33
|
+
host: detectHost(),
|
|
34
|
+
harnessWired: exists(join(projectDir, ".karajan", "harness", "pretooluse-sentinel.mjs")),
|
|
35
|
+
policyDeclared: exists(join(projectDir, ".karajan", "policy.yml")),
|
|
36
|
+
policyWorkflow: exists(join(projectDir, ".github", "workflows", "kj-policy.yml")),
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -286,7 +286,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
// KJC-TSK-0733 PL-A — policy as code: motor determinista en modo warn.
|
|
289
|
-
const policyCmd = program.command("policy").description("Policy as code (.karajan/policy.yml, vocabulario cerrado) —
|
|
289
|
+
const policyCmd = program.command("policy").description("Policy as code (.karajan/policy.yml, vocabulario cerrado): eval/check deterministas, grant con caducidad, anchor del decision log — deny en commit y CI");
|
|
290
290
|
policyCmd.command("eval")
|
|
291
291
|
.description("Evalúa UNA tool call: imprime {decision, rule_id, reason}; --strict devuelve exit 2 en deny (contrato para adaptadores de hooks)")
|
|
292
292
|
.option("--role <role>", "Rol del agente", "coder")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rolesCommand } from "../commands/roles.js";
|
|
2
|
-
import { agentsCommand } from "../commands/agents.js";
|
|
2
|
+
import { agentsCommand, ASSIGNABLE_ROLES, VALID_PROVIDERS } from "../commands/agents.js";
|
|
3
3
|
import { withConfig } from "./_shared.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -30,6 +30,22 @@ export function registerRolesSkills(program, { pkgVersion }) {
|
|
|
30
30
|
.command("agents [subcommand] [role] [provider]")
|
|
31
31
|
.description("List or change AI agent assignments per role (e.g. kj agents set coder gemini)")
|
|
32
32
|
.option("--global", "Persist change to kj.config.yml (default for CLI)")
|
|
33
|
+
// KJC-TSK-0755: el --help enumera valores REALES (de las constantes, no
|
|
34
|
+
// duplicados a mano) — la firma genérica obligaba a preguntar.
|
|
35
|
+
.addHelpText("after", [
|
|
36
|
+
"",
|
|
37
|
+
"Subcommands:",
|
|
38
|
+
" list (default) Show the provider assigned to each role",
|
|
39
|
+
" set Change one: kj agents set <role> <provider>",
|
|
40
|
+
"",
|
|
41
|
+
`Roles: ${ASSIGNABLE_ROLES.join(", ")}`,
|
|
42
|
+
`Providers: ${VALID_PROVIDERS.join(", ")} (kj doctor shows which are installed)`,
|
|
43
|
+
"",
|
|
44
|
+
"Examples:",
|
|
45
|
+
" kj agents # current assignments",
|
|
46
|
+
" kj agents set reviewer codex",
|
|
47
|
+
" kj agents set solomon agy --global",
|
|
48
|
+
].join("\n"))
|
|
33
49
|
.action(async (subcommand, role, provider, flags) => {
|
|
34
50
|
await withConfig(pkgVersion, "agents", {}, async ({ config }) => {
|
|
35
51
|
await agentsCommand({ config, subcommand: subcommand || "list", role, provider, global: flags.global });
|
package/src/commands/agents.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { loadConfig, writeConfig, getConfigPath, getProjectConfigPath, loadProjectConfig, resolveRole } from "../config.js";
|
|
2
2
|
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Exportados para que el --help se autoalimente de las constantes reales
|
|
5
|
+
// (KJC-TSK-0755, hallazgo de campo: la firma genérica obligaba a preguntar).
|
|
6
|
+
export const ASSIGNABLE_ROLES = [
|
|
5
7
|
"coder", "reviewer", "planner", "refactorer", "triage",
|
|
6
8
|
"researcher", "tester", "security", "solomon"
|
|
7
9
|
];
|
|
8
10
|
|
|
9
|
-
const VALID_PROVIDERS = KNOWN_AGENTS.map((a) => a.name);
|
|
11
|
+
export const VALID_PROVIDERS = KNOWN_AGENTS.map((a) => a.name);
|
|
10
12
|
|
|
11
13
|
export function listAgents(config, sessionOverrides = {}, projectConfig = null) {
|
|
12
14
|
return ASSIGNABLE_ROLES.map((role) => {
|
package/src/commands/doctor.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import readline from "node:readline";
|
|
15
15
|
import { runChecks as runCheckPipeline, toLegacyShape } from "../checks/runner.js";
|
|
16
|
+
import { collectGuaranteeFacts } from "../checks/guarantee-level.js";
|
|
16
17
|
import { STATUS } from "../checks/types.js";
|
|
17
18
|
import { getSystemChecks } from "../checks/system.js";
|
|
18
19
|
import { getConfigFileChecks } from "../checks/config-files.js";
|
|
@@ -140,16 +141,31 @@ export async function runChecks({ config }) {
|
|
|
140
141
|
*/
|
|
141
142
|
export async function doctorCommand({ config, checkOnly = false, yes = false, json = false, verbose = false, projectOnly = false }) {
|
|
142
143
|
const report = await runDoctor({ config, checkOnly, yes, projectOnly });
|
|
144
|
+
// KJC-TSK-0756 (campo, Pedro): el guarantee level del host actual se
|
|
145
|
+
// DECLARA en runtime — la doctrina no vive solo en el README.
|
|
146
|
+
report.guarantee = collectGuaranteeFacts({ projectDir: config?.projectDir || process.cwd() });
|
|
143
147
|
|
|
144
148
|
if (json) {
|
|
145
149
|
console.log(JSON.stringify(report, null, 2));
|
|
146
150
|
} else {
|
|
147
151
|
printHuman(report, { verbose });
|
|
152
|
+
printGuaranteeLevel(report.guarantee);
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
return hasBlockingFailure(report) ? 1 : 0;
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
// KJC-TSK-0756: la seccion de guarantee level — consola es el medio del
|
|
159
|
+
// doctor (excepcion no-console de commands).
|
|
160
|
+
function printGuaranteeLevel(g) {
|
|
161
|
+
const mark = (t) => (t.active ? "ACTIVE " : "OFF ");
|
|
162
|
+
console.log();
|
|
163
|
+
console.log(`Guarantee level (host: ${g.host ?? "desconocido"}) — kj jamas finge una supervision que no puede aplicar:`);
|
|
164
|
+
console.log(` ${mark(g.tierA)} A tool-time (Sentinel en el turno): ${g.tierA.reason}`);
|
|
165
|
+
console.log(` ${mark(g.tierB)} B commit-time (git gates, el SUELO): ${g.tierB.reason}`);
|
|
166
|
+
console.log(` ${mark(g.tierC)} C merge-time (re-check en CI): ${g.tierC.reason}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
function hasBlockingFailure(report) {
|
|
154
170
|
return report.checks.some((c) => c.status === STATUS.FAIL || c.status === STATUS.TIMEOUT);
|
|
155
171
|
}
|
|
@@ -13,6 +13,7 @@ import { runOneShotReview } from "../review/one-shot-review.js";
|
|
|
13
13
|
import { runSolomonArbitration } from "../review/solomon-arbitration.js";
|
|
14
14
|
import { ensureGateTrackable } from "../review/gate-gitignore.js";
|
|
15
15
|
import { runSonarPregate, formatSonarFinding } from "../review/sonar-pregate.js";
|
|
16
|
+
import { runMutationPregate, formatSurvivor } from "../review/mutation-pregate.js";
|
|
16
17
|
import { checkCardFirst } from "../review/card-first.js";
|
|
17
18
|
import { checkTestsWithCode } from "../review/tests-with-code.js";
|
|
18
19
|
import { loadPrivacyList, scanText } from "../privacy/scan.js";
|
|
@@ -325,6 +326,28 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
// MUT-A (KJC-TSK-0716): mutation pre-gate — opt-in (method_gates.mutation),
|
|
330
|
+
// SOLO en --staged (jamás en pre-commit: cuesta minutos; y jamás en --range:
|
|
331
|
+
// el scope es el ÍNDICE y anotaría trabajo ajeno — catch de codex). block
|
|
332
|
+
// cierra ANTES de gastar reviewer; warn viaja como advisory en su task.
|
|
333
|
+
const mut = flags.staged && !flags.range ? await runMutationPregate({ config, projectDir }) : { enabled: false, ok: true };
|
|
334
|
+
if (mut.enabled) {
|
|
335
|
+
if (mut.available === false) {
|
|
336
|
+
console.log(`⚠ mutation pre-gate no disponible: ${mut.reason} — degradado avisando (method_gates.mutation)`);
|
|
337
|
+
} else if (mut.survived.length > 0) {
|
|
338
|
+
console.log(`Mutantes supervivientes en el diff — score ${mut.score ?? "n/a"}%:`);
|
|
339
|
+
for (const m of mut.survived) console.log(` ✗ ${formatSurvivor(m)}`);
|
|
340
|
+
if (!mut.ok) {
|
|
341
|
+
console.log("✗ mutation gate: la suite verde no PRUEBA el cambio — mata los supervivientes antes del review (method_gates.mutation: block).");
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
return { verdict: "rejected", reviewer: "mutation", issues: mut.survived.map((m) => ({ severity: "high", file: m.file, description: formatSurvivor(m) })) };
|
|
344
|
+
}
|
|
345
|
+
task = `${task || "Review the following diff for correctness, security and maintainability."}\n\n`
|
|
346
|
+
+ `Surviving mutants on the changed lines (weak asserts — fold into your review):\n`
|
|
347
|
+
+ mut.survived.map((m) => `- ${formatSurvivor(m)}`).join("\n");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
328
351
|
const record = await runOneShotReview({ diff, task, config, logger, projectDir });
|
|
329
352
|
printVerdict(record);
|
|
330
353
|
process.exitCode = record.verdict === "approved" ? 0 : 1;
|
|
@@ -71,6 +71,9 @@ const B = {
|
|
|
71
71
|
[
|
|
72
72
|
...(tdd ? ["TDD is ON: the failing test exists FIRST, then the code."] : []),
|
|
73
73
|
"The suite is never left red, and never goes green by deleting or skipping tests.",
|
|
74
|
+
...(config?.method_gates?.mutation
|
|
75
|
+
? ["Green is not proof: run `kj mutate --since <base>` after the suite passes and BEFORE review — kill surviving mutants (weak asserts) first."]
|
|
76
|
+
: []),
|
|
74
77
|
],
|
|
75
78
|
"green suite + the list of new/changed tests and what each one pins.");
|
|
76
79
|
},
|
|
@@ -61,7 +61,9 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
61
61
|
\`kj_rag_query\` tool: reach for it before grepping by hand.
|
|
62
62
|
- ${trackingLine(stateBackend, boardName)}
|
|
63
63
|
- Tests prove behavior: the failing test exists first (TDD), and the suite
|
|
64
|
-
is never left red.
|
|
64
|
+
is never left red. Green is not proof: with \`method_gates.mutation\`
|
|
65
|
+
declared, run \`kj mutate --since <base>\` after green and BEFORE the
|
|
66
|
+
review — a surviving mutant is a weak assert; kill it first.
|
|
65
67
|
- Every diff is reviewed by a DIFFERENT AI before it is committed
|
|
66
68
|
(\`kj review --staged\`): verdicts bind to the exact diff — change the code
|
|
67
69
|
and it must be reviewed again. Disagree with a rejection? \`kj solomon\`.
|
|
@@ -299,7 +299,62 @@ process.stdin.on("end", () => {
|
|
|
299
299
|
// en cualquier posicion, y una ruta entrecomillada con espacios es
|
|
300
300
|
// invisible al tokenizador: ambas => deny conservador (endpoint
|
|
301
301
|
// pedido por el reviewer; friccion asumida, el remedio es literal).
|
|
302
|
-
|
|
302
|
+
// KJC-BUG-0140: pares de comillas REALES, no regex sobre el texto —
|
|
303
|
+
// la comilla de CIERRE de un string + un redirect literal + la de
|
|
304
|
+
// apertura del siguiente creaban la ilusion de ruta con espacios y
|
|
305
|
+
// denegaban el flujo estandar commit -m "..." > log && echo "ok".
|
|
306
|
+
// Se recorre alternando estado (doctrina del parseCommand del motor):
|
|
307
|
+
// solo el CONTENIDO realmente entrecomillado con slash y espacio
|
|
308
|
+
// deniega — incluidas comillas a mitad de token (scp host:"/a b/x")
|
|
309
|
+
// y la comilla sin cerrar (no verificable).
|
|
310
|
+
// ...y por TOKEN completo, no por span: spans adyacentes concatenan
|
|
311
|
+
// ("dir with"" spaces/file" es UNA ruta) — catch de codex.
|
|
312
|
+
const quotedPathWithSpaces = (s) => {
|
|
313
|
+
// Un backslash pegado a comilla o blanco (comilla escapada,
|
|
314
|
+
// espacio escapado) altera donde CIERRA el string o esconde
|
|
315
|
+
// espacios: opaco => deny (doctrina del motor) — catch de codex.
|
|
316
|
+
// Caracteres construidos con fromCharCode: CERO escaping en este
|
|
317
|
+
// template — fuente y artefacto identicos, nada que malinterpretar
|
|
318
|
+
// (el doble-escaping confundio cuatro reviews; solomon desestimo
|
|
319
|
+
// la primera y las demas se volvieron ilegibles igual).
|
|
320
|
+
// El punto escapado de un sed y similares siguen libres.
|
|
321
|
+
const BS = String.fromCharCode(92);
|
|
322
|
+
const TAB = String.fromCharCode(9);
|
|
323
|
+
const NL = String.fromCharCode(10);
|
|
324
|
+
for (let i = s.indexOf(BS); i !== -1; i = s.indexOf(BS, i + 1)) {
|
|
325
|
+
const n = s[i + 1] || "";
|
|
326
|
+
if (n === '"' || n === "'" || n === " " || n === TAB) return true;
|
|
327
|
+
}
|
|
328
|
+
let q = null;
|
|
329
|
+
let tok = "";
|
|
330
|
+
let hadQuote = false;
|
|
331
|
+
const flush = () => {
|
|
332
|
+
const bad = hadQuote && tok.includes("/") && tok.includes(" ");
|
|
333
|
+
tok = "";
|
|
334
|
+
hadQuote = false;
|
|
335
|
+
return bad;
|
|
336
|
+
};
|
|
337
|
+
for (const ch of s) {
|
|
338
|
+
if (q) {
|
|
339
|
+
if (ch === q) q = null;
|
|
340
|
+
else tok += ch;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (ch === '"' || ch === "'") {
|
|
344
|
+
q = ch;
|
|
345
|
+
hadQuote = true;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (ch === " " || ch === TAB || ch === NL || ch === ";" || ch === "|" || ch === "&" || ch === "<" || ch === ">" || ch === "(" || ch === ")") {
|
|
349
|
+
if (flush()) return true;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
tok += ch;
|
|
353
|
+
}
|
|
354
|
+
if (q !== null) return true; // comilla sin cerrar: no verificable
|
|
355
|
+
return flush();
|
|
356
|
+
};
|
|
357
|
+
if (/\\$\\(|\`/.test(cmd) || quotedPathWithSpaces(cmd)) {
|
|
303
358
|
if (process.env.KJ_ALLOW_CROSS_LANE === "1") { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
304
359
|
else {
|
|
305
360
|
console.error("kj sentinel: sustitucion de comandos o ruta entrecomillada con espacios en un comando mutador — no verificable por el guard de carriles (MONO-0); usa valores/rutas LITERALES sin sustitucion (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
@@ -344,7 +399,7 @@ process.stdin.on("end", () => {
|
|
|
344
399
|
const actorRole = process.env.KJ_POLICY_ROLE || "coder";
|
|
345
400
|
const pres = spawnSync("kj", ["policy", "eval", "--strict", "--role", actorRole, "--tool", tool, "--input", JSON.stringify(input)], { cwd: ROOT, encoding: "utf8" });
|
|
346
401
|
if (pres.error || pres.status === null) {
|
|
347
|
-
console.error("kj sentinel: .karajan/policy.yml declara enforcement pero kj no es ejecutable — la policy no se puede evaluar; restaura kj en el PATH (npm i -g karajan-code) o retira la policy conscientemente.");
|
|
402
|
+
console.error("kj sentinel: .karajan/policy.yml declara enforcement pero kj no es ejecutable — la policy no se puede evaluar; restaura kj en el PATH (npm i -g @karajan-family/code, o el nombre legacy karajan-code) o retira la policy conscientemente.");
|
|
348
403
|
process.exit(2);
|
|
349
404
|
}
|
|
350
405
|
if (pres.status === 2) {
|
|
@@ -19,18 +19,18 @@ import { extraWorkflowsFor, mutationWorkflowFor, qualityWorkflowFor, policyWorkf
|
|
|
19
19
|
// npx del workflow de policy (jamás @latest en CI). Lectura LAZY: en el
|
|
20
20
|
// bundle SEA no hay package.json junto al módulo y una lectura eager a
|
|
21
21
|
// nivel de módulo tumbaba el binario entero al cargar (ENOENT /package.json).
|
|
22
|
-
let
|
|
23
|
-
function
|
|
24
|
-
if (
|
|
22
|
+
let _ownPkg;
|
|
23
|
+
function ownPkg() {
|
|
24
|
+
if (_ownPkg === undefined) {
|
|
25
25
|
try {
|
|
26
|
-
|
|
26
|
+
_ownPkg = JSON.parse(
|
|
27
27
|
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8"),
|
|
28
|
-
)
|
|
28
|
+
);
|
|
29
29
|
} catch {
|
|
30
|
-
|
|
30
|
+
_ownPkg = null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
return
|
|
33
|
+
return _ownPkg;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const BLOCK_VERSION = 1;
|
|
@@ -81,9 +81,9 @@ export function installWorkflows({
|
|
|
81
81
|
// PL-C: el tier C solo existe donde el proyecto DECLARA policy. Versión a
|
|
82
82
|
// pinear: la plumbeada por el CLI (SEA) o la propia; "latest" es último
|
|
83
83
|
// recurso inalcanzable en los caminos reales.
|
|
84
|
-
const pinned = kjVersion ??
|
|
84
|
+
const pinned = kjVersion ?? ownPkg()?.version ?? "latest";
|
|
85
85
|
const policy = existsSync(join(projectDir, ".karajan", "policy.yml"))
|
|
86
|
-
? { file: "kj-policy.yml", blockId: "wf-policy", body: policyWorkflowFor(pinned) }
|
|
86
|
+
? { file: "kj-policy.yml", blockId: "wf-policy", body: policyWorkflowFor(pinned, ownPkg()?.name ?? "karajan-code") }
|
|
87
87
|
: null;
|
|
88
88
|
const all = [...WORKFLOWS, ...(quality ? [quality] : []), ...extras, ...(mut ? [mut] : []), ...(policy ? [policy] : [])];
|
|
89
89
|
const results = [];
|
|
@@ -57,7 +57,7 @@ export const WORKFLOWS = [
|
|
|
57
57
|
// PL-C (KJC-TSK-0735) — tier C del ADR 0001: re-check merge-blocking en CI
|
|
58
58
|
// (cubre el hook local manipulado). Solo se siembra con policy declarada;
|
|
59
59
|
// npx PINEADO a la versión del kj que corrió harden — jamás @latest en CI.
|
|
60
|
-
export const policyWorkflowFor = (kjVersion) => [
|
|
60
|
+
export const policyWorkflowFor = (kjVersion, kjName = "karajan-code") => [
|
|
61
61
|
"name: Policy",
|
|
62
62
|
"on:",
|
|
63
63
|
" pull_request:",
|
|
@@ -77,7 +77,7 @@ export const policyWorkflowFor = (kjVersion) => [
|
|
|
77
77
|
" env:",
|
|
78
78
|
" BASE_REF: ${{ github.base_ref }}",
|
|
79
79
|
" run: |",
|
|
80
|
-
` if [ -f bin/kj.js ]; then npm ci --ignore-scripts && KJ="node bin/kj.js"; else KJ="npx --yes
|
|
80
|
+
` if [ -f bin/kj.js ]; then npm ci --ignore-scripts && KJ="node bin/kj.js"; else KJ="npx --yes ${kjName}@${kjVersion}"; fi`,
|
|
81
81
|
' $KJ policy check --range "origin/${BASE_REF}...HEAD" --strict',
|
|
82
82
|
"", // línea en blanco pre-marcador: prettier la exige tras un block scalar
|
|
83
83
|
].join("\n");
|
package/src/mutate/diff-scope.js
CHANGED
|
@@ -89,8 +89,12 @@ export function buildScope({ language, files, ranges }) {
|
|
|
89
89
|
* the injectable git runner (defaults to `runCommand`).
|
|
90
90
|
* @param {{since: string, language: string, toRef?: string, run?: Function}} params
|
|
91
91
|
*/
|
|
92
|
-
export async function getDiffScope({ since, language, toRef = "HEAD", run = runCommand }) {
|
|
93
|
-
|
|
92
|
+
export async function getDiffScope({ since, language, toRef = "HEAD", staged = false, run = runCommand }) {
|
|
93
|
+
// MUT-A (KJC-TSK-0716): staged=true scopea al ÍNDICE (git diff --cached) —
|
|
94
|
+
// lo que el review gate evalúa. since..HEAD con since=HEAD sería un diff
|
|
95
|
+
// VACÍO y el pre-gate no mutaría nada (catch de codex, agravado).
|
|
96
|
+
const args = staged ? ["diff", "--unified=0", "--cached"] : ["diff", "--unified=0", `${since}..${toRef}`];
|
|
97
|
+
const result = await run("git", args).catch(
|
|
94
98
|
() => null,
|
|
95
99
|
);
|
|
96
100
|
if (!result || result.exitCode !== 0) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation pre-gate (MUT-A, KJC-TSK-0716) — audita "tests prove behavior":
|
|
3
|
+
* asserts flojos dejan mutantes vivos con la suite verde. OPT-IN
|
|
4
|
+
* (method_gates.mutation: warn|block — cuesta minutos) y SOLO en --staged:
|
|
5
|
+
* supervivientes como advisory al reviewer; block cierra ANTES con la
|
|
6
|
+
* lista exacta. Indisponible = degrada avisando, nunca en silencio.
|
|
7
|
+
*/
|
|
8
|
+
import { runMutation } from "../mutate/runner.js";
|
|
9
|
+
import { getDiffScope } from "../mutate/diff-scope.js";
|
|
10
|
+
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
11
|
+
import { getMutationTool } from "../mutate/tool-registry.js";
|
|
12
|
+
|
|
13
|
+
export const formatSurvivor = (s) =>
|
|
14
|
+
`${s.file}:${s.line} (${s.mutator ?? s.status ?? "?"}) — mátalo con un assert que distinga el cambio`;
|
|
15
|
+
|
|
16
|
+
async function defaultMutate({ projectDir }) {
|
|
17
|
+
const { language } = await detectProjectStack(projectDir);
|
|
18
|
+
const tool = getMutationTool(language);
|
|
19
|
+
if (!tool.supported) throw new Error(`lenguaje no soportado (${language ?? "desconocido"}) — ${tool.reason}`);
|
|
20
|
+
const scope = await getDiffScope({ staged: true, language });
|
|
21
|
+
if (scope.empty) return { result: { score: 100, killed: 0, total: 0, survived: [] } };
|
|
22
|
+
const outcome = await runMutation({ binary: tool.binary, args: scope.args, cwd: projectDir });
|
|
23
|
+
if (!outcome.result) throw new Error(`la herramienta ${tool.id} no produjo un informe legible`);
|
|
24
|
+
return outcome;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns {Promise<{enabled: boolean, ok: boolean, available?: boolean, mode?: string, survived?: object[], score?: number, reason?: string}>}
|
|
29
|
+
*/
|
|
30
|
+
export async function runMutationPregate({ config = {}, projectDir = process.cwd(), deps = {} } = {}) {
|
|
31
|
+
const mode = config?.method_gates?.mutation;
|
|
32
|
+
if (mode !== "warn" && mode !== "block") return { enabled: false, ok: true };
|
|
33
|
+
const { mutateFn = defaultMutate } = deps;
|
|
34
|
+
let outcome;
|
|
35
|
+
try {
|
|
36
|
+
outcome = await mutateFn({ projectDir });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Sin herramienta no hay medición: se degrada DICIENDO qué red cayó.
|
|
39
|
+
return { enabled: true, ok: true, available: false, mode, reason: err.message };
|
|
40
|
+
}
|
|
41
|
+
// Payload malformado del runner = misma degradación que el error (catch
|
|
42
|
+
// de codex): jamás crashear el review gate por un informe roto.
|
|
43
|
+
if (!outcome?.result || !Array.isArray(outcome.result.survived ?? [])) {
|
|
44
|
+
return { enabled: true, ok: true, available: false, mode, reason: "el runner de mutación devolvió un informe ilegible" };
|
|
45
|
+
}
|
|
46
|
+
const { survived = [], score } = outcome.result;
|
|
47
|
+
return { enabled: true, available: true, mode, survived, score, ok: mode === "warn" || survived.length === 0 };
|
|
48
|
+
}
|
package/src/start/sweep.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// brief+tests; existing/legacy = + drift, harden advisory, rag + qmd status.
|
|
8
8
|
// Collaborators are injected (opts.deps) so it stays trivially testable.
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
+
import { readdir } from "node:fs/promises";
|
|
11
|
+
import { join, relative } from "node:path";
|
|
10
12
|
import { execFileSync } from "node:child_process";
|
|
11
13
|
|
|
12
14
|
import { collectAll } from "../onboarder/collectors/index.js";
|
|
@@ -23,11 +25,50 @@ const CODE_EXT = new Set([
|
|
|
23
25
|
".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".py", ".go",
|
|
24
26
|
".rs", ".java", ".php", ".rb", ".c", ".cc", ".cpp", ".cs",
|
|
25
27
|
]);
|
|
28
|
+
// KJC-BUG-0141 — infra projects (k8s/terraform/shell/Docker) ARE source code,
|
|
29
|
+
// and code nested deeper than the display tree's maxDepth must still count.
|
|
30
|
+
const INFRA_EXT = new Set([".yaml", ".yml", ".tf", ".hcl", ".sh"]);
|
|
31
|
+
const INFRA_NAMES = new Set(["Dockerfile", "Makefile", "Vagrantfile"]);
|
|
32
|
+
const SCAN_IGNORED = new Set([
|
|
33
|
+
"node_modules", "dist", "build", "out", "vendor", "coverage", "target", "__pycache__",
|
|
34
|
+
]);
|
|
35
|
+
const SCAN_MAX_DEPTH = 6;
|
|
26
36
|
const SCAFFOLD_MAX_FILES = 3;
|
|
27
37
|
const CI_MARKERS = [".github/workflows", ".gitlab-ci.yml"];
|
|
28
38
|
|
|
29
39
|
const safe = async (fn) => { try { return await fn(); } catch { return null; } };
|
|
30
40
|
|
|
41
|
+
const isTestPath = (p) => {
|
|
42
|
+
const posix = p.replaceAll("\\", "/");
|
|
43
|
+
return /\.(test|spec)\./.test(posix) || /(^|\/)(test|tests|__tests__)\//.test(posix);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Deep source counter with its own walk: the display tree stops at depth 2,
|
|
48
|
+
* which made backend/app/… projects look empty (KJC-BUG-0141).
|
|
49
|
+
* @returns {Promise<{code: number, infra: number}>}
|
|
50
|
+
*/
|
|
51
|
+
export async function deepSourceScan(projectDir, { maxDepth = SCAN_MAX_DEPTH } = {}) {
|
|
52
|
+
const counts = { code: 0, infra: 0 };
|
|
53
|
+
const walk = async (dir, depth) => {
|
|
54
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
55
|
+
for (const ent of entries) {
|
|
56
|
+
if (ent.name.startsWith(".") || SCAN_IGNORED.has(ent.name)) continue;
|
|
57
|
+
const full = join(dir, ent.name);
|
|
58
|
+
if (ent.isDirectory()) {
|
|
59
|
+
if (depth < maxDepth) await walk(full, depth + 1);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!ent.isFile() || isTestPath(relative(projectDir, full))) continue;
|
|
63
|
+
const ext = ent.name.slice(ent.name.lastIndexOf("."));
|
|
64
|
+
if (CODE_EXT.has(ext)) counts.code += 1;
|
|
65
|
+
else if (INFRA_EXT.has(ext) || INFRA_NAMES.has(ent.name)) counts.infra += 1;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
await walk(projectDir, 0);
|
|
69
|
+
return counts;
|
|
70
|
+
}
|
|
71
|
+
|
|
31
72
|
/** Count real source files in a tree bundle, skipping tests. */
|
|
32
73
|
export function countCodeFiles(tree = []) {
|
|
33
74
|
let n = 0;
|
|
@@ -35,8 +76,7 @@ export function countCodeFiles(tree = []) {
|
|
|
35
76
|
if (node.kind === "dir") n += countCodeFiles(node.children);
|
|
36
77
|
else if (node.kind === "file") {
|
|
37
78
|
const ext = node.path.slice(node.path.lastIndexOf("."));
|
|
38
|
-
|
|
39
|
-
if (CODE_EXT.has(ext) && !isTest) n += 1;
|
|
79
|
+
if (CODE_EXT.has(ext) && !isTestPath(node.path)) n += 1;
|
|
40
80
|
}
|
|
41
81
|
}
|
|
42
82
|
return n;
|
|
@@ -73,20 +113,22 @@ function readRagStatus(projectDir) {
|
|
|
73
113
|
*/
|
|
74
114
|
export async function runReadOnlySweep(projectDir, { declared = null, profile = "standard", deps = {} } = {}) {
|
|
75
115
|
const d = {
|
|
76
|
-
collectAll, detectTestFramework, checkHarden, compareHarden,
|
|
116
|
+
collectAll, detectTestFramework, checkHarden, compareHarden, sourceScan: deepSourceScan,
|
|
77
117
|
detectQmd, ragStatus: readRagStatus, gitAgeDays: lastCommitAgeDays, ...deps,
|
|
78
118
|
};
|
|
79
119
|
|
|
80
120
|
const brief = await safe(() => d.collectAll(projectDir));
|
|
81
121
|
const tests = await safe(() => d.detectTestFramework(projectDir));
|
|
122
|
+
const scan = await safe(() => d.sourceScan(projectDir));
|
|
82
123
|
|
|
83
124
|
const tree = brief?.tree ?? [];
|
|
84
125
|
const present = brief?.configs?.present ?? [];
|
|
85
|
-
const codeFiles = countCodeFiles(tree);
|
|
126
|
+
const codeFiles = scan ? scan.code + scan.infra : countCodeFiles(tree);
|
|
86
127
|
const commitCount = brief?.git?.commitCount ?? 0;
|
|
87
128
|
|
|
88
129
|
const signals = {
|
|
89
130
|
hasSourceCode: codeFiles > 0,
|
|
131
|
+
infraFiles: scan?.infra ?? 0,
|
|
90
132
|
scaffoldingOnly: codeFiles > 0 && codeFiles <= SCAFFOLD_MAX_FILES && commitCount <= 1,
|
|
91
133
|
hasTests: Boolean(tests?.hasTests),
|
|
92
134
|
hasCI: CI_MARKERS.some((m) => present.includes(m)),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isSea } from "node:sea";
|
|
@@ -8,7 +9,25 @@ const CACHE_FILE = "update-check.json";
|
|
|
8
9
|
// KJC-TSK-0690: 6h — kj can ship several releases a day; a session should
|
|
9
10
|
// not work a full day blind. KJ_NO_UPDATE_CHECK=1 opts out (CI).
|
|
10
11
|
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
11
|
-
|
|
12
|
+
// MIG-A (KJC-TSK-0751): el nombre npm se lee del PROPIO manifest — con el
|
|
13
|
+
// dual-publish del scope, el mismo código vive como karajan-code y como
|
|
14
|
+
// @karajan-family/code y cada instalación se auto-actualiza por SU nombre.
|
|
15
|
+
// Lazy + fallback: en el bundle SEA no hay package.json junto al módulo.
|
|
16
|
+
let _pkgName;
|
|
17
|
+
export function packageName() {
|
|
18
|
+
if (_pkgName) return _pkgName;
|
|
19
|
+
try {
|
|
20
|
+
_pkgName = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).name || "karajan-code";
|
|
21
|
+
} catch {
|
|
22
|
+
_pkgName = "karajan-code";
|
|
23
|
+
}
|
|
24
|
+
return _pkgName;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// URL del registro con el nombre CODIFICADO — el scoped @karajan-family/code
|
|
28
|
+
// lleva @ y / que rompen la ruta sin encode (catch de codex).
|
|
29
|
+
export const registryLatestUrl = (name = packageName()) =>
|
|
30
|
+
`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`;
|
|
12
31
|
const CHANGELOG_URL = "https://raw.githubusercontent.com/manufosela/karajan-code/main/CHANGELOG.md";
|
|
13
32
|
const HIGHLIGHT_MAX = 160;
|
|
14
33
|
|
|
@@ -55,10 +74,10 @@ export function updateInstruction({ channel, platform = process.platform }) {
|
|
|
55
74
|
: `Re-run the installer: curl -fsSL ${INSTALL_SH_URL} | sh`;
|
|
56
75
|
}
|
|
57
76
|
if (channel === "npm") {
|
|
58
|
-
return `Run: npm install -g ${
|
|
77
|
+
return `Run: npm install -g ${packageName()}`;
|
|
59
78
|
}
|
|
60
79
|
// Channel unknown — offer both paths, never silently pick a wrong one.
|
|
61
|
-
return `Update: npm install -g ${
|
|
80
|
+
return `Update: npm install -g ${packageName()} (or re-run the binary installer — see README)`;
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
/**
|
|
@@ -100,7 +119,7 @@ export async function checkForUpdate(currentVersion) {
|
|
|
100
119
|
// Fetch from npm (timeout 3s, don't block)
|
|
101
120
|
const controller = new AbortController();
|
|
102
121
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
103
|
-
const res = await fetch(
|
|
122
|
+
const res = await fetch(registryLatestUrl(), {
|
|
104
123
|
signal: controller.signal,
|
|
105
124
|
headers: { "Accept": "application/json" },
|
|
106
125
|
});
|
|
@@ -174,7 +193,7 @@ export async function performSelfUpdate({ currentVersion, exec, logger = console
|
|
|
174
193
|
try {
|
|
175
194
|
// Registry over HTTP, not `npm view` — standalone-binary machines may
|
|
176
195
|
// not have npm at all.
|
|
177
|
-
const res = await fetchFn(
|
|
196
|
+
const res = await fetchFn(registryLatestUrl(), {
|
|
178
197
|
headers: { Accept: "application/json" },
|
|
179
198
|
});
|
|
180
199
|
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
@@ -213,7 +232,7 @@ export async function performSelfUpdate({ currentVersion, exec, logger = console
|
|
|
213
232
|
}
|
|
214
233
|
} else {
|
|
215
234
|
// No stdio:inherit — capture and drop npm's warnings on the success path.
|
|
216
|
-
await run("npm", ["install", "-g", `${
|
|
235
|
+
await run("npm", ["install", "-g", `${packageName()}@latest`]);
|
|
217
236
|
}
|
|
218
237
|
} catch (err) {
|
|
219
238
|
if (err.stdout) logger.error(err.stdout);
|