@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.
Files changed (196) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +248 -2
  3. package/dist/agent-kit/apply.js +232 -0
  4. package/dist/agent-kit/guidance.js +236 -0
  5. package/dist/agent-kit/index.js +5 -0
  6. package/dist/agent-kit/integrations.js +121 -0
  7. package/dist/agent-kit/plugin.js +115 -0
  8. package/dist/artifact-generator/index.js +75 -0
  9. package/dist/browser-verify/driver.js +2 -0
  10. package/dist/browser-verify/index.js +4 -0
  11. package/dist/browser-verify/playwright.js +82 -0
  12. package/dist/browser-verify/runner.js +175 -0
  13. package/dist/browser-verify/spec.js +103 -0
  14. package/dist/cli/args.js +77 -0
  15. package/dist/cli/baseline.js +64 -0
  16. package/dist/cli/cli.js +121 -0
  17. package/dist/cli/commands-browser.js +104 -0
  18. package/dist/cli/commands-discover.js +312 -0
  19. package/dist/cli/commands-enforcement.js +35 -0
  20. package/dist/cli/commands-governance.js +179 -0
  21. package/dist/cli/commands-hook.js +269 -0
  22. package/dist/cli/commands-patterns.js +324 -0
  23. package/dist/cli/commands-setup.js +485 -0
  24. package/dist/cli/commands-validate.js +157 -0
  25. package/dist/cli/commands.js +517 -0
  26. package/dist/cli/config.js +197 -0
  27. package/dist/cli/diff.js +66 -0
  28. package/dist/cli/enforcement-log.js +83 -0
  29. package/dist/cli/enforcement-report.js +96 -0
  30. package/dist/cli/errors.js +10 -0
  31. package/dist/cli/format.js +22 -0
  32. package/dist/cli/index.js +5 -0
  33. package/dist/cli/io.js +2 -0
  34. package/dist/cli/main.js +31 -0
  35. package/dist/cli/pipeline.js +262 -0
  36. package/dist/cli/publish.js +77 -0
  37. package/dist/cli/report.js +127 -0
  38. package/dist/cli/starter-rules.js +57 -0
  39. package/dist/cli/store.js +60 -0
  40. package/dist/cli/version.js +5 -0
  41. package/dist/cli/watch.js +68 -0
  42. package/dist/cli/workflow.js +52 -0
  43. package/dist/compiler-angular/adapter.js +115 -0
  44. package/dist/compiler-angular/analyze.js +222 -0
  45. package/dist/compiler-angular/classes.js +446 -0
  46. package/dist/compiler-angular/declared.js +285 -0
  47. package/dist/compiler-angular/emit.js +183 -0
  48. package/dist/compiler-angular/index.js +4 -0
  49. package/dist/compiler-angular/template.js +142 -0
  50. package/dist/compiler-core/adapter.js +2 -0
  51. package/dist/compiler-core/adapters/inventory.js +312 -0
  52. package/dist/compiler-core/compile.js +148 -0
  53. package/dist/compiler-core/component-facts.js +123 -0
  54. package/dist/compiler-core/discovery/docs.js +66 -0
  55. package/dist/compiler-core/discovery/entries.js +270 -0
  56. package/dist/compiler-core/discovery/exports.js +207 -0
  57. package/dist/compiler-core/discovery/graph.js +59 -0
  58. package/dist/compiler-core/discovery/index.js +103 -0
  59. package/dist/compiler-core/discovery/packages.js +148 -0
  60. package/dist/compiler-core/discovery/schema.js +144 -0
  61. package/dist/compiler-core/discovery/storybook.js +232 -0
  62. package/dist/compiler-core/discovery/support.js +82 -0
  63. package/dist/compiler-core/discovery/tokens.js +186 -0
  64. package/dist/compiler-core/discovery/tooling.js +146 -0
  65. package/dist/compiler-core/fs.js +89 -0
  66. package/dist/compiler-core/glob.js +3 -0
  67. package/dist/compiler-core/index.js +16 -0
  68. package/dist/compiler-core/ir-builder.js +68 -0
  69. package/dist/compiler-core/json-schema.js +11 -0
  70. package/dist/compiler-core/testing/index.js +81 -0
  71. package/dist/compiler-core/ts/ast.js +166 -0
  72. package/dist/compiler-core/ts/members.js +163 -0
  73. package/dist/compiler-core/ts/program.js +81 -0
  74. package/dist/compiler-core/ts/static-eval.js +145 -0
  75. package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
  76. package/dist/compiler-core/walk.js +46 -0
  77. package/dist/compiler-ir/index.js +4 -0
  78. package/dist/compiler-ir/ir.js +67 -0
  79. package/dist/compiler-ir/json-schema.js +6 -0
  80. package/dist/compiler-ir/validate.js +116 -0
  81. package/dist/compiler-react/adapter.js +147 -0
  82. package/dist/compiler-react/analyze.js +199 -0
  83. package/dist/compiler-react/ast.js +105 -0
  84. package/dist/compiler-react/behavior.js +75 -0
  85. package/dist/compiler-react/detect.js +558 -0
  86. package/dist/compiler-react/emit.js +188 -0
  87. package/dist/compiler-react/index.js +2 -0
  88. package/dist/compiler-react/program.js +58 -0
  89. package/dist/compiler-react/props.js +157 -0
  90. package/dist/compiler-react/styling.js +138 -0
  91. package/dist/compiler-react/usage.js +116 -0
  92. package/dist/compiler-react/variants.js +111 -0
  93. package/dist/compiler-storybook/adapter.js +253 -0
  94. package/dist/compiler-storybook/csf.js +521 -0
  95. package/dist/compiler-storybook/emit.js +202 -0
  96. package/dist/compiler-storybook/index-json.js +41 -0
  97. package/dist/compiler-storybook/index.js +5 -0
  98. package/dist/compiler-storybook/mdx.js +120 -0
  99. package/dist/compiler-storybook/static-eval.js +145 -0
  100. package/dist/compiler-tokens/adapter.js +158 -0
  101. package/dist/compiler-tokens/css.js +59 -0
  102. package/dist/compiler-tokens/index.js +5 -0
  103. package/dist/compiler-tokens/json-lines.js +106 -0
  104. package/dist/compiler-tokens/json-tokens.js +52 -0
  105. package/dist/compiler-vue/adapter.js +107 -0
  106. package/dist/compiler-vue/analyze.js +344 -0
  107. package/dist/compiler-vue/declared.js +343 -0
  108. package/dist/compiler-vue/emit.js +129 -0
  109. package/dist/compiler-vue/index.js +5 -0
  110. package/dist/compiler-vue/script.js +472 -0
  111. package/dist/compiler-vue/sfc.js +44 -0
  112. package/dist/compiler-vue/template.js +73 -0
  113. package/dist/contract/archetype-checks.js +153 -0
  114. package/dist/contract/components.js +102 -0
  115. package/dist/contract/conflicts.js +39 -0
  116. package/dist/contract/contract.js +46 -0
  117. package/dist/contract/evidence.js +84 -0
  118. package/dist/contract/fact-path.js +39 -0
  119. package/dist/contract/facts.js +62 -0
  120. package/dist/contract/index.js +16 -0
  121. package/dist/contract/integrity.js +21 -0
  122. package/dist/contract/json-schema.js +11 -0
  123. package/dist/contract/knowledge.js +95 -0
  124. package/dist/contract/pattern-checks.js +254 -0
  125. package/dist/contract/patterns.js +225 -0
  126. package/dist/contract/primitives.js +81 -0
  127. package/dist/contract/sources.js +30 -0
  128. package/dist/contract/tokens.js +41 -0
  129. package/dist/contract/validate.js +253 -0
  130. package/dist/contract-query/archetype.js +188 -0
  131. package/dist/contract-query/index.js +6 -0
  132. package/dist/contract-query/patterns.js +298 -0
  133. package/dist/contract-query/scan.js +164 -0
  134. package/dist/contract-query/search.js +132 -0
  135. package/dist/contract-query/system.js +148 -0
  136. package/dist/contract-query/validate-code.js +239 -0
  137. package/dist/contract-query/validate-usage.js +150 -0
  138. package/dist/contract-query/views.js +288 -0
  139. package/dist/governance/decisions.js +229 -0
  140. package/dist/governance/index.js +5 -0
  141. package/dist/governance/patterns-file.js +156 -0
  142. package/dist/governance/patterns.js +510 -0
  143. package/dist/governance/policies.js +148 -0
  144. package/dist/mcp/index.js +7 -0
  145. package/dist/mcp/loader.js +70 -0
  146. package/dist/mcp/protocol.js +27 -0
  147. package/dist/mcp/resources.js +138 -0
  148. package/dist/mcp/server.js +177 -0
  149. package/dist/mcp/stdio.js +28 -0
  150. package/dist/mcp/tools.js +267 -0
  151. package/dist/reconciliation/code.js +62 -0
  152. package/dist/reconciliation/components.js +401 -0
  153. package/dist/reconciliation/conflicts.js +126 -0
  154. package/dist/reconciliation/facts.js +41 -0
  155. package/dist/reconciliation/index.js +7 -0
  156. package/dist/reconciliation/reconcile.js +134 -0
  157. package/dist/reconciliation/resolve.js +158 -0
  158. package/dist/reconciliation/review.js +81 -0
  159. package/dist/reconciliation/storybook.js +370 -0
  160. package/dist/reconciliation/tokens.js +179 -0
  161. package/dist/reconciliation/util.js +55 -0
  162. package/dist/semantic-engine/additive.js +51 -0
  163. package/dist/semantic-engine/compounds.js +91 -0
  164. package/dist/semantic-engine/documents.js +157 -0
  165. package/dist/semantic-engine/enrich.js +42 -0
  166. package/dist/semantic-engine/index.js +7 -0
  167. package/dist/semantic-engine/infer.js +38 -0
  168. package/dist/semantic-engine/tokens.js +120 -0
  169. package/dist/shared/canonical.js +65 -0
  170. package/dist/shared/diagnostic.js +78 -0
  171. package/dist/shared/glob.js +98 -0
  172. package/dist/shared/index.js +4 -0
  173. package/dist/shared/json.js +2 -0
  174. package/dist/validation/card.js +50 -0
  175. package/dist/validation/discovery/arrangements.js +162 -0
  176. package/dist/validation/discovery/discover.js +70 -0
  177. package/dist/validation/discovery/index.js +8 -0
  178. package/dist/validation/discovery/model.js +127 -0
  179. package/dist/validation/discovery/promote.js +24 -0
  180. package/dist/validation/discovery/reviews.js +63 -0
  181. package/dist/validation/discovery/shapes.js +47 -0
  182. package/dist/validation/discovery/similar-files.js +146 -0
  183. package/dist/validation/discovery/util.js +47 -0
  184. package/dist/validation/index.js +11 -0
  185. package/dist/validation/interaction-facts.js +341 -0
  186. package/dist/validation/interaction-workflows.js +633 -0
  187. package/dist/validation/literals.js +251 -0
  188. package/dist/validation/pattern-rules.js +608 -0
  189. package/dist/validation/policy.js +241 -0
  190. package/dist/validation/report.js +163 -0
  191. package/dist/validation/rules.js +269 -0
  192. package/dist/validation/scan.js +295 -0
  193. package/dist/validation/text.js +34 -0
  194. package/dist/validation/validate-code.js +513 -0
  195. package/dist/validation/validate-usage.js +196 -0
  196. package/package.json +38 -4
