litcodex-ai 0.3.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/bin/litcodex.js +12 -0
  4. package/dist/cli.d.ts +23 -0
  5. package/dist/cli.js +183 -0
  6. package/dist/config-migration/backup.d.ts +2 -0
  7. package/dist/config-migration/backup.js +42 -0
  8. package/dist/config-migration/catalog.d.ts +22 -0
  9. package/dist/config-migration/catalog.js +99 -0
  10. package/dist/config-migration/cli.d.ts +14 -0
  11. package/dist/config-migration/cli.js +85 -0
  12. package/dist/config-migration/config-paths.d.ts +4 -0
  13. package/dist/config-migration/config-paths.js +64 -0
  14. package/dist/config-migration/errors.d.ts +11 -0
  15. package/dist/config-migration/errors.js +28 -0
  16. package/dist/config-migration/index.d.ts +44 -0
  17. package/dist/config-migration/index.js +210 -0
  18. package/dist/config-migration/multi-agent-v2-guard.d.ts +2 -0
  19. package/dist/config-migration/multi-agent-v2-guard.js +106 -0
  20. package/dist/config-migration/root-settings.d.ts +6 -0
  21. package/dist/config-migration/root-settings.js +104 -0
  22. package/dist/config-migration/state.d.ts +16 -0
  23. package/dist/config-migration/state.js +40 -0
  24. package/dist/config-migration/toml-shape.d.ts +8 -0
  25. package/dist/config-migration/toml-shape.js +107 -0
  26. package/dist/install/codex.d.ts +34 -0
  27. package/dist/install/codex.js +94 -0
  28. package/dist/install/doctor.d.ts +12 -0
  29. package/dist/install/doctor.js +83 -0
  30. package/dist/install/errors.d.ts +19 -0
  31. package/dist/install/errors.js +43 -0
  32. package/dist/install/execute.d.ts +39 -0
  33. package/dist/install/execute.js +193 -0
  34. package/dist/install/index.d.ts +19 -0
  35. package/dist/install/index.js +193 -0
  36. package/dist/install/marketplace.d.ts +5 -0
  37. package/dist/install/marketplace.js +10 -0
  38. package/dist/install/plan.d.ts +3 -0
  39. package/dist/install/plan.js +54 -0
  40. package/dist/install/render-plan.d.ts +3 -0
  41. package/dist/install/render-plan.js +10 -0
  42. package/dist/install/types.d.ts +45 -0
  43. package/dist/install/types.js +5 -0
  44. package/model-catalog.json +31 -0
  45. package/node_modules/@litcodex/lit-loop/CHANGELOG.md +19 -0
  46. package/node_modules/@litcodex/lit-loop/LICENSE +21 -0
  47. package/node_modules/@litcodex/lit-loop/NOTICE +8 -0
  48. package/node_modules/@litcodex/lit-loop/README.md +37 -0
  49. package/node_modules/@litcodex/lit-loop/agents/litcodex-explorer.toml +75 -0
  50. package/node_modules/@litcodex/lit-loop/agents/litcodex-librarian.toml +98 -0
  51. package/node_modules/@litcodex/lit-loop/agents/litcodex-litwork-reviewer.toml +21 -0
  52. package/node_modules/@litcodex/lit-loop/agents/litcodex-metis.toml +64 -0
  53. package/node_modules/@litcodex/lit-loop/agents/litcodex-momus.toml +68 -0
  54. package/node_modules/@litcodex/lit-loop/agents/litcodex-plan.toml +163 -0
  55. package/node_modules/@litcodex/lit-loop/directive.md +85 -0
  56. package/node_modules/@litcodex/lit-loop/directives/lit-plan.md +286 -0
  57. package/node_modules/@litcodex/lit-loop/directives/litgoal.md +103 -0
  58. package/node_modules/@litcodex/lit-loop/directives/litwork.md +363 -0
  59. package/node_modules/@litcodex/lit-loop/dist/_scaffold.d.ts +1 -0
  60. package/node_modules/@litcodex/lit-loop/dist/_scaffold.js +3 -0
  61. package/node_modules/@litcodex/lit-loop/dist/cli.d.ts +6 -0
  62. package/node_modules/@litcodex/lit-loop/dist/cli.js +44 -0
  63. package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.d.ts +18 -0
  64. package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.js +94 -0
  65. package/node_modules/@litcodex/lit-loop/dist/codex-hook.d.ts +38 -0
  66. package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +126 -0
  67. package/node_modules/@litcodex/lit-loop/dist/directive.d.ts +35 -0
  68. package/node_modules/@litcodex/lit-loop/dist/directive.js +80 -0
  69. package/node_modules/@litcodex/lit-loop/dist/goal-status.d.ts +12 -0
  70. package/node_modules/@litcodex/lit-loop/dist/goal-status.js +25 -0
  71. package/node_modules/@litcodex/lit-loop/dist/guards.d.ts +73 -0
  72. package/node_modules/@litcodex/lit-loop/dist/guards.js +215 -0
  73. package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +17 -0
  74. package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +94 -0
  75. package/node_modules/@litcodex/lit-loop/dist/loop-cli.d.ts +19 -0
  76. package/node_modules/@litcodex/lit-loop/dist/loop-cli.js +106 -0
  77. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-render.d.ts +7 -0
  78. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-render.js +39 -0
  79. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-types.d.ts +52 -0
  80. package/node_modules/@litcodex/lit-loop/dist/loop-doctor-types.js +7 -0
  81. package/node_modules/@litcodex/lit-loop/dist/loop-doctor.d.ts +21 -0
  82. package/node_modules/@litcodex/lit-loop/dist/loop-doctor.js +283 -0
  83. package/node_modules/@litcodex/lit-loop/dist/loop-errors.d.ts +15 -0
  84. package/node_modules/@litcodex/lit-loop/dist/loop-errors.js +43 -0
  85. package/node_modules/@litcodex/lit-loop/dist/loop-handlers.d.ts +18 -0
  86. package/node_modules/@litcodex/lit-loop/dist/loop-handlers.js +311 -0
  87. package/node_modules/@litcodex/lit-loop/dist/loop-model.d.ts +51 -0
  88. package/node_modules/@litcodex/lit-loop/dist/loop-model.js +165 -0
  89. package/node_modules/@litcodex/lit-loop/dist/loop-stdout.d.ts +6 -0
  90. package/node_modules/@litcodex/lit-loop/dist/loop-stdout.js +11 -0
  91. package/node_modules/@litcodex/lit-loop/dist/loop-types.d.ts +26 -0
  92. package/node_modules/@litcodex/lit-loop/dist/loop-types.js +8 -0
  93. package/node_modules/@litcodex/lit-loop/dist/markers.d.ts +9 -0
  94. package/node_modules/@litcodex/lit-loop/dist/markers.js +14 -0
  95. package/node_modules/@litcodex/lit-loop/dist/modes.d.ts +15 -0
  96. package/node_modules/@litcodex/lit-loop/dist/modes.js +56 -0
  97. package/node_modules/@litcodex/lit-loop/dist/state-paths.d.ts +41 -0
  98. package/node_modules/@litcodex/lit-loop/dist/state-paths.js +111 -0
  99. package/node_modules/@litcodex/lit-loop/dist/state-store.d.ts +39 -0
  100. package/node_modules/@litcodex/lit-loop/dist/state-store.js +419 -0
  101. package/node_modules/@litcodex/lit-loop/dist/state-types.d.ts +90 -0
  102. package/node_modules/@litcodex/lit-loop/dist/state-types.js +61 -0
  103. package/node_modules/@litcodex/lit-loop/dist/trigger.d.ts +54 -0
  104. package/node_modules/@litcodex/lit-loop/dist/trigger.js +75 -0
  105. package/node_modules/@litcodex/lit-loop/package.json +27 -0
  106. package/package.json +30 -0
