@invariant-ui/cli 0.0.0-stage → 0.1.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/LICENSE +105 -0
- package/README.md +248 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/ast.js +105 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/program.js +58 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-storybook/static-eval.js +145 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { applyPlan, CLIENTS, planIntegration, validateLaunch } from "../agent-kit/index.js";
|
|
4
|
+
import { renderArtifacts, writeArtifacts } from "../artifact-generator/index.js";
|
|
5
|
+
import { compile, nodeFs, toArtifactEntries } from "../compiler-core/index.js";
|
|
6
|
+
import { contractJsonSchemas } from "../contract/index.js";
|
|
7
|
+
import { patternsFileJsonSchema } from "../governance/index.js";
|
|
8
|
+
import { ContractSource, McpServer, serveStdio } from "../mcp/index.js";
|
|
9
|
+
import { hasErrors } from "../shared/index.js";
|
|
10
|
+
import { CONFIG_FILE, detectConfig, isDirectory, isSafeRelativePath, loadConfig, outputDirectory, SOURCE_TYPES, slug, writeConfig } from "./config.js";
|
|
11
|
+
import { diffContracts, formatDiff } from "./diff.js";
|
|
12
|
+
import { CliError } from "./errors.js";
|
|
13
|
+
import { countBySeverity, formatDiagnostic, shortValue, table } from "./format.js";
|
|
14
|
+
import { adaptersFor, runLayeredPipeline, runPipeline } from "./pipeline.js";
|
|
15
|
+
import { checkOutput, loadCompiled } from "./store.js";
|
|
16
|
+
import { CLI_NAME, CLI_VERSION, NPM_PACKAGE } from "./version.js";
|
|
17
|
+
import { watchProject } from "./watch.js";
|
|
18
|
+
export const print = (c, text) => c.io.stdout(`${text}\n`);
|
|
19
|
+
export const json = (c, value) => print(c, JSON.stringify(value, null, 2));
|
|
20
|
+
export async function requireConfig(c) {
|
|
21
|
+
const loaded = await loadConfig({
|
|
22
|
+
cwd: c.cwd,
|
|
23
|
+
file: c.args.values.config,
|
|
24
|
+
trustConfig: c.args.values["trust-config"] === true
|
|
25
|
+
});
|
|
26
|
+
if (!loaded)
|
|
27
|
+
throw new CliError(`no ${CONFIG_FILE} found. Run \`${CLI_NAME} init\` first.`);
|
|
28
|
+
return loaded;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Where a command may write its own files (candidates, browser results). That is the output directory, except that a
|
|
32
|
+
* contract read from inside node_modules (`--dir node_modules/@acme/ui/invariant`) is never written to: files go to the
|
|
33
|
+
* project's own output directory instead.
|
|
34
|
+
*/
|
|
35
|
+
export async function writableDir(c) {
|
|
36
|
+
const dir = await outputDir(c);
|
|
37
|
+
if (!path.relative(c.cwd, dir).split(path.sep).includes("node_modules"))
|
|
38
|
+
return dir;
|
|
39
|
+
const loaded = await loadConfig({
|
|
40
|
+
cwd: c.cwd,
|
|
41
|
+
file: c.args.values.config,
|
|
42
|
+
trustConfig: c.args.values["trust-config"] === true
|
|
43
|
+
});
|
|
44
|
+
return path.resolve(c.cwd, outputDirectory(loaded?.config));
|
|
45
|
+
}
|
|
46
|
+
export async function outputDir(c) {
|
|
47
|
+
if (c.args.values.dir) {
|
|
48
|
+
if (!isSafeRelativePath(c.args.values.dir))
|
|
49
|
+
throw new CliError("--dir must be a relative path inside the project");
|
|
50
|
+
return path.resolve(c.cwd, c.args.values.dir);
|
|
51
|
+
}
|
|
52
|
+
const loaded = await loadConfig({
|
|
53
|
+
cwd: c.cwd,
|
|
54
|
+
file: c.args.values.config,
|
|
55
|
+
trustConfig: c.args.values["trust-config"] === true
|
|
56
|
+
});
|
|
57
|
+
return path.resolve(c.cwd, outputDirectory(loaded?.config));
|
|
58
|
+
}
|
|
59
|
+
export async function init(c) {
|
|
60
|
+
const file = path.join(c.cwd, CONFIG_FILE);
|
|
61
|
+
if (existsSync(file) && !c.args.values.force)
|
|
62
|
+
throw new CliError(`${CONFIG_FILE} already exists. Use --force to replace it.`);
|
|
63
|
+
const config = await detectConfig(c.cwd);
|
|
64
|
+
writeConfig(file, config);
|
|
65
|
+
print(c, `wrote ${CONFIG_FILE} with ${config.sources.length} source(s):`);
|
|
66
|
+
for (const s of config.sources)
|
|
67
|
+
print(c, ` ${s.id} ${s.type}${s.path ? ` ${s.path}` : ""}`);
|
|
68
|
+
if (config.sources.length === 0)
|
|
69
|
+
print(c, "nothing detected; add sources with `invariant source add <type> <path>`");
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
/** Code configuration is never edited: loading it executes it. Point at the safe alternative instead. */
|
|
73
|
+
const CODE_CONFIG_HINT = (command) => `${command} edits JSON configuration; this configuration is code, and the CLI never rewrites code. ` +
|
|
74
|
+
`Edit it by hand, or run \`${CLI_NAME} init\` to write ${CONFIG_FILE}, which wins over code files.`;
|
|
75
|
+
export async function source(c) {
|
|
76
|
+
const [, sub, type, rawPath] = c.args.positionals;
|
|
77
|
+
if (sub === "list") {
|
|
78
|
+
const { config } = await requireConfig(c);
|
|
79
|
+
if (c.args.values.json)
|
|
80
|
+
json(c, config.sources);
|
|
81
|
+
else
|
|
82
|
+
print(c, config.sources.length === 0
|
|
83
|
+
? "no sources configured"
|
|
84
|
+
: table(["id", "type", "path"], config.sources.map((s) => [s.id, s.type, s.path ?? "."])));
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (sub === "remove") {
|
|
88
|
+
const id = type;
|
|
89
|
+
if (!id)
|
|
90
|
+
throw new CliError("usage: invariant source remove <id>");
|
|
91
|
+
const loaded = await requireConfig(c);
|
|
92
|
+
if (!loaded.editable)
|
|
93
|
+
throw new CliError(CODE_CONFIG_HINT("source remove"));
|
|
94
|
+
if (!loaded.config.sources.some((s) => s.id === id)) {
|
|
95
|
+
const known = loaded.config.sources.map((s) => s.id).join(", ") || "none";
|
|
96
|
+
throw new CliError(`no source with id "${id}" (configured: ${known})`);
|
|
97
|
+
}
|
|
98
|
+
writeConfig(loaded.file, { ...loaded.config, sources: loaded.config.sources.filter((s) => s.id !== id) });
|
|
99
|
+
print(c, `removed ${id}`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (sub !== "add")
|
|
103
|
+
throw new CliError("usage: invariant source <add|remove|list> ...");
|
|
104
|
+
if (!type || !SOURCE_TYPES.includes(type))
|
|
105
|
+
throw new CliError(`usage: invariant source add <${SOURCE_TYPES.join("|")}> [path] [--id <id>]`);
|
|
106
|
+
const loaded = await requireConfig(c);
|
|
107
|
+
if (!loaded.editable)
|
|
108
|
+
throw new CliError(CODE_CONFIG_HINT("source add"));
|
|
109
|
+
const dir = rawPath === undefined ? undefined : rawPath.replace(/^\.\//, "").replace(/\/+$/, "") || ".";
|
|
110
|
+
if (dir !== undefined) {
|
|
111
|
+
if (!isSafeRelativePath(dir))
|
|
112
|
+
throw new CliError(`path must be relative and inside the project: ${rawPath}`);
|
|
113
|
+
if (!isDirectory(path.resolve(c.cwd, dir)))
|
|
114
|
+
throw new CliError(`not a directory: ${rawPath}`);
|
|
115
|
+
}
|
|
116
|
+
const id = c.args.values.id ?? slug(dir && dir !== "." ? `${type}-${dir}` : type);
|
|
117
|
+
if (loaded.config.sources.some((s) => s.id === id))
|
|
118
|
+
throw new CliError(`a source with id "${id}" already exists`);
|
|
119
|
+
const next = {
|
|
120
|
+
...loaded.config,
|
|
121
|
+
sources: [
|
|
122
|
+
...loaded.config.sources,
|
|
123
|
+
{ id, type: type, ...(dir && dir !== "." ? { path: dir } : {}) }
|
|
124
|
+
]
|
|
125
|
+
};
|
|
126
|
+
writeConfig(loaded.file, next);
|
|
127
|
+
print(c, `added ${id} (${type}${dir && dir !== "." ? ` at ${dir}` : ""})`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
export async function analyze(c) {
|
|
131
|
+
const { config } = await requireConfig(c);
|
|
132
|
+
const result = await compile({ fs: nodeFs(c.cwd), adapters: adaptersFor(config), clock: c.io.clock });
|
|
133
|
+
const out = path.join(await outputDir(c), "analysis");
|
|
134
|
+
const artifacts = renderArtifacts(toArtifactEntries(result), {
|
|
135
|
+
tool: { name: CLI_NAME, version: CLI_VERSION },
|
|
136
|
+
generatedAt: c.io.clock(),
|
|
137
|
+
sources: result.sources
|
|
138
|
+
});
|
|
139
|
+
await writeArtifacts(out, artifacts);
|
|
140
|
+
const entities = {};
|
|
141
|
+
for (const ir of result.irDocuments)
|
|
142
|
+
for (const e of ir.entities)
|
|
143
|
+
entities[e.kind] = (entities[e.kind] ?? 0) + 1;
|
|
144
|
+
print(c, `project: ${result.project.packages.length} packages, ${result.project.storybooks.length} storybooks, ${result.project.tokenFiles.length} token files, ${result.project.docFiles.length} docs`);
|
|
145
|
+
print(c, `sources: ${result.sources.length}`);
|
|
146
|
+
for (const s of result.sources)
|
|
147
|
+
print(c, ` ${s.id} ${s.type} ${s.completeness}`);
|
|
148
|
+
print(c, `entities: ${Object.entries(entities)
|
|
149
|
+
.sort()
|
|
150
|
+
.map(([k, n]) => `${k} ${n}`)
|
|
151
|
+
.join(", ") || "none"}`);
|
|
152
|
+
print(c, `diagnostics: ${countBySeverity(result.diagnostics)}`);
|
|
153
|
+
for (const d of result.diagnostics.filter((x) => x.severity === "error").slice(0, 10))
|
|
154
|
+
print(c, ` ${formatDiagnostic(d)}`);
|
|
155
|
+
print(c, `wrote ${path.relative(c.cwd, out) || "."}/ (${artifacts.length} files)`);
|
|
156
|
+
return hasErrors(result.diagnostics) ? 1 : 0;
|
|
157
|
+
}
|
|
158
|
+
/** Compiles from sources, or from the contract a library ships when the config says `extends`. */
|
|
159
|
+
async function compileProject(c, config) {
|
|
160
|
+
if (config.extends) {
|
|
161
|
+
if (!existsSync(path.resolve(c.cwd, config.extends.contract, "invariant.contract.json")))
|
|
162
|
+
throw new CliError(`extends.contract points at ${config.extends.contract}, which has no invariant.contract.json. Install the design system package, or fix the path.`);
|
|
163
|
+
return runLayeredPipeline({ cwd: c.cwd, config, clock: c.io.clock });
|
|
164
|
+
}
|
|
165
|
+
if (config.sources.length === 0)
|
|
166
|
+
throw new CliError(`${CONFIG_FILE} has no sources, so there is nothing to compile. Add one with \`${CLI_NAME} source add <type> [path]\`, or, if this project uses a design system from a package, set extends.contract (see \`${CLI_NAME} setup\`).`);
|
|
167
|
+
return runPipeline({ cwd: c.cwd, config, clock: c.io.clock, keepIr: c.args.values["keep-ir"] === true });
|
|
168
|
+
}
|
|
169
|
+
async function compileOnce(c) {
|
|
170
|
+
const { config } = await requireConfig(c);
|
|
171
|
+
const result = await compileProject(c, config);
|
|
172
|
+
const out = await outputDir(c);
|
|
173
|
+
await writeArtifacts(out, result.artifacts);
|
|
174
|
+
const contract = result.final.contract;
|
|
175
|
+
print(c, `compiled ${contract.metadata.name}`);
|
|
176
|
+
for (const m of result.mapping.slice(0, 12)) {
|
|
177
|
+
const shown = m.sources.slice(0, 4).join(", ") || "nothing";
|
|
178
|
+
const more = m.sources.length > 4 ? `, +${m.sources.length - 4} more` : "";
|
|
179
|
+
print(c, ` ${m.configId} (${m.type}${m.path ? ` ${m.path}` : ""}) -> ${shown}${more}`);
|
|
180
|
+
}
|
|
181
|
+
if (result.mapping.length > 12)
|
|
182
|
+
print(c, ` … ${result.mapping.length - 12} more`);
|
|
183
|
+
print(c, `components ${contract.components.length}, tokens ${contract.tokens.length}, themes ${contract.themes.length}, guidelines ${contract.guidelines.length}`);
|
|
184
|
+
print(c, `conflicts ${contract.conflicts.length}, needs review ${result.review.length}`);
|
|
185
|
+
print(c, `patterns ${result.governance.patterns}${result.governance.patternsSkipped > 0 ? ` (${result.governance.patternsSkipped} skipped)` : ""}${result.governance.patternsAwaitingApproval > 0 ? `, ${result.governance.patternsAwaitingApproval} awaiting approval` : ""}, policies ${result.governance.policies}, decisions applied ${result.governance.decisionsApplied}${result.governance.decisionsStale > 0 ? ` (${result.governance.decisionsStale} stale)` : ""}`);
|
|
186
|
+
print(c, `diagnostics: ${countBySeverity(result.diagnostics)}`);
|
|
187
|
+
for (const d of result.diagnostics.filter((x) => x.severity === "error").slice(0, 10))
|
|
188
|
+
print(c, ` ${formatDiagnostic(d)}`);
|
|
189
|
+
print(c, `wrote ${path.relative(c.cwd, out) || "."}/ (${result.artifacts.length} files)`);
|
|
190
|
+
return result.ok ? 0 : 1;
|
|
191
|
+
}
|
|
192
|
+
const WATCH_LIST = 3;
|
|
193
|
+
/** `compile`, or with `--watch` compile now and again after every settled change until stopped. */
|
|
194
|
+
export async function compileCommand(c) {
|
|
195
|
+
if (!c.args.values.watch)
|
|
196
|
+
return compileOnce(c);
|
|
197
|
+
const dir = await outputDir(c);
|
|
198
|
+
const stamp = () => c.io.clock().slice(11, 19);
|
|
199
|
+
const cycle = async () => {
|
|
200
|
+
const started = Date.now();
|
|
201
|
+
try {
|
|
202
|
+
const code = await compileOnce(c);
|
|
203
|
+
print(c, `[${stamp()}] ${code === 0 ? "ok" : "finished with errors"} in ${Date.now() - started} ms`);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// A broken config or a failed read must not end the watcher: say what is wrong and keep listening.
|
|
207
|
+
print(c, `[${stamp()}] ${error instanceof CliError ? error.message : String(error)}`);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
await cycle();
|
|
211
|
+
print(c, `watching ${c.cwd} for changes (Ctrl+C to stop). Every change recompiles all sources.`);
|
|
212
|
+
await watchProject({
|
|
213
|
+
root: c.cwd,
|
|
214
|
+
outputDir: path.relative(c.cwd, dir) || ".",
|
|
215
|
+
...(c.io.signal ? { signal: c.io.signal } : {}),
|
|
216
|
+
onChange: async (files) => {
|
|
217
|
+
const shown = files.slice(0, WATCH_LIST).join(", ");
|
|
218
|
+
const more = files.length > WATCH_LIST ? ` and ${files.length - WATCH_LIST} more` : "";
|
|
219
|
+
print(c, `[${stamp()}] changed: ${shown}${more}`);
|
|
220
|
+
await cycle();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
print(c, "stopped watching");
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
const frameworks = (x) => [...new Set(x.implementations.map((i) => i.framework))].join(",");
|
|
227
|
+
export async function components(c) {
|
|
228
|
+
const [, sub, query] = c.args.positionals;
|
|
229
|
+
const { contract, evidence } = loadCompiled(await outputDir(c));
|
|
230
|
+
if (sub === "list") {
|
|
231
|
+
const conflicts = (id) => contract.conflicts.filter((x) => x.subject === id).length;
|
|
232
|
+
const inferred = (x) => Object.values(x.facts).filter((f) => f.status === "inferred").length;
|
|
233
|
+
if (c.args.values.json)
|
|
234
|
+
json(c, contract.components.map((x) => ({
|
|
235
|
+
id: x.id,
|
|
236
|
+
name: x.name,
|
|
237
|
+
frameworks: frameworks(x).split(","),
|
|
238
|
+
props: x.props.length,
|
|
239
|
+
conflicts: conflicts(x.id)
|
|
240
|
+
})));
|
|
241
|
+
else
|
|
242
|
+
print(c, contract.components.length === 0
|
|
243
|
+
? "no components"
|
|
244
|
+
: table(["id", "name", "frameworks", "props", "events", "slots", "conflicts", "inferred"], contract.components.map((x) => [
|
|
245
|
+
x.id,
|
|
246
|
+
x.name,
|
|
247
|
+
frameworks(x),
|
|
248
|
+
x.props.length,
|
|
249
|
+
x.events.length,
|
|
250
|
+
x.slots.length,
|
|
251
|
+
conflicts(x.id),
|
|
252
|
+
inferred(x)
|
|
253
|
+
])));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (sub !== "inspect" || !query)
|
|
257
|
+
throw new CliError("usage: invariant components <list|inspect <name>>");
|
|
258
|
+
const q = query.toLowerCase();
|
|
259
|
+
const matches = contract.components.filter((x) => x.id === q || x.id === `component:${q}` || x.name.toLowerCase() === q);
|
|
260
|
+
if (matches.length === 0)
|
|
261
|
+
throw new CliError(`no component named "${query}". Try \`${CLI_NAME} components list\`.`, 1);
|
|
262
|
+
if (matches.length > 1)
|
|
263
|
+
throw new CliError(`"${query}" is ambiguous: ${matches.map((m) => m.id).join(", ")}. Use the id.`, 1);
|
|
264
|
+
const comp = matches[0];
|
|
265
|
+
const related = contract.conflicts.filter((x) => x.subject === comp.id);
|
|
266
|
+
if (c.args.values.json) {
|
|
267
|
+
json(c, { component: comp, conflicts: related });
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
const evidenceById = new Map(evidence.evidence.map((e) => [e.id, e]));
|
|
271
|
+
const where = (ids) => {
|
|
272
|
+
const e = ids.map((id) => evidenceById.get(id)).find((x) => x !== undefined);
|
|
273
|
+
return e?.location.type === "file"
|
|
274
|
+
? `${e.location.path}${e.location.line ? `:${e.location.line}` : ""}`
|
|
275
|
+
: (e?.sourceRef ?? "");
|
|
276
|
+
};
|
|
277
|
+
const status = (p) => {
|
|
278
|
+
const f = comp.facts[p];
|
|
279
|
+
return f ? (f.status === "inferred" ? `inferred ${f.confidence}` : f.status) : "";
|
|
280
|
+
};
|
|
281
|
+
print(c, `${comp.name} (${comp.id})`);
|
|
282
|
+
if (comp.description)
|
|
283
|
+
print(c, comp.description);
|
|
284
|
+
print(c, `lifecycle: ${comp.lifecycle ?? "unspecified"}`);
|
|
285
|
+
print(c, "\nimplementations");
|
|
286
|
+
print(c, table(["id", "framework", "package", "export", "status"], comp.implementations.map((i) => [
|
|
287
|
+
i.id,
|
|
288
|
+
i.framework,
|
|
289
|
+
i.packageName ?? "",
|
|
290
|
+
i.exportName ?? "",
|
|
291
|
+
status(`implementations.${i.id}`)
|
|
292
|
+
])));
|
|
293
|
+
if (comp.props.length > 0) {
|
|
294
|
+
print(c, "\nprops");
|
|
295
|
+
print(c, table(["name", "type", "required", "default", "values", "status"], comp.props.map((p) => [
|
|
296
|
+
p.name,
|
|
297
|
+
shortValue(p.type.text, 36),
|
|
298
|
+
p.required ? "yes" : "",
|
|
299
|
+
shortValue(p.default, 16),
|
|
300
|
+
shortValue(p.allowedValues, 40),
|
|
301
|
+
status(`props.${p.name}.type.text`)
|
|
302
|
+
])));
|
|
303
|
+
}
|
|
304
|
+
if (comp.events.length > 0)
|
|
305
|
+
print(c, `\nevents: ${comp.events.map((e) => e.name).join(", ")}`);
|
|
306
|
+
if (comp.slots.length > 0)
|
|
307
|
+
print(c, `slots: ${comp.slots.map((s) => s.name).join(", ")}`);
|
|
308
|
+
if (comp.variants.length > 0)
|
|
309
|
+
print(c, `variants: ${comp.variants.map((v) => `${v.name}=[${v.values.join("|")}]${v.default ? ` (default ${v.default})` : ""}`).join("; ")}`);
|
|
310
|
+
if (comp.accessibility?.role)
|
|
311
|
+
print(c, `accessibility role: ${comp.accessibility.role}`);
|
|
312
|
+
if (comp.composition.length > 0)
|
|
313
|
+
print(c, `composition: ${comp.composition.map((r, i) => `${r.type} ${r.target}${comp.facts[`composition.${i}`]?.status === "inferred" ? " (inferred)" : ""}`).join("; ")}`);
|
|
314
|
+
if (related.length > 0) {
|
|
315
|
+
print(c, "\nconflicts");
|
|
316
|
+
print(c, table(["severity", "type", "path", "claims"], related.map((x) => [
|
|
317
|
+
x.severity,
|
|
318
|
+
x.type,
|
|
319
|
+
x.path ?? "",
|
|
320
|
+
x.claims.map((cl) => `${cl.sourceId}=${shortValue(cl.value ?? "(absent)", 24)}`).join(" vs ")
|
|
321
|
+
])));
|
|
322
|
+
}
|
|
323
|
+
if (c.args.values.evidence) {
|
|
324
|
+
print(c, "\nevidence");
|
|
325
|
+
print(c, table(["fact", "status", "where"], Object.entries(comp.facts)
|
|
326
|
+
.filter(([p]) => !p.startsWith("extensions.") && !p.includes(".extensions."))
|
|
327
|
+
.map(([p, f]) => [p, f.status, where(f.evidenceIds)])));
|
|
328
|
+
}
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
export async function tokens(c) {
|
|
332
|
+
if (c.args.positionals[1] !== "list")
|
|
333
|
+
throw new CliError("usage: invariant tokens list");
|
|
334
|
+
const { contract } = loadCompiled(await outputDir(c));
|
|
335
|
+
if (c.args.values.json) {
|
|
336
|
+
json(c, contract.tokens);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
print(c, contract.tokens.length === 0
|
|
340
|
+
? "no tokens"
|
|
341
|
+
: table(["id", "category", "value", "modes"], contract.tokens.map((t) => [
|
|
342
|
+
t.id,
|
|
343
|
+
`${t.category}${t.facts.category?.status === "inferred" ? "*" : ""}`,
|
|
344
|
+
"aliasOf" in t.value && t.value.aliasOf ? `-> ${t.value.aliasOf}` : shortValue(t.value.value, 48),
|
|
345
|
+
Object.keys(t.modes ?? {}).join(",")
|
|
346
|
+
])));
|
|
347
|
+
if (contract.tokens.some((t) => t.facts.category?.status === "inferred"))
|
|
348
|
+
print(c, "\n* category is inferred, not declared");
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
export async function conflicts(c) {
|
|
352
|
+
if (c.args.positionals[1] !== "list")
|
|
353
|
+
throw new CliError("usage: invariant conflicts list [--severity s] [--type t] [--subject id]");
|
|
354
|
+
const { contract } = loadCompiled(await outputDir(c));
|
|
355
|
+
const { severity, type, subject } = c.args.values;
|
|
356
|
+
const rows = contract.conflicts.filter((x) => (!severity || x.severity === severity) &&
|
|
357
|
+
(!type || x.type === type) &&
|
|
358
|
+
(!subject || x.subject === subject || x.subject === `component:${subject.toLowerCase()}`));
|
|
359
|
+
if (c.args.values.json) {
|
|
360
|
+
json(c, rows);
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
print(c, rows.length === 0
|
|
364
|
+
? "no conflicts"
|
|
365
|
+
: table(["severity", "type", "subject", "path", "claims"], rows.map((x) => [
|
|
366
|
+
x.severity,
|
|
367
|
+
x.type,
|
|
368
|
+
x.subject,
|
|
369
|
+
x.path ?? "",
|
|
370
|
+
x.claims.map((cl) => `${cl.sourceId}=${shortValue(cl.value ?? "(absent)", 24)}`).join(" vs ")
|
|
371
|
+
])));
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
export async function verify(c) {
|
|
375
|
+
const dir = await outputDir(c);
|
|
376
|
+
const { compiled, diagnostics } = checkOutput(dir);
|
|
377
|
+
const all = [...diagnostics];
|
|
378
|
+
let changes;
|
|
379
|
+
if (c.args.values.fresh) {
|
|
380
|
+
const { config } = await requireConfig(c);
|
|
381
|
+
const fresh = await compileProject(c, config);
|
|
382
|
+
if (fresh.final.contract.integrity.contentHash !== compiled.contract.integrity.contentHash) {
|
|
383
|
+
changes = diffContracts(compiled.contract, fresh.final.contract);
|
|
384
|
+
all.push({
|
|
385
|
+
code: "verify.stale",
|
|
386
|
+
severity: "error",
|
|
387
|
+
message: "the sources have changed since the contract was compiled. Run `invariant compile`."
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (c.args.values.json)
|
|
392
|
+
json(c, { ok: !hasErrors(all), diagnostics: all, ...(changes ? { changes } : {}) });
|
|
393
|
+
else {
|
|
394
|
+
for (const d of all)
|
|
395
|
+
print(c, formatDiagnostic(d));
|
|
396
|
+
if (changes)
|
|
397
|
+
for (const line of formatDiff(changes))
|
|
398
|
+
print(c, line);
|
|
399
|
+
print(c, hasErrors(all)
|
|
400
|
+
? `invalid: ${countBySeverity(all)}`
|
|
401
|
+
: `valid: contract ${compiled.contract.integrity.contentHash.slice(0, 12)}, ${countBySeverity(all)}`);
|
|
402
|
+
}
|
|
403
|
+
return hasErrors(all) ? 1 : 0;
|
|
404
|
+
}
|
|
405
|
+
export async function exportCommand(c) {
|
|
406
|
+
const to = c.args.values.to;
|
|
407
|
+
if (!to)
|
|
408
|
+
throw new CliError("usage: invariant export --to <directory>");
|
|
409
|
+
if (!isSafeRelativePath(to))
|
|
410
|
+
throw new CliError("--to must be a relative path inside the project");
|
|
411
|
+
const dir = await outputDir(c);
|
|
412
|
+
const { diagnostics } = checkOutput(dir);
|
|
413
|
+
if (hasErrors(diagnostics) && !c.args.values.force) {
|
|
414
|
+
for (const d of diagnostics.filter((x) => x.severity === "error"))
|
|
415
|
+
print(c, formatDiagnostic(d));
|
|
416
|
+
throw new CliError("the compiled output is invalid; fix it or pass --force", 1);
|
|
417
|
+
}
|
|
418
|
+
const target = path.resolve(c.cwd, to);
|
|
419
|
+
if (path.resolve(target) === path.resolve(dir))
|
|
420
|
+
throw new CliError("--to must differ from the output directory");
|
|
421
|
+
if (existsSync(target) && readdirSync(target).length > 0 && !c.args.values.force)
|
|
422
|
+
throw new CliError(`${to} is not empty. Use --force to write into it.`);
|
|
423
|
+
mkdirSync(target, { recursive: true });
|
|
424
|
+
const files = [
|
|
425
|
+
"invariant.contract.json",
|
|
426
|
+
"invariant.evidence.json",
|
|
427
|
+
"invariant.diagnostics.json",
|
|
428
|
+
"invariant.review.json",
|
|
429
|
+
"invariant.report.html",
|
|
430
|
+
"invariant.manifest.json"
|
|
431
|
+
];
|
|
432
|
+
for (const f of files)
|
|
433
|
+
if (existsSync(path.join(dir, f)))
|
|
434
|
+
cpSync(path.join(dir, f), path.join(target, f));
|
|
435
|
+
mkdirSync(path.join(target, "schema"), { recursive: true });
|
|
436
|
+
for (const [name, schema] of Object.entries({
|
|
437
|
+
...contractJsonSchemas(),
|
|
438
|
+
"patterns-file-v1.schema.json": patternsFileJsonSchema()
|
|
439
|
+
}))
|
|
440
|
+
writeFileSync(path.join(target, "schema", name), `${JSON.stringify(schema, null, 2)}\n`);
|
|
441
|
+
print(c, `exported to ${to}/ (contract, evidence, diagnostics, review, report, manifest, schema/ with the contract and pattern-definition schemas)`);
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
export async function mcp(c) {
|
|
445
|
+
const { input, output } = c.io;
|
|
446
|
+
if (!input || !output)
|
|
447
|
+
throw new CliError("`mcp` needs standard input and output streams");
|
|
448
|
+
const dir = await outputDir(c);
|
|
449
|
+
const server = new McpServer({ source: new ContractSource(dir), version: CLI_VERSION });
|
|
450
|
+
// stdout carries only protocol messages; everything human-readable goes to stderr.
|
|
451
|
+
c.io.stderr(`invariant mcp: serving the contract in ${path.relative(c.cwd, dir) || "."} (read-only, stdio)\n`);
|
|
452
|
+
await serveStdio(input, output, server, c.io.stderr);
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
export async function agent(c) {
|
|
456
|
+
const [, sub, target] = c.args.positionals;
|
|
457
|
+
if (sub === "list") {
|
|
458
|
+
print(c, CLIENTS.join("\n"));
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
if (sub !== "init" || !target)
|
|
462
|
+
throw new CliError(`usage: invariant agent init <${CLIENTS.join("|")}|all> [--command exe] [--command-arg arg] [--dry-run] [--force]`);
|
|
463
|
+
const clients = target === "all"
|
|
464
|
+
? [...CLIENTS]
|
|
465
|
+
: CLIENTS.includes(target)
|
|
466
|
+
? [target]
|
|
467
|
+
: undefined;
|
|
468
|
+
if (!clients)
|
|
469
|
+
throw new CliError(`unknown client "${target}". Choose one of: ${CLIENTS.join(", ")}, all`);
|
|
470
|
+
const dir = await outputDir(c);
|
|
471
|
+
const { contract } = loadCompiled(dir);
|
|
472
|
+
const rel = path.relative(c.cwd, dir).split(path.sep).join("/") || ".";
|
|
473
|
+
if (rel.startsWith(".."))
|
|
474
|
+
throw new CliError("the output directory must be inside the project so a committed config can refer to it");
|
|
475
|
+
// With no --command, use `invariant` if it is installed. Someone running through npx has no such command, so the
|
|
476
|
+
// agent's config starts it through npx instead (it installs on first use). A missing PATH means we cannot tell.
|
|
477
|
+
const searchPath = c.io.env?.PATH;
|
|
478
|
+
const installed = searchPath === undefined ||
|
|
479
|
+
searchPath.split(path.delimiter).some((d) => d !== "" && existsSync(path.join(d, "invariant")));
|
|
480
|
+
const viaNpx = c.args.values.command === undefined && !installed;
|
|
481
|
+
const base = [...(viaNpx ? ["-y", NPM_PACKAGE] : []), ...(c.args.values["command-arg"] ?? [])];
|
|
482
|
+
const launch = {
|
|
483
|
+
command: c.args.values.command ?? (viaNpx ? "npx" : "invariant"),
|
|
484
|
+
args: [...base, "mcp", "--dir", rel]
|
|
485
|
+
};
|
|
486
|
+
// The hook resolves the project itself (Claude Code exports CLAUDE_PROJECT_DIR), so it needs no --dir.
|
|
487
|
+
const hookLaunch = { command: launch.command, args: [...base, "hook"] };
|
|
488
|
+
if (viaNpx)
|
|
489
|
+
print(c, `${CLI_NAME} is not on your PATH, so the agent will start it with npx ${NPM_PACKAGE} (it installs on first use).`);
|
|
490
|
+
const problem = validateLaunch(launch);
|
|
491
|
+
const hookProblem = validateLaunch(hookLaunch);
|
|
492
|
+
if (problem)
|
|
493
|
+
throw new CliError(problem);
|
|
494
|
+
if (hookProblem)
|
|
495
|
+
throw new CliError(hookProblem);
|
|
496
|
+
let skipped = 0;
|
|
497
|
+
for (const client of clients) {
|
|
498
|
+
const plan = planIntegration(client, contract, launch, hookLaunch);
|
|
499
|
+
print(c, `${client}${c.args.values["dry-run"] ? " (dry run)" : ""}`);
|
|
500
|
+
for (const r of applyPlan(c.cwd, plan.files, {
|
|
501
|
+
force: c.args.values.force === true,
|
|
502
|
+
dryRun: c.args.values["dry-run"] === true
|
|
503
|
+
})) {
|
|
504
|
+
if (r.action === "skipped")
|
|
505
|
+
skipped++;
|
|
506
|
+
print(c, ` ${r.action.padEnd(9)} ${r.path}${r.reason ? ` (${r.reason})` : ""}`);
|
|
507
|
+
}
|
|
508
|
+
for (const note of plan.notes)
|
|
509
|
+
print(c, ` note: ${note}`);
|
|
510
|
+
for (const s of plan.snippets)
|
|
511
|
+
print(c, ` ${s.title}:\n${s.text.replace(/^/gm, " ")}`);
|
|
512
|
+
}
|
|
513
|
+
if (skipped > 0)
|
|
514
|
+
print(c, `${skipped} file(s) were left alone; see the reasons above.`);
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
//# sourceMappingURL=commands.js.map
|