litcodex-ai 0.3.0 → 0.3.2
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/dist/cli.js +12 -1
- package/dist/install/execute.d.ts +5 -1
- package/dist/install/execute.js +2 -0
- package/dist/install/index.js +61 -11
- package/dist/postinstall.d.ts +1 -0
- package/dist/postinstall.js +35 -0
- package/dist/ui.d.ts +69 -0
- package/dist/ui.js +264 -0
- package/node_modules/@litcodex/lit-loop/package.json +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { runDoctorCli, runInstallCli, runUninstallCli } from "./install/index.js
|
|
|
20
20
|
import { LITCODEX_REPO_URL } from "./install/marketplace.js";
|
|
21
21
|
import { buildInstallPlan } from "./install/plan.js";
|
|
22
22
|
import { renderInstallPlan } from "./install/render-plan.js";
|
|
23
|
+
import { renderBanner, shouldDecorate } from "./ui.js";
|
|
23
24
|
/** Error code emitted when a subcommand is not in the routing table. */
|
|
24
25
|
export const UNKNOWN_COMMAND_CODE = "LITCODEX_INSTALL_UNKNOWN_COMMAND";
|
|
25
26
|
/** Router usage code (EX_USAGE) for an unknown `config` sub-subcommand. */
|
|
@@ -169,7 +170,17 @@ export async function runCli(argv) {
|
|
|
169
170
|
return runUserPromptSubmitHookCli(process.stdin, process.stdout, process.stderr);
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
+
// Reliable post-install greeting: the bare `litcodex` and `litcodex --help` surfaces are where
|
|
174
|
+
// users actually meet the CLI (npm hides postinstall output by default), so decorate them with
|
|
175
|
+
// the fire title banner on an interactive TTY. `--version` and error paths stay plain.
|
|
176
|
+
const result = dispatch(argv);
|
|
177
|
+
if (!isVersion &&
|
|
178
|
+
result.exitCode === 0 &&
|
|
179
|
+
result.stdout.length > 0 &&
|
|
180
|
+
shouldDecorate({ isTty: Boolean(process.stdout.isTTY), env: process.env })) {
|
|
181
|
+
process.stdout.write(renderBanner({ version: manifest.version, color: true }));
|
|
182
|
+
}
|
|
183
|
+
return dispatchExit(result);
|
|
173
184
|
}
|
|
174
185
|
/** Write a pure dispatch result to the process streams and return its exit code. */
|
|
175
186
|
function dispatchExit(result) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MigrateOptions, MigrateResult } from "../config-migration/index.js";
|
|
2
2
|
import { type ReadonlyFsLike, type SpawnLike } from "./codex.js";
|
|
3
|
-
import type { InstallResult, InstallStep } from "./types.js";
|
|
3
|
+
import type { InstallResult, InstallStep, InstallStepResult } from "./types.js";
|
|
4
4
|
/** Minimal writable-fs surface for agents-install (injectable for tests). */
|
|
5
5
|
export interface WritableFsLike {
|
|
6
6
|
existsSync(p: string): boolean;
|
|
@@ -31,6 +31,10 @@ export interface ExecuteDeps {
|
|
|
31
31
|
readonly writeFs?: WritableFsLike;
|
|
32
32
|
/** Absolute path to the bundled agents source dir (defaults to resolved @litcodex/lit-loop/agents). */
|
|
33
33
|
readonly agentsSourceDir?: string;
|
|
34
|
+
/** Optional callback fired before each step begins (for TUI spinners). */
|
|
35
|
+
readonly onStepStart?: (step: InstallStep) => void;
|
|
36
|
+
/** Optional callback fired after each step completes (for TUI spinners). */
|
|
37
|
+
readonly onStepEnd?: (result: InstallStepResult) => void;
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Run the plan in order. Returns a `Promise<InstallResult>` (config-update is async). Throws a
|
package/dist/install/execute.js
CHANGED
|
@@ -29,7 +29,9 @@ export async function executeInstallPlan(steps, deps) {
|
|
|
29
29
|
}
|
|
30
30
|
const results = [];
|
|
31
31
|
for (const step of steps) {
|
|
32
|
+
deps.onStepStart?.(step);
|
|
32
33
|
const result = await runStep(step, codexBin, deps);
|
|
34
|
+
deps.onStepEnd?.(result);
|
|
33
35
|
results.push(result);
|
|
34
36
|
}
|
|
35
37
|
return {
|
package/dist/install/index.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// spawn or fs write (renders the plan only). Unknown/bad flags exit 1.
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { migrateCodexConfig } from "../config-migration/index.js";
|
|
12
|
+
import { humanLabel, renderBanner, Spinner, shouldDecorate } from "../ui.js";
|
|
11
13
|
import { resolveCodexHome } from "./codex.js";
|
|
12
14
|
import { renderDoctorText, runDoctor } from "./doctor.js";
|
|
13
15
|
import { exitCodeForInstallError, InstallError, toErrorJson } from "./errors.js";
|
|
@@ -72,25 +74,70 @@ export async function runInstallCli(args, io = defaultIo()) {
|
|
|
72
74
|
catch (err) {
|
|
73
75
|
return emitError(io, err);
|
|
74
76
|
}
|
|
77
|
+
const decorate = shouldDecorate({
|
|
78
|
+
isTty: Boolean(io.stdout.isTTY),
|
|
79
|
+
env: io.env,
|
|
80
|
+
noTui: opts.noTui,
|
|
81
|
+
json: opts.json,
|
|
82
|
+
});
|
|
75
83
|
const plan = buildInstallPlan(opts);
|
|
76
84
|
if (opts.dryRun) {
|
|
77
85
|
// ZERO spawns, ZERO fs writes: render and return.
|
|
86
|
+
if (decorate) {
|
|
87
|
+
const version = createRequire(import.meta.url)("../../package.json").version;
|
|
88
|
+
io.stdout.write(renderBanner({ version, color: true }));
|
|
89
|
+
}
|
|
78
90
|
io.stdout.write(`${renderInstallPlan(plan)}\n`);
|
|
79
91
|
return 0;
|
|
80
92
|
}
|
|
81
93
|
let result;
|
|
82
94
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
if (decorate) {
|
|
96
|
+
const version = createRequire(import.meta.url)("../../package.json").version;
|
|
97
|
+
io.stdout.write(renderBanner({ version, color: true }));
|
|
98
|
+
const spinner = new Spinner(io.stdout, true);
|
|
99
|
+
result = await executeInstallPlan(plan, {
|
|
100
|
+
spawn: realSpawn,
|
|
101
|
+
fs: realFs,
|
|
102
|
+
env: io.env,
|
|
103
|
+
now: () => Date.now(),
|
|
104
|
+
repoRoot: io.repoRoot,
|
|
105
|
+
migrateConfig: migrateCodexConfig,
|
|
106
|
+
force: opts.force,
|
|
107
|
+
codexHome: opts.codexHome,
|
|
108
|
+
writeFs: realWriteFs,
|
|
109
|
+
onStepStart: (step) => {
|
|
110
|
+
spinner.start(humanLabel(step.kind));
|
|
111
|
+
},
|
|
112
|
+
onStepEnd: (stepResult) => {
|
|
113
|
+
const label = humanLabel(stepResult.kind);
|
|
114
|
+
switch (stepResult.status) {
|
|
115
|
+
case "ok":
|
|
116
|
+
spinner.succeed(label);
|
|
117
|
+
break;
|
|
118
|
+
case "skipped":
|
|
119
|
+
spinner.skip(label);
|
|
120
|
+
break;
|
|
121
|
+
case "failed":
|
|
122
|
+
spinner.fail(label);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
result = await executeInstallPlan(plan, {
|
|
130
|
+
spawn: realSpawn,
|
|
131
|
+
fs: realFs,
|
|
132
|
+
env: io.env,
|
|
133
|
+
now: () => Date.now(),
|
|
134
|
+
repoRoot: io.repoRoot,
|
|
135
|
+
migrateConfig: migrateCodexConfig,
|
|
136
|
+
force: opts.force,
|
|
137
|
+
codexHome: opts.codexHome,
|
|
138
|
+
writeFs: realWriteFs,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
94
141
|
}
|
|
95
142
|
catch (err) {
|
|
96
143
|
return emitError(io, err);
|
|
@@ -98,6 +145,9 @@ export async function runInstallCli(args, io = defaultIo()) {
|
|
|
98
145
|
if (opts.json) {
|
|
99
146
|
io.stdout.write(`${JSON.stringify(result)}\n`);
|
|
100
147
|
}
|
|
148
|
+
else if (decorate) {
|
|
149
|
+
io.stdout.write(`\n \x1b[38;2;255;106;0m\u{1F525} litcodex install: complete\x1b[0m\n\n`);
|
|
150
|
+
}
|
|
101
151
|
else {
|
|
102
152
|
for (const step of result.steps) {
|
|
103
153
|
io.stdout.write(`${step.status} ${step.kind}: ${step.detail}\n`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// npm postinstall welcome banner for litcodex-ai.
|
|
2
|
+
//
|
|
3
|
+
// Prints a fire/ember-branded welcome + next-steps box ONLY on a real `npm install -g` (not CI,
|
|
4
|
+
// not workspace installs, not dev). Wraps everything so a failure exits 0 silently — a postinstall
|
|
5
|
+
// must never break `npm install`. No network, no fs writes, no deps beyond Node built-ins.
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { renderBanner } from "./ui.js";
|
|
8
|
+
function main() {
|
|
9
|
+
// Gate: only show on a real global install, not CI/workspace/dev.
|
|
10
|
+
if (process.env["npm_config_global"] !== "true")
|
|
11
|
+
return;
|
|
12
|
+
if (process.env["CI"])
|
|
13
|
+
return;
|
|
14
|
+
const color = Boolean(process.stdout.isTTY) && !process.env["NO_COLOR"];
|
|
15
|
+
const version = createRequire(import.meta.url)("../package.json").version;
|
|
16
|
+
const banner = renderBanner({ version, color });
|
|
17
|
+
process.stdout.write(banner);
|
|
18
|
+
if (color) {
|
|
19
|
+
const arrow = "\x1b[38;2;255;106;0m›\x1b[0m";
|
|
20
|
+
process.stdout.write(` \x1b[1mGet started\x1b[0m\n`);
|
|
21
|
+
process.stdout.write(` ${arrow} \x1b[1mlitcodex install\x1b[0m \x1b[2mregister the Codex plugin + hook\x1b[0m\n`);
|
|
22
|
+
process.stdout.write(` ${arrow} then type \x1b[1mlit\x1b[0m in Codex \x1b[2mand the loop ignites\x1b[0m 🔥\n\n`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
process.stdout.write(" Get started\n");
|
|
26
|
+
process.stdout.write(" > litcodex install register the Codex plugin + hook\n");
|
|
27
|
+
process.stdout.write(" > then type lit in Codex and the loop ignites\n\n");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
main();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Never fail npm install.
|
|
35
|
+
}
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { InstallStepKind } from "./install/types.js";
|
|
2
|
+
/** True when the environment permits ANSI decoration. */
|
|
3
|
+
export declare function shouldDecorate(opts: {
|
|
4
|
+
isTty: boolean;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
noTui?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}): boolean;
|
|
9
|
+
/** Wrap text in the SGR reset sequence. */
|
|
10
|
+
export declare function reset(text: string): string;
|
|
11
|
+
/** Wrap text in bold. */
|
|
12
|
+
export declare function bold(text: string, color: boolean): string;
|
|
13
|
+
/** Wrap text in dim. */
|
|
14
|
+
export declare function dim(text: string, color: boolean): string;
|
|
15
|
+
/** Apply a 24-bit foreground color via SGR 38;2;r;g;b. */
|
|
16
|
+
export declare function fg(text: string, r: number, g: number, b: number, color: boolean): string;
|
|
17
|
+
export declare function orange(text: string, color: boolean): string;
|
|
18
|
+
export declare function amber(text: string, color: boolean): string;
|
|
19
|
+
export declare function ember(text: string, color: boolean): string;
|
|
20
|
+
export declare function green(text: string, color: boolean): string;
|
|
21
|
+
export declare function red(text: string, color: boolean): string;
|
|
22
|
+
interface RGB {
|
|
23
|
+
r: number;
|
|
24
|
+
g: number;
|
|
25
|
+
b: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Apply a multi-stop RGB gradient across the visible characters of `text`.
|
|
29
|
+
* ANSI sequences and spaces are skipped (they keep the adjacent color).
|
|
30
|
+
* When `color` is false, returns the text unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export declare function gradient(text: string, stops: readonly RGB[], color: boolean): string;
|
|
33
|
+
/** The canonical fire/ember gradient: orange #ff6a00 → amber #ffb020 → ember red #d43a00. */
|
|
34
|
+
export declare const FIRE_GRADIENT: readonly RGB[];
|
|
35
|
+
/**
|
|
36
|
+
* Render the litcodex title banner. Returns a multi-line string.
|
|
37
|
+
*
|
|
38
|
+
* Color mode: fire gradient wordmark, dimmed subtitle, box-drawing accents.
|
|
39
|
+
* Plain mode: clean ASCII art, no escape codes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderBanner(opts: {
|
|
42
|
+
version: string;
|
|
43
|
+
subtitle?: string;
|
|
44
|
+
color: boolean;
|
|
45
|
+
}): string;
|
|
46
|
+
export declare class Spinner {
|
|
47
|
+
private readonly stream;
|
|
48
|
+
private readonly color;
|
|
49
|
+
private timer;
|
|
50
|
+
private frameIdx;
|
|
51
|
+
private label;
|
|
52
|
+
constructor(stream: NodeJS.WritableStream, color: boolean);
|
|
53
|
+
/** Start the spinner with a label. No-op when color is off. */
|
|
54
|
+
start(label: string): void;
|
|
55
|
+
/** Mark the current step as succeeded. */
|
|
56
|
+
succeed(label?: string): void;
|
|
57
|
+
/** Mark the current step as failed. */
|
|
58
|
+
fail(label?: string): void;
|
|
59
|
+
/** Mark the current step as skipped. */
|
|
60
|
+
skip(label?: string): void;
|
|
61
|
+
/** Stop the spinner without printing a status line. */
|
|
62
|
+
stop(): void;
|
|
63
|
+
private render;
|
|
64
|
+
private finish;
|
|
65
|
+
private clearTimer;
|
|
66
|
+
}
|
|
67
|
+
/** Return a human-friendly label for an install step kind. */
|
|
68
|
+
export declare function humanLabel(kind: InstallStepKind): string;
|
|
69
|
+
export {};
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// TUI polish — hand-rolled ANSI toolkit for the litcodex CLI (zero deps).
|
|
2
|
+
//
|
|
3
|
+
// Provides a fire/ember-branded title banner, animated per-step spinners, and color helpers.
|
|
4
|
+
// All visual effects are gated behind `shouldDecorate` and are pure no-ops when color/TTY is
|
|
5
|
+
// disabled, so CI/non-TTY/`--json`/`--no-tui` output is byte-identical to the pre-TUI baseline.
|
|
6
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
7
|
+
const ESC = "\x1b[";
|
|
8
|
+
const RESET_CODE = `${ESC}0m`;
|
|
9
|
+
/** True when the environment permits ANSI decoration. */
|
|
10
|
+
export function shouldDecorate(opts) {
|
|
11
|
+
if (!opts.isTty)
|
|
12
|
+
return false;
|
|
13
|
+
if (opts.env["CI"])
|
|
14
|
+
return false;
|
|
15
|
+
if (opts.env["NO_COLOR"])
|
|
16
|
+
return false;
|
|
17
|
+
if (opts.noTui)
|
|
18
|
+
return false;
|
|
19
|
+
if (opts.json)
|
|
20
|
+
return false;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
/** Wrap text in the SGR reset sequence. */
|
|
24
|
+
export function reset(text) {
|
|
25
|
+
return `${text}${RESET_CODE}`;
|
|
26
|
+
}
|
|
27
|
+
/** Wrap text in bold. */
|
|
28
|
+
export function bold(text, color) {
|
|
29
|
+
return color ? `${ESC}1m${text}${RESET_CODE}` : text;
|
|
30
|
+
}
|
|
31
|
+
/** Wrap text in dim. */
|
|
32
|
+
export function dim(text, color) {
|
|
33
|
+
return color ? `${ESC}2m${text}${RESET_CODE}` : text;
|
|
34
|
+
}
|
|
35
|
+
/** Apply a 24-bit foreground color via SGR 38;2;r;g;b. */
|
|
36
|
+
export function fg(text, r, g, b, color) {
|
|
37
|
+
return color ? `${ESC}38;2;${r};${g};${b}m${text}${RESET_CODE}` : text;
|
|
38
|
+
}
|
|
39
|
+
// Named colors (fire/ember palette).
|
|
40
|
+
export function orange(text, color) {
|
|
41
|
+
return fg(text, 255, 106, 0, color);
|
|
42
|
+
}
|
|
43
|
+
export function amber(text, color) {
|
|
44
|
+
return fg(text, 255, 176, 32, color);
|
|
45
|
+
}
|
|
46
|
+
export function ember(text, color) {
|
|
47
|
+
return fg(text, 212, 58, 0, color);
|
|
48
|
+
}
|
|
49
|
+
export function green(text, color) {
|
|
50
|
+
return fg(text, 80, 200, 80, color);
|
|
51
|
+
}
|
|
52
|
+
export function red(text, color) {
|
|
53
|
+
return fg(text, 230, 60, 60, color);
|
|
54
|
+
}
|
|
55
|
+
function lerp(a, b, t) {
|
|
56
|
+
return Math.round(a + (b - a) * t);
|
|
57
|
+
}
|
|
58
|
+
function at(arr, i) {
|
|
59
|
+
return arr[i];
|
|
60
|
+
}
|
|
61
|
+
function interpolateRgb(stops, t) {
|
|
62
|
+
if (stops.length === 1)
|
|
63
|
+
return at(stops, 0);
|
|
64
|
+
const segCount = stops.length - 1;
|
|
65
|
+
const seg = Math.min(Math.floor(t * segCount), segCount - 1);
|
|
66
|
+
const localT = t * segCount - seg;
|
|
67
|
+
const a = at(stops, seg);
|
|
68
|
+
const b = at(stops, seg + 1);
|
|
69
|
+
return { r: lerp(a.r, b.r, localT), g: lerp(a.g, b.g, localT), b: lerp(a.b, b.b, localT) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Apply a multi-stop RGB gradient across the visible characters of `text`.
|
|
73
|
+
* ANSI sequences and spaces are skipped (they keep the adjacent color).
|
|
74
|
+
* When `color` is false, returns the text unchanged.
|
|
75
|
+
*/
|
|
76
|
+
export function gradient(text, stops, color) {
|
|
77
|
+
if (!color || stops.length === 0)
|
|
78
|
+
return text;
|
|
79
|
+
// Count visible (non-space, non-ANSI) characters to distribute the gradient.
|
|
80
|
+
const chars = [...text];
|
|
81
|
+
const visible = [];
|
|
82
|
+
let inEsc = false;
|
|
83
|
+
for (let i = 0; i < chars.length; i++) {
|
|
84
|
+
const ch = chars[i];
|
|
85
|
+
if (ch === "\x1b") {
|
|
86
|
+
inEsc = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (inEsc) {
|
|
90
|
+
if (/[A-Za-z]/.test(ch))
|
|
91
|
+
inEsc = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch !== " ")
|
|
95
|
+
visible.push(i);
|
|
96
|
+
}
|
|
97
|
+
if (visible.length === 0)
|
|
98
|
+
return text;
|
|
99
|
+
// Build output with per-visible-char coloring.
|
|
100
|
+
let out = "";
|
|
101
|
+
let visIdx = 0;
|
|
102
|
+
inEsc = false;
|
|
103
|
+
for (let i = 0; i < chars.length; i++) {
|
|
104
|
+
const ch = chars[i];
|
|
105
|
+
if (ch === "\x1b") {
|
|
106
|
+
inEsc = true;
|
|
107
|
+
out += ch;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (inEsc) {
|
|
111
|
+
out += ch;
|
|
112
|
+
if (/[A-Za-z]/.test(ch))
|
|
113
|
+
inEsc = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === " ") {
|
|
117
|
+
out += ch;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const t = visible.length === 1 ? 0 : visIdx / (visible.length - 1);
|
|
121
|
+
const c = interpolateRgb(stops, t);
|
|
122
|
+
out += `${ESC}38;2;${c.r};${c.g};${c.b}m${ch}`;
|
|
123
|
+
visIdx++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return `${out}${RESET_CODE}`;
|
|
127
|
+
}
|
|
128
|
+
// ── Fire gradient stops ──────────────────────────────────────────────────────
|
|
129
|
+
/** The canonical fire/ember gradient: orange #ff6a00 → amber #ffb020 → ember red #d43a00. */
|
|
130
|
+
export const FIRE_GRADIENT = [
|
|
131
|
+
{ r: 255, g: 106, b: 0 },
|
|
132
|
+
{ r: 255, g: 176, b: 32 },
|
|
133
|
+
{ r: 212, g: 58, b: 0 },
|
|
134
|
+
];
|
|
135
|
+
// ── Banner ───────────────────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Render the litcodex title banner. Returns a multi-line string.
|
|
138
|
+
*
|
|
139
|
+
* Color mode: fire gradient wordmark, dimmed subtitle, box-drawing accents.
|
|
140
|
+
* Plain mode: clean ASCII art, no escape codes.
|
|
141
|
+
*/
|
|
142
|
+
export function renderBanner(opts) {
|
|
143
|
+
const { version, color } = opts;
|
|
144
|
+
const subtitle = opts.subtitle ?? "loop-native agent harness for Codex";
|
|
145
|
+
if (color) {
|
|
146
|
+
// Heavy rule, gradient-filled across its full width (orange → amber → ember).
|
|
147
|
+
const rule = gradient("━".repeat(46), FIRE_GRADIENT, true);
|
|
148
|
+
// Letter-spaced wordmark so the gradient breathes across each glyph (echoes the cover).
|
|
149
|
+
const wordmark = bold(gradient("l i t c o d e x", FIRE_GRADIENT, true), true);
|
|
150
|
+
return [
|
|
151
|
+
"",
|
|
152
|
+
` ${rule}`,
|
|
153
|
+
"",
|
|
154
|
+
` ${fg("🔥", 255, 106, 0, true)} ${wordmark} ${dim(`v${version}`, true)}`,
|
|
155
|
+
` ${dim(subtitle, true)}`,
|
|
156
|
+
"",
|
|
157
|
+
` ${rule}`,
|
|
158
|
+
"",
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
// Plain ASCII — no escape codes, still clean + the same composition.
|
|
162
|
+
const rule = "━".repeat(46);
|
|
163
|
+
return ["", ` ${rule}`, "", ` :: l i t c o d e x v${version}`, ` ${subtitle}`, "", ` ${rule}`, ""].join("\n");
|
|
164
|
+
}
|
|
165
|
+
// ── Spinner ──────────────────────────────────────────────────────────────────
|
|
166
|
+
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
167
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
168
|
+
/** Clear the current line and move the cursor to column 0. */
|
|
169
|
+
const CLEAR_LINE = `\r${ESC}2K`;
|
|
170
|
+
export class Spinner {
|
|
171
|
+
constructor(stream, color) {
|
|
172
|
+
this.timer = null;
|
|
173
|
+
this.frameIdx = 0;
|
|
174
|
+
this.label = "";
|
|
175
|
+
this.stream = stream;
|
|
176
|
+
this.color = color;
|
|
177
|
+
}
|
|
178
|
+
/** Start the spinner with a label. No-op when color is off. */
|
|
179
|
+
start(label) {
|
|
180
|
+
this.label = label;
|
|
181
|
+
if (!this.color)
|
|
182
|
+
return;
|
|
183
|
+
this.frameIdx = 0;
|
|
184
|
+
this.render();
|
|
185
|
+
this.timer = setInterval(() => {
|
|
186
|
+
this.frameIdx = (this.frameIdx + 1) % BRAILLE_FRAMES.length;
|
|
187
|
+
this.render();
|
|
188
|
+
}, SPINNER_INTERVAL_MS);
|
|
189
|
+
}
|
|
190
|
+
/** Mark the current step as succeeded. */
|
|
191
|
+
succeed(label) {
|
|
192
|
+
this.finish(label ?? this.label, "ok");
|
|
193
|
+
}
|
|
194
|
+
/** Mark the current step as failed. */
|
|
195
|
+
fail(label) {
|
|
196
|
+
this.finish(label ?? this.label, "failed");
|
|
197
|
+
}
|
|
198
|
+
/** Mark the current step as skipped. */
|
|
199
|
+
skip(label) {
|
|
200
|
+
this.finish(label ?? this.label, "skipped");
|
|
201
|
+
}
|
|
202
|
+
/** Stop the spinner without printing a status line. */
|
|
203
|
+
stop() {
|
|
204
|
+
this.clearTimer();
|
|
205
|
+
if (this.color) {
|
|
206
|
+
this.stream.write(CLEAR_LINE);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
render() {
|
|
210
|
+
const frame = amber(BRAILLE_FRAMES[this.frameIdx % BRAILLE_FRAMES.length], this.color);
|
|
211
|
+
this.stream.write(`${CLEAR_LINE} ${frame} ${this.label}`);
|
|
212
|
+
}
|
|
213
|
+
finish(label, status) {
|
|
214
|
+
this.clearTimer();
|
|
215
|
+
if (this.color) {
|
|
216
|
+
this.stream.write(CLEAR_LINE);
|
|
217
|
+
const icon = statusIcon(status, true);
|
|
218
|
+
this.stream.write(` ${icon} ${label}\n`);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const icon = statusIcon(status, false);
|
|
222
|
+
this.stream.write(`${icon} ${label}\n`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
clearTimer() {
|
|
226
|
+
if (this.timer !== null) {
|
|
227
|
+
clearInterval(this.timer);
|
|
228
|
+
this.timer = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function statusIcon(status, color) {
|
|
233
|
+
if (color) {
|
|
234
|
+
switch (status) {
|
|
235
|
+
case "ok":
|
|
236
|
+
return green("✓", true);
|
|
237
|
+
case "failed":
|
|
238
|
+
return red("✗", true);
|
|
239
|
+
case "skipped":
|
|
240
|
+
return dim("○", true);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
switch (status) {
|
|
244
|
+
case "ok":
|
|
245
|
+
return "[ok]";
|
|
246
|
+
case "failed":
|
|
247
|
+
return "[fail]";
|
|
248
|
+
case "skipped":
|
|
249
|
+
return "[skip]";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ── Human labels for install step kinds ──────────────────────────────────────
|
|
253
|
+
const STEP_LABELS = {
|
|
254
|
+
"marketplace-add": "Registering marketplace",
|
|
255
|
+
"plugin-add": "Installing plugin",
|
|
256
|
+
"hooks-register": "Wiring UserPromptSubmit hook",
|
|
257
|
+
"agents-install": "Installing litwork agents",
|
|
258
|
+
"config-update": "Updating Codex config",
|
|
259
|
+
verify: "Running doctor",
|
|
260
|
+
};
|
|
261
|
+
/** Return a human-friendly label for an install step kind. */
|
|
262
|
+
export function humanLabel(kind) {
|
|
263
|
+
return STEP_LABELS[kind];
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litcodex-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Codex loop harness installer. Run `npx litcodex-ai install` to set up the LitCodex Codex platform: the bare `lit` hook and the durable lit-loop runtime.",
|
|
5
5
|
"keywords": ["codex", "litcodex", "lit-loop", "ai-agents", "orchestration"],
|
|
6
6
|
"author": "LitCodex Authors",
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": ["bin", "dist", "model-catalog.json", "README.md", "LICENSE"],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@litcodex/lit-loop": "0.3.
|
|
17
|
+
"@litcodex/lit-loop": "0.3.2"
|
|
18
18
|
},
|
|
19
19
|
"bundledDependencies": ["@litcodex/lit-loop"],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc -p tsconfig.build.json",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
23
|
"test": "vitest --run",
|
|
24
|
+
"postinstall": "node dist/postinstall.js",
|
|
24
25
|
"prepack": "node ../../scripts/prepack-bundle-component.mjs",
|
|
25
26
|
"postpack": "node ../../scripts/postpack-clean-component.mjs"
|
|
26
27
|
},
|