@cruxy/cli 0.11.0 → 0.13.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 +26 -7
- package/dist/cli/commands/skills.js +12 -10
- package/dist/cli/program.js +7 -6
- package/dist/cli/repl.js +11 -9
- package/dist/cli/session-factory.d.ts +2 -1
- package/dist/cli/session-factory.js +10 -3
- 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/config/schema.d.ts +123 -0
- package/dist/config/schema.js +40 -0
- package/dist/errors/constructors.d.ts +21 -0
- package/dist/errors/constructors.js +58 -0
- package/dist/errors/format.js +8 -8
- package/dist/errors/types.d.ts +5 -0
- package/dist/errors/types.js +11 -0
- 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/sandbox/detect.d.ts +22 -0
- package/dist/sandbox/detect.js +67 -0
- package/dist/sandbox/docker-runtime.d.ts +32 -0
- package/dist/sandbox/docker-runtime.js +263 -0
- package/dist/sandbox/index.d.ts +7 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/policy.d.ts +17 -0
- package/dist/sandbox/policy.js +90 -0
- package/dist/sandbox/service.d.ts +57 -0
- package/dist/sandbox/service.js +64 -0
- package/dist/sandbox/types.d.ts +114 -0
- package/dist/sandbox/types.js +17 -0
- package/dist/subagent/orchestrator.d.ts +7 -0
- package/dist/subagent/orchestrator.js +22 -6
- package/dist/testing/run-tests-tool.d.ts +5 -1
- package/dist/testing/run-tests-tool.js +8 -1
- package/dist/testing/sandbox-runner.d.ts +16 -0
- package/dist/testing/sandbox-runner.js +47 -0
- 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/tools/shell/run-command.js +35 -1
- package/dist/tools/types.d.ts +10 -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,10 +1,11 @@
|
|
|
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
|
+
import { SandboxService } from "../../sandbox/index.js";
|
|
8
9
|
import { runInteractive } from "../repl.js";
|
|
9
10
|
import { buildAgentSession } from "../session-factory.js";
|
|
10
11
|
import { apiKeyEnvVar, maybeRunOnboarding } from "../onboard.js";
|
|
@@ -13,8 +14,10 @@ export function runCommand() {
|
|
|
13
14
|
.description("run cruxy on a prompt (one-shot), or with no prompt for an interactive session")
|
|
14
15
|
.argument("[prompt...]", "the task for cruxy to perform (omit for interactive)")
|
|
15
16
|
.option("--plan", "plan mode: propose a step-by-step plan for approval before executing")
|
|
17
|
+
.option("--sandbox", "run shell + test commands inside an isolated container (fails loud if no runtime)")
|
|
16
18
|
.action(async (promptParts, opts) => {
|
|
17
19
|
const prompt = promptParts.join(" ").trim();
|
|
20
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
18
21
|
const interactive = prompt === "";
|
|
19
22
|
// No prompt and stdin isn't a terminal: there's no way to read input and
|
|
20
23
|
// nothing to do — fail fast instead of hanging on a line that never comes.
|
|
@@ -23,8 +26,8 @@ export function runCommand() {
|
|
|
23
26
|
}
|
|
24
27
|
const { config, sources } = loadConfig();
|
|
25
28
|
let apiKey = resolveApiKey(config.model.provider);
|
|
26
|
-
logger.info(
|
|
27
|
-
logger.info(
|
|
29
|
+
logger.info(t.muted(`model: ${config.model.provider}/${config.model.model}`));
|
|
30
|
+
logger.info(t.muted(`config: ${sources.project ?? sources.global ?? "defaults"}`));
|
|
28
31
|
// First-run with no key (and a TTY) → guided onboarding instead of the
|
|
29
32
|
// dead-end auth error. The first-win demo is offered only in the no-prompt
|
|
30
33
|
// (REPL) path; with a real prompt, that prompt IS the first win.
|
|
@@ -41,7 +44,7 @@ export function runCommand() {
|
|
|
41
44
|
const result = await onboarding;
|
|
42
45
|
if (!result.completed) {
|
|
43
46
|
// Aborted/failed mid-setup — guidance already shown; exit cleanly.
|
|
44
|
-
logger.print(
|
|
47
|
+
logger.print(t.muted("run `cruxy login` to finish setup."));
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
47
50
|
apiKey = result.apiKey ?? resolveApiKey(config.model.provider);
|
|
@@ -60,7 +63,23 @@ export function runCommand() {
|
|
|
60
63
|
const checkpoints = config.checkpoint.enabled
|
|
61
64
|
? new CheckpointService({ root: process.cwd(), config })
|
|
62
65
|
: undefined;
|
|
63
|
-
|
|
66
|
+
// Sandbox (C.16): opt-in via --sandbox or sandbox.enabled. Resolving the
|
|
67
|
+
// service probes the runtime and THROWS CRUXY_E_SANDBOX_UNAVAILABLE if it
|
|
68
|
+
// is missing — fail loud here, before the agent starts, rather than
|
|
69
|
+
// silently running un-sandboxed. When off, ctx.sandbox stays undefined and
|
|
70
|
+
// execution runs on the host, unchanged.
|
|
71
|
+
const sandboxEnabled = opts.sandbox ?? config.sandbox.enabled;
|
|
72
|
+
const sandbox = sandboxEnabled
|
|
73
|
+
? await SandboxService.create({
|
|
74
|
+
config,
|
|
75
|
+
cwd: process.cwd(),
|
|
76
|
+
reporter: renderer,
|
|
77
|
+
})
|
|
78
|
+
: undefined;
|
|
79
|
+
if (sandbox) {
|
|
80
|
+
logger.info(t.muted(`sandbox: ${sandbox.runtimeName} (network ${config.sandbox.network})`));
|
|
81
|
+
}
|
|
82
|
+
const session = buildAgentSession(config, apiKey, process.cwd(), Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox);
|
|
64
83
|
if (interactive) {
|
|
65
84
|
await runInteractive(session, undefined, renderer, checkpoints);
|
|
66
85
|
return;
|
|
@@ -69,7 +88,7 @@ export function runCommand() {
|
|
|
69
88
|
// One-shot: a single turn, then exit. Preserves scripting/pipe use.
|
|
70
89
|
// Assistant text streams to stdout delta by delta (same as the REPL);
|
|
71
90
|
// piped output degrades to the plain renderer (no ANSI, chrome on stderr).
|
|
72
|
-
logger.print(`${
|
|
91
|
+
logger.print(`${t.accent("cruxy")} ${t.muted(t.glyph.caret)} ${prompt}\n`);
|
|
73
92
|
// Provider/network/auth failures propagate to the top-level boundary,
|
|
74
93
|
// which classifies them (e.g. CRUXY_E_GATEWAY_UNREACHABLE) and exits with
|
|
75
94
|
// 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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CruxyConfig } from "../config/index.js";
|
|
2
2
|
import type { ApprovalDecision } from "../approval/index.js";
|
|
3
3
|
import type { CheckpointService } from "../checkpoint/index.js";
|
|
4
|
+
import type { SandboxService } from "../sandbox/index.js";
|
|
4
5
|
import type { StreamRenderer } from "../render/index.js";
|
|
5
6
|
import { type ApproveAction } from "../tools/index.js";
|
|
6
7
|
import { Session } from "../agent/index.js";
|
|
@@ -23,4 +24,4 @@ export declare function withCheckpointGate(requestApproval: (action: ApproveActi
|
|
|
23
24
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
24
25
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
25
26
|
*/
|
|
26
|
-
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService): Session;
|
|
27
|
+
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService): Session;
|