@cruxy/cli 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/approval/classify.js +7 -3
  2. package/dist/approval/policy.d.ts +6 -0
  3. package/dist/approval/policy.js +15 -3
  4. package/dist/approval/types.d.ts +8 -1
  5. package/dist/checkpoint/index.d.ts +1 -0
  6. package/dist/checkpoint/index.js +1 -0
  7. package/dist/checkpoint/set.d.ts +44 -0
  8. package/dist/checkpoint/set.js +142 -0
  9. package/dist/checkpoint/types.d.ts +47 -0
  10. package/dist/cli/session-factory.js +11 -0
  11. package/dist/config/schema.d.ts +134 -8
  12. package/dist/config/schema.js +45 -1
  13. package/dist/errors/constructors.d.ts +66 -0
  14. package/dist/errors/constructors.js +186 -0
  15. package/dist/errors/types.d.ts +43 -0
  16. package/dist/errors/types.js +64 -0
  17. package/dist/sandbox/docker-runtime.js +4 -1
  18. package/dist/sandbox/policy.d.ts +12 -3
  19. package/dist/sandbox/policy.js +17 -3
  20. package/dist/sandbox/types.d.ts +10 -1
  21. package/dist/tools/file/paths.d.ts +10 -17
  22. package/dist/tools/file/paths.js +11 -58
  23. package/dist/web/demarcate.d.ts +13 -0
  24. package/dist/web/demarcate.js +78 -0
  25. package/dist/web/fetch.d.ts +11 -0
  26. package/dist/web/fetch.js +174 -0
  27. package/dist/web/index.d.ts +7 -0
  28. package/dist/web/index.js +7 -0
  29. package/dist/web/provider.d.ts +29 -0
  30. package/dist/web/provider.js +77 -0
  31. package/dist/web/search.d.ts +17 -0
  32. package/dist/web/search.js +42 -0
  33. package/dist/web/ssrf.d.ts +55 -0
  34. package/dist/web/ssrf.js +223 -0
  35. package/dist/web/tools.d.ts +20 -0
  36. package/dist/web/tools.js +81 -0
  37. package/dist/web/types.d.ts +62 -0
  38. package/dist/web/types.js +1 -0
  39. package/dist/workspace/index.d.ts +5 -0
  40. package/dist/workspace/index.js +3 -0
  41. package/dist/workspace/resolve.d.ts +54 -0
  42. package/dist/workspace/resolve.js +96 -0
  43. package/dist/workspace/select.d.ts +41 -0
  44. package/dist/workspace/select.js +44 -0
  45. package/dist/workspace/types.d.ts +30 -0
  46. package/dist/workspace/types.js +15 -0
  47. package/dist/workspace/workspace.d.ts +56 -0
  48. package/dist/workspace/workspace.js +180 -0
  49. package/package.json +2 -1
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Multi-repo workspace model (C.26). A session may operate across N repo/package
3
+ * roots. The {@link Workspace} is the single value that carries the declared root
4
+ * set; every path-taking tool resolves *through* it rather than through a bare
5
+ * `cwd` string.
6
+ *
7
+ * Two invariants the type system helps enforce:
8
+ * • The root set is **immutable for the session** — a {@link Workspace} exposes
9
+ * no mutator, so nothing the model can call adds a root. The set only grows by
10
+ * the CLI constructing a *new* Workspace from an explicit human act (argv, or
11
+ * an interactive add-root that prompts + trusts first).
12
+ * • Roots are addressed by a stable **name** (R1), matched **exactly** — never
13
+ * by prefix or nearest-match. An unknown name is a fail-loud refusal.
14
+ */
15
+ export {};
@@ -0,0 +1,56 @@
1
+ import type { DeclaredRoot, RootSpec } from "./types.js";
2
+ /**
3
+ * The declared workspace root set for a session (C.26). Immutable by design: it
4
+ * exposes readers only, no mutator. The root set grows solely by the CLI building
5
+ * a *new* Workspace from an explicit human act — so no tool, and nothing the model
6
+ * emits, can add a root. Roots are addressed by exact name (R1).
7
+ */
8
+ export declare class Workspace {
9
+ private readonly rootsByName;
10
+ private readonly ordered;
11
+ private readonly primaryRoot;
12
+ constructor(roots: readonly DeclaredRoot[]);
13
+ /** All declared roots, in declaration order. */
14
+ roots(): readonly DeclaredRoot[];
15
+ /** The primary root — the default for bare relative paths and git/instructions. */
16
+ primary(): DeclaredRoot;
17
+ /** True when more than one root is declared (i.e. a genuine multi-repo session). */
18
+ get isMultiRoot(): boolean;
19
+ /**
20
+ * Look up a root by EXACT name (R1). Never fuzzy-, prefix-, or nearest-matched
21
+ * — a silent near-match would be a cross-root misfire.
22
+ * @throws {CruxyError} CRUXY_E_ROOT_UNKNOWN if no root has that exact name.
23
+ */
24
+ rootByName(name: string): DeclaredRoot;
25
+ /** Like {@link rootByName} but returns undefined instead of throwing. */
26
+ tryRootByName(name: string): DeclaredRoot | undefined;
27
+ /**
28
+ * The single declared root that contains `absPath`. Roots never overlap (that's
29
+ * refused at declaration), so at most one can match.
30
+ * @throws CRUXY_E_ROOT_UNKNOWN if the path is inside no declared root.
31
+ * @throws CRUXY_E_ROOT_AMBIGUOUS if — defensively — it matches more than one.
32
+ */
33
+ rootContaining(absPath: string): DeclaredRoot;
34
+ }
35
+ /**
36
+ * Build a {@link Workspace} from declared specs (R1, ⚖︎#5). Each path is resolved
37
+ * against `process.cwd()`, must exist and be a directory, and the set must not
38
+ * overlap (a root nested in / equal to another is refused — declare the monorepo
39
+ * root OR its packages, never both). The first spec is primary unless one is
40
+ * marked. Names are explicit-or-basename, validated, and deduped.
41
+ *
42
+ * This is the ONLY constructor of a Workspace from user input; it is called by the
43
+ * CLI from argv and by the interactive add-root path — never from a tool.
44
+ *
45
+ * @throws CRUXY_E_ROOT_OVERLAP on a nested/overlapping/duplicate root.
46
+ * @throws CRUXY_E_USAGE on a missing path, non-directory, or bad/duplicate name.
47
+ */
48
+ export declare function buildWorkspace(specs: readonly RootSpec[], opts?: {
49
+ cwd?: string;
50
+ }): Promise<Workspace>;
51
+ /**
52
+ * Build a trivial single-root workspace from one absolute path — the back-compat
53
+ * bridge for the many call sites that still pass a single `cwd`. The one root is
54
+ * primary and named by its basename.
55
+ */
56
+ export declare function singleRootWorkspace(absPath: string): Workspace;
@@ -0,0 +1,180 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { rootAmbiguous, rootOverlap, rootUnknown, usageError, } from "../errors/index.js";
4
+ import { isInside } from "./resolve.js";
5
+ /**
6
+ * The declared workspace root set for a session (C.26). Immutable by design: it
7
+ * exposes readers only, no mutator. The root set grows solely by the CLI building
8
+ * a *new* Workspace from an explicit human act — so no tool, and nothing the model
9
+ * emits, can add a root. Roots are addressed by exact name (R1).
10
+ */
11
+ export class Workspace {
12
+ rootsByName;
13
+ ordered;
14
+ primaryRoot;
15
+ constructor(roots) {
16
+ if (roots.length === 0) {
17
+ throw usageError("a workspace needs at least one root");
18
+ }
19
+ const byName = new Map();
20
+ for (const r of roots)
21
+ byName.set(r.name, Object.freeze({ ...r }));
22
+ const primary = roots.filter((r) => r.primary);
23
+ if (primary.length !== 1) {
24
+ throw usageError(`a workspace must have exactly one primary root (found ${primary.length})`);
25
+ }
26
+ this.ordered = Object.freeze([...roots]);
27
+ this.rootsByName = byName;
28
+ this.primaryRoot = primary[0];
29
+ }
30
+ /** All declared roots, in declaration order. */
31
+ roots() {
32
+ return this.ordered;
33
+ }
34
+ /** The primary root — the default for bare relative paths and git/instructions. */
35
+ primary() {
36
+ return this.primaryRoot;
37
+ }
38
+ /** True when more than one root is declared (i.e. a genuine multi-repo session). */
39
+ get isMultiRoot() {
40
+ return this.ordered.length > 1;
41
+ }
42
+ /**
43
+ * Look up a root by EXACT name (R1). Never fuzzy-, prefix-, or nearest-matched
44
+ * — a silent near-match would be a cross-root misfire.
45
+ * @throws {CruxyError} CRUXY_E_ROOT_UNKNOWN if no root has that exact name.
46
+ */
47
+ rootByName(name) {
48
+ const found = this.rootsByName.get(name);
49
+ if (!found) {
50
+ throw rootUnknown(name, this.ordered.map((r) => r.name));
51
+ }
52
+ return found;
53
+ }
54
+ /** Like {@link rootByName} but returns undefined instead of throwing. */
55
+ tryRootByName(name) {
56
+ return this.rootsByName.get(name);
57
+ }
58
+ /**
59
+ * The single declared root that contains `absPath`. Roots never overlap (that's
60
+ * refused at declaration), so at most one can match.
61
+ * @throws CRUXY_E_ROOT_UNKNOWN if the path is inside no declared root.
62
+ * @throws CRUXY_E_ROOT_AMBIGUOUS if — defensively — it matches more than one.
63
+ */
64
+ rootContaining(absPath) {
65
+ const target = path.resolve(absPath);
66
+ const hits = this.ordered.filter((r) => isInside(r.absPath, target));
67
+ if (hits.length === 0) {
68
+ throw rootUnknown(absPath, this.ordered.map((r) => r.name));
69
+ }
70
+ if (hits.length > 1) {
71
+ // Unreachable while overlap is refused at declaration — belt-and-suspenders.
72
+ throw rootAmbiguous(absPath, hits.map((r) => r.name));
73
+ }
74
+ return hits[0];
75
+ }
76
+ }
77
+ /** A valid root name: non-empty, no path separators, not a traversal token. */
78
+ function assertValidName(name) {
79
+ if (name.length === 0 ||
80
+ name.includes("/") ||
81
+ name.includes("\\") ||
82
+ name === "." ||
83
+ name === "..") {
84
+ throw usageError(`invalid workspace root name "${name}"`, [
85
+ "root names must be non-empty and contain no path separators",
86
+ ]);
87
+ }
88
+ }
89
+ /** Derive a stable, unique name from a path basename, deduping with -2, -3, … */
90
+ function uniqueBasename(absPath, taken) {
91
+ const base = path.basename(absPath) || "root";
92
+ if (!taken.has(base))
93
+ return base;
94
+ for (let i = 2;; i++) {
95
+ const candidate = `${base}-${i}`;
96
+ if (!taken.has(candidate))
97
+ return candidate;
98
+ }
99
+ }
100
+ /**
101
+ * Build a {@link Workspace} from declared specs (R1, ⚖︎#5). Each path is resolved
102
+ * against `process.cwd()`, must exist and be a directory, and the set must not
103
+ * overlap (a root nested in / equal to another is refused — declare the monorepo
104
+ * root OR its packages, never both). The first spec is primary unless one is
105
+ * marked. Names are explicit-or-basename, validated, and deduped.
106
+ *
107
+ * This is the ONLY constructor of a Workspace from user input; it is called by the
108
+ * CLI from argv and by the interactive add-root path — never from a tool.
109
+ *
110
+ * @throws CRUXY_E_ROOT_OVERLAP on a nested/overlapping/duplicate root.
111
+ * @throws CRUXY_E_USAGE on a missing path, non-directory, or bad/duplicate name.
112
+ */
113
+ export async function buildWorkspace(specs, opts = {}) {
114
+ if (specs.length === 0) {
115
+ throw usageError("no workspace roots declared");
116
+ }
117
+ const baseCwd = opts.cwd ?? process.cwd();
118
+ const taken = new Set();
119
+ const resolved = [];
120
+ for (const spec of specs) {
121
+ const absPath = path.resolve(baseCwd, spec.path);
122
+ let stat;
123
+ try {
124
+ stat = await fs.stat(absPath);
125
+ }
126
+ catch {
127
+ throw usageError(`workspace root does not exist: ${spec.path}`, [
128
+ `resolved to ${absPath}`,
129
+ ]);
130
+ }
131
+ if (!stat.isDirectory()) {
132
+ throw usageError(`workspace root is not a directory: ${spec.path}`, [
133
+ `resolved to ${absPath}`,
134
+ ]);
135
+ }
136
+ if (spec.name !== undefined) {
137
+ assertValidName(spec.name);
138
+ if (taken.has(spec.name)) {
139
+ throw usageError(`duplicate workspace root name "${spec.name}"`);
140
+ }
141
+ taken.add(spec.name);
142
+ }
143
+ resolved.push({ absPath, name: spec.name });
144
+ }
145
+ // Overlap/nesting refusal (⚖︎#5): any pair where one contains or equals the
146
+ // other is rejected at declaration.
147
+ for (let i = 0; i < resolved.length; i++) {
148
+ for (let j = i + 1; j < resolved.length; j++) {
149
+ const a = resolved[i].absPath;
150
+ const b = resolved[j].absPath;
151
+ if (a === b || isInside(a, b) || isInside(b, a)) {
152
+ throw rootOverlap(a, b);
153
+ }
154
+ }
155
+ }
156
+ // Assign names (explicit first so basenames dedupe around them), then declare.
157
+ for (const r of resolved) {
158
+ if (r.name === undefined) {
159
+ r.name = uniqueBasename(r.absPath, taken);
160
+ taken.add(r.name);
161
+ }
162
+ }
163
+ const declared = resolved.map((r, i) => ({
164
+ name: r.name,
165
+ absPath: r.absPath,
166
+ primary: i === 0,
167
+ }));
168
+ return new Workspace(declared);
169
+ }
170
+ /**
171
+ * Build a trivial single-root workspace from one absolute path — the back-compat
172
+ * bridge for the many call sites that still pass a single `cwd`. The one root is
173
+ * primary and named by its basename.
174
+ */
175
+ export function singleRootWorkspace(absPath) {
176
+ const abs = path.resolve(absPath);
177
+ return new Workspace([
178
+ { name: path.basename(abs) || "root", absPath: abs, primary: true },
179
+ ]);
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "fastembed": "^2.1.0",
35
35
  "picocolors": "^1.1.1",
36
36
  "tinyglobby": "^0.2.10",
37
+ "undici": "^6.21.0",
37
38
  "zod": "^3.23.8",
38
39
  "zod-to-json-schema": "^3.23.5",
39
40
  "@cruxy/sdk": "0.2.0"