@intentius/chant 0.19.1 → 0.20.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/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +210 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2
2
|
import { join, basename } from "path";
|
|
3
|
+
import { auditIntrinsics } from "./check-lexicon-intrinsics";
|
|
4
|
+
import { checkExamplesBuild } from "./check-lexicon-examples";
|
|
3
5
|
|
|
4
6
|
// ── Types ────────────────────────────────────────────────────────────
|
|
5
7
|
|
|
@@ -65,7 +67,7 @@ function countSubdirs(dir: string): number {
|
|
|
65
67
|
/**
|
|
66
68
|
* Run all completeness checks against a lexicon directory.
|
|
67
69
|
*/
|
|
68
|
-
export function checkLexicon(dir: string): CheckResult {
|
|
70
|
+
export async function checkLexicon(dir: string): Promise<CheckResult> {
|
|
69
71
|
const items: CheckItem[] = [];
|
|
70
72
|
|
|
71
73
|
// ── Tier 1: Required ───────────────────────────────────────────
|
|
@@ -117,6 +119,70 @@ export function checkLexicon(dir: string): CheckResult {
|
|
|
117
119
|
pass: existsSync(join(dir, "dist/manifest.json")),
|
|
118
120
|
});
|
|
119
121
|
|
|
122
|
+
// chant #1067 — chantVersion was "optional, unchecked": a lexicon could
|
|
123
|
+
// declare (or omit) a core-version constraint and nothing failed. This
|
|
124
|
+
// does not validate compatibility against the running core version — that
|
|
125
|
+
// needs a live check at plugin-load time (a different tool: candidates are
|
|
126
|
+
// `loadPlugin`/`loadPlugins` in ../plugins.ts), which is a deliberate
|
|
127
|
+
// non-goal here (see PR description). It closes the narrower gap that the
|
|
128
|
+
// field can be silently absent or malformed in the shipped manifest.
|
|
129
|
+
const manifestJson = readOr(join(dir, "dist/manifest.json"));
|
|
130
|
+
let chantVersionOk = false;
|
|
131
|
+
let chantVersionDetail: string | undefined;
|
|
132
|
+
if (manifestJson) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(manifestJson) as { chantVersion?: unknown };
|
|
135
|
+
const cv = parsed.chantVersion;
|
|
136
|
+
chantVersionOk = typeof cv === "string" && /\d/.test(cv);
|
|
137
|
+
chantVersionDetail = chantVersionOk ? `chantVersion: ${cv as string}` : `chantVersion: ${JSON.stringify(cv)}`;
|
|
138
|
+
} catch {
|
|
139
|
+
chantVersionDetail = "dist/manifest.json is not valid JSON";
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
chantVersionDetail = "dist/manifest.json missing or unreadable";
|
|
143
|
+
}
|
|
144
|
+
items.push({
|
|
145
|
+
name: "dist/manifest.json declares a chantVersion",
|
|
146
|
+
tier: 1,
|
|
147
|
+
pass: chantVersionOk,
|
|
148
|
+
detail: chantVersionDetail,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// chant #1067 — packaging shape ("all 11 lexicons route exports['.'].default
|
|
152
|
+
// at ./src/index.ts and delete emitted JS in build") was real but
|
|
153
|
+
// unenforced; a lexicon shipping differently would break consumers
|
|
154
|
+
// silently. Cheap structural check on package.json, not a build execution.
|
|
155
|
+
const packageJsonRaw = readOr(join(dir, "package.json"));
|
|
156
|
+
let packagingOk = false;
|
|
157
|
+
let packagingDetail: string | undefined;
|
|
158
|
+
if (packageJsonRaw) {
|
|
159
|
+
try {
|
|
160
|
+
const pkg = JSON.parse(packageJsonRaw) as {
|
|
161
|
+
exports?: { "."?: { default?: string } };
|
|
162
|
+
scripts?: { build?: string };
|
|
163
|
+
};
|
|
164
|
+
const defaultExport = pkg.exports?.["."]?.default;
|
|
165
|
+
const buildScript = pkg.scripts?.build ?? "";
|
|
166
|
+
const exportOk = defaultExport === "./src/index.ts";
|
|
167
|
+
const deletesEmittedJs = /-delete/.test(buildScript) && /\.js/.test(buildScript);
|
|
168
|
+
packagingOk = exportOk && deletesEmittedJs;
|
|
169
|
+
const problems: string[] = [];
|
|
170
|
+
if (!exportOk) problems.push(`exports["."].default is ${JSON.stringify(defaultExport)}, expected "./src/index.ts"`);
|
|
171
|
+
if (!deletesEmittedJs) problems.push(`build script doesn't delete emitted .js from dist/: ${JSON.stringify(buildScript)}`);
|
|
172
|
+
packagingDetail = problems.length > 0 ? problems.join("; ") : undefined;
|
|
173
|
+
} catch {
|
|
174
|
+
packagingDetail = "package.json is not valid JSON";
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
packagingDetail = "package.json missing or unreadable";
|
|
178
|
+
}
|
|
179
|
+
items.push({
|
|
180
|
+
name: 'package.json routes exports["."].default at ./src/index.ts and deletes emitted JS in build',
|
|
181
|
+
tier: 1,
|
|
182
|
+
pass: packagingOk,
|
|
183
|
+
detail: packagingDetail,
|
|
184
|
+
});
|
|
185
|
+
|
|
120
186
|
const exampleCount = countSubdirs(join(dir, "examples"));
|
|
121
187
|
items.push({
|
|
122
188
|
name: "At least 1 example in examples/",
|
|
@@ -125,6 +191,58 @@ export function checkLexicon(dir: string): CheckResult {
|
|
|
125
191
|
detail: exampleCount > 0 ? `${exampleCount} example(s)` : undefined,
|
|
126
192
|
});
|
|
127
193
|
|
|
194
|
+
// chant #1067 — every shipped example must actually build. The prior
|
|
195
|
+
// checks only ever counted example directories; none tried to build one,
|
|
196
|
+
// so `lexicons/aws/examples/core-concepts` shipped with a discovery-time
|
|
197
|
+
// "Duplicate export name" error while this tool reported "All tier-1
|
|
198
|
+
// checks passed." See ./check-lexicon-examples.ts for exactly what
|
|
199
|
+
// "builds" means here (discovery + serialization, not post-synth/lint).
|
|
200
|
+
const exampleBuilds = await checkExamplesBuild(dir);
|
|
201
|
+
const brokenExamples = exampleBuilds.filter((e) => !e.ok);
|
|
202
|
+
items.push({
|
|
203
|
+
name: "Every shipped example builds",
|
|
204
|
+
tier: 1,
|
|
205
|
+
pass: brokenExamples.length === 0,
|
|
206
|
+
detail:
|
|
207
|
+
brokenExamples.length > 0
|
|
208
|
+
? brokenExamples.map((e) => `${e.example}: ${e.detail}`).join(" | ")
|
|
209
|
+
: exampleBuilds.length > 0
|
|
210
|
+
? `${exampleBuilds.length} example(s) built`
|
|
211
|
+
: undefined,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// chant #1067 — foldability (isTag) is required and validated, not an
|
|
215
|
+
// optional flag nobody checks. #1039 shipped with it wrong in both
|
|
216
|
+
// directions (aws's Sub missing isTag; gitlab's reference() claiming
|
|
217
|
+
// isTag: true for a plain call) and nothing caught either half. See
|
|
218
|
+
// ./check-lexicon-intrinsics.ts for what "exported"/"matches" mean.
|
|
219
|
+
const intrinsicAudit = auditIntrinsics(dir);
|
|
220
|
+
const unexportedIntrinsics = intrinsicAudit.filter((i) => !i.exported);
|
|
221
|
+
items.push({
|
|
222
|
+
name: "Registered intrinsics are exported by the package",
|
|
223
|
+
tier: 1,
|
|
224
|
+
pass: unexportedIntrinsics.length === 0,
|
|
225
|
+
detail:
|
|
226
|
+
unexportedIntrinsics.length > 0
|
|
227
|
+
? unexportedIntrinsics.map((i) => i.detail).join(" | ")
|
|
228
|
+
: intrinsicAudit.length > 0
|
|
229
|
+
? `${intrinsicAudit.length} intrinsic(s) checked`
|
|
230
|
+
: undefined,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const mismatchedIntrinsics = intrinsicAudit.filter((i) => !i.ok);
|
|
234
|
+
items.push({
|
|
235
|
+
name: "Registered intrinsics' isTag matches how they're authored",
|
|
236
|
+
tier: 1,
|
|
237
|
+
pass: mismatchedIntrinsics.length === 0,
|
|
238
|
+
detail:
|
|
239
|
+
mismatchedIntrinsics.length > 0
|
|
240
|
+
? mismatchedIntrinsics.map((i) => i.detail).join(" | ")
|
|
241
|
+
: intrinsicAudit.length > 0
|
|
242
|
+
? `${intrinsicAudit.length} intrinsic(s) checked`
|
|
243
|
+
: undefined,
|
|
244
|
+
});
|
|
245
|
+
|
|
128
246
|
const hasPluginTest = findFiles(join(dir, "src"), (n) => n === "plugin.test.ts").length > 0;
|
|
129
247
|
items.push({
|
|
130
248
|
name: "plugin.test.ts exists",
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -39,6 +39,26 @@ export function isLintRule(value: unknown): value is LintRule {
|
|
|
39
39
|
/**
|
|
40
40
|
* Load custom lint rules from plugin files.
|
|
41
41
|
* Each plugin file is dynamically imported and all exports conforming to LintRule are collected.
|
|
42
|
+
*
|
|
43
|
+
* chant #1051 — this (and `loadLocalRules`, `../../lint/rule-loader.ts`, for
|
|
44
|
+
* `.chant/rules/*.ts`) is unconditional project-source `import()` with no
|
|
45
|
+
* `--sandbox` equivalent, the same class of gap #1051 closes for
|
|
46
|
+
* `discoverComponents`. Deliberately NOT sandboxed here, and not a "decide by
|
|
47
|
+
* omission": a `LintRule` is not a one-shot data producer like `Component`/
|
|
48
|
+
* `Declarable` — its `check(context)` is a function the rule engine invokes
|
|
49
|
+
* inline, once per linted file, for the whole `chant lint` run. There is no
|
|
50
|
+
* cheap "collect once, JSON-serialize the result, hand it back" shape the way
|
|
51
|
+
* there is for a `Component` (plain JSON) or an entity (a wire codec) —
|
|
52
|
+
* `check` has to keep running as live JS in whichever process calls it. A
|
|
53
|
+
* shallow fix that sandboxed only the *import* step (mirroring
|
|
54
|
+
* `discoverComponents`) would give a false sense of safety: it would close
|
|
55
|
+
* off "malicious top-level module code" but leave `check()`'s own executable
|
|
56
|
+
* body — the dominant part of a rule's attack surface, since it runs for
|
|
57
|
+
* every file, every lint invocation — entirely unsandboxed regardless. A real
|
|
58
|
+
* fix needs either the AST/`ts.SourceFile` itself to cross the sandbox
|
|
59
|
+
* boundary or the whole custom-rule evaluation to run inside the child; both
|
|
60
|
+
* are materially harder, separate design problems from this issue's `Component`
|
|
61
|
+
* fix and are tracked separately (chant #1052).
|
|
42
62
|
*/
|
|
43
63
|
export async function loadPluginRules(
|
|
44
64
|
plugins: string[],
|
|
@@ -122,6 +142,13 @@ export interface LintOptions {
|
|
|
122
142
|
format: "stylish" | "json" | "sarif";
|
|
123
143
|
/** Rules to use (defaults to all) */
|
|
124
144
|
rules?: LintRule[];
|
|
145
|
+
/**
|
|
146
|
+
* chant #1051 — opt-in: discover `*.component.ts` files (for the COMP*
|
|
147
|
+
* composition checks) in a sandboxed child process instead of the CLI's
|
|
148
|
+
* own process (`chant lint --sandbox`). See `discoverComponents`'s
|
|
149
|
+
* `sandbox` option (../../components/discover.ts).
|
|
150
|
+
*/
|
|
151
|
+
sandbox?: boolean;
|
|
125
152
|
}
|
|
126
153
|
|
|
127
154
|
/**
|
|
@@ -296,6 +323,7 @@ async function resolveRegistryContext(
|
|
|
296
323
|
|
|
297
324
|
async function runComponentCheckDiagnostics(
|
|
298
325
|
infraPath: string,
|
|
326
|
+
sandbox?: boolean,
|
|
299
327
|
): Promise<{ diagnostics: LintDiagnostic[]; suppressed: Array<LintDiagnostic & { reason?: string }> }> {
|
|
300
328
|
// Discovery stays scoped to the lint arg; COMP* severity overrides come from
|
|
301
329
|
// the project-root config, same as the AST-rule pass.
|
|
@@ -304,7 +332,7 @@ async function runComponentCheckDiagnostics(
|
|
|
304
332
|
const allCheckIds = new Set(checks.map((c) => c.id));
|
|
305
333
|
|
|
306
334
|
const registryContext = await resolveRegistryContext(infraPath);
|
|
307
|
-
const raw = await runComponentChecks(infraPath, checks, registryContext);
|
|
335
|
+
const raw = await runComponentChecks(infraPath, checks, registryContext, sandbox);
|
|
308
336
|
|
|
309
337
|
const fileDirectivesCache = new Map<string, ReturnType<typeof parseDisableComments>>();
|
|
310
338
|
const fileLevelDisable = (
|
|
@@ -439,7 +467,7 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
439
467
|
// structurally distinct check family (whole-project, post-discovery,
|
|
440
468
|
// see ../../lint/component-checks.ts) but the same `chant lint` output and
|
|
441
469
|
// the same error-severity gating as every COR/EVL diagnostic.
|
|
442
|
-
const componentResult = await runComponentCheckDiagnostics(infraPath);
|
|
470
|
+
const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox);
|
|
443
471
|
diagnostics.push(...componentResult.diagnostics);
|
|
444
472
|
suppressed.push(...componentResult.suppressed);
|
|
445
473
|
|
|
@@ -487,7 +515,7 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
487
515
|
// `*.component.ts` file on their behalf), but a fix applied to another
|
|
488
516
|
// rule could still be in the same file a component was discovered from —
|
|
489
517
|
// re-run for consistency with the AST re-lint above.
|
|
490
|
-
const postComponentResult = await runComponentCheckDiagnostics(infraPath);
|
|
518
|
+
const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox);
|
|
491
519
|
diagnostics.push(...postComponentResult.diagnostics);
|
|
492
520
|
suppressed.push(...postComponentResult.suppressed);
|
|
493
521
|
}
|
|
@@ -307,7 +307,7 @@ export async function printOnboardResult(result: OnboardResult, name: string): P
|
|
|
307
307
|
console.log("");
|
|
308
308
|
console.log("Lexicon completeness:");
|
|
309
309
|
const { checkLexicon, printCheckResult } = await import("./check-lexicon");
|
|
310
|
-
const checkResult = checkLexicon(lexiconDir);
|
|
310
|
+
const checkResult = await checkLexicon(lexiconDir);
|
|
311
311
|
printCheckResult(checkResult, false);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, writeFile, rm, realpath } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, dirname, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* chant #1051 — regression test for `discoverComponents`'s exposure:
|
|
11
|
+
* `packages/core/src/components/discover.ts` used to `await import()` every
|
|
12
|
+
* `*.component.ts` file unconditionally, in the CLI's own process, with no
|
|
13
|
+
* `--fold`/`--sandbox` equivalent at all — the same class of bug #1045 fixed
|
|
14
|
+
* for lexicon-resource discovery, but for the parallel `*.component.ts`
|
|
15
|
+
* convention (`chant list --components`, `chant describe --components`,
|
|
16
|
+
* `chant graph --components`, `chant build --components --generate`, `chant
|
|
17
|
+
* run --components`, `chant lint`'s COMP* checks, `chant lifecycle
|
|
18
|
+
* diff/plan --live`).
|
|
19
|
+
*
|
|
20
|
+
* Deliberately an end-to-end test of the real CLI entry point (`main.ts`,
|
|
21
|
+
* spawned as a genuine child process — the same way `bin/chant` invokes it),
|
|
22
|
+
* NOT a call to `discoverComponents()`/`listComponents()` directly: calling
|
|
23
|
+
* either of those skips the exact thing this test exists to prove — that the
|
|
24
|
+
* `--sandbox` flag is actually wired from `chant`'s argv, through `main.ts`,
|
|
25
|
+
* into the CLI handler, into `discoverComponents`'s `sandbox` option. This
|
|
26
|
+
* mirrors `./security-boundary.test.ts`'s rationale (the #1045 Phase 2 bypass
|
|
27
|
+
* was caught precisely because a differential calling `build()` directly
|
|
28
|
+
* couldn't see a hole in the CLI layer above it).
|
|
29
|
+
*
|
|
30
|
+
* Evidence is routed through `chant list --components`'s own JSON stdout (a
|
|
31
|
+
* component NAME), never stderr and never a bare "did it print EXFIL" check —
|
|
32
|
+
* a suppressed/swallowed console message from the sandboxed child can't fake
|
|
33
|
+
* this: the hostile fixture always exports a syntactically valid `Component`
|
|
34
|
+
* (wrapping every privileged attempt in try/catch, so discovery never simply
|
|
35
|
+
* errors out) whose `name` field encodes exactly what it managed to do:
|
|
36
|
+
* "OK" for each of read/write/spawn that succeeded, or the Permission
|
|
37
|
+
* Model's real `err.code` when denied, plus the ambient env var count —
|
|
38
|
+
* mirroring how the #1045 PR itself was verified ("unsandboxed produced
|
|
39
|
+
* read-u-env77, sandboxed produced blocked-erraccessdenied-env2").
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const mainTsPath = resolve(thisDir, "main.ts");
|
|
44
|
+
const repoRoot = resolve(thisDir, "../../../..");
|
|
45
|
+
|
|
46
|
+
describe("CLI end-to-end — discoverComponents must not execute *.component.ts unsandboxed when --sandbox is passed", () => {
|
|
47
|
+
let testDir: string;
|
|
48
|
+
let escapedMarkerPath: string;
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
testDir = await realpath(
|
|
52
|
+
await (async () => {
|
|
53
|
+
const dir = join(tmpdir(), `chant-component-security-boundary-test-${Date.now()}-${Math.random()}`);
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
return dir;
|
|
56
|
+
})(),
|
|
57
|
+
);
|
|
58
|
+
// Outside the project directory chant scans — the sandbox's
|
|
59
|
+
// `--allow-fs-write` allowance (there isn't one) and `--allow-fs-read`
|
|
60
|
+
// scope (project dir + bundle dir only) must both reject this.
|
|
61
|
+
escapedMarkerPath = join(testDir, "..", `chant-component-escaped-${Date.now()}.txt`);
|
|
62
|
+
|
|
63
|
+
await writeFile(join(testDir, "chant.config.ts"), "export default {};\n");
|
|
64
|
+
|
|
65
|
+
// A hostile component file: attempts a filesystem read outside the
|
|
66
|
+
// project, a filesystem write outside the project, and a child-process
|
|
67
|
+
// spawn, all at module top level (before its one real Component export),
|
|
68
|
+
// wrapping each attempt so a permission denial never throws discovery
|
|
69
|
+
// into "import error" — every signal is folded into the component's own
|
|
70
|
+
// `name`, which is what `chant list --components --format json` prints
|
|
71
|
+
// to STDOUT, so suppressed child stderr/console output cannot fake a
|
|
72
|
+
// pass here.
|
|
73
|
+
await writeFile(
|
|
74
|
+
join(testDir, "evil.component.ts"),
|
|
75
|
+
[
|
|
76
|
+
'import { readFileSync, writeFileSync } from "node:fs";',
|
|
77
|
+
'import { execSync } from "node:child_process";',
|
|
78
|
+
"",
|
|
79
|
+
"function attempt(label, fn) {",
|
|
80
|
+
" try {",
|
|
81
|
+
" fn();",
|
|
82
|
+
' return label + "OK";',
|
|
83
|
+
" } catch (err) {",
|
|
84
|
+
' const code = err && err.code ? String(err.code).replace(/[^A-Za-z0-9]/g, "") : "ERR";',
|
|
85
|
+
" return label + code;",
|
|
86
|
+
" }",
|
|
87
|
+
"}",
|
|
88
|
+
"",
|
|
89
|
+
`const readTag = attempt("read", () => { readFileSync("/etc/hosts", "utf-8"); });`,
|
|
90
|
+
`const writeTag = attempt("write", () => { writeFileSync(${JSON.stringify(escapedMarkerPath)}, "pwned"); });`,
|
|
91
|
+
`const spawnTag = attempt("spawn", () => { execSync("true"); });`,
|
|
92
|
+
"const envCount = Object.keys(process.env).length;",
|
|
93
|
+
"",
|
|
94
|
+
"export const probe = {",
|
|
95
|
+
" name: `probe-${readTag}-${writeTag}-${spawnTag}-env${envCount}`,",
|
|
96
|
+
" dependsOn: [],",
|
|
97
|
+
' deploy: [{ phase: "Apply", steps: [{ kind: "shell", command: "echo ok" }] }],',
|
|
98
|
+
"};",
|
|
99
|
+
"",
|
|
100
|
+
].join("\n"),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
await rm(testDir, { recursive: true, force: true });
|
|
106
|
+
await rm(escapedMarkerPath, { force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Spawn the real CLI (`main.ts`, exactly as `bin/chant` invokes it via `npx tsx`) and capture its output. */
|
|
110
|
+
function runCli(args: string[]): { status: number | null; stdout: string; stderr: string } {
|
|
111
|
+
const result = spawnSync("npx", ["tsx", mainTsPath, ...args], {
|
|
112
|
+
cwd: repoRoot,
|
|
113
|
+
encoding: "utf-8",
|
|
114
|
+
timeout: 30_000,
|
|
115
|
+
env: { ...process.env, CHANT_SECRET_FOR_TEST: "should-never-leak" },
|
|
116
|
+
});
|
|
117
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Pull the discovered component's `name` field out of `chant list --components --format json`'s stdout. */
|
|
121
|
+
function probeNameFrom(stdout: string): string {
|
|
122
|
+
const parsed = JSON.parse(stdout) as Array<{ name: string }>;
|
|
123
|
+
const found = parsed.find((c) => c.name.startsWith("probe-"));
|
|
124
|
+
expect(found, `expected a "probe-*" component in stdout: ${stdout}`).toBeDefined();
|
|
125
|
+
return found!.name;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test("without --sandbox, the hostile *.component.ts file executes fully (establishes the fixture is genuinely hostile)", () => {
|
|
129
|
+
const { status, stdout } = runCli(["list", testDir, "--components", "--format", "json"]);
|
|
130
|
+
expect(status).toBe(0);
|
|
131
|
+
|
|
132
|
+
const name = probeNameFrom(stdout);
|
|
133
|
+
expect(name).toMatch(/^probe-readOK-writeOK-spawnOK-env\d+$/);
|
|
134
|
+
|
|
135
|
+
// A real env var count, not the sandbox's scrubbed-to-just-PATH count.
|
|
136
|
+
const envCount = Number(name.match(/env(\d+)$/)![1]);
|
|
137
|
+
expect(envCount).toBeGreaterThan(2);
|
|
138
|
+
|
|
139
|
+
expect(existsSync(escapedMarkerPath), "hostile file's write should have landed unsandboxed").toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("with --sandbox, the hostile *.component.ts file is denied filesystem read/write, process spawn, and sees a scrubbed environment", () => {
|
|
143
|
+
const { status, stdout } = runCli(["list", testDir, "--components", "--format", "json", "--sandbox"]);
|
|
144
|
+
expect(status).toBe(0);
|
|
145
|
+
|
|
146
|
+
const name = probeNameFrom(stdout);
|
|
147
|
+
expect(name).toMatch(/^probe-readERRACCESSDENIED-writeERRACCESSDENIED-spawnERRACCESSDENIED-env\d+$/);
|
|
148
|
+
|
|
149
|
+
// Only PATH (deliberately kept, see `./sandbox/run.ts`) plus whatever the
|
|
150
|
+
// OS itself injects into every child regardless of the `env` passed to
|
|
151
|
+
// `fork()` (macOS adds `__CF_USER_TEXT_ENCODING`) survive the spawn-time
|
|
152
|
+
// env scrub — a small constant, nowhere near the ambient environment's
|
|
153
|
+
// real size. Mirrors the #1045 PR's own reported number for this exact
|
|
154
|
+
// shape of check ("blocked-erraccessdenied-env2").
|
|
155
|
+
const envCount = Number(name.match(/env(\d+)$/)![1]);
|
|
156
|
+
expect(envCount).toBeLessThanOrEqual(2);
|
|
157
|
+
|
|
158
|
+
expect(existsSync(escapedMarkerPath), "sandboxed write must not have landed").toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("`chant lint --sandbox` (COMP* checks) also does not let the hostile file's write land", () => {
|
|
162
|
+
const { status } = runCli(["lint", testDir, "--sandbox", "--format", "json"]);
|
|
163
|
+
// Not asserting on exit code (COMP* rules may or may not flag this
|
|
164
|
+
// fixture) — the write escaping the sandbox is the actual property
|
|
165
|
+
// under test, matching how `./security-boundary.test.ts` treats `chant
|
|
166
|
+
// lint`'s coverage of the same discovery path.
|
|
167
|
+
expect(status === 0 || status === 1).toBe(true);
|
|
168
|
+
expect(existsSync(escapedMarkerPath)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -21,9 +21,12 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
21
21
|
// plugins (those implementing `generateComponentPipeline`, #688), not a
|
|
22
22
|
// hard-coded core list — `generateComponentsPipeline` returns a descriptive
|
|
23
23
|
// error when the target lexicon has no generator.
|
|
24
|
-
const result = await generateComponentsPipeline(
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const result = await generateComponentsPipeline(
|
|
25
|
+
args.path,
|
|
26
|
+
lexicon,
|
|
27
|
+
{ env: args.env },
|
|
28
|
+
args.sandbox,
|
|
29
|
+
);
|
|
27
30
|
|
|
28
31
|
if (!result.success) {
|
|
29
32
|
console.error(formatError({ message: result.error ?? "Failed to generate CI pipeline from components" }));
|
|
@@ -83,6 +86,16 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
|
|
|
83
86
|
console.error(formatInfo(formatWarningMsg));
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
// #1064 — `--param name=value`, repeated, into a flat { name: value } record.
|
|
90
|
+
const params = args.param?.length
|
|
91
|
+
? Object.fromEntries(
|
|
92
|
+
args.param.map((entry) => {
|
|
93
|
+
const eq = entry.indexOf("=");
|
|
94
|
+
return eq === -1 ? [entry, ""] : [entry.slice(0, eq), entry.slice(eq + 1)];
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
86
99
|
if (args.watch) {
|
|
87
100
|
const cleanup = buildCommandWatch({
|
|
88
101
|
path: args.path,
|
|
@@ -90,6 +103,10 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
|
|
|
90
103
|
format: buildFormat,
|
|
91
104
|
serializers,
|
|
92
105
|
plugins,
|
|
106
|
+
fold: args.fold,
|
|
107
|
+
sandbox: args.sandbox,
|
|
108
|
+
params,
|
|
109
|
+
paramsFile: args.paramsFile,
|
|
93
110
|
});
|
|
94
111
|
process.on("SIGINT", () => {
|
|
95
112
|
cleanup();
|
|
@@ -107,6 +124,10 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
|
|
|
107
124
|
plugins,
|
|
108
125
|
verbose: args.verbose,
|
|
109
126
|
env: args.env,
|
|
127
|
+
fold: args.fold,
|
|
128
|
+
sandbox: args.sandbox,
|
|
129
|
+
params,
|
|
130
|
+
paramsFile: args.paramsFile,
|
|
110
131
|
});
|
|
111
132
|
|
|
112
133
|
// When --lexicon filters to a subset, suppress "No serializer" warnings for excluded lexicons
|
|
@@ -79,7 +79,7 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
79
79
|
if (!component || !digest) {
|
|
80
80
|
console.error(formatError({
|
|
81
81
|
message: "--component and --digest are required",
|
|
82
|
-
hint: "chant components release <env> --component <name> --digest <sha256:...> [--git-sha <sha>] [--run-id <id>] [--actor <name>]",
|
|
82
|
+
hint: "chant components release <env> --component <name> --digest <sha256:...> [--git-sha <sha>] [--run-id <id>] [--actor <name>] [--approver <name>]",
|
|
83
83
|
}));
|
|
84
84
|
return 1;
|
|
85
85
|
}
|
|
@@ -87,6 +87,10 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
87
87
|
const gitSha = args.gitSha ?? (await getHeadCommit().catch(() => undefined));
|
|
88
88
|
const runId = args.runId ?? process.env.GITHUB_RUN_ID ?? process.env.CI_PIPELINE_ID ?? `local-${Date.now()}`;
|
|
89
89
|
const actor = args.actor ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER;
|
|
90
|
+
// Who approved a gated change (#1035). Optional — omitted for an ungated
|
|
91
|
+
// change, and never defaulted to `actor`, since recording the approver as the
|
|
92
|
+
// triggerer would defeat the separation-of-duties the field exists to attest.
|
|
93
|
+
const approver = args.approver;
|
|
90
94
|
|
|
91
95
|
if (!gitSha) {
|
|
92
96
|
console.error(formatError({ message: "Could not resolve --git-sha (not in a git repo?) — pass it explicitly." }));
|
|
@@ -111,6 +115,7 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
111
115
|
runId,
|
|
112
116
|
timestamp,
|
|
113
117
|
actor,
|
|
118
|
+
...(approver ? { approver } : {}),
|
|
114
119
|
});
|
|
115
120
|
await pushLifecycle();
|
|
116
121
|
if (args.json) {
|
|
@@ -277,7 +282,9 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
277
282
|
// Discover components (best-effort) so a live/owned-but-never-recorded
|
|
278
283
|
// component still gets a row, not just those that appear in the ledger.
|
|
279
284
|
const { config } = await loadChantConfig(resolve("."));
|
|
280
|
-
const discovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? ".")
|
|
285
|
+
const discovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? "."), {
|
|
286
|
+
sandbox: args.sandbox,
|
|
287
|
+
});
|
|
281
288
|
const allComponents = [...discovery.components.keys()];
|
|
282
289
|
|
|
283
290
|
// Component -> live entity/resource name(s) it owns (#598), when declared
|
package/src/cli/handlers/dev.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function runDevOnboard(ctx: CommandContext): Promise<number> {
|
|
|
41
41
|
export async function runDevCheckLexicon(ctx: CommandContext): Promise<number> {
|
|
42
42
|
const dir = ctx.args.extraPositional ?? ".";
|
|
43
43
|
const { checkLexicon, printCheckResult } = await import("../commands/check-lexicon");
|
|
44
|
-
const result = checkLexicon(resolve(dir));
|
|
44
|
+
const result = await checkLexicon(resolve(dir));
|
|
45
45
|
printCheckResult(result, ctx.args.format === "json");
|
|
46
46
|
return result.tier1Pass ? 0 : 1;
|
|
47
47
|
}
|
|
@@ -109,7 +109,9 @@ async function runGraphLive(
|
|
|
109
109
|
// and unions the results. A project with no components (or whose discovery
|
|
110
110
|
// errors) yields no stacks, so `observeResources` falls back to its
|
|
111
111
|
// original single-stack call — unchanged.
|
|
112
|
-
const componentsDiscovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? ".")
|
|
112
|
+
const componentsDiscovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? "."), {
|
|
113
|
+
sandbox: args.sandbox,
|
|
114
|
+
});
|
|
113
115
|
const stacks = new Set<string>();
|
|
114
116
|
if (componentsDiscovery.errors.length === 0) {
|
|
115
117
|
for (const { component } of componentsDiscovery.components.values()) {
|
|
@@ -207,7 +209,7 @@ async function runGraphLive(
|
|
|
207
209
|
*/
|
|
208
210
|
async function runComponentGraph(ctx: CommandContext): Promise<number> {
|
|
209
211
|
const projectPath = resolve(ctx.args.path === "." ? "." : ctx.args.path);
|
|
210
|
-
const graph = await computeComponentGraph(projectPath);
|
|
212
|
+
const graph = await computeComponentGraph(projectPath, ctx.args.sandbox);
|
|
211
213
|
|
|
212
214
|
if (!graph.success) {
|
|
213
215
|
console.error(formatError({ message: graph.error ?? "Failed to compute component graph" }));
|
|
@@ -254,7 +256,7 @@ async function runComponentGraphView(
|
|
|
254
256
|
): Promise<number> {
|
|
255
257
|
const projectPath = resolve(ctx.args.path === "." ? "." : ctx.args.path);
|
|
256
258
|
|
|
257
|
-
const lint = await lintCommand({ path: ctx.args.path, format: "stylish" });
|
|
259
|
+
const lint = await lintCommand({ path: ctx.args.path, format: "stylish", sandbox: ctx.args.sandbox });
|
|
258
260
|
if (!lint.success) {
|
|
259
261
|
console.error(
|
|
260
262
|
formatError({
|
|
@@ -264,7 +266,7 @@ async function runComponentGraphView(
|
|
|
264
266
|
return 1;
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
const graph = await computeComponentGraph(projectPath);
|
|
269
|
+
const graph = await computeComponentGraph(projectPath, ctx.args.sandbox);
|
|
268
270
|
if (!graph.success) {
|
|
269
271
|
console.error(formatError({ message: graph.error ?? "Failed to compute component graph" }));
|
|
270
272
|
return 1;
|
|
@@ -316,7 +318,7 @@ async function runGraphView(
|
|
|
316
318
|
}
|
|
317
319
|
|
|
318
320
|
// Gate: only emit for lint-clean source.
|
|
319
|
-
const lint = await lintCommand({ path: ctx.args.path, format: "stylish" });
|
|
321
|
+
const lint = await lintCommand({ path: ctx.args.path, format: "stylish", sandbox: ctx.args.sandbox });
|
|
320
322
|
if (!lint.success) {
|
|
321
323
|
console.error(
|
|
322
324
|
formatError({
|
|
@@ -286,7 +286,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
286
286
|
let componentStacks: string[] = [];
|
|
287
287
|
if (!target.stack) {
|
|
288
288
|
try {
|
|
289
|
-
const disc = await discoverComponents(target.root);
|
|
289
|
+
const disc = await discoverComponents(target.root, { sandbox: args.sandbox });
|
|
290
290
|
const set = new Set<string>();
|
|
291
291
|
for (const { component } of disc.components.values()) for (const s of cfnDeployStacks(component.deploy)) set.add(s);
|
|
292
292
|
componentStacks = [...set];
|
|
@@ -698,7 +698,7 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
698
698
|
// single-stack project → the original single-call path is kept.
|
|
699
699
|
let componentStacks: string[] = [];
|
|
700
700
|
try {
|
|
701
|
-
const disc = await discoverComponents(resolveBuildRoot(args, config));
|
|
701
|
+
const disc = await discoverComponents(resolveBuildRoot(args, config), { sandbox: args.sandbox });
|
|
702
702
|
const set = new Set<string>();
|
|
703
703
|
for (const { component } of disc.components.values()) for (const s of cfnDeployStacks(component.deploy)) set.add(s);
|
|
704
704
|
componentStacks = [...set];
|
package/src/cli/handlers/lint.ts
CHANGED
|
@@ -16,6 +16,7 @@ export async function runLint(ctx: CommandContext): Promise<number> {
|
|
|
16
16
|
path: args.path,
|
|
17
17
|
fix: args.fix,
|
|
18
18
|
format: lintFormat,
|
|
19
|
+
sandbox: args.sandbox,
|
|
19
20
|
});
|
|
20
21
|
process.on("SIGINT", () => {
|
|
21
22
|
cleanup();
|
|
@@ -29,6 +30,7 @@ export async function runLint(ctx: CommandContext): Promise<number> {
|
|
|
29
30
|
path: args.path,
|
|
30
31
|
fix: args.fix,
|
|
31
32
|
format: lintFormat,
|
|
33
|
+
sandbox: args.sandbox,
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
printLintResult(result);
|
package/src/cli/handlers/misc.ts
CHANGED
|
@@ -92,7 +92,7 @@ export async function runList(ctx: CommandContext): Promise<number> {
|
|
|
92
92
|
// `lexicon`/`entityType`), so it is a separate branch rather than folded
|
|
93
93
|
// into `listCommand`'s Declarable-shaped `ListEntity` rows.
|
|
94
94
|
if (args.components) {
|
|
95
|
-
const result = await listComponents(args.path);
|
|
95
|
+
const result = await listComponents(args.path, args.sandbox);
|
|
96
96
|
if (!result.success) {
|
|
97
97
|
for (const e of result.errors) console.error(formatError({ message: e }));
|
|
98
98
|
return 1;
|
|
@@ -143,7 +143,7 @@ export async function runDescribe(ctx: CommandContext): Promise<number> {
|
|
|
143
143
|
// `Component` (its full JSON contract projection) rather than a lexicon
|
|
144
144
|
// resource's resolved props bag.
|
|
145
145
|
if (args.components) {
|
|
146
|
-
const result = await describeComponent(args.extraPositional ?? ".", component);
|
|
146
|
+
const result = await describeComponent(args.extraPositional ?? ".", component, args.sandbox);
|
|
147
147
|
if (!result.success || !result.described) {
|
|
148
148
|
console.log(result.output);
|
|
149
149
|
return 1;
|
|
@@ -29,7 +29,7 @@ export interface WorkflowHandleRaw {
|
|
|
29
29
|
result(): Promise<unknown>;
|
|
30
30
|
describe(): Promise<WorkflowExecutionDescription>;
|
|
31
31
|
fetchHistory(): Promise<WorkflowHistoryRaw>;
|
|
32
|
-
signal(signalName: string): Promise<void>;
|
|
32
|
+
signal(signalName: string, ...args: unknown[]): Promise<void>;
|
|
33
33
|
cancel(): Promise<void>;
|
|
34
34
|
}
|
|
35
35
|
|