@@ -0,0 +1,34 @@
1
+ /** The minimal fs surface the installer needs (read-only discovery; injectable for tests). */
2
+ export interface ReadonlyFsLike {
3
+ existsSync(path: string): boolean;
4
+ readFileSync(path: string, encoding: "utf8"): string;
5
+ }
6
+ /** The spawn shape used by the executor + probes (argv array; never a shell string). */
7
+ export type SpawnLike = (cmd: string, args: readonly string[], opts: {
8
+ stdio: "inherit" | "pipe";
9
+ }) => {
10
+ status: number | null;
11
+ error?: Error;
12
+ stdout?: string;
13
+ stderr?: string;
14
+ };
15
+ /** Resolve the absolute Codex home: trimmed `CODEX_HOME`, else `~/.codex`. */
16
+ export declare function resolveCodexHome(env: NodeJS.ProcessEnv): string;
17
+ /**
18
+ * Locate an absolute path to a `codex` file, or null. Pure: only `fs.existsSync` + path joins,
19
+ * NO spawn (a present-but-broken binary is "found"; execution failure is a later step error).
20
+ * Order (addendum A2.1): CODEX_BIN override → CODEX_HOME/bin → PATH scan.
21
+ */
22
+ export declare function findCodexBinary(env: NodeJS.ProcessEnv, fs: ReadonlyFsLike): string | null;
23
+ /** True if `codex plugin marketplace list` stdout names the LitCodex marketplace. Read-only. */
24
+ export declare function probeMarketplaceRegistered(codexBin: string, spawn: SpawnLike): boolean;
25
+ /**
26
+ * True iff `codex plugin list` shows `litcodex@litcodex` with an INSTALLED status. Read-only.
27
+ *
28
+ * VERIFY-LIVE (codex-cli 0.139.x): once the marketplace is added, the plugin ref appears in the list
29
+ * BEFORE it is installed, as `litcodex@litcodex not installed …`. Matching the ref string alone is
30
+ * a false positive that makes `install` skip the real `codex plugin add` (plugin never installs, hook
31
+ * never fires) and `doctor` report a phantom install. So parse the row's STATUS column: the ref must
32
+ * be on a line that says `installed` and NOT `not installed`.
33
+ */
34
+ export declare function probePluginInstalled(codexBin: string, spawn: SpawnLike): boolean;
@@ -0,0 +1,94 @@
1
+ // M12 / T17 — Codex discovery + read-only probes (S12 §codex.ts; addendum A2/A5).
2
+ //
3
+ // SELF-CONTAINED: this module owns the ONE place that ever spawns a child process, and that child
4
+ // is always `codex` (the host runtime) — never an exec-wrapper, never a harness/forwarder package. Discovery
5
+ // is pure (fs.existsSync + path joins, no spawn). The probes spawn `codex … list` read-only so
6
+ // idempotency is M12's property (probe-before-spawn), independent of Codex's re-add exit codes.
7
+ import { homedir } from "node:os";
8
+ import { delimiter, join, resolve } from "node:path";
9
+ import { LITCODEX_MARKETPLACE, LITCODEX_PLUGIN_REF } from "./marketplace.js";
10
+ /** Resolve the absolute Codex home: trimmed `CODEX_HOME`, else `~/.codex`. */
11
+ export function resolveCodexHome(env) {
12
+ const raw = env["CODEX_HOME"]?.trim();
13
+ if (raw !== undefined && raw.length > 0) {
14
+ return resolve(raw);
15
+ }
16
+ const home = env["HOME"]?.trim() || homedir();
17
+ return resolve(home, ".codex");
18
+ }
19
+ function platformBin() {
20
+ return process.platform === "win32" ? "codex.exe" : "codex";
21
+ }
22
+ /**
23
+ * Locate an absolute path to a `codex` file, or null. Pure: only `fs.existsSync` + path joins,
24
+ * NO spawn (a present-but-broken binary is "found"; execution failure is a later step error).
25
+ * Order (addendum A2.1): CODEX_BIN override → CODEX_HOME/bin → PATH scan.
26
+ */
27
+ export function findCodexBinary(env, fs) {
28
+ const override = env["CODEX_BIN"]?.trim();
29
+ if (override !== undefined && override.length > 0) {
30
+ const abs = resolve(override);
31
+ // An explicit override that points nowhere is a hard "not found" — do NOT fall through.
32
+ return fs.existsSync(abs) ? abs : null;
33
+ }
34
+ const homeBin = join(resolveCodexHome(env), "bin", platformBin());
35
+ if (fs.existsSync(homeBin)) {
36
+ return homeBin;
37
+ }
38
+ const pathVar = env["PATH"] ?? "";
39
+ for (const dir of pathVar.split(delimiter)) {
40
+ if (dir.length === 0) {
41
+ continue;
42
+ }
43
+ const candidate = join(dir, platformBin());
44
+ if (fs.existsSync(candidate)) {
45
+ return candidate;
46
+ }
47
+ if (process.platform === "win32") {
48
+ for (const ext of (env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";")) {
49
+ const winCandidate = join(dir, `codex${ext.toLowerCase()}`);
50
+ if (fs.existsSync(winCandidate)) {
51
+ return winCandidate;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ /** True if `codex plugin marketplace list` stdout names the LitCodex marketplace. Read-only. */
59
+ export function probeMarketplaceRegistered(codexBin, spawn) {
60
+ const res = spawn(codexBin, ["plugin", "marketplace", "list"], { stdio: "pipe" });
61
+ if (res.error || res.status !== 0) {
62
+ // "cannot determine" → "not present" (the add step is the authoritative source of truth).
63
+ return false;
64
+ }
65
+ return (res.stdout ?? "").includes(LITCODEX_MARKETPLACE);
66
+ }
67
+ /**
68
+ * True iff `codex plugin list` shows `litcodex@litcodex` with an INSTALLED status. Read-only.
69
+ *
70
+ * VERIFY-LIVE (codex-cli 0.139.x): once the marketplace is added, the plugin ref appears in the list
71
+ * BEFORE it is installed, as `litcodex@litcodex not installed …`. Matching the ref string alone is
72
+ * a false positive that makes `install` skip the real `codex plugin add` (plugin never installs, hook
73
+ * never fires) and `doctor` report a phantom install. So parse the row's STATUS column: the ref must
74
+ * be on a line that says `installed` and NOT `not installed`.
75
+ */
76
+ export function probePluginInstalled(codexBin, spawn) {
77
+ const res = spawn(codexBin, ["plugin", "list"], { stdio: "pipe" });
78
+ if (res.error || res.status !== 0) {
79
+ return false;
80
+ }
81
+ const out = res.stdout ?? "";
82
+ for (const line of out.split(/\r?\n/)) {
83
+ if (!line.includes(LITCODEX_PLUGIN_REF)) {
84
+ continue;
85
+ }
86
+ if (/\bnot installed\b/i.test(line)) {
87
+ continue; // available-but-not-installed row → not a real install
88
+ }
89
+ if (/\binstalled\b/i.test(line)) {
90
+ return true;
91
+ }
92
+ }
93
+ return false;
94
+ }
@@ -0,0 +1,12 @@
1
+ import { type ReadonlyFsLike, type SpawnLike } from "./codex.js";
2
+ import type { DoctorReport } from "./types.js";
3
+ export interface DoctorDeps {
4
+ readonly spawn: SpawnLike;
5
+ readonly fs: ReadonlyFsLike;
6
+ readonly env: NodeJS.ProcessEnv;
7
+ readonly repoRoot: string;
8
+ /** Hook verification (defaults to the M14 resolvers over repoRoot). */
9
+ readonly verifyHook?: (repoRoot: string) => void;
10
+ }
11
+ export declare function runDoctor(deps: DoctorDeps): DoctorReport;
12
+ export declare function renderDoctorText(report: DoctorReport): string;
@@ -0,0 +1,83 @@
1
+ // M12 / T17 — doctor health report (S12 §doctor; A2 §6.6 — installer-side install health).
2
+ //
3
+ // Read-only: spawns ONLY the `codex … list` probes (never a mutating command). Reports whether the
4
+ // marketplace is registered, the plugin installed, the hook wired, the config managed, and whether
5
+ // a `codex` binary was even found. `ok` is true only when every check passes.
6
+ import { join } from "node:path";
7
+ import { findCodexBinary, probeMarketplaceRegistered, probePluginInstalled, resolveCodexHome, } from "./codex.js";
8
+ /** Sentinel file that proves agents-install has run at least once. */
9
+ const AGENTS_SENTINEL = "litcodex-litwork-reviewer.toml";
10
+ export function runDoctor(deps) {
11
+ const codexBin = findCodexBinary(deps.env, deps.fs);
12
+ const codexBinaryFound = codexBin !== null;
13
+ const issues = [];
14
+ if (!codexBinaryFound) {
15
+ issues.push("Codex CLI not found on PATH or in CODEX_HOME.");
16
+ return {
17
+ ok: false,
18
+ marketplaceRegistered: false,
19
+ pluginInstalled: false,
20
+ hooksWired: false,
21
+ configManaged: false,
22
+ codexBinaryFound: false,
23
+ agentsInstalled: false,
24
+ issues,
25
+ };
26
+ }
27
+ const marketplaceRegistered = probeMarketplaceRegistered(codexBin, deps.spawn);
28
+ if (!marketplaceRegistered) {
29
+ issues.push("LitCodex marketplace is not registered. Run `litcodex install`.");
30
+ }
31
+ const pluginInstalled = probePluginInstalled(codexBin, deps.spawn);
32
+ if (!pluginInstalled) {
33
+ issues.push("LitCodex plugin is not installed. Run `litcodex install`.");
34
+ }
35
+ let hooksWired = true;
36
+ try {
37
+ (deps.verifyHook ?? (() => undefined))(deps.repoRoot);
38
+ }
39
+ catch {
40
+ hooksWired = false;
41
+ issues.push("Bundled UserPromptSubmit hook payload is missing or malformed.");
42
+ }
43
+ const configManaged = true; // M13 owns config detail; install-side doctor reports presence only.
44
+ const codexHome = resolveCodexHome(deps.env);
45
+ const agentsInstalled = deps.fs.existsSync(join(codexHome, "agents", AGENTS_SENTINEL));
46
+ if (!agentsInstalled) {
47
+ issues.push("LitCodex litwork agent roles are not installed. Run `litcodex install`.");
48
+ }
49
+ return {
50
+ ok: issues.length === 0,
51
+ marketplaceRegistered,
52
+ pluginInstalled,
53
+ hooksWired,
54
+ configManaged,
55
+ codexBinaryFound,
56
+ agentsInstalled,
57
+ issues,
58
+ };
59
+ }
60
+ export function renderDoctorText(report) {
61
+ const lines = [
62
+ "litcodex doctor",
63
+ ` codex binary found: ${yn(report.codexBinaryFound)}`,
64
+ ` marketplace registered: ${yn(report.marketplaceRegistered)}`,
65
+ ` plugin installed: ${yn(report.pluginInstalled)}`,
66
+ ` hook wired: ${yn(report.hooksWired)}`,
67
+ ` config managed: ${yn(report.configManaged)}`,
68
+ ` agents installed: ${yn(report.agentsInstalled)}`,
69
+ ];
70
+ if (report.issues.length > 0) {
71
+ lines.push("Issues:");
72
+ for (const issue of report.issues) {
73
+ lines.push(` - ${issue}`);
74
+ }
75
+ }
76
+ else {
77
+ lines.push("All checks passed.");
78
+ }
79
+ return lines.join("\n");
80
+ }
81
+ function yn(value) {
82
+ return value ? "yes" : "no";
83
+ }
@@ -0,0 +1,19 @@
1
+ export declare const INSTALL_ERROR_CODES: readonly ["LITCODEX_INSTALL_CODEX_NOT_FOUND", "LITCODEX_INSTALL_MARKETPLACE_ADD_FAILED", "LITCODEX_INSTALL_PLUGIN_ADD_FAILED", "LITCODEX_INSTALL_HOOKS_MISSING", "LITCODEX_INSTALL_AGENTS_MISSING", "LITCODEX_INSTALL_CONFIG_WRITE_FAILED", "LITCODEX_INSTALL_UNKNOWN_COMMAND", "LITCODEX_INSTALL_BAD_FLAG"];
2
+ export type InstallErrorCode = (typeof INSTALL_ERROR_CODES)[number];
3
+ export declare class InstallError extends Error {
4
+ readonly name = "InstallError";
5
+ readonly code: InstallErrorCode;
6
+ readonly details: Readonly<Record<string, unknown>>;
7
+ constructor(code: InstallErrorCode, message: string, details?: Readonly<Record<string, unknown>>);
8
+ }
9
+ /** Process exit code for an installer error (S12 §Exit-code table). */
10
+ export declare function exitCodeForInstallError(code: InstallErrorCode): number;
11
+ /** Machine-readable error envelope (written to stderr alongside a human prefix line). */
12
+ export declare function toErrorJson(err: unknown): {
13
+ ok: false;
14
+ error: {
15
+ code: string;
16
+ message: string;
17
+ details: Record<string, unknown>;
18
+ };
19
+ };
@@ -0,0 +1,43 @@
1
+ // M12 / T17 — typed installer error + JSON renderer (S12 §errors.ts).
2
+ export const INSTALL_ERROR_CODES = [
3
+ "LITCODEX_INSTALL_CODEX_NOT_FOUND",
4
+ "LITCODEX_INSTALL_MARKETPLACE_ADD_FAILED",
5
+ "LITCODEX_INSTALL_PLUGIN_ADD_FAILED",
6
+ "LITCODEX_INSTALL_HOOKS_MISSING",
7
+ "LITCODEX_INSTALL_AGENTS_MISSING",
8
+ "LITCODEX_INSTALL_CONFIG_WRITE_FAILED",
9
+ "LITCODEX_INSTALL_UNKNOWN_COMMAND",
10
+ "LITCODEX_INSTALL_BAD_FLAG",
11
+ ];
12
+ export class InstallError extends Error {
13
+ constructor(code, message, details = {}) {
14
+ super(message);
15
+ this.name = "InstallError";
16
+ this.code = code;
17
+ this.details = details;
18
+ }
19
+ }
20
+ /** Process exit code for an installer error (S12 §Exit-code table). */
21
+ export function exitCodeForInstallError(code) {
22
+ switch (code) {
23
+ case "LITCODEX_INSTALL_UNKNOWN_COMMAND":
24
+ case "LITCODEX_INSTALL_BAD_FLAG":
25
+ return 1;
26
+ case "LITCODEX_INSTALL_CODEX_NOT_FOUND":
27
+ return 2;
28
+ case "LITCODEX_INSTALL_MARKETPLACE_ADD_FAILED":
29
+ case "LITCODEX_INSTALL_PLUGIN_ADD_FAILED":
30
+ case "LITCODEX_INSTALL_HOOKS_MISSING":
31
+ case "LITCODEX_INSTALL_AGENTS_MISSING":
32
+ case "LITCODEX_INSTALL_CONFIG_WRITE_FAILED":
33
+ return 3;
34
+ }
35
+ }
36
+ /** Machine-readable error envelope (written to stderr alongside a human prefix line). */
37
+ export function toErrorJson(err) {
38
+ if (err instanceof InstallError) {
39
+ return { ok: false, error: { code: err.code, message: err.message, details: { ...err.details } } };
40
+ }
41
+ const message = err instanceof Error ? err.message : String(err);
42
+ return { ok: false, error: { code: "LITCODEX_INSTALL_UNKNOWN_COMMAND", message, details: {} } };
43
+ }
@@ -0,0 +1,39 @@
1
+ import type { MigrateOptions, MigrateResult } from "../config-migration/index.js";
2
+ import { type ReadonlyFsLike, type SpawnLike } from "./codex.js";
3
+ import type { InstallResult, InstallStep } from "./types.js";
4
+ /** Minimal writable-fs surface for agents-install (injectable for tests). */
5
+ export interface WritableFsLike {
6
+ existsSync(p: string): boolean;
7
+ mkdirSync(p: string, o: {
8
+ recursive: true;
9
+ }): void;
10
+ readdirSync(p: string): string[];
11
+ readFileSync(p: string, e: "utf8"): string;
12
+ writeFileSync(p: string, d: string): void;
13
+ }
14
+ /** Injectable side-effect surface (every default is overridable for tests). */
15
+ export interface ExecuteDeps {
16
+ readonly spawn: SpawnLike;
17
+ readonly fs: ReadonlyFsLike;
18
+ readonly env: NodeJS.ProcessEnv;
19
+ readonly now: () => number;
20
+ /** Absolute repo root used to resolve the bundled marketplace metadata for `hooks-register`. */
21
+ readonly repoRoot: string;
22
+ /** In-process config migration (defaults to the real M13 engine). */
23
+ readonly migrateConfig: (opts: MigrateOptions) => Promise<MigrateResult>;
24
+ /** Hook verification (defaults to the M14 resolvers over `repoRoot`). */
25
+ readonly verifyHook?: (repoRoot: string) => void;
26
+ /** Whether `--force` re-runs skippable steps despite a present probe. */
27
+ readonly force?: boolean;
28
+ /** Resolved Codex home (passed to `migrateCodexConfig` as `cwd`). */
29
+ readonly codexHome?: string;
30
+ /** Writable fs surface for agents-install (defaults to node:fs). */
31
+ readonly writeFs?: WritableFsLike;
32
+ /** Absolute path to the bundled agents source dir (defaults to resolved @litcodex/lit-loop/agents). */
33
+ readonly agentsSourceDir?: string;
34
+ }
35
+ /**
36
+ * Run the plan in order. Returns a `Promise<InstallResult>` (config-update is async). Throws a
37
+ * typed `InstallError` on the first non-recoverable step failure; the dry-run path never calls this.
38
+ */
39
+ export declare function executeInstallPlan(steps: readonly InstallStep[], deps: ExecuteDeps): Promise<InstallResult>;
@@ -0,0 +1,193 @@
1
+ // M12 / T17 — install execution (S12 §execute.ts; addendum A3/A4/A5).
2
+ //
3
+ // SELF-CONTAINED + IDEMPOTENT. The ONLY child process ever spawned is `codex`. Each skippable
4
+ // registration step is PROBE-GATED before spawn (addendum A5.1) so a re-install is a guaranteed
5
+ // no-op regardless of Codex's own re-add exit codes. `config-update` is in-process: it calls
6
+ // `migrateCodexConfig({ env, cwd: codexHome, mode: "install" })` (A3 C4) and maps every
7
+ // `CodexConfigMigrationError` → `LITCODEX_INSTALL_CONFIG_WRITE_FAILED` (exit 3). `hooks-register`
8
+ // is an in-process verification probe (A4) over the bundled metadata — never a config write.
9
+ // `agents-install` copies bundled litwork agent .toml files into <codexHome>/agents/ with
10
+ // backup-safe + idempotent semantics.
11
+ import * as nodeFs from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import { dirname, isAbsolute, join } from "node:path";
14
+ import { CodexConfigMigrationError } from "../config-migration/errors.js";
15
+ import { findCodexBinary, probeMarketplaceRegistered, probePluginInstalled, } from "./codex.js";
16
+ import { InstallError } from "./errors.js";
17
+ import { LITCODEX_MARKETPLACE, LITCODEX_PLUGIN } from "./marketplace.js";
18
+ /**
19
+ * Run the plan in order. Returns a `Promise<InstallResult>` (config-update is async). Throws a
20
+ * typed `InstallError` on the first non-recoverable step failure; the dry-run path never calls this.
21
+ */
22
+ export async function executeInstallPlan(steps, deps) {
23
+ // Pre-flight: discover `codex` (pure, no spawn). Null → hard "not found", exit 2, zero mutation.
24
+ const codexBin = findCodexBinary(deps.env, deps.fs);
25
+ if (codexBin === null) {
26
+ throw new InstallError("LITCODEX_INSTALL_CODEX_NOT_FOUND", "Codex CLI not found on PATH or in CODEX_HOME.", {
27
+ source: deps.env["CODEX_BIN"]?.trim() ? "CODEX_BIN" : "PATH",
28
+ });
29
+ }
30
+ const results = [];
31
+ for (const step of steps) {
32
+ const result = await runStep(step, codexBin, deps);
33
+ results.push(result);
34
+ }
35
+ return {
36
+ ok: results.every((r) => r.status !== "failed"),
37
+ steps: results,
38
+ codexHome: deps.codexHome ?? "",
39
+ };
40
+ }
41
+ async function runStep(step, codexBin, deps) {
42
+ switch (step.kind) {
43
+ case "marketplace-add":
44
+ return runRegistration(step, codexBin, deps, "marketplace");
45
+ case "plugin-add":
46
+ return runRegistration(step, codexBin, deps, "plugin");
47
+ case "hooks-register":
48
+ return runHooksRegister(step, deps);
49
+ case "agents-install":
50
+ return runAgentsInstall(step, deps);
51
+ case "config-update":
52
+ return runConfigUpdate(step, deps);
53
+ case "verify":
54
+ return ok(step, "doctor passed");
55
+ }
56
+ }
57
+ /** Copy bundled litwork agent .toml files into <codexHome>/agents/ (backup-safe, idempotent). */
58
+ function runAgentsInstall(step, deps) {
59
+ const wfs = deps.writeFs ?? nodeFs;
60
+ // Resolve source dir: injected override or resolve via require from repoRoot.
61
+ const sourceDir = deps.agentsSourceDir ??
62
+ join(dirname(createRequire(`${deps.repoRoot}/package.json`).resolve("@litcodex/lit-loop/package.json")), "agents");
63
+ // Validate source dir has .toml files.
64
+ if (!wfs.existsSync(sourceDir)) {
65
+ throw new InstallError("LITCODEX_INSTALL_AGENTS_MISSING", "Bundled litwork agent roles are missing.", {
66
+ sourceDir,
67
+ });
68
+ }
69
+ const allFiles = wfs.readdirSync(sourceDir);
70
+ const tomlFiles = allFiles.filter((f) => f.endsWith(".toml")).sort();
71
+ if (tomlFiles.length === 0) {
72
+ throw new InstallError("LITCODEX_INSTALL_AGENTS_MISSING", "Bundled litwork agent roles are missing.", {
73
+ sourceDir,
74
+ });
75
+ }
76
+ // Target dir: <codexHome>/agents (mkdir -p). codexHome must be an absolute path.
77
+ const codexHome = deps.codexHome ?? "";
78
+ if (!codexHome || !isAbsolute(codexHome)) {
79
+ throw new InstallError("LITCODEX_INSTALL_AGENTS_MISSING", "Codex home is not set; cannot install agents.", {
80
+ codexHome,
81
+ });
82
+ }
83
+ const targetDir = join(codexHome, "agents");
84
+ wfs.mkdirSync(targetDir, { recursive: true });
85
+ let installed = 0;
86
+ let unchanged = 0;
87
+ // Format timestamp as YYYYMMDDTHHMMSSZ from deps.now().
88
+ const ts = new Date(deps.now())
89
+ .toISOString()
90
+ .replace(/[-:]/g, "")
91
+ .replace(/\.\d{3}Z$/, "Z");
92
+ const pid = process.pid;
93
+ for (const file of tomlFiles) {
94
+ const sourcePath = join(sourceDir, file);
95
+ const targetPath = join(targetDir, file);
96
+ const sourceContent = wfs.readFileSync(sourcePath, "utf8");
97
+ if (wfs.existsSync(targetPath)) {
98
+ const existingContent = wfs.readFileSync(targetPath, "utf8");
99
+ if (existingContent === sourceContent) {
100
+ unchanged++;
101
+ continue;
102
+ }
103
+ // Back up the differing file before overwriting.
104
+ const backupPath = join(targetDir, `${file}.litcodex-bak.${ts}.${pid}`);
105
+ wfs.writeFileSync(backupPath, existingContent);
106
+ }
107
+ wfs.writeFileSync(targetPath, sourceContent);
108
+ installed++;
109
+ }
110
+ return mark(step, "ok", `${installed} agent role(s) installed, ${unchanged} unchanged`);
111
+ }
112
+ /** A probe-gated spawn registration step (marketplace-add / plugin-add). */
113
+ function runRegistration(step, codexBin, deps, target) {
114
+ const present = target === "marketplace"
115
+ ? probeMarketplaceRegistered(codexBin, deps.spawn)
116
+ : probePluginInstalled(codexBin, deps.spawn);
117
+ if (present && deps.force !== true) {
118
+ return { kind: step.kind, status: "skipped", detail: `${describe(target)} already registered` };
119
+ }
120
+ if (step.command === null) {
121
+ return ok(step, `${describe(target)} registered`);
122
+ }
123
+ // step.command[0] is the literal "codex"; we replace it with the resolved binary path.
124
+ const args = step.command.slice(1);
125
+ const res = deps.spawn(codexBin, args, { stdio: "inherit" });
126
+ if (res.error || res.status === null || res.status !== 0) {
127
+ throw new InstallError(target === "marketplace" ? "LITCODEX_INSTALL_MARKETPLACE_ADD_FAILED" : "LITCODEX_INSTALL_PLUGIN_ADD_FAILED", `codex ${args.join(" ")} failed`, { status: res.status, signalKilled: res.status === null });
128
+ }
129
+ return ok(step, `${describe(target)} registered`);
130
+ }
131
+ function describe(target) {
132
+ return target === "marketplace"
133
+ ? `${LITCODEX_MARKETPLACE} marketplace`
134
+ : `${LITCODEX_PLUGIN}@${LITCODEX_MARKETPLACE} plugin`;
135
+ }
136
+ /** Verify the host wired the bundled UserPromptSubmit hook; fail loudly if the payload is broken. */
137
+ function runHooksRegister(step, deps) {
138
+ try {
139
+ const verify = deps.verifyHook ?? defaultVerifyHook;
140
+ verify(deps.repoRoot);
141
+ }
142
+ catch (err) {
143
+ throw new InstallError("LITCODEX_INSTALL_HOOKS_MISSING", "Bundled lit-loop UserPromptSubmit hook payload is missing or malformed.", { cause: err instanceof Error ? err.message : String(err) });
144
+ }
145
+ return { kind: step.kind, status: "skipped", detail: "UserPromptSubmit hook wired (manifest-driven)" };
146
+ }
147
+ /** Default hook verification: load + resolve the M14 metadata over repoRoot (dev/test only). */
148
+ function defaultVerifyHook(repoRoot) {
149
+ // Resolved lazily so the installer never hard-depends on the un-bundled @litcodex/plugin tree.
150
+ // (In a published tarball this path is exercised via the injected verifyHook; here it backs the
151
+ // in-repo run.) The require is wrapped so a missing resolver surfaces as a typed HOOKS_MISSING.
152
+ const req = createRequireForRepo(repoRoot);
153
+ const meta = req("@litcodex/plugin/dist/metadata.js");
154
+ const metadata = meta.loadMarketplaceMetadata(repoRoot);
155
+ const handler = meta.resolveUserPromptSubmitHook(metadata);
156
+ if (!handler.command.includes("hook user-prompt-submit") || !handler.command.includes("cli.js")) {
157
+ throw new Error("hook command literal drift");
158
+ }
159
+ }
160
+ function createRequireForRepo(repoRoot) {
161
+ return createRequire(`${repoRoot}/package.json`);
162
+ }
163
+ /** In-process config migration via the M13 engine; collapses all M13 codes → CONFIG_WRITE_FAILED. */
164
+ async function runConfigUpdate(step, deps) {
165
+ let res;
166
+ const migrateOpts = { env: deps.env, mode: "install" };
167
+ if (deps.codexHome !== undefined) {
168
+ migrateOpts.cwd = deps.codexHome;
169
+ }
170
+ try {
171
+ res = await deps.migrateConfig(migrateOpts);
172
+ }
173
+ catch (e) {
174
+ if (e instanceof CodexConfigMigrationError) {
175
+ throw new InstallError("LITCODEX_INSTALL_CONFIG_WRITE_FAILED", e.message, {
176
+ cause: e.code,
177
+ configPath: e.configPath,
178
+ ...e.details,
179
+ });
180
+ }
181
+ throw e;
182
+ }
183
+ const detail = res.changed.length > 0
184
+ ? `config.toml managed keys updated (${res.changed.length} file(s))`
185
+ : "config.toml already current";
186
+ return mark(step, "ok", detail);
187
+ }
188
+ function ok(step, detail) {
189
+ return mark(step, "ok", detail);
190
+ }
191
+ function mark(step, status, detail) {
192
+ return { kind: step.kind, status, detail };
193
+ }
@@ -0,0 +1,19 @@
1
+ export { renderDoctorText, runDoctor } from "./doctor.js";
2
+ export { INSTALL_ERROR_CODES, InstallError, toErrorJson } from "./errors.js";
3
+ export { executeInstallPlan } from "./execute.js";
4
+ export { buildInstallPlan } from "./plan.js";
5
+ export { INSTALL_PLAN_HEADER, renderInstallPlan } from "./render-plan.js";
6
+ export type { DoctorReport, InstallOptions, InstallResult, InstallStep } from "./types.js";
7
+ /** Process IO sink (injectable for tests; defaults to the real process streams). */
8
+ export interface RouteIo {
9
+ readonly stdout: NodeJS.WritableStream;
10
+ readonly stderr: NodeJS.WritableStream;
11
+ readonly env: NodeJS.ProcessEnv;
12
+ readonly repoRoot: string;
13
+ }
14
+ /** `litcodex install [flags]` — render plan (dry-run) or execute against `codex`. */
15
+ export declare function runInstallCli(args: readonly string[], io?: RouteIo): Promise<number>;
16
+ /** `litcodex doctor [--json]` — read-only health report. Exit 0 when ok, 4 otherwise. */
17
+ export declare function runDoctorCli(args: readonly string[], io?: RouteIo): number;
18
+ /** `litcodex uninstall [--json]` — remove the registered plugin via `codex plugin remove`. */
19
+ export declare function runUninstallCli(args: readonly string[], io?: RouteIo): number;