@@ -0,0 +1,485 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { PATTERNS_FILE } from "../governance/index.js";
5
+ import { BASELINE_FILE } from "./baseline.js";
6
+ import { run } from "./cli.js";
7
+ import { outputDir, print } from "./commands.js";
8
+ import { POSTURES } from "./commands-hook.js";
9
+ import { CONFIG_FILE, detectConfig, loadConfig, writeConfig } from "./config.js";
10
+ import { CliError } from "./errors.js";
11
+ import { applyPublish, planPublish } from "./publish.js";
12
+ import { starterRules } from "./starter-rules.js";
13
+ import { loadCompiled } from "./store.js";
14
+ import { CLI_NAME } from "./version.js";
15
+ import { installStepsFor, WORKFLOW_PATH, workflowYaml } from "./workflow.js";
16
+ const TOP = 5;
17
+ const APP_DIRS = ["src", "app", "pages", "apps"];
18
+ const CLIENT_DIRS = [
19
+ [".claude", "claude-code"],
20
+ [".cursor", "cursor"],
21
+ [".codex", "codex"],
22
+ [".windsurf", "windsurf"]
23
+ ];
24
+ /** The person setting this up: --as, INVARIANT_USER, else git's user.name. Recorded, not authenticated. */
25
+ function whoIs(c) {
26
+ const given = c.args.values.as ?? c.io.env?.INVARIANT_USER;
27
+ if (given && given.trim() !== "")
28
+ return given.trim();
29
+ try {
30
+ const name = execFileSync("git", ["config", "user.name"], {
31
+ cwd: c.cwd,
32
+ encoding: "utf8",
33
+ stdio: ["ignore", "pipe", "ignore"]
34
+ }).trim();
35
+ return name === "" ? undefined : name;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ const slugId = (name) => `composition.${name
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9]+/g, "-")
44
+ .replace(/^-+|-+$/g, "")
45
+ .slice(0, 48)}`.replace(/\.$/, ".pattern");
46
+ /** Folders under node_modules that ship a compiled contract (`<package>/invariant/invariant.contract.json`). */
47
+ function shippedContracts(cwd) {
48
+ const modules = path.join(cwd, "node_modules");
49
+ if (!existsSync(modules))
50
+ return [];
51
+ const found = [];
52
+ const check = (rel) => {
53
+ if (existsSync(path.join(cwd, rel, "invariant", "invariant.contract.json")))
54
+ found.push(`${rel}/invariant`);
55
+ };
56
+ for (const name of readdirSync(modules).sort()) {
57
+ if (name.startsWith("."))
58
+ continue;
59
+ if (name.startsWith("@")) {
60
+ for (const inner of readdirSync(path.join(modules, name)).sort())
61
+ check(`node_modules/${name}/${inner}`);
62
+ }
63
+ else
64
+ check(`node_modules/${name}`);
65
+ }
66
+ return found;
67
+ }
68
+ /**
69
+ * `invariant setup`: compile, look at the app code, then show one plan and ask once. Yes confirms the contract and does
70
+ * everything in the plan; n stops; c asks about each step. Approval only ever follows a confirmed contract.
71
+ */
72
+ export async function setup(c) {
73
+ const started = Date.now();
74
+ const say = (m = "") => print(c, m);
75
+ const yes = c.args.values.yes === true;
76
+ const carry = [
77
+ ...(c.args.values.config ? ["--config", c.args.values.config] : []),
78
+ ...(c.args.values.dir ? ["--dir", c.args.values.dir] : []),
79
+ ...(c.args.values["trust-config"] ? ["--trust-config"] : [])
80
+ ];
81
+ const sub = async (...argv) => {
82
+ let out = "";
83
+ let err = "";
84
+ const code = await run([...argv, ...carry], {
85
+ ...c.io,
86
+ cwd: c.cwd,
87
+ stdout: (t) => {
88
+ out += t;
89
+ },
90
+ stderr: (t) => {
91
+ err += t;
92
+ }
93
+ });
94
+ return { code, out, err };
95
+ };
96
+ /** true or false when someone answered or --yes decided; undefined when there is no one to ask. */
97
+ const ask = async (question, byDefault) => {
98
+ if (!c.io.prompt)
99
+ return undefined;
100
+ const a = (await c.io.prompt(`${question} ${byDefault ? "[Y/n]" : "[y/N]"} `))?.trim().toLowerCase();
101
+ if (a === undefined || a === "")
102
+ return byDefault;
103
+ return a.startsWith("y");
104
+ };
105
+ const posture = c.args.values.posture;
106
+ if (posture !== undefined && !POSTURES.includes(posture))
107
+ throw new CliError(`--posture must be one of ${POSTURES.join(", ")}`);
108
+ const only = c.args.values.only?.trim().toLowerCase();
109
+ if (only !== undefined && only !== "none" && !/^\d+(\s*,\s*\d+)*$/.test(only))
110
+ throw new CliError("--only takes the numbers of the patterns to add (like 1,3), or none");
111
+ say(`${CLI_NAME} setup`);
112
+ // 1. Detect and compile. A project with no design-system sources of its own uses the contract its library ships.
113
+ const configPath = path.resolve(c.cwd, c.args.values.config ?? CONFIG_FILE);
114
+ let consumer = false;
115
+ if (!existsSync(configPath)) {
116
+ const detected = c.args.values.contract ? undefined : await detectConfig(c.cwd);
117
+ if (detected && detected.sources.length > 0) {
118
+ const r = await sub("init");
119
+ if (r.code !== 0)
120
+ throw new CliError(r.err.trim() || "init failed", r.code);
121
+ say(`1. detected sources\n${r.out.trim().split("\n").slice(1).join("\n")}`);
122
+ }
123
+ else {
124
+ consumer = true;
125
+ let from = c.args.values.contract?.replace(/\\/g, "/").replace(/\/+$/, "");
126
+ if (!from) {
127
+ const found = shippedContracts(c.cwd);
128
+ if (found.length === 0)
129
+ throw new CliError("no design system source found here, and no installed package ships a contract (no node_modules/<package>/invariant/). Install your design system package, pass --contract <folder>, or run setup in the design system's own repository.");
130
+ if (found.length === 1)
131
+ from = found[0];
132
+ else {
133
+ const list = found.map((f, i) => ` ${i + 1}. ${f}`).join("\n");
134
+ const pick = c.io.prompt
135
+ ? Number((await c.io.prompt(`1. Several contracts are installed:\n${list}\n Which one? [1-${found.length}] `)) ?? "")
136
+ : Number.NaN;
137
+ if (!Number.isInteger(pick) || pick < 1 || pick > found.length)
138
+ throw new CliError(`several installed packages ship a contract, so say which: --contract <folder>\n${list}`);
139
+ from = found[pick - 1];
140
+ }
141
+ }
142
+ if (!from)
143
+ throw new CliError("--contract needs a folder");
144
+ const base = loadCompiled(path.resolve(c.cwd, from));
145
+ const config = {
146
+ name: base.contract.metadata.name,
147
+ sources: [],
148
+ extends: { contract: from }
149
+ };
150
+ writeConfig(configPath, config);
151
+ say(`1. no design system source here. Using the contract shipped in ${from} (${base.contract.metadata.name}, ${base.contract.components.length} components) and wrote ${path.basename(configPath)}.`);
152
+ }
153
+ }
154
+ else {
155
+ const existing = (await loadConfig({
156
+ cwd: c.cwd,
157
+ file: c.args.values.config,
158
+ trustConfig: c.args.values["trust-config"] === true
159
+ }))?.config;
160
+ consumer = existing?.extends !== undefined;
161
+ say(`1. using ${path.basename(configPath)}${consumer ? ` (extends ${existing?.extends?.contract})` : ""}`);
162
+ }
163
+ const compiled = await sub("compile");
164
+ const dir = await outputDir(c);
165
+ if (!existsSync(path.join(dir, "invariant.contract.json")))
166
+ throw new CliError(`compile did not produce a contract.\n${(compiled.err || compiled.out).trim()}`, 1);
167
+ const { contract } = loadCompiled(dir);
168
+ const names = contract.components.map((x) => x.name);
169
+ say(`2. ${consumer ? "compiled with your own patterns on top" : "compiled"}: ${contract.components.length} components${names.length > 0 ? ` (${names.slice(0, 6).join(", ")}${names.length > 6 ? `, +${names.length - 6} more` : ""})` : ""}, ${contract.tokens.length} tokens, ${contract.conflicts.length} conflicts${compiled.code === 1 ? "\n compile reported errors; see invariant/invariant.diagnostics.json" : ""}`);
170
+ // 2. Gather everything the plan needs. Nothing here changes the project except the compile output and the candidates file.
171
+ const by = whoIs(c);
172
+ const config = (await loadConfig({
173
+ cwd: c.cwd,
174
+ file: c.args.values.config,
175
+ trustConfig: c.args.values["trust-config"] === true
176
+ }))?.config;
177
+ const sourcePaths = (config?.sources ?? [])
178
+ .map((s) => ("path" in s && typeof s.path === "string" ? s.path.replace(/^\.\//, "") : ""))
179
+ .filter((p) => p !== "" && p !== ".");
180
+ const isDir = (p) => existsSync(path.join(c.cwd, p)) && statSync(path.join(c.cwd, p)).isDirectory();
181
+ // A folder that holds the design system's own component files is the design system, not an application.
182
+ const implFiles = contract.components.flatMap((x) => x.implementations.flatMap((i) => {
183
+ const file = i.extensions?.react?.file;
184
+ return typeof file === "string" ? sourcePaths.map((p) => `${p}/${file}`).concat(file) : [];
185
+ }));
186
+ const holdsDesignSystem = (d) => !consumer && implFiles.some((f) => f.startsWith(`${d}/`));
187
+ const skipped = [];
188
+ const appPaths = c.args.values.paths
189
+ ? c.args.values.paths
190
+ .split(",")
191
+ .map((p) => p.trim())
192
+ .filter(Boolean)
193
+ : APP_DIRS.filter((d) => {
194
+ if (!isDir(d) || sourcePaths.some((s) => s === d))
195
+ return false;
196
+ if (!holdsDesignSystem(d))
197
+ return true;
198
+ skipped.push(d);
199
+ return false;
200
+ });
201
+ if (skipped.length > 0)
202
+ say(` skipped ${skipped.join(", ")}: it holds the design system's own components`);
203
+ let top = [];
204
+ if (appPaths.length === 0)
205
+ say("3. no application folder found (looked for src, app, pages, apps). Pass --paths a,b to look elsewhere.");
206
+ else {
207
+ const found = await sub("discover", ...appPaths, "--json");
208
+ if (found.code === 0) {
209
+ const file = JSON.parse(found.out);
210
+ top = file.candidates
211
+ .filter((x) => x.status === "proposed" && x.subsumedBy === undefined)
212
+ .sort((a, b) => b.support.files - a.support.files || (a.id < b.id ? -1 : 1))
213
+ .slice(0, TOP);
214
+ if (top.length === 0)
215
+ say(`3. looked in ${appPaths.join(", ")}: no structure recurs often enough to propose (needs 3 files).`);
216
+ else {
217
+ say(`3. found ${top.length} recurring structure(s) in ${appPaths.join(", ")}:`);
218
+ for (const [i, x] of top.entries())
219
+ say(` ${i + 1}. ${x.suggestedName} (${x.support.files} files)`);
220
+ }
221
+ }
222
+ else
223
+ say(`3. discovery failed: ${found.err.trim()}`);
224
+ }
225
+ const rulesFor = (x) => starterRules(x.structure.tree);
226
+ const countViolations = async () => {
227
+ if (appPaths.length === 0)
228
+ return 0;
229
+ const r = await sub("validate", ...appPaths, "--format", "json", "--no-baseline");
230
+ try {
231
+ return JSON.parse(r.out).violations.length;
232
+ }
233
+ catch {
234
+ return 0;
235
+ }
236
+ };
237
+ const baselineWanted = appPaths.length > 0 && !existsSync(path.join(c.cwd, BASELINE_FILE));
238
+ const existingViolations = baselineWanted && c.args.values["no-baseline"] !== true ? await countViolations() : 0;
239
+ const clients = c.args.values["no-agent"] === true
240
+ ? []
241
+ : CLIENT_DIRS.filter(([d]) => existsSync(path.join(c.cwd, d))).map(([, n]) => n);
242
+ const workflowFile = path.join(c.cwd, WORKFLOW_PATH);
243
+ const wantsCi = c.args.values.ci === true;
244
+ const ciPossible = c.args.values["no-ci"] !== true &&
245
+ !existsSync(workflowFile) &&
246
+ (wantsCi || existsSync(path.join(c.cwd, ".git")) || existsSync(path.join(c.cwd, ".github")));
247
+ if (existsSync(workflowFile) && c.args.values["no-ci"] !== true)
248
+ say(` ${WORKFLOW_PATH} already exists, so it will be left alone.`);
249
+ const publishFinding = consumer || c.args.values["no-publish"] === true || !config
250
+ ? undefined
251
+ : planPublish(c.cwd, config, path.relative(c.cwd, dir) || "invariant");
252
+ const publish = publishFinding?.ok ? publishFinding.plan : undefined;
253
+ const publishNeeded = publish !== undefined && !(publish.hasFiles && publish.hasPrepack);
254
+ if (publishFinding && !publishFinding.ok)
255
+ say(` not wiring publishing: ${publishFinding.reason}.`);
256
+ // 3. One plan, one question.
257
+ const noRules = c.args.values["no-rules"] === true;
258
+ const ruleCount = noRules ? 0 : top.reduce((n, x) => n + rulesFor(x).length, 0);
259
+ const planLines = (approving) => [
260
+ ...(top.length > 0
261
+ ? [
262
+ approving
263
+ ? `add ${top.length} recurring pattern(s) from ${appPaths.join(", ")}, approved by ${by ?? "you"}`
264
+ : `add ${top.length} recurring pattern(s) from ${appPaths.join(", ")} as proposals (not approved)`
265
+ ]
266
+ : []),
267
+ ...(approving && ruleCount > 0
268
+ ? [
269
+ `turn on ${ruleCount} starter rule(s) as warnings: they flag new code, and existing code is hidden by the baseline`
270
+ ]
271
+ : []),
272
+ ...(existingViolations > 0
273
+ ? [`hide ${existingViolations} existing violation(s) so only new ones are flagged`]
274
+ : []),
275
+ ...(clients.length > 0 ? [`connect ${clients.join(", ")}`] : []),
276
+ ...(ciPossible ? [`add ${WORKFLOW_PATH}, which checks every pull request commit`] : []),
277
+ ...(publishNeeded && publish
278
+ ? [`publish the contract with ${publish.name} (adds "invariant" to files and a prepack script)`]
279
+ : [])
280
+ ];
281
+ let mode;
282
+ let confirmed = false;
283
+ if (c.args.values["confirm-contract"] === true) {
284
+ mode = "quick";
285
+ confirmed = true;
286
+ }
287
+ else if (yes)
288
+ mode = "quick";
289
+ else if (c.io.prompt) {
290
+ const lines = planLines(true);
291
+ say();
292
+ say("Plan. Yes confirms the contract above is right, and does all of this:");
293
+ if (lines.length === 0)
294
+ say(" (nothing else to do)");
295
+ for (const l of lines)
296
+ say(` - ${l}`);
297
+ const a = ((await c.io.prompt("Looks right? [Y/n, or c to choose each step] ")) ?? "")
298
+ .trim()
299
+ .toLowerCase();
300
+ if (a === "c")
301
+ mode = "custom";
302
+ else if (a === "" || a.startsWith("y")) {
303
+ mode = "quick";
304
+ confirmed = true;
305
+ }
306
+ else
307
+ mode = "stop";
308
+ }
309
+ else
310
+ mode = "none";
311
+ if (mode === "stop") {
312
+ say();
313
+ say(`Stopped. Nothing was approved or added. The contract is compiled in ${path.relative(c.cwd, dir) || "."}/. If it looks wrong, check ${path.basename(configPath)} (or pass --paths, --contract) and run ${CLI_NAME} setup again.`);
314
+ return 0;
315
+ }
316
+ if (mode === "none") {
317
+ say();
318
+ say(`not asking (no terminal). Nothing was added. Re-run with --yes to add proposals, or --confirm-contract --as <name> once you have checked the contract above.`);
319
+ return 0;
320
+ }
321
+ // Decisions. Quick takes the plan. Custom asks about each step, as setup used to.
322
+ let chosen = top;
323
+ let approve = false;
324
+ let withRules = false;
325
+ let wantAgents = clients.length > 0;
326
+ let wantBaseline = existingViolations > 0;
327
+ let wantCi = ciPossible;
328
+ let wantPublish = publishNeeded;
329
+ if (mode === "quick") {
330
+ if (only !== undefined) {
331
+ const keep = only === "none" ? new Set() : new Set(only.split(/\s*,\s*/).map((n) => Number(n) - 1));
332
+ chosen = top.filter((_, i) => keep.has(i));
333
+ }
334
+ approve = confirmed;
335
+ withRules = confirmed && !noRules;
336
+ }
337
+ else {
338
+ confirmed = (await ask(" Does this look like your design system?", false)) === true;
339
+ if (top.length > 0 && c.io.prompt) {
340
+ const a = ((await c.io.prompt(" Add the patterns? Enter for all, numbers to skip (like 2,3), n for none: ")) ??
341
+ "")
342
+ .trim()
343
+ .toLowerCase();
344
+ if (a === "n" || a === "no")
345
+ chosen = [];
346
+ else if (a !== "") {
347
+ const skip = new Set(a.split(/[,\s]+/).map((n) => Number(n) - 1));
348
+ chosen = top.filter((_, i) => !skip.has(i));
349
+ }
350
+ }
351
+ if (chosen.length > 0 && confirmed)
352
+ approve = (await ask(` Approve ${chosen.length} pattern(s) as ${by}?`, true)) === true;
353
+ const n = chosen.reduce((m, x) => m + rulesFor(x).length, 0);
354
+ if (approve && n > 0 && !noRules)
355
+ withRules = (await ask(` Turn on ${n} starter rule(s) as warnings?`, true)) === true;
356
+ if (clients.length > 0)
357
+ wantAgents = (await ask(` Connect ${clients.join(", ")}?`, true)) === true;
358
+ if (existingViolations > 0)
359
+ wantBaseline =
360
+ (await ask(` Hide ${existingViolations} existing violation(s) so only new ones are flagged?`, true)) === true;
361
+ if (ciPossible)
362
+ wantCi =
363
+ wantsCi ||
364
+ (await ask(" Add a GitHub Action that checks every commit on pull requests?", true)) === true;
365
+ if (publishNeeded && publish)
366
+ wantPublish = (await ask(` Add the contract to what ${publish.name} publishes?`, true)) === true;
367
+ }
368
+ if ((confirmed || chosen.length > 0) && !by)
369
+ throw new CliError("say who is deciding: --as <name> (or set INVARIANT_USER). Names are recorded, not authenticated.");
370
+ say(confirmed
371
+ ? ` contract confirmed by ${by}.`
372
+ : " contract not confirmed, so nothing is approved and no rule is turned on.");
373
+ // 4. Do it.
374
+ const did = [];
375
+ const promoted = [];
376
+ if (chosen.length > 0 && by) {
377
+ const taken = new Set();
378
+ const existing = existsSync(path.join(c.cwd, PATTERNS_FILE))
379
+ ? (JSON.parse(readFileSync(path.join(c.cwd, PATTERNS_FILE), "utf8")).patterns ?? [])
380
+ : [];
381
+ for (const p of existing)
382
+ if (p.id)
383
+ taken.add(p.id.replace(/^pattern:/, ""));
384
+ for (const x of chosen) {
385
+ let id = slugId(x.suggestedName);
386
+ for (let n = 2; taken.has(id); n++)
387
+ id = `${slugId(x.suggestedName)}-${n}`;
388
+ taken.add(id);
389
+ const a = await sub("candidates", "accept", x.id, "--as", by, "--note", "added by invariant setup");
390
+ const p = a.code === 0 ? await sub("candidates", "promote", x.id, "--id", id, "--as", by) : a;
391
+ if (p.code === 0)
392
+ promoted.push({ id, cand: x });
393
+ else
394
+ say(` skipped ${x.suggestedName}: ${(p.err || p.out).trim().split("\n")[0]}`);
395
+ }
396
+ }
397
+ let approved = 0;
398
+ let rulesOn = 0;
399
+ if (promoted.length > 0) {
400
+ const file = path.resolve(c.cwd, PATTERNS_FILE);
401
+ const doc = JSON.parse(readFileSync(file, "utf8"));
402
+ const at = c.io.clock();
403
+ const globs = appPaths.map((p) => `${p.replace(/\/+$/, "")}/**`);
404
+ for (const def of doc.patterns) {
405
+ const hit = promoted.find((x) => x.id === def.id);
406
+ if (!hit || def.approval !== undefined || !approve || !by)
407
+ continue;
408
+ def.approval = {
409
+ approvedBy: by,
410
+ approvedAt: at,
411
+ reference: `invariant setup, contract confirmed by ${by}`
412
+ };
413
+ approved++;
414
+ const rules = withRules ? rulesFor(hit.cand) : [];
415
+ if (rules.length > 0) {
416
+ def.constraints = rules;
417
+ def.appliesTo = { paths: globs, frameworks: ["react"] };
418
+ rulesOn += rules.length;
419
+ }
420
+ }
421
+ writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
422
+ const r = await sub("compile");
423
+ if (r.code === 1)
424
+ say(" compile reported errors after adding patterns; see invariant/invariant.diagnostics.json");
425
+ did.push(approved > 0
426
+ ? `patterns ${approved} approved by ${by}. They guide agents.`
427
+ : `patterns ${promoted.length} added as proposals (not approved). Approve with \`${CLI_NAME} review\` once you have read them.`);
428
+ if (rulesOn > 0)
429
+ did.push(`rules ${rulesOn} starter rule(s), as warnings`);
430
+ }
431
+ if (posture !== undefined && config) {
432
+ writeConfig(configPath, {
433
+ ...config,
434
+ enforcement: { ...config.enforcement, hooks: posture }
435
+ });
436
+ did.push(`enforcement writes are ${posture === "gate-errors" ? "blocked when they break an error rule" : posture === "warn" ? "never blocked; findings are reported after the write" : "not checked"} (enforcement.hooks = ${posture})`);
437
+ }
438
+ if (wantAgents)
439
+ for (const client of clients) {
440
+ const r = await sub("agent", "init", client);
441
+ did.push(r.code === 0
442
+ ? `agents connected ${client}`
443
+ : `agents could not connect ${client}: ${(r.err || r.out).trim().split("\n")[0]}`);
444
+ }
445
+ else if (c.args.values["no-agent"] !== true && clients.length === 0)
446
+ did.push(`agents none detected. Run \`${CLI_NAME} agent init <claude-code|cursor|codex|windsurf>\` when you use one.`);
447
+ if (baselineWanted && wantBaseline) {
448
+ const count = await countViolations();
449
+ if (count > 0) {
450
+ const w = await sub("validate", ...appPaths, "--update-baseline");
451
+ did.push(`baseline ${w.out
452
+ .trim()
453
+ .split("\n")[0]
454
+ ?.replace(/^wrote [^:]+: /, "") ?? ""}`);
455
+ }
456
+ }
457
+ let ciAdded = false;
458
+ if (wantCi) {
459
+ mkdirSync(path.dirname(workflowFile), { recursive: true });
460
+ writeFileSync(workflowFile, workflowYaml(appPaths, consumer ? installStepsFor((f) => existsSync(path.join(c.cwd, f))) : []));
461
+ ciAdded = true;
462
+ did.push(`CI wrote ${WORKFLOW_PATH}. Commit it with the ${path.relative(c.cwd, dir) || "invariant"}/ folder, so CI has a contract to check against.`);
463
+ }
464
+ const hasCi = ciAdded || existsSync(workflowFile);
465
+ if (wantPublish && publish) {
466
+ const edits = applyPublish(publish);
467
+ const ignored = [".gitignore", ".npmignore"].filter((f) => existsSync(path.join(c.cwd, f)) &&
468
+ /^\/?invariant\/?$/m.test(readFileSync(path.join(c.cwd, f), "utf8")));
469
+ did.push(`publish ${publish.name}: ${edits.join(" and ") || "already set up"}. Apps that install it can run ${CLI_NAME} setup.${ignored.length > 0 ? ` Warning: ${ignored.join(" and ")} ignores invariant/, so it would not be published.` : ""}`);
470
+ }
471
+ say();
472
+ say("Set up:");
473
+ say(` contract ${contract.components.length} components, ${contract.tokens.length} tokens (${path.relative(c.cwd, dir) || "."}/)`);
474
+ for (const l of did)
475
+ say(` ${l}`);
476
+ const first = promoted.length > 0 && withRules ? rulesFor(promoted[0].cand)[0] : undefined;
477
+ if (first && appPaths.length > 0)
478
+ say(`\nSee it work: break this, then run ${CLI_NAME} validate ${appPaths.join(" ")}\n "${first.description}"`);
479
+ say(`\ndone in ${Math.max(1, Math.round((Date.now() - started) / 1000))}s.`);
480
+ if (!hasCi)
481
+ say(`Add this to CI: ${CLI_NAME} verify --fresh && ${CLI_NAME} validate ${appPaths.length > 0 ? appPaths.join(" ") : "src/"}`);
482
+ say(`Review: ${CLI_NAME} patterns list`);
483
+ return 0;
484
+ }
485
+ //# sourceMappingURL=commands-setup.js.map
@@ -0,0 +1,157 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_IGNORED_DIRS, globToRegExp, matchesAny } from "../compiler-core/index.js";
4
+ import { DesignSystem } from "../contract-query/index.js";
5
+ import { formatGithub, formatText, patternSummaryText, RULES, shouldFail, summarize, toJson, toSarif, validateCode } from "../validation/index.js";
6
+ import { applyBaseline, BASELINE_FILE, makeBaseline, readBaseline, writeBaseline } from "./baseline.js";
7
+ import { json, outputDir, print } from "./commands.js";
8
+ import { loadConfig } from "./config.js";
9
+ import { CliError } from "./errors.js";
10
+ import { table } from "./format.js";
11
+ import { loadCompiled } from "./store.js";
12
+ import { CLI_NAME, CLI_VERSION } from "./version.js";
13
+ /** File types `validate` and `hook` read. */
14
+ export const EXTENSIONS = /\.(tsx|jsx|ts|js|mjs|cjs|vue|html)$/i;
15
+ const MAX_FILES = 5000;
16
+ const FRAMEWORKS = ["react", "vue", "angular", "html"];
17
+ const FORMATS = ["text", "json", "github", "sarif"];
18
+ const FAIL_ON = ["error", "warning", "never"];
19
+ /** The framework a file's name implies. Also used by `invariant hook`. */
20
+ export function frameworkFor(file) {
21
+ if (/\.vue$/i.test(file))
22
+ return "vue";
23
+ if (/\.component\.html$/i.test(file))
24
+ return "angular";
25
+ if (/\.html$/i.test(file))
26
+ return "html";
27
+ return undefined;
28
+ }
29
+ export function collect(cwd, target, ignore, out) {
30
+ const abs = path.resolve(cwd, target);
31
+ const rel = path.relative(cwd, abs).split(path.sep).join("/");
32
+ if (rel.startsWith("..") || path.isAbsolute(rel))
33
+ throw new CliError(`${target} is outside the project`);
34
+ let stat;
35
+ try {
36
+ stat = statSync(abs);
37
+ }
38
+ catch {
39
+ throw new CliError(`not found: ${target}`);
40
+ }
41
+ if (stat.isFile()) {
42
+ if (!EXTENSIONS.test(abs))
43
+ throw new CliError(`${target} is not a file type validate reads (${EXTENSIONS.source})`);
44
+ out.push(rel);
45
+ return;
46
+ }
47
+ for (const entry of readdirSync(abs, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
48
+ const childRel = rel === "" ? entry.name : `${rel}/${entry.name}`;
49
+ if (entry.isSymbolicLink())
50
+ continue;
51
+ if (entry.isDirectory()) {
52
+ if (DEFAULT_IGNORED_DIRS.includes(entry.name) || matchesAny(`${childRel}/`, ignore))
53
+ continue;
54
+ collect(cwd, childRel, ignore, out);
55
+ }
56
+ else if (entry.isFile() &&
57
+ EXTENSIONS.test(entry.name) &&
58
+ !/\.d\.ts$/i.test(entry.name) &&
59
+ !matchesAny(childRel, ignore))
60
+ out.push(childRel);
61
+ if (out.length > MAX_FILES)
62
+ throw new CliError(`more than ${MAX_FILES} files; narrow the paths or add validation.ignore`);
63
+ }
64
+ }
65
+ /** `invariant validate <paths...>`: checks application code against the compiled contract. */
66
+ export async function validate(c) {
67
+ const targets = c.args.positionals.slice(1);
68
+ if (targets.length === 0)
69
+ throw new CliError(`usage: ${CLI_NAME} validate <path...> [--format text|json|github|sarif] [--fail-on error|warning|never]. To check the compiled output itself, use \`${CLI_NAME} verify\`.`);
70
+ const format = c.args.values.format ?? "text";
71
+ if (!FORMATS.includes(format))
72
+ throw new CliError(`--format must be one of ${FORMATS.join(", ")}`);
73
+ const framework = c.args.values.framework;
74
+ if (framework && !FRAMEWORKS.includes(framework))
75
+ throw new CliError(`--framework must be one of ${FRAMEWORKS.join(", ")}`);
76
+ const loaded = await loadConfig({
77
+ cwd: c.cwd,
78
+ file: c.args.values.config,
79
+ trustConfig: c.args.values["trust-config"] === true
80
+ });
81
+ const failOn = c.args.values["fail-on"] ?? loaded?.config.validation?.failOn ?? "error";
82
+ if (!FAIL_ON.includes(failOn))
83
+ throw new CliError(`--fail-on must be one of ${FAIL_ON.join(", ")}`);
84
+ const { contract, evidence } = loadCompiled(await outputDir(c));
85
+ const ds = new DesignSystem(contract, evidence);
86
+ const files = [];
87
+ const ignore = (loaded?.config.validation?.ignore ?? []).map((g) => globToRegExp(g));
88
+ for (const t of targets)
89
+ collect(c.cwd, t, ignore, files);
90
+ const unique = [...new Set(files)].sort();
91
+ const rules = (loaded?.config.validation?.rules ?? {});
92
+ const violations = [];
93
+ const evaluations = [];
94
+ const checked = new Set();
95
+ for (const file of unique) {
96
+ const code = readFileSync(path.resolve(c.cwd, file), "utf8");
97
+ const result = validateCode(ds, { code, filename: file, framework: framework ?? frameworkFor(file) }, { rules });
98
+ for (const id of result.summary.componentsChecked)
99
+ checked.add(id);
100
+ for (const f of result.findings)
101
+ violations.push({ ...f, file });
102
+ for (const e of result.evaluations)
103
+ evaluations.push({ ...e, file });
104
+ }
105
+ const baselineFile = path.resolve(c.cwd, BASELINE_FILE);
106
+ if (c.args.values["update-baseline"]) {
107
+ const baseline = makeBaseline(violations);
108
+ writeBaseline(baselineFile, baseline);
109
+ const total = baseline.violations.reduce((n, v) => n + v.count, 0);
110
+ print(c, `wrote ${BASELINE_FILE}: ${total} existing violation(s) in ${unique.length} file(s) are now hidden. New violations are still reported. Commit the file, and fix the old ones when you can.`);
111
+ return 0;
112
+ }
113
+ let shown = violations;
114
+ if (!c.args.values["no-baseline"]) {
115
+ let baseline;
116
+ try {
117
+ baseline = readBaseline(baselineFile);
118
+ }
119
+ catch (error) {
120
+ throw new CliError(error.message, 2);
121
+ }
122
+ if (baseline) {
123
+ const applied = applyBaseline(violations, baseline);
124
+ shown = applied.fresh;
125
+ if (applied.hidden > 0)
126
+ c.io.stderr(`${applied.hidden} existing violation(s) hidden by ${BASELINE_FILE}. Show them with --no-baseline.\n`);
127
+ }
128
+ }
129
+ const summary = summarize(shown, unique.length, checked.size, evaluations);
130
+ if (format === "json")
131
+ json(c, toJson(shown, summary, contract.integrity.contentHash, evaluations));
132
+ else if (format === "sarif")
133
+ json(c, toSarif(shown, { name: CLI_NAME, version: CLI_VERSION }, summary));
134
+ else if (format === "github") {
135
+ const lines = formatGithub(shown);
136
+ if (lines !== "")
137
+ print(c, lines);
138
+ const patterns = patternSummaryText(summary);
139
+ if (patterns !== "")
140
+ print(c, patterns);
141
+ print(c, `${summary.errors} errors, ${summary.warnings} warnings, ${summary.info} info in ${summary.files} files`);
142
+ }
143
+ else
144
+ print(c, formatText(shown, summary, evaluations));
145
+ return shouldFail(summary, failOn) ? 1 : 0;
146
+ }
147
+ /** `invariant rules`: the rules `validate` applies. */
148
+ export async function rules(c) {
149
+ if (c.args.values.json) {
150
+ json(c, RULES);
151
+ return 0;
152
+ }
153
+ print(c, table(["rule", "default", "origin", "plan rule", "title"], RULES.map((r) => [r.code, r.severity, r.origin, r.plan, r.title])));
154
+ print(c, "\nOverride severity (or turn a rule off) with validation.rules in invariant.config.json. Policy rules take their severity from the approved policy.");
155
+ return 0;
156
+ }
157
+ //# sourceMappingURL=commands-validate.js.map