codictator 0.1.3 → 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.
@@ -2,4 +2,4 @@ import type { Result } from "../kernel/index.ts";
2
2
  import type { CommandRequest } from "./shared.ts";
3
3
  export type Handler = (req: CommandRequest) => Promise<Result<number>>;
4
4
  export declare const COMMANDS: Readonly<Record<string, Handler>>;
5
- export declare const USAGE = "usage: codictator <init|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
5
+ export declare const USAGE = "usage: codictator <init|setup|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
@@ -9,11 +9,13 @@ import { handleReview } from "./proofs.js";
9
9
  import { handleTestCmd } from "./proofs-test.js";
10
10
  import { handleRemediate } from "./remediation.js";
11
11
  import { handleContext, handleReport } from "./report.js";
12
+ import { handleSetup } from "./setup.js";
12
13
  import { handleTask } from "./task-advance.js";
13
14
  import { handleExplain, handleReplay } from "./transparency.js";
14
15
  import { handleWaive } from "./waivers.js";
15
16
  export const COMMANDS = {
16
17
  init: handleInit,
18
+ setup: handleSetup,
17
19
  "install-hooks": handleInstallHooks,
18
20
  regen: handleRegen,
19
21
  uninstall: handleUninstall,
@@ -36,4 +38,4 @@ export const COMMANDS = {
36
38
  adapter: handleAdapter,
37
39
  serve: handleServe,
38
40
  };
39
- export const USAGE = "usage: codictator <init|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
41
+ export const USAGE = "usage: codictator <init|setup|install-hooks|regen|uninstall|verify|status|explain|replay|report|context|hook|task|test|review|waive|baseline|deps|remediate|ci|generate|pack|adapter|serve> [--json] [--cwd <path>] [--quiet]";
@@ -1,5 +1,7 @@
1
1
  import type { Result } from "../kernel/index.ts";
2
2
  import type { CommandRequest } from "./shared.ts";
3
3
  export declare function handleRegen(req: CommandRequest): Promise<Result<number>>;
4
+ export declare function writeHookFiles(root: string): Promise<Result<true>>;
5
+ export declare function configureHooksPath(root: string, req: CommandRequest): Promise<void>;
4
6
  export declare function handleInstallHooks(req: CommandRequest): Promise<Result<number>>;
5
7
  export declare function handleUninstall(req: CommandRequest): Promise<Result<number>>;
@@ -3,12 +3,13 @@
3
3
  // install-hooks writes the hooks and points core.hooksPath at them;
4
4
  // uninstall removes generated files with the CI warning.
5
5
  import { parseArgs } from "node:util";
6
- import { renderCustomRegenFragment } from "../features/adapters/index.js";
6
+ import { renderCustomRegenFragment, withCodictatorOwner, } from "../features/adapters/index.js";
7
7
  import { ENGINE_VERSION } from "../features/config/index.js";
8
8
  import { EXPECTED_HOOKS_PATH, HOOK_NAMES, hookInstallPlan, renderHookScript, } from "../features/integrity/index.js";
9
9
  import { err, ok, resolveRepoRoot } from "../kernel/index.js";
10
+ import { initPreamble } from "./init-plan.js";
10
11
  import { guardrailPlan, writePlan } from "./plan.js";
11
- import { checkHuman, commandEnv, fail, loadRepoConfig, portsFor, printHuman, printJson, readStateText, usageError, } from "./shared.js";
12
+ import { fail, loadRepoConfig, portsFor, printHuman, printJson, readStateText, usageError, } from "./shared.js";
12
13
  function repoNameOf(root) {
13
14
  return root.replaceAll("\\", "/").split("/").pop() ?? "repo";
14
15
  }
@@ -67,8 +68,12 @@ export async function handleRegen(req) {
67
68
  tunables: configured.tunables,
68
69
  architecture: configured.architecture,
69
70
  tools: configured.tools,
71
+ }, req.engineVersion ?? ENGINE_VERSION, root.value);
72
+ // SPEC §4.14.1: custom fragments keep the historical codictator owner.
73
+ const customOwned = withCodictatorOwner(customFiles.value);
74
+ const wrote = await writePlan(root.value, {
75
+ files: [...plan.files, ...customOwned],
70
76
  }, req.engineVersion ?? ENGINE_VERSION);
