@cruxy/cli 0.11.0 → 0.12.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/dist/approval/prompt.js +17 -15
- package/dist/cli/commands/checkpoint.js +6 -4
- package/dist/cli/commands/config.js +10 -7
- package/dist/cli/commands/index.js +16 -15
- package/dist/cli/commands/init.js +5 -3
- package/dist/cli/commands/login.js +5 -3
- package/dist/cli/commands/pr.js +8 -7
- package/dist/cli/commands/rollback.js +7 -6
- package/dist/cli/commands/run.js +7 -6
- package/dist/cli/commands/skills.js +12 -10
- package/dist/cli/program.js +7 -6
- package/dist/cli/repl.js +11 -9
- package/dist/components/frame.js +3 -1
- package/dist/components/fuzzy.d.ts +4 -4
- package/dist/components/fuzzy.js +14 -13
- package/dist/components/select.js +8 -7
- package/dist/errors/format.js +8 -8
- package/dist/onboarding/flow.js +6 -6
- package/dist/onboarding/steps.js +11 -11
- package/dist/plan/approve.js +6 -6
- package/dist/plan/render.js +26 -18
- package/dist/render/capabilities.js +4 -0
- package/dist/render/diff.d.ts +6 -7
- package/dist/render/diff.js +33 -22
- package/dist/render/highlight.d.ts +3 -3
- package/dist/render/highlight.js +15 -15
- package/dist/render/index.d.ts +1 -1
- package/dist/render/plain-renderer.d.ts +2 -1
- package/dist/render/plain-renderer.js +7 -6
- package/dist/render/state.d.ts +7 -2
- package/dist/render/state.js +16 -10
- package/dist/render/tty-renderer.d.ts +2 -1
- package/dist/render/tty-renderer.js +20 -17
- package/dist/render/types.d.ts +7 -0
- package/dist/subagent/orchestrator.js +21 -6
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/resolve.d.ts +32 -0
- package/dist/theme/resolve.js +73 -0
- package/dist/theme/tokens.d.ts +104 -0
- package/dist/theme/tokens.js +52 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +7 -4
- package/package.json +1 -1
package/dist/approval/prompt.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { readSingleKey } from "../components/input.js";
|
|
4
3
|
import { renderActionPreview } from "../render/diff.js";
|
|
4
|
+
import { themeForColor } from "../theme/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* Render the action, read one key, and map it to a {@link PromptChoice}. `n`/`t`
|
|
7
7
|
* read a follow-up line (reason / instruction). Anything else — including EOF —
|
|
@@ -35,33 +35,35 @@ export async function promptForApproval(request, io) {
|
|
|
35
35
|
}
|
|
36
36
|
/** Render the full prompt block: header, detail (diff or command+cwd), choices. */
|
|
37
37
|
export function render(request, color) {
|
|
38
|
-
const
|
|
38
|
+
const t = themeForColor(color);
|
|
39
39
|
const destructive = request.tier === "destructive";
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// The risk mark carries meaning by shape (`!` vs `?`), not by color alone —
|
|
41
|
+
// so it survives NO_COLOR (U.1 accessibility groundwork).
|
|
42
|
+
const mark = destructive ? t.danger(t.strong("!")) : t.warning("?");
|
|
43
|
+
const label = destructive ? t.danger(t.strong(" (destructive)")) : "";
|
|
42
44
|
const lines = [];
|
|
43
|
-
lines.push(`${mark} cruxy wants to ${
|
|
44
|
-
lines.push(detail(request,
|
|
45
|
-
lines.push(choices(request.scope,
|
|
45
|
+
lines.push(`${mark} cruxy wants to ${t.strong(request.summary)}${label}`);
|
|
46
|
+
lines.push(detail(request, t));
|
|
47
|
+
lines.push(choices(request.scope, t));
|
|
46
48
|
return lines.filter((l) => l !== "").join("\n") + " ";
|
|
47
49
|
}
|
|
48
50
|
/** The action detail: a diff for file actions, the command + cwd for shell/test. */
|
|
49
|
-
function detail(request,
|
|
51
|
+
function detail(request, t) {
|
|
50
52
|
if (request.action.kind === "shell" || request.action.kind === "test") {
|
|
51
53
|
return [
|
|
52
|
-
` ${
|
|
53
|
-
` ${
|
|
54
|
+
` ${t.muted("$")} ${request.action.command ?? ""}`,
|
|
55
|
+
` ${t.muted(`in ${request.cwd}`)}`,
|
|
54
56
|
].join("\n");
|
|
55
57
|
}
|
|
56
|
-
return renderActionPreview(request.action.preview,
|
|
58
|
+
return renderActionPreview(request.action.preview, t);
|
|
57
59
|
}
|
|
58
60
|
/** The choices line, including a short label of what an `a` grant would cover. */
|
|
59
|
-
function choices(scope,
|
|
61
|
+
function choices(scope, t) {
|
|
60
62
|
const grant = scopeLabel(scope);
|
|
61
63
|
const a = grant === null
|
|
62
|
-
? `${
|
|
63
|
-
: `[a] allow ${
|
|
64
|
-
return ` ${
|
|
64
|
+
? `${t.muted("[a] allow this kind (n/a here)")}`
|
|
65
|
+
: `[a] allow ${t.strong(grant)} this session`;
|
|
66
|
+
return ` ${t.muted(`[y] approve once ${t.glyph.sep}`)} ${a} ${t.muted(`${t.glyph.sep} [n] reject ${t.glyph.sep} [t] reject & instruct:`)}`;
|
|
65
67
|
}
|
|
66
68
|
/** Short human label for what a session grant would allow, or null if none. */
|
|
67
69
|
function scopeLabel(scope) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { loadConfig } from "../../config/index.js";
|
|
4
3
|
import { CheckpointService } from "../../checkpoint/index.js";
|
|
4
|
+
import { shouldUseColor } from "../../errors/index.js";
|
|
5
|
+
import { themeForColor } from "../../theme/index.js";
|
|
5
6
|
import { logger } from "../../utils/logger.js";
|
|
6
7
|
/**
|
|
7
8
|
* `cruxy checkpoint` (C.32) — inspect the working-tree snapshots that back
|
|
@@ -14,18 +15,19 @@ export function checkpointCommand() {
|
|
|
14
15
|
.command("list")
|
|
15
16
|
.description("list saved checkpoints, newest first")
|
|
16
17
|
.action(async () => {
|
|
18
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
17
19
|
const { config } = loadConfig();
|
|
18
20
|
const service = new CheckpointService({ root: process.cwd(), config });
|
|
19
21
|
const checkpoints = await service.list();
|
|
20
22
|
if (checkpoints.length === 0) {
|
|
21
|
-
logger.print(
|
|
23
|
+
logger.print(t.muted("no checkpoints yet — one is created automatically before an agent run's first file change"));
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
26
|
for (const c of checkpoints) {
|
|
25
27
|
const files = `${c.files.length} file${c.files.length === 1 ? "" : "s"}`;
|
|
26
|
-
logger.print(`${
|
|
28
|
+
logger.print(`${t.accent(c.id)} ${t.muted(c.createdAt)} ${t.muted(`[${c.store}]`)} ${files} ${c.runSummary}`);
|
|
27
29
|
}
|
|
28
|
-
logger.print(
|
|
30
|
+
logger.print(t.muted(`\nrestore one with \`cruxy rollback <id>\` (or \`cruxy rollback\` for the newest)`));
|
|
29
31
|
});
|
|
30
32
|
return cmd;
|
|
31
33
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { configKeyUnknown, shouldUseColor } from "../../errors/index.js";
|
|
3
|
+
import { themeForColor } from "../../theme/index.js";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
5
|
import { loadConfig, getPath, setValue, initConfig, globalConfigPath, findProjectConfig, } from "../../config/index.js";
|
|
6
6
|
export function configCommand() {
|
|
@@ -35,24 +35,27 @@ export function configCommand() {
|
|
|
35
35
|
? (findProjectConfig() ?? "cruxy.config.json")
|
|
36
36
|
: globalConfigPath();
|
|
37
37
|
const written = setValue(key, value, file);
|
|
38
|
-
|
|
38
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
39
|
+
logger.print(`${t.success("set")} ${t.strong(key)} = ${value} ${t.muted(`(${written})`)}`);
|
|
39
40
|
});
|
|
40
41
|
cmd
|
|
41
42
|
.command("path")
|
|
42
43
|
.description("show config file locations")
|
|
43
44
|
.action(() => {
|
|
45
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
44
46
|
const project = findProjectConfig();
|
|
45
|
-
logger.print(`${
|
|
46
|
-
logger.print(`${
|
|
47
|
+
logger.print(`${t.strong("global:")} ${globalConfigPath()}`);
|
|
48
|
+
logger.print(`${t.strong("project:")} ${project ?? t.muted("(none found)")}`);
|
|
47
49
|
});
|
|
48
50
|
cmd
|
|
49
51
|
.command("init")
|
|
50
52
|
.description("write a default global config file")
|
|
51
53
|
.action(() => {
|
|
54
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
52
55
|
const { path, created } = initConfig(globalConfigPath());
|
|
53
56
|
logger.print(created
|
|
54
|
-
? `${
|
|
55
|
-
: `${
|
|
57
|
+
? `${t.success("created")} ${path}`
|
|
58
|
+
: `${t.warning("exists")} ${path} ${t.muted("(left unchanged)")}`);
|
|
56
59
|
});
|
|
57
60
|
return cmd;
|
|
58
61
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import pc from "picocolors";
|
|
4
3
|
import { loadConfig } from "../../config/index.js";
|
|
5
|
-
import { CruxyError, indexFailed } from "../../errors/index.js";
|
|
4
|
+
import { CruxyError, indexFailed, shouldUseColor } from "../../errors/index.js";
|
|
5
|
+
import { themeForColor } from "../../theme/index.js";
|
|
6
6
|
import { getIndexService, indexDbPath, resetIndexServices, } from "../../indexing/index.js";
|
|
7
7
|
import { logger } from "../../utils/logger.js";
|
|
8
8
|
/**
|
|
@@ -16,6 +16,7 @@ export function indexCommand() {
|
|
|
16
16
|
.option("--status", "show index status without building")
|
|
17
17
|
.option("--force", "re-embed every file, ignoring content hashes")
|
|
18
18
|
.action(async (opts) => {
|
|
19
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
19
20
|
const { config } = loadConfig();
|
|
20
21
|
if (!config.index.enabled) {
|
|
21
22
|
logger.warn("codebase indexing is disabled (index.enabled = false)");
|
|
@@ -25,21 +26,21 @@ export function indexCommand() {
|
|
|
25
26
|
if (opts.status) {
|
|
26
27
|
// Don't create an empty DB just to report "no index built yet".
|
|
27
28
|
if (config.index.store !== "memory" && !existsSync(indexDbPath(cwd))) {
|
|
28
|
-
logger.print(`${
|
|
29
|
+
logger.print(`${t.strong("index:")} ${t.muted("not built yet")} — run ${t.strong("cruxy index")}`);
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
const service = await getIndexService(cwd, config, logger);
|
|
32
|
-
printStatus(service.status());
|
|
33
|
+
printStatus(service.status(), t);
|
|
33
34
|
await resetIndexServices();
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
|
-
logger.info(
|
|
37
|
-
logger.info(
|
|
37
|
+
logger.info(t.muted(`indexing ${cwd} ${t.glyph.ellipsis}`));
|
|
38
|
+
logger.info(t.muted("(first run may download the local embedding model)"));
|
|
38
39
|
const service = await getIndexService(cwd, config, logger);
|
|
39
40
|
try {
|
|
40
41
|
const stats = await service.index({ force: opts.force });
|
|
41
|
-
logger.print(`${
|
|
42
|
-
|
|
42
|
+
logger.print(`${t.success("indexed")} ${stats.filesIndexed} file(s), ${stats.chunksIndexed} chunk(s) ` +
|
|
43
|
+
t.muted(`(${stats.filesSkipped} unchanged, ${stats.filesPurged} purged, ${stats.durationMs}ms)`));
|
|
43
44
|
}
|
|
44
45
|
catch (err) {
|
|
45
46
|
// Typed index failures (embedder/store unavailable) already carry a
|
|
@@ -52,11 +53,11 @@ export function indexCommand() {
|
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
|
-
function printStatus(status) {
|
|
56
|
-
logger.print(`${
|
|
57
|
-
logger.print(`${
|
|
58
|
-
logger.print(`${
|
|
59
|
-
(status.dim ?
|
|
60
|
-
logger.print(`${
|
|
61
|
-
logger.print(`${
|
|
56
|
+
function printStatus(status, t) {
|
|
57
|
+
logger.print(`${t.strong("index:")} ${status.exists ? t.success("built") : t.warning("empty")}`);
|
|
58
|
+
logger.print(`${t.strong("store:")} ${status.storePath ?? "(in-memory)"}`);
|
|
59
|
+
logger.print(`${t.strong("embedder:")} ${status.embedderId ?? t.muted("(none)")}` +
|
|
60
|
+
(status.dim ? t.muted(`${t.sep}${status.dim}d`) : ""));
|
|
61
|
+
logger.print(`${t.strong("files:")} ${status.files}`);
|
|
62
|
+
logger.print(`${t.strong("chunks:")} ${status.chunks}`);
|
|
62
63
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import
|
|
2
|
+
import { shouldUseColor } from "../../errors/index.js";
|
|
3
|
+
import { themeForColor } from "../../theme/index.js";
|
|
3
4
|
import { logger } from "../../utils/logger.js";
|
|
4
5
|
import { loadConfig } from "../../config/index.js";
|
|
5
6
|
import { createDefaultDeps, defaultOnboardingIO, runOnboarding, } from "../../onboarding/index.js";
|
|
@@ -13,8 +14,9 @@ export function initCommand() {
|
|
|
13
14
|
return new Command("init")
|
|
14
15
|
.description("set up cruxy in this project (key + CRUXY.md + a first run)")
|
|
15
16
|
.action(async () => {
|
|
17
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
16
18
|
if (!process.stdin.isTTY) {
|
|
17
|
-
logger.print(
|
|
19
|
+
logger.print(t.muted("cruxy init is interactive — run it in a terminal, or export your key as an environment variable."));
|
|
18
20
|
process.exitCode = 1;
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
@@ -33,7 +35,7 @@ export function initCommand() {
|
|
|
33
35
|
}),
|
|
34
36
|
});
|
|
35
37
|
if (!result.completed) {
|
|
36
|
-
logger.print(
|
|
38
|
+
logger.print(t.muted("setup not completed — run `cruxy init` again to resume."));
|
|
37
39
|
process.exitCode = 1;
|
|
38
40
|
}
|
|
39
41
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import
|
|
2
|
+
import { shouldUseColor } from "../../errors/index.js";
|
|
3
|
+
import { themeForColor } from "../../theme/index.js";
|
|
3
4
|
import { logger } from "../../utils/logger.js";
|
|
4
5
|
import { loadConfig } from "../../config/index.js";
|
|
5
6
|
import { createDefaultDeps, defaultOnboardingIO, runOnboarding, } from "../../onboarding/index.js";
|
|
@@ -13,8 +14,9 @@ export function loginCommand() {
|
|
|
13
14
|
return new Command("login")
|
|
14
15
|
.description("set or replace your API key (validated, saved to ~/.cruxy)")
|
|
15
16
|
.action(async () => {
|
|
17
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
16
18
|
if (!process.stdin.isTTY) {
|
|
17
|
-
logger.print(
|
|
19
|
+
logger.print(t.muted("cruxy login is interactive — run it in a terminal, or export your key as an environment variable."));
|
|
18
20
|
process.exitCode = 1;
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
@@ -29,7 +31,7 @@ export function loginCommand() {
|
|
|
29
31
|
deps: createDefaultDeps({ config, cwd: process.cwd() }),
|
|
30
32
|
});
|
|
31
33
|
if (!result.completed) {
|
|
32
|
-
logger.print(
|
|
34
|
+
logger.print(t.muted("login not completed."));
|
|
33
35
|
process.exitCode = 1;
|
|
34
36
|
}
|
|
35
37
|
});
|
package/dist/cli/commands/pr.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { createProvider } from "@cruxy/sdk";
|
|
4
3
|
import { logger } from "../../utils/logger.js";
|
|
5
4
|
import { loadConfig, resolveApiKey } from "../../config/index.js";
|
|
6
|
-
import { authMissingKey } from "../../errors/index.js";
|
|
5
|
+
import { authMissingKey, shouldUseColor } from "../../errors/index.js";
|
|
6
|
+
import { themeForColor } from "../../theme/index.js";
|
|
7
7
|
import { ApprovalService } from "../../approval/index.js";
|
|
8
8
|
import { createForgeProvider, createPrService, generateWithLlm, loadCommitGuidance, resolveForgeToken, } from "../../vcs/index.js";
|
|
9
9
|
/**
|
|
@@ -20,6 +20,7 @@ export function prCommand() {
|
|
|
20
20
|
.option("--body <body>", "PR body")
|
|
21
21
|
.option("--draft", "open the pull request as a draft")
|
|
22
22
|
.action(async (opts) => {
|
|
23
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
23
24
|
const cwd = process.cwd();
|
|
24
25
|
const { config } = loadConfig();
|
|
25
26
|
// The model writes the PR content, so we need a provider key just like
|
|
@@ -54,7 +55,7 @@ export function prCommand() {
|
|
|
54
55
|
skillBody: guidance.skillBody,
|
|
55
56
|
}),
|
|
56
57
|
});
|
|
57
|
-
logger.info(
|
|
58
|
+
logger.info(t.muted("generating pull request content…"));
|
|
58
59
|
const outcome = await service.openPullRequest({
|
|
59
60
|
base: opts.base,
|
|
60
61
|
title: opts.title,
|
|
@@ -62,16 +63,16 @@ export function prCommand() {
|
|
|
62
63
|
draft: opts.draft,
|
|
63
64
|
});
|
|
64
65
|
if (!outcome.approved) {
|
|
65
|
-
logger.print(
|
|
66
|
+
logger.print(t.warning("pull request not opened — approval declined."));
|
|
66
67
|
if (outcome.feedback)
|
|
67
|
-
logger.print(
|
|
68
|
+
logger.print(t.muted(outcome.feedback));
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
71
|
const label = outcome.alreadyExists
|
|
71
72
|
? "a pull request already exists"
|
|
72
73
|
: "opened pull request";
|
|
73
|
-
logger.print(`${
|
|
74
|
-
logger.print(
|
|
74
|
+
logger.print(`${t.success(t.glyph.success)} ${label} (${outcome.branch} ${t.glyph.arrow} ${outcome.base})`);
|
|
75
|
+
logger.print(t.accent(outcome.url));
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
/** Environment variable that holds the API key for a provider. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import
|
|
2
|
+
import { themeForColor } from "../../theme/index.js";
|
|
3
3
|
import { loadConfig } from "../../config/index.js";
|
|
4
4
|
import { CheckpointService } from "../../checkpoint/index.js";
|
|
5
5
|
import { ApprovalService, defaultPromptIO } from "../../approval/index.js";
|
|
@@ -58,6 +58,7 @@ export function rollbackCommand() {
|
|
|
58
58
|
// act. There is no flag to bypass this, by design.
|
|
59
59
|
if (!interactive)
|
|
60
60
|
throw rollbackApprovalRequired();
|
|
61
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
61
62
|
const { config } = loadConfig();
|
|
62
63
|
const root = process.cwd();
|
|
63
64
|
const service = new CheckpointService({ root, config });
|
|
@@ -71,7 +72,7 @@ export function rollbackCommand() {
|
|
|
71
72
|
if (id === undefined) {
|
|
72
73
|
const picked = await pickCheckpoint(service);
|
|
73
74
|
if (picked === null) {
|
|
74
|
-
logger.print(
|
|
75
|
+
logger.print(t.muted("rollback cancelled — nothing was changed"));
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
id = picked?.id;
|
|
@@ -81,16 +82,16 @@ export function rollbackCommand() {
|
|
|
81
82
|
interactive,
|
|
82
83
|
});
|
|
83
84
|
if (result.kind === "noop") {
|
|
84
|
-
logger.print(
|
|
85
|
+
logger.print(t.muted(`working tree already matches checkpoint ${result.checkpoint.id} — nothing to roll back`));
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
88
|
if (result.kind === "rejected") {
|
|
88
|
-
logger.print(
|
|
89
|
+
logger.print(t.muted("rollback declined — nothing was changed"));
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
const { recreated, reverted, deleted } = result.applied;
|
|
92
|
-
logger.print(`${
|
|
93
|
+
logger.print(`${t.success(t.glyph.success)} restored checkpoint ${t.accent(result.checkpoint.id)} — ` +
|
|
93
94
|
`${reverted} reverted, ${recreated} recreated, ${deleted} deleted`);
|
|
94
|
-
logger.print(
|
|
95
|
+
logger.print(t.muted("note: commits, pushes, and PRs made during the run are not undone"));
|
|
95
96
|
});
|
|
96
97
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { logger } from "../../utils/logger.js";
|
|
4
3
|
import { loadConfig, resolveApiKey } from "../../config/index.js";
|
|
5
|
-
import { authMissingKey, usageError } from "../../errors/index.js";
|
|
4
|
+
import { authMissingKey, shouldUseColor, usageError, } from "../../errors/index.js";
|
|
6
5
|
import { createRenderer } from "../../render/index.js";
|
|
6
|
+
import { themeForColor } from "../../theme/index.js";
|
|
7
7
|
import { CheckpointService } from "../../checkpoint/index.js";
|
|
8
8
|
import { runInteractive } from "../repl.js";
|
|
9
9
|
import { buildAgentSession } from "../session-factory.js";
|
|
@@ -15,6 +15,7 @@ export function runCommand() {
|
|
|
15
15
|
.option("--plan", "plan mode: propose a step-by-step plan for approval before executing")
|
|
16
16
|
.action(async (promptParts, opts) => {
|
|
17
17
|
const prompt = promptParts.join(" ").trim();
|
|
18
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
18
19
|
const interactive = prompt === "";
|
|
19
20
|
// No prompt and stdin isn't a terminal: there's no way to read input and
|
|
20
21
|
// nothing to do — fail fast instead of hanging on a line that never comes.
|
|
@@ -23,8 +24,8 @@ export function runCommand() {
|
|
|
23
24
|
}
|
|
24
25
|
const { config, sources } = loadConfig();
|
|
25
26
|
let apiKey = resolveApiKey(config.model.provider);
|
|
26
|
-
logger.info(
|
|
27
|
-
logger.info(
|
|
27
|
+
logger.info(t.muted(`model: ${config.model.provider}/${config.model.model}`));
|
|
28
|
+
logger.info(t.muted(`config: ${sources.project ?? sources.global ?? "defaults"}`));
|
|
28
29
|
// First-run with no key (and a TTY) → guided onboarding instead of the
|
|
29
30
|
// dead-end auth error. The first-win demo is offered only in the no-prompt
|
|
30
31
|
// (REPL) path; with a real prompt, that prompt IS the first win.
|
|
@@ -41,7 +42,7 @@ export function runCommand() {
|
|
|
41
42
|
const result = await onboarding;
|
|
42
43
|
if (!result.completed) {
|
|
43
44
|
// Aborted/failed mid-setup — guidance already shown; exit cleanly.
|
|
44
|
-
logger.print(
|
|
45
|
+
logger.print(t.muted("run `cruxy login` to finish setup."));
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
48
|
apiKey = result.apiKey ?? resolveApiKey(config.model.provider);
|
|
@@ -69,7 +70,7 @@ export function runCommand() {
|
|
|
69
70
|
// One-shot: a single turn, then exit. Preserves scripting/pipe use.
|
|
70
71
|
// Assistant text streams to stdout delta by delta (same as the REPL);
|
|
71
72
|
// piped output degrades to the plain renderer (no ANSI, chrome on stderr).
|
|
72
|
-
logger.print(`${
|
|
73
|
+
logger.print(`${t.accent("cruxy")} ${t.muted(t.glyph.caret)} ${prompt}\n`);
|
|
73
74
|
// Provider/network/auth failures propagate to the top-level boundary,
|
|
74
75
|
// which classifies them (e.g. CRUXY_E_GATEWAY_UNREACHABLE) and exits with
|
|
75
76
|
// the matching code — a one-shot run must fail non-zero on error.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { getSkillService, resetSkillServices } from "../../skills/index.js";
|
|
3
|
+
import { shouldUseColor } from "../../errors/index.js";
|
|
4
|
+
import { themeForColor } from "../../theme/index.js";
|
|
4
5
|
import { logger } from "../../utils/logger.js";
|
|
5
6
|
/**
|
|
6
7
|
* `cruxy skills` — list the skills available to the agent (the same catalog the
|
|
@@ -13,37 +14,38 @@ export function skillsCommand() {
|
|
|
13
14
|
.description("list the skills available to the agent")
|
|
14
15
|
.option("--status", "show source directories and validation errors")
|
|
15
16
|
.action(async (opts) => {
|
|
17
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
16
18
|
const service = getSkillService(process.cwd(), logger);
|
|
17
19
|
const status = await service.status();
|
|
18
20
|
if (status.entries.length === 0) {
|
|
19
|
-
logger.print(
|
|
21
|
+
logger.print(t.muted("no skills found"));
|
|
20
22
|
}
|
|
21
23
|
else {
|
|
22
24
|
for (const entry of status.entries) {
|
|
23
|
-
logger.print(`${
|
|
25
|
+
logger.print(`${t.strong(entry.name)} ${t.muted(`[${entry.source}]`)}`);
|
|
24
26
|
logger.print(` ${entry.description}`);
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
if (!opts.status) {
|
|
28
30
|
if (status.errors.length > 0) {
|
|
29
|
-
logger.print(
|
|
31
|
+
logger.print(t.warning(`\n${status.errors.length} skill(s) failed validation — run ${t.strong("cruxy skills --status")} for details`));
|
|
30
32
|
}
|
|
31
33
|
resetSkillServices();
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
logger.print(`\n${
|
|
36
|
+
logger.print(`\n${t.strong("sources")} ${t.muted("(precedence high → low):")}`);
|
|
35
37
|
for (const { source, dir } of status.sources) {
|
|
36
|
-
logger.print(` ${source.padEnd(8)} ${
|
|
38
|
+
logger.print(` ${source.padEnd(8)} ${t.muted(dir)}`);
|
|
37
39
|
}
|
|
38
40
|
logger.print("");
|
|
39
41
|
if (status.errors.length === 0) {
|
|
40
|
-
logger.print(
|
|
42
|
+
logger.print(t.success("no validation errors"));
|
|
41
43
|
}
|
|
42
44
|
else {
|
|
43
|
-
logger.print(
|
|
45
|
+
logger.print(t.warning(`validation errors (${status.errors.length}):`));
|
|
44
46
|
for (const err of status.errors) {
|
|
45
|
-
logger.print(` ${
|
|
46
|
-
logger.print(` ${
|
|
47
|
+
logger.print(` ${t.danger(t.glyph.failure)} ${t.strong(err.name)} ${t.muted(`[${err.source}]`)}: ${err.message}`);
|
|
48
|
+
logger.print(` ${t.muted(err.dir)}`);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
resetSkillServices();
|
package/dist/cli/program.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { APP_NAME, APP_VERSION, APP_DESCRIPTION } from "../constants.js";
|
|
4
3
|
import { logger } from "../utils/logger.js";
|
|
5
|
-
import { usageError } from "../errors/index.js";
|
|
4
|
+
import { shouldUseColor, usageError } from "../errors/index.js";
|
|
5
|
+
import { themeForColor } from "../theme/index.js";
|
|
6
6
|
import { runCommand } from "./commands/run.js";
|
|
7
7
|
import { configCommand } from "./commands/config.js";
|
|
8
8
|
import { indexCommand } from "./commands/index.js";
|
|
@@ -63,11 +63,12 @@ export function buildProgram() {
|
|
|
63
63
|
await onboarding;
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
logger.print(
|
|
66
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
67
|
+
logger.print(t.accent(`${APP_NAME} v${APP_VERSION}`));
|
|
68
|
+
logger.print(t.muted("an agentic coding CLI\n"));
|
|
68
69
|
logger.print("The interactive REPL lands in C.3 (terminal UI).");
|
|
69
|
-
logger.print(`For now try: ${
|
|
70
|
-
logger.print(`See all commands: ${
|
|
70
|
+
logger.print(`For now try: ${t.strong('cruxy run "<task>"')} or ${t.strong("cruxy config path")}`);
|
|
71
|
+
logger.print(`See all commands: ${t.strong("cruxy --help")}`);
|
|
71
72
|
});
|
|
72
73
|
// Throw CommanderError instead of calling process.exit, and suppress
|
|
73
74
|
// Commander's own "error:" line — so parse errors (unknown command/option,
|
package/dist/cli/repl.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
|
-
import pc from "picocolors";
|
|
3
2
|
import { makeReplCompleter } from "../components/index.js";
|
|
3
|
+
import { themeForColor } from "../theme/index.js";
|
|
4
4
|
import { formatError, fromUnknown, isVerbose, shouldUseColor, } from "../errors/index.js";
|
|
5
5
|
import { createRenderer } from "../render/index.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
-
|
|
7
|
+
/** The REPL prompts on stdout; its chrome resolves against stdout's color. */
|
|
8
|
+
const theme = themeForColor(shouldUseColor(process.stdout));
|
|
9
|
+
const PROMPT = `${theme.accent("cruxy")} ${theme.muted(theme.glyph.caret)} `;
|
|
8
10
|
/**
|
|
9
11
|
* The REPL's slash commands — the autocomplete candidate set (U.7). Keep in
|
|
10
12
|
* sync with the dispatch below and the HELP text.
|
|
@@ -92,7 +94,7 @@ function printReplError(err) {
|
|
|
92
94
|
* passes its own so the approval prompt's status-suspend hook shares it.
|
|
93
95
|
*/
|
|
94
96
|
export async function runInteractive(session, io = defaultIO(), renderer = createRenderer(io.output, process.stderr), checkpoints) {
|
|
95
|
-
logger.print(
|
|
97
|
+
logger.print(theme.muted("interactive session — /help for commands, /exit or Ctrl+D to quit"));
|
|
96
98
|
try {
|
|
97
99
|
await replLoop(session, io, renderer, checkpoints);
|
|
98
100
|
}
|
|
@@ -105,25 +107,25 @@ async function replLoop(session, io, renderer, checkpoints) {
|
|
|
105
107
|
const line = await readLine(io, PROMPT);
|
|
106
108
|
// EOF / Ctrl+D.
|
|
107
109
|
if (line === null) {
|
|
108
|
-
logger.print(
|
|
110
|
+
logger.print(theme.muted("\nbye"));
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
const trimmed = line.trim();
|
|
112
114
|
if (trimmed === "")
|
|
113
115
|
continue; // empty line → reprompt, no model call
|
|
114
116
|
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
115
|
-
logger.print(
|
|
117
|
+
logger.print(theme.muted("bye"));
|
|
116
118
|
return;
|
|
117
119
|
}
|
|
118
120
|
if (trimmed === "/clear") {
|
|
119
121
|
session.clear();
|
|
120
|
-
logger.print(
|
|
122
|
+
logger.print(theme.muted("history cleared"));
|
|
121
123
|
continue;
|
|
122
124
|
}
|
|
123
125
|
if (trimmed === "/compact") {
|
|
124
126
|
try {
|
|
125
127
|
const n = await session.compact();
|
|
126
|
-
logger.print(
|
|
128
|
+
logger.print(theme.muted(n ? `compacted ${n} older messages` : "nothing to compact yet"));
|
|
127
129
|
}
|
|
128
130
|
catch (err) {
|
|
129
131
|
printReplError(err);
|
|
@@ -132,7 +134,7 @@ async function replLoop(session, io, renderer, checkpoints) {
|
|
|
132
134
|
}
|
|
133
135
|
if (trimmed === "/reload") {
|
|
134
136
|
const loaded = session.reloadProjectInstructions();
|
|
135
|
-
logger.print(
|
|
137
|
+
logger.print(theme.muted(loaded
|
|
136
138
|
? "reloaded project instructions (CRUXY.md)"
|
|
137
139
|
: "no project instructions found"));
|
|
138
140
|
continue;
|
|
@@ -140,7 +142,7 @@ async function replLoop(session, io, renderer, checkpoints) {
|
|
|
140
142
|
if (trimmed === "/plan") {
|
|
141
143
|
session.setPlanMode(!session.getPlanMode());
|
|
142
144
|
const on = session.getPlanMode();
|
|
143
|
-
logger.print(
|
|
145
|
+
logger.print(theme.muted(on
|
|
144
146
|
? "plan mode on — the next prompt proposes a plan for approval"
|
|
145
147
|
: "plan mode off"));
|
|
146
148
|
continue;
|
package/dist/components/frame.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveTheme } from "../theme/index.js";
|
|
1
2
|
/** Erase the current line and return the cursor to column 0 (same as U.2). */
|
|
2
3
|
const CLEAR_LINE = "\r\x1b[2K";
|
|
3
4
|
/** Move the cursor up one row. */
|
|
@@ -11,6 +12,7 @@ export function stripAnsi(text) {
|
|
|
11
12
|
}
|
|
12
13
|
export function createFrame(write, caps) {
|
|
13
14
|
let drawn = 0;
|
|
15
|
+
const ellipsis = resolveTheme(caps).glyph.ellipsis;
|
|
14
16
|
/**
|
|
15
17
|
* Truncate to width-1 (cursor rests after the last cell; a full-width row
|
|
16
18
|
* would auto-wrap on some terminals). Width is measured on VISIBLE
|
|
@@ -23,7 +25,7 @@ export function createFrame(write, caps) {
|
|
|
23
25
|
const plain = stripAnsi(line);
|
|
24
26
|
if (plain.length <= room)
|
|
25
27
|
return line;
|
|
26
|
-
return plain.slice(0, room - 1) +
|
|
28
|
+
return plain.slice(0, room - 1) + ellipsis;
|
|
27
29
|
};
|
|
28
30
|
const erase = () => {
|
|
29
31
|
if (drawn === 0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type Theme } from "../theme/index.js";
|
|
2
2
|
import { type ComponentIO, type InteractiveResult } from "./input.js";
|
|
3
3
|
/**
|
|
4
4
|
* Fuzzy finding (U.7): a deterministic, honest subsequence scorer (pure,
|
|
@@ -36,10 +36,10 @@ export interface RankedItem<T> {
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function rankItems<T>(items: readonly T[], toLabel: (item: T) => string, query: string): RankedItem<T>[];
|
|
38
38
|
/**
|
|
39
|
-
* Bold the matched characters of a label
|
|
40
|
-
*
|
|
39
|
+
* Bold the matched characters of a label via the theme's accent role. With
|
|
40
|
+
* color off (NO_COLOR, pipe) the roles are identity — plain text, zero ANSI.
|
|
41
41
|
*/
|
|
42
|
-
export declare function highlightMatch(label: string, positions: readonly number[],
|
|
42
|
+
export declare function highlightMatch(label: string, positions: readonly number[], theme: Theme): string;
|
|
43
43
|
export interface FuzzyFindOptions<T> {
|
|
44
44
|
/** Label an item filters/renders under. Required — items are opaque. */
|
|
45
45
|
toLabel: (item: T) => string;
|