71
- const wrote = await writePlan(root.value, { files: [...plan.files, ...customFiles.value] }, req.engineVersion ?? ENGINE_VERSION);
72
77
  if (!wrote.ok) {
73
78
  return fail("CDX-E903", wrote.error.message, wrote.error.remediation, req.json);
74
79
  }
@@ -89,7 +94,7 @@ export async function handleRegen(req) {
89
94
  function encode(text) {
90
95
  return new TextEncoder().encode(text);
91
96
  }
92
- async function writeHookFiles(root) {
97
+ export async function writeHookFiles(root) {
93
98
  const ports = await portsFor(root);
94
99
  if (!ports.ok) {
95
100
  return err(ports.error);
@@ -107,7 +112,7 @@ async function writeHookFiles(root) {
107
112
  }
108
113
  return ok(true);
109
114
  }
110
- async function configureHooksPath(root, req) {
115
+ export async function configureHooksPath(root, req) {
111
116
  const ports = await portsFor(root);
112
117
  if (!ports.ok) {
113
118
  return;
@@ -169,21 +174,18 @@ export async function handleUninstall(req) {
169
174
  if (rejected !== undefined) {
170
175
  return rejected;
171
176
  }
172
- const human = checkHuman(commandEnv(), []);
173
- if (!human.ok) {
174
- return fail("CDX-E107", human.error.message, human.error.remediation, req.json);
177
+ const preamble = await initPreamble(req);
178
+ if (typeof preamble !== "string") {
179
+ return preamble;
175
180
  }
176
- const root = await resolveRepoRoot(req.cwd);
177
- if (!root.ok) {
178
- return fail("CDX-E703", root.error.message, root.error.remediation, req.json);
179
- }
180
- await removeGuardrails(root.value);
181
+ const root = preamble;
182
+ await removeGuardrails(root);
181
183
  const note = "CI will fail until its workflow and required check are removed too.";
182
184
  if (req.json) {
183
- printJson({ uninstalled: true, root: root.value, note });
185
+ printJson({ uninstalled: true, root, note });
184
186
  }
185
187
  else {
186
- printHuman(`uninstalled guardrails from ${root.value}`, req.quiet);
188
+ printHuman(`uninstalled guardrails from ${root}`, req.quiet);
187
189
  printHuman(note, req.quiet);
188
190
  }
189
191
  return ok(0);
@@ -20,4 +20,11 @@ export declare function emitInitResult(req: CommandRequest, root: string, writte
20
20
  path: string;
21
21
  }>, baselineWritten: boolean): Result<number>;
22
22
  export declare function initPreamble(req: CommandRequest): Promise<string | Result<number>>;
23
+ export interface InitSequence {
24
+ readonly written: ReadonlyArray<{
25
+ path: string;
26
+ }>;
27
+ readonly baseline: boolean;
28
+ }
29
+ export declare function runInitSequence(root: string, req: CommandRequest, flags: InitFlags, planned: ReadonlyArray<string>, engineVersion: string): Promise<Result<InitSequence>>;
23
30
  export declare function buildGuardrailPlan(root: string, flags: InitFlags, planned: ReadonlyArray<string>, engineVersion?: string): Promise<ReturnType<typeof guardrailPlan>>;
@@ -4,8 +4,8 @@
4
4
  import { CODICTATOR_MODULES } from "../features/arch/index.js";
5
5
  import { ENGINE_VERSION } from "../features/config/index.js";
6
6
  import { captureBaseline } from "../features/remediation/index.js";
7
- import { ok, parseJson, resolveRepoRoot, stableStringify, } from "../kernel/index.js";
8
- import { buildHelperTool, guardrailPlan } from "./plan.js";
7
+ import { err, ok, parseJson, resolveRepoRoot, stableStringify, } from "../kernel/index.js";
8
+ import { buildHelperTool, guardrailPlan, writePlan } from "./plan.js";
9
9
  import { checkHuman, commandEnv, fail, loadRepoConfig, printHuman, printJson, readStateText, writeStateText, } from "./shared.js";
10
10
  // SPEC §4.19.2: a helper tool proposal names the helper role, the binary
11
11
  // and the exact pin from the human's registry lookup: role=command@pin.
@@ -114,6 +114,25 @@ export async function initPreamble(req) {
114
114
  }
115
115
  return root.value;
116
116
  }
117
+ // SPEC §4.13.1: the shared write-then-guard tail of the init path: plan,
118
+ // write, fresh-config guard, baseline guard. init and setup run the same
119
+ // tail; one helper owns it.
120
+ export async function runInitSequence(root, req, flags, planned, engineVersion) {
121
+ const plan = await buildGuardrailPlan(root, flags, planned, engineVersion);
122
+ const wrote = await writePlan(root, plan, engineVersion);
123
+ if (!wrote.ok) {
124
+ return fail("CDX-E903", wrote.error.message, wrote.error.remediation, req.json);
125
+ }
126
+ const fresh = await checkFreshConfig(root, req);
127
+ if (!fresh.ok) {
128
+ return err(fresh.error);
129
+ }
130
+ const baseline = await ensureBaseline(root, req);
131
+ if (!baseline.ok) {
132
+ return fail(baseline.error.code, baseline.error.message, baseline.error.remediation, req.json);
133
+ }
134
+ return ok({ written: wrote.value, baseline: baseline.value });
135
+ }
117
136
  // The plan assembly handleInit writes: read-only inputs in, GuardrailPlan
118
137
  // out. Writing stays on the command so the plan builder never touches
119
138
  // the disk.
@@ -1,3 +1,5 @@
1
1
  import type { Result } from "../kernel/index.ts";
2
+ import { type InitFlags } from "./init-plan.ts";
2
3
  import type { CommandRequest } from "./shared.ts";
4
+ export declare function parseInitFlags(args: ReadonlyArray<string>): InitFlags | undefined;
3
5
  export declare function handleInit(req: CommandRequest): Promise<Result<number>>;
package/dist/cli/init.js CHANGED
@@ -4,9 +4,9 @@
4
4
  import { parseArgs } from "node:util";
5
5
  import { ENGINE_VERSION } from "../features/config/index.js";
6
6
  import { isBuiltinPackId } from "../features/packs/index.js";
7
- import { buildGuardrailPlan, checkFreshConfig, defaultHarnesses, emitInitResult, ensureBaseline, initPreamble, reportExisting, } from "./init-plan.js";
8
- import { KNOWN_HELPER_ROLES, writePlan } from "./plan.js";
9
- import { fail, readStateText, usageError } from "./shared.js";
7
+ import { defaultHarnesses, emitInitResult, initPreamble, reportExisting, runInitSequence, } from "./init-plan.js";
8
+ import { KNOWN_HELPER_ROLES } from "./plan.js";
9
+ import { readStateText, usageError } from "./shared.js";
10
10
  const KNOWN_PROFILES = new Set([
11
11
  "app",
12
12
  "library",
@@ -55,7 +55,7 @@ function narrowInitTools(specs) {
55
55
  }
56
56
  return tools;
57
57
  }
58
- function parseInitFlags(args) {
58
+ export function parseInitFlags(args) {
59
59
  try {
60
60
  const parsed = parseArgs({
61
61
  args: [...args],
@@ -105,18 +105,9 @@ export async function handleInit(req) {
105
105
  if (existing !== undefined && !flags.repair) {
106
106
  return reportExisting(req, root);
107
107
  }
108
- const plan = await buildGuardrailPlan(root, flags, planned, engineVersion);
109
- const wrote = await writePlan(root, plan, engineVersion);
110
- if (!wrote.ok) {
111
- return fail("CDX-E903", wrote.error.message, wrote.error.remediation, req.json);
108
+ const sequenced = await runInitSequence(root, req, flags, planned, engineVersion);
109
+ if (!sequenced.ok) {
110
+ return sequenced;
112
111
  }
113
- const fresh = await checkFreshConfig(root, req);
114
- if (!fresh.ok) {
115
- return fresh;
116
- }
117
- const baseline = await ensureBaseline(root, req);
118
- if (!baseline.ok) {
119
- return fail(baseline.error.code, baseline.error.message, baseline.error.remediation, req.json);
120
- }
121
- return emitInitResult(req, root, wrote.value, baseline.value);
112
+ return emitInitResult(req, root, sequenced.value.written, sequenced.value.baseline);
122
113
  }
@@ -10,6 +10,8 @@ export interface GuardrailPlan {
10
10
  readonly files: ReadonlyArray<{
11
11
  path: string;
12
12
  content: string;
13
+ owner: string;
14
+ mode: "file";
13
15
  }>;
14
16
  }
15
17
  export interface PlanExtras {
@@ -18,7 +20,7 @@ export interface PlanExtras {
18
20
  readonly architecture?: unknown;
19
21
  readonly tools?: unknown;
20
22
  }
21
- export declare function guardrailPlan(harnesses: ReadonlyArray<string>, profile: string, repoName: string, existingAgents: string | undefined, extras?: PlanExtras, engineVersion?: string): GuardrailPlan;
23
+ export declare function guardrailPlan(harnesses: ReadonlyArray<string>, profile: string, repoName: string, existingAgents: string | undefined, extras?: PlanExtras, engineVersion?: string, root?: string): GuardrailPlan;
22
24
  export declare function writePlan(root: string, plan: GuardrailPlan, engineVersion?: string): Promise<Result<Array<{
23
25
  path: string;
24
26
  hash: string;
package/dist/cli/plan.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // SPEC §4.14.1 + §4.14.5: guardrail plan builders. The file list (config,
2
2
  // steering block, CI workflow, hooks, launcher) and the manifest write are
3
3
  // shared by init and regen, so regeneration stays byte-identical.
4
+ import { renderHarnessFiles } from "../features/adapters/index.js";
4
5
  import { renderWorkflow } from "../features/ci/index.js";
5
6
  import { ENGINE_VERSION } from "../features/config/index.js";
6
7
  import { buildManifest, EXPECTED_HOOKS_PATH, HOOK_NAMES, hookInstallPlan, renderHookScript, } from "../features/integrity/index.js";
@@ -65,30 +66,65 @@ function withManagedBlock(existing) {
65
66
  const after = end + MANAGED_END.length;
66
67
  return `${existing.slice(0, start)}${block}${existing.slice(after)}`;
67
68
  }
68
- export function guardrailPlan(harnesses, profile, repoName, existingAgents, extras = {}, engineVersion = ENGINE_VERSION) {
69
+ export function guardrailPlan(harnesses, profile, repoName, existingAgents, extras = {}, engineVersion = ENGINE_VERSION, root = ".") {
69
70
  const files = [];
70
71
  files.push({
71
72
  path: ".codictator/config.json",
72
73
  content: configText(harnesses, profile, extras, engineVersion),
74
+ owner: "codictator",
75
+ mode: "file",
76
+ });
77
+ files.push({
78
+ path: "AGENTS.md",
79
+ content: withManagedBlock(existingAgents),
80
+ owner: "codictator",
81
+ mode: "file",
73
82
  });
74
- files.push({ path: "AGENTS.md", content: withManagedBlock(existingAgents) });
75
83
  const workflow = renderWorkflow(repoName);
76
84
  files.push({
77
85
  path: ".github/workflows/codictator.yml",
78
86
  content: workflow.text ?? "",
87
+ owner: "codictator",
88
+ mode: "file",
79
89
  });
80
90
  for (const hook of HOOK_NAMES) {
81
91
  files.push({
82
92
  path: `${EXPECTED_HOOKS_PATH}/${hook}`,
83
93
  content: renderHookScript(hook),
94
+ owner: "codictator",
95
+ mode: "file",
84
96
  });
85
97
  }
86
98
  const plan = hookInstallPlan({});
87
99
  files.push({
88
100
  path: `${EXPECTED_HOOKS_PATH}/.gitattributes`,
89
101
  content: plan.gitattributes,
102
+ owner: "codictator",
103
+ mode: "file",
104
+ });
105
+ files.push({
106
+ path: ".codictator/bin/run.mjs",
107
+ content: LAUNCHER_TEXT,
108
+ owner: "codictator",
109
+ mode: "file",
90
110
  });
91
- files.push({ path: ".codictator/bin/run.mjs", content: LAUNCHER_TEXT });
111
+ // SPEC §4.13.1 step 2 + §4.5: every configured built-in harness owns
112
+ // its rendered files; universal renders nothing, custom:* stays with
113
+ // renderCustomHarnessFiles. Renders ignore the input, so the "." default
114
+ // keeps the init path working unchanged. SPEC §4.6.2 + core/guardrail-manifest:
115
+ // one row per path, first occurrence wins: base files go first so the
116
+ // plan's managed AGENTS.md beats any harness-rendered AGENTS.md, then
117
+ // harness files follow in sorted harness-id order, skipping paths
118
+ // already emitted (cursor/gemini-cli AGENTS.md, codex-cli/muse-code
119
+ // .agents/skills overlap), so writePlan writes each path once.
120
+ const seen = new Set(files.map((entry) => entry.path));
121
+ const sortedHarnesses = [...new Set(harnesses)].sort();
122
+ for (const artifact of renderHarnessFiles(sortedHarnesses, root)) {
123
+ if (!seen.has(artifact.path)) {
124
+ seen.add(artifact.path);
125
+ files.push({ ...artifact, mode: "file" });
126
+ }
127
+ }
92
128
  return { files };
93
129
  }
94
130
  export async function writePlan(root, plan, engineVersion = ENGINE_VERSION) {
@@ -107,12 +143,15 @@ export async function writePlan(root, plan, engineVersion = ENGINE_VERSION) {
107
143
  hash: sha256Text(normalizeText(file.content)),
108
144
  });
109
145
  }
110
- const manifest = buildManifest(engineVersion, written.map((entry) => ({
111
- path: entry.path,
112
- owner: "codictator",
113
- mode: "file",
114
- hash: entry.hash,
115
- })));
146
+ const manifest = buildManifest(engineVersion, written.map((entry, index) => {
147
+ const planned = plan.files[index];
148
+ return {
149
+ path: entry.path,
150
+ owner: planned === undefined ? "codictator" : planned.owner,
151
+ mode: planned === undefined ? "file" : planned.mode,
152
+ hash: entry.hash,
153
+ };
154
+ }));
116
155
  const saved = await ports.value.fs.write(".codictator/manifest.json", encode(`${stableStringify(manifest)}\n`));
117
156
  if (!saved.ok) {
118
157
  return err(saved.error);
@@ -6,6 +6,7 @@ import { exitFor } from "./shared.js";
6
6
  // these before invoking anything.
7
7
  const ALWAYS_HUMAN = new Set([
8
8
  "init",
9
+ "setup",
9
10
  "uninstall",
10
11
  "waive",
11
12
  ]);
@@ -0,0 +1,7 @@
1
+ import type { Result } from "../kernel/index.ts";
2
+ import type { SetupStep } from "./setup.ts";
3
+ import type { CommandRequest } from "./shared.ts";
4
+ export declare function ensureGitignoreEntries(root: string): Promise<Result<{
5
+ added: Array<string>;
6
+ }>>;
7
+ export declare function setupGitignoreStep(req: CommandRequest, root: string, steps: Array<SetupStep>): Promise<Result<number>>;
@@ -0,0 +1,41 @@
1
+ import { err, ok } from "../kernel/index.js";
2
+ import { fail, printHuman, readStateText, writeStateText } from "./shared.js";
3
+ const SETUP_GITIGNORE_ENTRIES = [
4
+ ".codictator/state/",
5
+ ".codictator/cache/",
6
+ ".codictator/tool/node_modules/",
7
+ ];
8
+ export async function ensureGitignoreEntries(root) {
9
+ const existing = await readStateText(root, ".gitignore");
10
+ const present = new Set((existing ?? "").split("\n").map((line) => line.trim()));
11
+ const missing = SETUP_GITIGNORE_ENTRIES.filter((entry) => !present.has(entry));
12
+ if (missing.length === 0) {
13
+ return ok({ added: [] });
14
+ }
15
+ const base = existing === undefined || existing === ""
16
+ ? ""
17
+ : existing.endsWith("\n")
18
+ ? existing
19
+ : `${existing}\n`;
20
+ const saved = await writeStateText(root, ".gitignore", `${base}${missing.join("\n")}\n`);
21
+ if (!saved.ok) {
22
+ return err(saved.error);
23
+ }
24
+ return ok({ added: [...missing] });
25
+ }
26
+ // SPEC §4.19.6 step 3: guarantee the manifest-owned .gitignore entries.
27
+ export async function setupGitignoreStep(req, root, steps) {
28
+ const ensured = await ensureGitignoreEntries(root);
29
+ if (!ensured.ok) {
30
+ return fail("CDX-E903", ensured.error.message, ensured.error.remediation, req.json);
31
+ }
32
+ steps.push({
33
+ step: "gitignore",
34
+ ok: true,
35
+ detail: { added: ensured.value.added },
36
+ });
37
+ if (ensured.value.added.length > 0) {
38
+ printHuman(`setup: added ${String(ensured.value.added.length)} .gitignore entries`, req.quiet);
39
+ }
40
+ return ok(0);
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { InitFlags } from "./init-plan.ts";
2
+ export declare const SETUP_EXTENSION_ID = "codictator.codictator-vscode";
3
+ export declare const SETUP_EXTENSION_INSTALL = "code --install-extension codictator.codictator-vscode";
4
+ export declare const SETUP_PACK_REPAIR = "npx codictator setup --repair --pack typescript";
5
+ export declare const SETUP_PACK_FRESH = "npx codictator setup --pack typescript";
6
+ export declare function setupHandoffLines(emptyPacks: boolean): Array<string>;
7
+ export declare function hasEffectivePacks(root: string, flags: InitFlags): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ // SPEC §4.19.6 step 8: pack-less handoff text. Split from setup.ts
2
+ // (orchestration) by responsibility so setup.ts stays below the
3
+ // core/file-size 300 cap; the effective pack list mirrors the plan data.
4
+ import { parseJson } from "../kernel/index.js";
5
+ import { readStateText } from "./shared.js";
6
+ // The extension empty-state lives on the vscode branch (§4.20.1);
7
+ // handoff here only prints the install command.
8
+ export const SETUP_EXTENSION_ID = "codictator.codictator-vscode";
9
+ export const SETUP_EXTENSION_INSTALL = "code --install-extension codictator.codictator-vscode";
10
+ export const SETUP_PACK_REPAIR = "npx codictator setup --repair --pack typescript";
11
+ export const SETUP_PACK_FRESH = "npx codictator setup --pack typescript";
12
+ export function setupHandoffLines(emptyPacks) {
13
+ if (!emptyPacks) {
14
+ return [
15
+ "setup complete: open this folder in VS Code to see the Codictator views",
16
+ `extension: ${SETUP_EXTENSION_INSTALL}`,
17
+ ];
18
+ }
19
+ return [
20
+ "Setup complete — core rules only. No language packs installed.",
21
+ "To add a language pack, run exactly one line and press Enter:",
22
+ ` ${SETUP_PACK_REPAIR}`,
23
+ "To install the VS Code extension, run:",
24
+ ` ${SETUP_EXTENSION_INSTALL}`,
25
+ `Fresh adoption omits --repair: ${SETUP_PACK_FRESH}`,
26
+ ];
27
+ }
28
+ // SPEC §4.19.6 step 8: the effective pack list mirrors the plan data:
29
+ // explicit --pack flags win, otherwise the written config owns it.
30
+ export async function hasEffectivePacks(root, flags) {
31
+ if (flags.packs.length > 0) {
32
+ return true;
33
+ }
34
+ const text = await readStateText(root, ".codictator/config.json");
35
+ if (text === undefined) {
36
+ return false;
37
+ }
38
+ const parsed = parseJson(text);
39
+ if (!parsed.ok || typeof parsed.value !== "object" || parsed.value === null) {
40
+ return false;
41
+ }
42
+ const packs = parsed.value.packs;
43
+ return Array.isArray(packs) && packs.length > 0;
44
+ }
@@ -0,0 +1,11 @@
1
+ import type { Result } from "../kernel/index.ts";
2
+ import type { SetupStep } from "./setup.ts";
3
+ import type { CommandRequest } from "./shared.ts";
4
+ export interface SetupInstallTarget {
5
+ readonly dir: string;
6
+ readonly lockRel: string;
7
+ readonly dirRel: string;
8
+ }
9
+ export declare function setupInstallLayout(req: CommandRequest, root: string, steps: Array<SetupStep>): Promise<Result<SetupInstallTarget>>;
10
+ export declare function setupNpmArgv(platform?: string, execPath?: string): ReadonlyArray<string>;
11
+ export declare function setupInstallHere(req: CommandRequest, root: string, target: SetupInstallTarget, steps: Array<SetupStep>): Promise<Result<number>>;
@@ -0,0 +1,84 @@
1
+ // SPEC §4.19.4 + §4.19.6 steps 4-5: the engine install layout and the
2
+ // clean install here. npm repos install at the root; other repos get
3
+ // .codictator/tool/package.json pinning the engine exactly, then npm ci
4
+ // runs in the directory owning the lockfile. A registry or network outage
5
+ // maps to CDX-E401 (exit 5); any other install failure is a missing
6
+ // engine, CDX-E003 (exit 6).
7
+ import path from "node:path";
8
+ import { ENGINE_VERSION } from "../features/config/index.js";
9
+ import { err, ok, stableStringify } from "../kernel/index.js";
10
+ import { fail, portsFor, printHuman, readStateText, writeStateText, } from "./shared.js";
11
+ export async function setupInstallLayout(req, root, steps) {
12
+ const rootManifest = await readStateText(root, "package.json");
13
+ if (rootManifest !== undefined) {
14
+ steps.push({ step: "install", ok: true, detail: { dir: "." } });
15
+ return ok({ dir: root, lockRel: "package-lock.json", dirRel: "." });
16
+ }
17
+ const toolManifest = await readStateText(root, ".codictator/tool/package.json");
18
+ if (toolManifest === undefined) {
19
+ const engineVersion = req.engineVersion ?? ENGINE_VERSION;
20
+ const saved = await writeStateText(root, ".codictator/tool/package.json", `${stableStringify({
21
+ name: "codictator-tool",
22
+ private: true,
23
+ dependencies: { codictator: engineVersion },
24
+ })}\n`);
25
+ if (!saved.ok) {
26
+ return err(saved.error);
27
+ }
28
+ }
29
+ steps.push({
30
+ step: "install",
31
+ ok: true,
32
+ detail: { dir: ".codictator/tool" },
33
+ });
34
+ return ok({
35
+ dir: `${root}/.codictator/tool`,
36
+ lockRel: ".codictator/tool/package-lock.json",
37
+ dirRel: ".codictator/tool",
38
+ });
39
+ }
40
+ function looksLikeRegistryOutage(output) {
41
+ return /ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|registry/i.test(output);
42
+ }
43
+ // SPEC §4.1.1: the child spawns with shell:false always, so on win32 the
44
+ // bare `npm` shim never resolves: it is an npm.cmd batch file, which the
45
+ // shell-less spawn cannot execute (ENOENT without, EINVAL with, the
46
+ // extension). The fix stays shell-free by exec'ing the real entry beside
47
+ // the running node: node <node-dir>/node_modules/npm/bin/npm-cli.js ci.
48
+ // POSIX keeps `npm` from PATH. Pure in platform and execPath so tests pin
49
+ // the exact proc argv without spawning anything.
50
+ export function setupNpmArgv(platform = process.platform, execPath = process.execPath) {
51
+ if (platform !== "win32") {
52
+ return ["npm", "ci"];
53
+ }
54
+ return [
55
+ execPath,
56
+ path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js"),
57
+ "ci",
58
+ ];
59
+ }
60
+ export async function setupInstallHere(req, root, target, steps) {
61
+ const lock = await readStateText(root, target.lockRel);
62
+ if (lock === undefined) {
63
+ return fail("CDX-E003", `no lockfile at ${target.lockRel}, so npm ci cannot run`, `Run npm install in ${target.dirRel} (needs network access), then rerun codictator setup.`, req.json);
64
+ }
65
+ const ports = await portsFor(root);
66
+ if (!ports.ok) {
67
+ return fail("CDX-E903", ports.error.message, ports.error.remediation, req.json);
68
+ }
69
+ const ran = await ports.value.proc.run(setupNpmArgv(), {
70
+ cwd: target.dir,
71
+ env: {},
72
+ timeoutMs: 300000,
73
+ maxOutputBytes: 4 * 1024 * 1024,
74
+ });
75
+ if (!ran.timedOut && !ran.truncated && ran.code === 0) {
76
+ steps.push({ step: "install-here", ok: true });
77
+ printHuman(`setup: npm ci passed in ${target.dirRel}`, req.quiet);
78
+ return ok(0);
79
+ }
80
+ if (ran.timedOut || looksLikeRegistryOutage(`${ran.stdout}\n${ran.stderr}`)) {
81
+ return fail("CDX-E401", "registry or network unavailable during install", "Retry with network access, then rerun codictator setup.", req.json);
82
+ }
83
+ return fail("CDX-E003", `engine install failed in ${target.dirRel} (npm ci exit ${String(ran.code)})`, `Run npm ci in ${target.dirRel}, then rerun codictator setup.`, req.json);
84
+ }
@@ -0,0 +1,9 @@
1
+ import type { Result } from "../kernel/index.ts";
2
+ import type { CommandRequest } from "./shared.ts";
3
+ export declare const SETUP_USAGE = "usage: codictator setup [--harness <id> ...] [--profile <app|library|cli|service>] [--pack <id> ...] [--tool <role>=<command>@<pin> ...] [--repair]";
4
+ export interface SetupStep {
5
+ readonly step: string;
6
+ readonly ok: boolean;
7
+ readonly detail?: Record<string, unknown>;
8
+ }
9
+ export declare function handleSetup(req: CommandRequest): Promise<Result<number>>;