@weatherboard/gyde-design 0.3.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 +21 -0
- package/README.md +264 -0
- package/adoption.mjs +138 -0
- package/agentdocs.mjs +246 -0
- package/boundaries.mjs +350 -0
- package/catalogue.mjs +506 -0
- package/cli.mjs +723 -0
- package/clientboundary.mjs +399 -0
- package/compound.mjs +123 -0
- package/docdrift.mjs +439 -0
- package/emit.mjs +862 -0
- package/enforcement.mjs +100 -0
- package/index.mjs +40 -0
- package/markup.mjs +177 -0
- package/migration.mjs +148 -0
- package/normalise.mjs +416 -0
- package/package.json +59 -0
- package/props.mjs +255 -0
- package/ratchet.mjs +290 -0
- package/rules.mjs +258 -0
- package/scan.mjs +291 -0
- package/stylex.mjs +178 -0
- package/tailwind.mjs +238 -0
- package/tokens.mjs +398 -0
- package/upgrade.mjs +344 -0
- package/usage.mjs +245 -0
- package/wiring.mjs +297 -0
- package/workflow.mjs +221 -0
- package/workspace.mjs +318 -0
package/cli.mjs
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* G-53 — `gyde design`, first version.
|
|
4
|
+
*
|
|
5
|
+
* Four commands.
|
|
6
|
+
*
|
|
7
|
+
* scan <path> measure a repository as it is
|
|
8
|
+
* plan <path> say exactly what would be emitted, and write nothing
|
|
9
|
+
* init <path> write it; refuse to overwrite anything
|
|
10
|
+
* gate <path> fail on anything new since the committed ledger
|
|
11
|
+
*
|
|
12
|
+
* `plan` and `init` call the same `buildEmission()`, so the dry run cannot
|
|
13
|
+
* describe a file the real run would not write. A scaffolder whose dry run and
|
|
14
|
+
* real run can differ is one nobody can review before it writes into their
|
|
15
|
+
* repository, and sharing the call is what makes that structural rather than a
|
|
16
|
+
* promise.
|
|
17
|
+
*
|
|
18
|
+
* Everything printed carries its denominator. CHARTER §5's rule reaches the CLI
|
|
19
|
+
* too: "Gyde did not measure this" and "Gyde found nothing here" must never
|
|
20
|
+
* render the same, so skipped packages are listed by name and rules that
|
|
21
|
+
* matched nothing are reported as suspicious rather than omitted.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
25
|
+
import { execFileSync } from "node:child_process";
|
|
26
|
+
import { join, resolve, dirname } from "node:path";
|
|
27
|
+
|
|
28
|
+
import { discover, summarise, detectScope, detectRunner } from "./workspace.mjs";
|
|
29
|
+
import { scan, format } from "./scan.mjs";
|
|
30
|
+
import { SEED, generateCss, validate } from "./tokens.mjs";
|
|
31
|
+
import {
|
|
32
|
+
seedBoundaries, seedDependencyBoundaries, checkBoundaries,
|
|
33
|
+
checkDependencyBoundaries, findVersionDrift,
|
|
34
|
+
} from "./boundaries.mjs";
|
|
35
|
+
import { emitTokens, emitSystem, emitConfig, SEED_COMPONENTS, SCAFFOLD_VERSION, NOT_UPGRADEABLE } from "./emit.mjs";
|
|
36
|
+
import { emitCatalogue } from "./catalogue.mjs";
|
|
37
|
+
import { emitAgentDocs } from "./agentdocs.mjs";
|
|
38
|
+
import { emitWorkflow } from "./workflow.mjs";
|
|
39
|
+
import { buildManifest, readManifest, writeManifest, applyUpgrade, formatUpgrade, MANIFEST } from "./upgrade.mjs";
|
|
40
|
+
import { exportedComponents, renderableComponents, checkWiring, formatWiring, WIRING } from "./wiring.mjs";
|
|
41
|
+
import { checkClientBoundary, formatClientBoundary, blocks as clientBoundaryBlocks } from "./clientboundary.mjs";
|
|
42
|
+
import { checkDocDrift, formatDocDrift } from "./docdrift.mjs";
|
|
43
|
+
import { reconcile } from "./adoption.mjs";
|
|
44
|
+
import { detectTailwind, formatTailwind } from "./tailwind.mjs";
|
|
45
|
+
import { migrationProgress, formatMigration } from "./migration.mjs";
|
|
46
|
+
import { checkStyleX, formatStyleX } from "./stylex.mjs";
|
|
47
|
+
import { blocksAt, formatSchedule, SCHEDULE } from "./enforcement.mjs";
|
|
48
|
+
import { buildUsage, guidance, formatUsage } from "./usage.mjs";
|
|
49
|
+
import { record, gate, formatGate, adopt, LEDGER_NOTE } from "./ratchet.mjs";
|
|
50
|
+
import { loadRules } from "./rules.mjs";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The single source of what would be written.
|
|
54
|
+
*
|
|
55
|
+
* `plan` prints these paths and `init` writes these contents — the same call, so
|
|
56
|
+
* the dry run cannot describe something the real run does not do. Any future
|
|
57
|
+
* emitter belongs here and nowhere else.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* The repository's default branch, read from git rather than assumed.
|
|
61
|
+
*
|
|
62
|
+
* `main` is the common case and not the universal one — the first repository
|
|
63
|
+
* this was installed into uses `staging`, where a hardcoded `branches: [main]`
|
|
64
|
+
* silently never fires. Returns null when it cannot tell, so the caller falls
|
|
65
|
+
* back explicitly instead of this function inventing an answer.
|
|
66
|
+
*/
|
|
67
|
+
function detectDefaultBranch(root) {
|
|
68
|
+
try {
|
|
69
|
+
const head = execFileSync("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
70
|
+
{ cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
71
|
+
return head.replace(/^origin\//, "") || null;
|
|
72
|
+
} catch { /* no remote, or a repo with no origin/HEAD set */ }
|
|
73
|
+
try {
|
|
74
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"],
|
|
75
|
+
{ cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
76
|
+
} catch { return null; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildEmission(design, { hasTokens, hasSystem, scope, rootDir }) {
|
|
80
|
+
const systemPath = design.systemPath || "packages/design-system";
|
|
81
|
+
const tokensPath = design.tokensPath || "packages/design-tokens";
|
|
82
|
+
const primitive = design.primitive || "@base-ui/react";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The names the emitted code imports.
|
|
86
|
+
*
|
|
87
|
+
* Derived from the workspace's own scope, never invented. A hardcoded
|
|
88
|
+
* placeholder made every emitted import resolve to nothing — a scaffolded
|
|
89
|
+
* repository that would not build, with every test green because each
|
|
90
|
+
* compared the emitted text against itself.
|
|
91
|
+
*
|
|
92
|
+
* A workspace with no scope at all has to say what it wants: guessing a name
|
|
93
|
+
* is how you get a package nobody can import and nobody can find.
|
|
94
|
+
*/
|
|
95
|
+
const ns = design.scope || scope;
|
|
96
|
+
if (!ns) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Could not determine an npm scope for this workspace, and will not invent one.\n" +
|
|
99
|
+
"Set `design.scope` in gyde.config.json (e.g. \"@acme\") — emitted code has to\n" +
|
|
100
|
+
"import the packages it emits, and a guessed name resolves to nothing.");
|
|
101
|
+
}
|
|
102
|
+
const systemPackage = design.systemPackage || `${ns}/design-system`;
|
|
103
|
+
const tokensPackage = design.tokensPackage || `${ns}/design-tokens`;
|
|
104
|
+
const cataloguePackage = design.cataloguePackage || `${ns}/design-catalogue`;
|
|
105
|
+
|
|
106
|
+
let files = {};
|
|
107
|
+
// An existing package is audited, never regenerated over. Emitting into a
|
|
108
|
+
// directory somebody already owns is the one thing a scaffolder must not do
|
|
109
|
+
// by default (G-47).
|
|
110
|
+
if (!hasTokens) files = { ...files, ...emitTokens(undefined, { path: tokensPath, tokensPackage }) };
|
|
111
|
+
if (!hasSystem) {
|
|
112
|
+
files = { ...files, ...emitSystem({ primitive, path: systemPath, tokensPackage, systemPackage }) };
|
|
113
|
+
// The catalogue is emitted with the set, never separately: it is generated
|
|
114
|
+
// from the same component list the barrel is, which is what makes coverage
|
|
115
|
+
// true by construction rather than by a test somebody has to keep passing.
|
|
116
|
+
files = { ...files, ...emitCatalogue({ components: SEED_COMPONENTS, path: design.cataloguePath || "apps/design", systemPackage, tokensPackage, cataloguePackage }) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The agent doc, decided by what is ON DISK — never by the caller's flags.
|
|
121
|
+
*
|
|
122
|
+
* Outside the `hasSystem` branch on purpose, and G-66 is why. `upgrade`
|
|
123
|
+
* passes `hasSystem: false` deliberately, so a scaffolded repository still
|
|
124
|
+
* receives component template fixes. Any doc decision taken inside that
|
|
125
|
+
* branch therefore reads "greenfield" in EVERY repository, including ones
|
|
126
|
+
* with fifty-six components of their own.
|
|
127
|
+
*
|
|
128
|
+
* That is not hypothetical — it shipped, an hour after the same bug was
|
|
129
|
+
* fixed one level up. System B received a document containing
|
|
130
|
+
* `### Button` and a prop table, for a Button Gyde has never seen. The flag
|
|
131
|
+
* said greenfield; the repository was not. A fix that trusts the same wrong
|
|
132
|
+
* input as the bug is not a fix.
|
|
133
|
+
*
|
|
134
|
+
* So the question is put to the filesystem instead. A parseable barrel with
|
|
135
|
+
* real components means the product owns the set and Gyde documents names
|
|
136
|
+
* only. No barrel means Gyde is about to emit the seed set, and can describe
|
|
137
|
+
* it fully because it wrote it.
|
|
138
|
+
*
|
|
139
|
+
* This is the one emitted artefact that changes what gets WRITTEN rather
|
|
140
|
+
* than judging what was: an agent that knows the set uses it, and one that
|
|
141
|
+
* does not gets audited for not using it — the expensive order.
|
|
142
|
+
*/
|
|
143
|
+
const theirs = renderableComponents(rootDir, systemPath);
|
|
144
|
+
files = { ...files, ...emitAgentDocs(theirs
|
|
145
|
+
? { components: theirs, systemPackage, verified: false }
|
|
146
|
+
: { components: SEED_COMPONENTS, systemPackage, verified: true }) };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The gate.
|
|
150
|
+
*
|
|
151
|
+
* Emitted even when a design system already exists, because the workflow is
|
|
152
|
+
* what makes the verdict binding and an adopting repository needs it most.
|
|
153
|
+
* Without it `gate` is a command somebody remembers to type, which is the
|
|
154
|
+
* most complete skip there is.
|
|
155
|
+
*/
|
|
156
|
+
// Detected ONCE, here, then handed to both the workflow and the config it is
|
|
157
|
+
// recorded in. Re-detecting on a later run is how a workflow silently
|
|
158
|
+
// changes under a repository (G-66).
|
|
159
|
+
const runsOn = design.runsOn || detectRunner(rootDir) || "ubuntu-latest";
|
|
160
|
+
const defaultBranch = design.defaultBranch || detectDefaultBranch(rootDir) || "main";
|
|
161
|
+
|
|
162
|
+
files = { ...files, ...emitWorkflow({
|
|
163
|
+
action: design.action || undefined,
|
|
164
|
+
failOn: design.failOn || undefined,
|
|
165
|
+
runsOn,
|
|
166
|
+
defaultBranch,
|
|
167
|
+
}) };
|
|
168
|
+
files = { ...files, ...emitConfig({ systemPath, tokensPath, primitive, scope: ns, foreignPaths: design.foreignPaths || [], defaultBranch, runsOn }) };
|
|
169
|
+
return files;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const PRIMITIVES = [
|
|
173
|
+
{ name: "@base-ui/react", label: "Base UI" },
|
|
174
|
+
{ name: "@base-ui-components/react", label: "Base UI (pre-1.0 package name)" },
|
|
175
|
+
{ name: "@radix-ui/react-dialog", label: "Radix", family: /^@radix-ui\// },
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/** Read gyde.config.json if the project has one. Absent is fine; guessed is not. */
|
|
179
|
+
function readConfig(root) {
|
|
180
|
+
const path = join(root, "gyde.config.json");
|
|
181
|
+
if (!existsSync(path)) return { present: false, design: {} };
|
|
182
|
+
try {
|
|
183
|
+
const json = JSON.parse(readFileSync(path, "utf8"));
|
|
184
|
+
return { present: true, design: json.design || {} };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw new Error(`gyde.config.json exists but could not be parsed: ${e.message}\n` +
|
|
187
|
+
"Refusing to continue on a default — a misread config is how a project ends up measured against a scope nobody chose.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** What the repository already uses, read from manifests rather than assumed. */
|
|
192
|
+
function detectStack(root, packages) {
|
|
193
|
+
const found = { primitives: [], styling: [], tokens: null };
|
|
194
|
+
for (const pkg of packages) {
|
|
195
|
+
const manifest = join(root, pkg.path === "." ? "" : pkg.path, "package.json");
|
|
196
|
+
if (!existsSync(manifest)) continue;
|
|
197
|
+
let json; try { json = JSON.parse(readFileSync(manifest, "utf8")); } catch { continue; }
|
|
198
|
+
const deps = { ...(json.dependencies || {}), ...(json.devDependencies || {}) };
|
|
199
|
+
for (const name of Object.keys(deps)) {
|
|
200
|
+
for (const p of PRIMITIVES) {
|
|
201
|
+
if (name === p.name || (p.family && p.family.test(name))) {
|
|
202
|
+
const label = `${p.label} (${name}@${deps[name]})`;
|
|
203
|
+
if (!found.primitives.some((x) => x.startsWith(p.label))) found.primitives.push(`${p.label} — first seen ${pkg.path}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (/^tailwindcss$/.test(name)) found.styling.push(`Tailwind ${deps[name]} in ${pkg.path}`);
|
|
207
|
+
if (/^@stylexjs\/stylex$/.test(name)) found.styling.push(`StyleX ${deps[name]} in ${pkg.path}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
found.primitives = [...new Set(found.primitives)];
|
|
211
|
+
found.styling = [...new Set(found.styling)];
|
|
212
|
+
return found;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cmdScan(root, config) {
|
|
216
|
+
const ws = discover(root);
|
|
217
|
+
const sum = summarise(ws);
|
|
218
|
+
|
|
219
|
+
console.log(`workspace ${sum.source}`);
|
|
220
|
+
console.log(` ${sum.carryUI.length} package(s) render UI: ${sum.carryUI.join(", ") || "none"}`);
|
|
221
|
+
if (sum.skipped.length) {
|
|
222
|
+
console.log(`skipped ${sum.skipped.length}, each with a reason:`);
|
|
223
|
+
for (const s of sum.skipped) console.log(` ${s.path} — ${s.because}`);
|
|
224
|
+
}
|
|
225
|
+
if (sum.uiWithoutPipeline.length) {
|
|
226
|
+
console.log(`no pipeline ${sum.uiWithoutPipeline.join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const stack = detectStack(root, ws.packages);
|
|
230
|
+
console.log(`primitives ${stack.primitives.join("; ") || "none detected"}`);
|
|
231
|
+
console.log(`styling ${stack.styling.join("; ") || "none detected"}`);
|
|
232
|
+
console.log("");
|
|
233
|
+
|
|
234
|
+
// The barrel is read once and handed to the scan, so markup adoption (G-73)
|
|
235
|
+
// and the compound check (G-72) are measured against the same export list the
|
|
236
|
+
// rest of this command reports on. Re-deriving it per call site is how the
|
|
237
|
+
// three `scan` invocations in this file drifted the first time.
|
|
238
|
+
const scanRoots = exportedComponents(root, config.design?.systemPath || "packages/design-system");
|
|
239
|
+
const result = scan(root, {
|
|
240
|
+
config: { ...config.design, componentRoots: scanRoots ? new Set(scanRoots) : null },
|
|
241
|
+
});
|
|
242
|
+
console.log(format(result));
|
|
243
|
+
|
|
244
|
+
const drift = findVersionDrift(root, { packages: ws.packages, match: /^(@radix-ui|@base-ui)/ });
|
|
245
|
+
if (drift.length) {
|
|
246
|
+
console.log(`\ndrift ${drift.length} primitive package(s) pinned at more than one version:`);
|
|
247
|
+
for (const d of drift.slice(0, 8)) {
|
|
248
|
+
console.log(` ${d.dependency} ${Object.entries(d.versions).map(([p, v]) => `${p}=${v}`).join(" ")}`);
|
|
249
|
+
}
|
|
250
|
+
if (drift.length > 8) console.log(` … and ${drift.length - 8} more`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const design = config.design || {};
|
|
254
|
+
const systemPath = design.systemPath || "packages/design-system";
|
|
255
|
+
const components = exportedComponents(root, systemPath);
|
|
256
|
+
|
|
257
|
+
if (components || ws.packages.some((p) => p.path === systemPath)) {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(formatWiring(checkWiring(root, ws.packages, {
|
|
260
|
+
systemPath,
|
|
261
|
+
systemPackage: design.systemPackage || null,
|
|
262
|
+
tokensPackage: design.tokensPackage || null,
|
|
263
|
+
})));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// G-67. Reported wherever the system is measured, because the defect it
|
|
267
|
+
// finds is invisible at every other gate: the source reads as correct and
|
|
268
|
+
// the build passes.
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log(formatClientBoundary(checkClientBoundary(root, { packages: ws.packages })));
|
|
271
|
+
|
|
272
|
+
// G-70. contract.md ranks docs-rot the highest-priority intake class, because
|
|
273
|
+
// a doc naming a deleted component still looks authoritative and an agent
|
|
274
|
+
// reads it first.
|
|
275
|
+
// G-98. Printed with the class count beside it, because the dependency must
|
|
276
|
+
// not be removed while classes still resolve through it.
|
|
277
|
+
// G-100. The positive half of G-98: one styling layer, and it is wired.
|
|
278
|
+
console.log("");
|
|
279
|
+
console.log(formatStyleX(checkStyleX(root, { packages: ws.packages, renders: sum.carryUI })));
|
|
280
|
+
|
|
281
|
+
console.log("");
|
|
282
|
+
console.log(formatTailwind(detectTailwind(root, {
|
|
283
|
+
packages: ws.packages,
|
|
284
|
+
utilityClasses: result.tailwindClasses,
|
|
285
|
+
cssDirectives: result.tailwindCssDirectives,
|
|
286
|
+
})));
|
|
287
|
+
|
|
288
|
+
console.log("");
|
|
289
|
+
console.log(formatDocDrift(checkDocDrift(root, {
|
|
290
|
+
systemPackage: design.systemPackage || null,
|
|
291
|
+
exports: components,
|
|
292
|
+
// G-96. Which documents the product declares are component contracts.
|
|
293
|
+
// Undeclared, the prose assertion does not run and the report says so.
|
|
294
|
+
contractDocs: design.contractDocs || [],
|
|
295
|
+
})));
|
|
296
|
+
|
|
297
|
+
if (components) {
|
|
298
|
+
const usage = buildUsage(root, {
|
|
299
|
+
components,
|
|
300
|
+
excludePaths: [systemPath, ...(design.usageExcludePaths || [])],
|
|
301
|
+
classPrefixes: design.classPrefixes || ["ds-"],
|
|
302
|
+
});
|
|
303
|
+
const found = guidance(usage);
|
|
304
|
+
console.log("");
|
|
305
|
+
console.log(formatUsage(usage, found));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!config.present) {
|
|
309
|
+
console.log(
|
|
310
|
+
"\nnote no gyde.config.json — every path was treated as product code.\n" +
|
|
311
|
+
" `systemPaths` and `foreignPaths` suppress findings, so they are\n" +
|
|
312
|
+
" configuration a project commits, never something Gyde infers.",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return result.findings.length === 0 ? 0 : 0; // scan reports; the gate decides (G-52)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cmdPlan(root, config) {
|
|
319
|
+
const ws = discover(root);
|
|
320
|
+
const sum = summarise(ws);
|
|
321
|
+
const stack = detectStack(root, ws.packages);
|
|
322
|
+
const design = config.design || {};
|
|
323
|
+
const systemPath = design.systemPath || "packages/design-system";
|
|
324
|
+
const tokensPath = design.tokensPath || "packages/design-tokens";
|
|
325
|
+
|
|
326
|
+
const alreadyHasSystem = ws.packages.some((p) => p.path === systemPath);
|
|
327
|
+
const alreadyHasTokens = ws.packages.some((p) => p.path === tokensPath);
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* An existing component library does not have to be at the path Gyde would
|
|
331
|
+
* have chosen.
|
|
332
|
+
*
|
|
333
|
+
* The first version checked only `packages/design-system` and declared
|
|
334
|
+
* System C GREENFIELD — a repository with 22 shadcn components, a
|
|
335
|
+
* token dictionary and two apps consuming it in 109 files. Scaffolding on top
|
|
336
|
+
* of that would have been the most destructive thing this tool could do, and
|
|
337
|
+
* it would have been done confidently. So look for the shape, not the name:
|
|
338
|
+
* a UI-bearing package that is not an application.
|
|
339
|
+
*/
|
|
340
|
+
const candidateLibraries = ws.packages.filter((p) =>
|
|
341
|
+
p.carriesUI && p.pipelines.every((x) => x.kind !== "next" && x.kind !== "expo") &&
|
|
342
|
+
p.path !== systemPath && p.path !== tokensPath);
|
|
343
|
+
|
|
344
|
+
const mode = alreadyHasSystem || alreadyHasTokens || candidateLibraries.length ? "adoption" : "greenfield";
|
|
345
|
+
|
|
346
|
+
console.log(`mode ${mode}`);
|
|
347
|
+
if (mode === "adoption") {
|
|
348
|
+
console.log(" a component library already exists here. Gyde records current debt");
|
|
349
|
+
console.log(" as allowances and ratchets from there. Amnesty never (CHARTER §4).");
|
|
350
|
+
for (const c of candidateLibraries) {
|
|
351
|
+
console.log(` found: ${c.path}${c.name ? ` (${c.name})` : ""} — not at Gyde's default path,`);
|
|
352
|
+
console.log(" so adopting it in place is a decision, not a default (G-61).");
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
console.log(" no UI-bearing library package found; the baseline starts and stays at zero.");
|
|
356
|
+
}
|
|
357
|
+
console.log("");
|
|
358
|
+
|
|
359
|
+
// The SAME call init makes. plan cannot describe a file init would not write.
|
|
360
|
+
const files = buildEmission(design, { hasTokens: alreadyHasTokens, hasSystem: alreadyHasSystem, scope: detectScope(ws).scope, rootDir: root });
|
|
361
|
+
console.log(`would emit ${Object.keys(files).length} file(s)`);
|
|
362
|
+
for (const path of Object.keys(files).sort()) {
|
|
363
|
+
const exists = existsSync(join(root, path));
|
|
364
|
+
console.log(` ${exists ? "SKIP" : " "} ${path}${exists ? " already exists — init refuses to overwrite" : ""}`);
|
|
365
|
+
}
|
|
366
|
+
if (alreadyHasTokens) console.log(` ${tokensPath}/ EXISTS — audited, never regenerated over`);
|
|
367
|
+
if (alreadyHasSystem) console.log(` ${systemPath}/ EXISTS — closure enforced on it, not replaced`);
|
|
368
|
+
|
|
369
|
+
console.log("\nwould wire");
|
|
370
|
+
const pipelines = sum.pipelines;
|
|
371
|
+
if (pipelines.length === 0) console.log(" nothing — no build pipeline was found in any UI package");
|
|
372
|
+
for (const p of pipelines) console.log(` ${p}`);
|
|
373
|
+
if (sum.uiWithoutPipeline.length) {
|
|
374
|
+
console.log(` NOT WIRED, and reported rather than skipped: ${sum.uiWithoutPipeline.join(", ")}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log("\nwould leave alone");
|
|
378
|
+
for (const s of sum.skipped) console.log(` ${s.path.padEnd(42)} ${s.because}`);
|
|
379
|
+
|
|
380
|
+
console.log("\nprimitive layer");
|
|
381
|
+
if (stack.primitives.length === 0) {
|
|
382
|
+
console.log(" none present — Gyde would install one (G-58 decides which)");
|
|
383
|
+
} else {
|
|
384
|
+
console.log(` ${stack.primitives.join("\n ")}`);
|
|
385
|
+
console.log(" present already. Whether Gyde adopts this in place or migrates it is G-61,");
|
|
386
|
+
console.log(" and it is an open question rather than something this command should assume.");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const boundaries = seedBoundaries({
|
|
390
|
+
primitive: design.primitive || "@base-ui/react",
|
|
391
|
+
systemPath, tokensPath,
|
|
392
|
+
appPaths: sum.carryUI.filter((p) => p !== systemPath && p !== tokensPath),
|
|
393
|
+
});
|
|
394
|
+
const { breaches } = checkBoundaries(root, boundaries);
|
|
395
|
+
const depBreaches = checkDependencyBoundaries(root, seedDependencyBoundaries({
|
|
396
|
+
primitive: design.primitive || "@base-ui/react", systemPath,
|
|
397
|
+
}), { packages: ws.packages });
|
|
398
|
+
|
|
399
|
+
console.log("\nboundaries it would enforce");
|
|
400
|
+
for (const b of boundaries) {
|
|
401
|
+
for (const f of b.forbid) {
|
|
402
|
+
console.log(` ${(b.from || "(everywhere)").padEnd(28)} may not import ${f.pattern.source}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
console.log(` → ${breaches.length} import breach(es), ${depBreaches.length} dependency breach(es) today`);
|
|
406
|
+
for (const b of breaches.slice(0, 5)) console.log(` ${b.file}:${b.line} ${b.source}`);
|
|
407
|
+
for (const b of depBreaches.slice(0, 5)) console.log(` ${b.package} declares ${b.dependency}@${b.version}`);
|
|
408
|
+
|
|
409
|
+
const roots = exportedComponents(root, design.systemPath || "packages/design-system");
|
|
410
|
+
const result = scan(root, { config: { ...design, componentRoots: roots ? new Set(roots) : null } });
|
|
411
|
+
console.log(`\nbaseline it would record`);
|
|
412
|
+
console.log(` ${result.findings.length} finding(s) across ${result.scope.filesWithStyling} file(s) with styling`);
|
|
413
|
+
console.log(` adoption ${result.adoption.percent ?? "n/a"}% (${result.adoption.tokenised} of ${result.adoption.of} declarations)`);
|
|
414
|
+
console.log(" This is a debt with a direction: the list may only ever get shorter.");
|
|
415
|
+
|
|
416
|
+
console.log("\nnothing was written. `gyde design init` executes exactly this emission.");
|
|
417
|
+
return 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Write the plan.
|
|
422
|
+
*
|
|
423
|
+
* Refuses to overwrite anything. Gyde emits once and the file becomes the
|
|
424
|
+
* product's (G-47); a scaffolder that clobbers an existing file has taken
|
|
425
|
+
* ownership of something it does not own, and it would do it silently.
|
|
426
|
+
*/
|
|
427
|
+
function cmdInit(root, config) {
|
|
428
|
+
const ws = discover(root);
|
|
429
|
+
const design = config.design || {};
|
|
430
|
+
const systemPath = design.systemPath || "packages/design-system";
|
|
431
|
+
const tokensPath = design.tokensPath || "packages/design-tokens";
|
|
432
|
+
|
|
433
|
+
const files = buildEmission(design, {
|
|
434
|
+
hasTokens: ws.packages.some((p) => p.path === tokensPath),
|
|
435
|
+
hasSystem: ws.packages.some((p) => p.path === systemPath),
|
|
436
|
+
scope: detectScope(ws).scope,
|
|
437
|
+
rootDir: root,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const written = [];
|
|
441
|
+
const refused = [];
|
|
442
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
443
|
+
const full = join(root, rel);
|
|
444
|
+
if (existsSync(full)) { refused.push(rel); continue; }
|
|
445
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
446
|
+
writeFileSync(full, contents);
|
|
447
|
+
written.push(rel);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Provenance for everything actually written. Without it an upgrade cannot
|
|
451
|
+
// tell the product's edit from Gyde's change, and has to choose between
|
|
452
|
+
// clobbering and doing nothing — which is shadcn's unsolved problem (G-61).
|
|
453
|
+
if (written.length) {
|
|
454
|
+
const emitted = Object.fromEntries(
|
|
455
|
+
written.filter((w) => !NOT_UPGRADEABLE.has(w)).map((w) => [w, files[w]]));
|
|
456
|
+
const existing = readManifest(root);
|
|
457
|
+
const manifest = buildManifest(emitted, { version: SCAFFOLD_VERSION });
|
|
458
|
+
if (existing) manifest.files = { ...existing.files, ...manifest.files };
|
|
459
|
+
writeManifest(root, manifest);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (const w of written.sort()) console.log(`wrote ${w}`);
|
|
463
|
+
for (const r of refused.sort()) console.log(`refused ${r} already exists; Gyde never overwrites`);
|
|
464
|
+
if (written.length) console.log(`wrote ${MANIFEST} (provenance: what was emitted, so an upgrade can tell your edits from ours)`);
|
|
465
|
+
|
|
466
|
+
console.log(`\n${written.length} written, ${refused.length} refused.`);
|
|
467
|
+
if (written.length) {
|
|
468
|
+
console.log("\nEvery emitted file is yours now. Replace the placeholder values —");
|
|
469
|
+
console.log("that is the point. What stays checked is the shape: closure, the token");
|
|
470
|
+
console.log("contract, and the boundary rules.");
|
|
471
|
+
}
|
|
472
|
+
// A half-finished scaffold must not look like a finished one.
|
|
473
|
+
return refused.length && !written.length ? 1 : 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Take a new template version into an already-scaffolded repository.
|
|
478
|
+
*
|
|
479
|
+
* The whole mechanism is in upgrade.mjs; this is the surface. `--dry-run`
|
|
480
|
+
* shares the same call, for the same reason `plan` and `init` do.
|
|
481
|
+
*/
|
|
482
|
+
function cmdUpgrade(root, config, { dryRun }) {
|
|
483
|
+
const ws = discover(root);
|
|
484
|
+
const design = config.design || {};
|
|
485
|
+
const systemPath = design.systemPath || "packages/design-system";
|
|
486
|
+
const tokensPath = design.tokensPath || "packages/design-tokens";
|
|
487
|
+
|
|
488
|
+
const manifest = readManifest(root);
|
|
489
|
+
if (!manifest) {
|
|
490
|
+
console.log(`no ${MANIFEST} — this repository was not scaffolded by a version of Gyde that`);
|
|
491
|
+
console.log("records provenance. Every emitted path is therefore UNTRACKED, and untracked");
|
|
492
|
+
console.log("means untouched: an upgrade would be a guess about which side owns each file.");
|
|
493
|
+
console.log("\nRun `init` in a scratch copy and diff, or adopt the current files by hand.");
|
|
494
|
+
return 1;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// What THIS version would emit for a fresh repo, regardless of what exists —
|
|
498
|
+
// an upgrade compares against the templates, not against a skip decision.
|
|
499
|
+
// The same call init makes, so a fresh scaffold reports no drift.
|
|
500
|
+
const next = buildEmission(design, { hasTokens: false, hasSystem: false, scope: detectScope(ws).scope, rootDir: root });
|
|
501
|
+
delete next["gyde.config.json"];
|
|
502
|
+
|
|
503
|
+
const result = applyUpgrade(root, next, manifest, { version: SCAFFOLD_VERSION, dryRun });
|
|
504
|
+
console.log(formatUpgrade(result, { manifest, version: SCAFFOLD_VERSION }));
|
|
505
|
+
|
|
506
|
+
if (!dryRun) {
|
|
507
|
+
writeManifest(root, result.nextManifest);
|
|
508
|
+
console.log(`\nprovenance updated. An unapplied change keeps its old baseline, so a`);
|
|
509
|
+
console.log("conflict is offered again next time rather than quietly becoming yours.");
|
|
510
|
+
} else {
|
|
511
|
+
console.log("\nnothing was written.");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Conflicts are not a failure of the upgrade — they are its output. But they
|
|
515
|
+
// need somebody, so they exit non-zero rather than scrolling past in CI.
|
|
516
|
+
return result.conflicts.length ? 1 : 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The gate.
|
|
521
|
+
*
|
|
522
|
+
* Reads the committed ledger, rescans, and fails on anything new. With no
|
|
523
|
+
* ledger it records one and says so — recording debt is the sanctioned entry
|
|
524
|
+
* path (CHARTER §4, amnesty never), and it is never a silent pass.
|
|
525
|
+
*/
|
|
526
|
+
/**
|
|
527
|
+
* Evidence that the gate actually ran.
|
|
528
|
+
*
|
|
529
|
+
* An exit code is not evidence: a step that was skipped, filtered out by a
|
|
530
|
+
* `paths:` rule, or never reached leaves a green job behind and no way to tell
|
|
531
|
+
* it from a clean pass. That is the failure `verify-tier` documents six times
|
|
532
|
+
* over, and CHARTER §5 exists because of it.
|
|
533
|
+
*
|
|
534
|
+
* WRITTEN ON EVERY EXIT PATH, INCLUDING THE FAILURES. The first version wrote
|
|
535
|
+
* it only after a verdict, so the baseline-recording run — which exits non-zero
|
|
536
|
+
* on purpose — produced none, and the workflow's proof step then reported "the
|
|
537
|
+
* gate did not run" for a gate that had just run and recorded a baseline.
|
|
538
|
+
*
|
|
539
|
+
* That is the same confusion this product is built to prevent, in its own
|
|
540
|
+
* code: a check that cannot distinguish ABSENT from DID SOMETHING ELSE will
|
|
541
|
+
* report the wrong one, confidently. So the evidence carries `why`, and every
|
|
542
|
+
* path writes it.
|
|
543
|
+
*/
|
|
544
|
+
function writeEvidence(root, result, { ok, why, clientBoundary = null, enforcement = null }) {
|
|
545
|
+
mkdirSync(join(root, ".gyde"), { recursive: true });
|
|
546
|
+
writeFileSync(join(root, ".gyde", "last-verdict.json"), JSON.stringify({
|
|
547
|
+
"//": "Written by `gyde-design gate`. Proof the gate ran, not just that a job went green.",
|
|
548
|
+
ranAt: process.env.GYDE_RUN_ID || null,
|
|
549
|
+
ok,
|
|
550
|
+
why,
|
|
551
|
+
findings: result ? result.findings.length : null,
|
|
552
|
+
filesWithStyling: result ? result.scope.filesWithStyling : null,
|
|
553
|
+
filesWalked: result ? result.scope.filesWalked : null,
|
|
554
|
+
/**
|
|
555
|
+
* G-74. The RECONCILED figure, not the token layer.
|
|
556
|
+
*
|
|
557
|
+
* action.yml reads `adoption.percent` for the job summary. Leaving the
|
|
558
|
+
* token number here while the CLI printed the weakest layer would have put
|
|
559
|
+
* 75% on a pull request and 20% in a terminal, for the same commit — which
|
|
560
|
+
* is the two-teams-disagree failure this issue exists to end, reintroduced
|
|
561
|
+
* by the fix for it.
|
|
562
|
+
*
|
|
563
|
+
* `percent` may be null when a layer could not be measured. The action
|
|
564
|
+
* already renders an empty adoption as `n/a`, which is the correct reading:
|
|
565
|
+
* not measured is not zero.
|
|
566
|
+
*/
|
|
567
|
+
adoption: result ? reconcile({
|
|
568
|
+
token: { ...result.adoption, used: result.adoption.tokenised },
|
|
569
|
+
markup: result.markup?.unknown ? null : result.markup?.adoption,
|
|
570
|
+
}) : null,
|
|
571
|
+
rulesThatMatchedNothing: result ? result.coverage.filter((c) => !c.matched).map((c) => c.rule) : [],
|
|
572
|
+
clientBoundary,
|
|
573
|
+
enforcement,
|
|
574
|
+
}, null, 2) + "\n");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function cmdGate(root, config) {
|
|
578
|
+
const design = config.design || {};
|
|
579
|
+
const ledgerPath = join(root, "gyde-allowance.json");
|
|
580
|
+
const roots = exportedComponents(root, design.systemPath || "packages/design-system");
|
|
581
|
+
const result = scan(root, { config: { ...design, componentRoots: roots ? new Set(roots) : null } });
|
|
582
|
+
|
|
583
|
+
if (!existsSync(ledgerPath)) {
|
|
584
|
+
const led = record(result.findings, {
|
|
585
|
+
recorded: process.env.GYDE_DATE || null,
|
|
586
|
+
rules: result.rulesRun,
|
|
587
|
+
});
|
|
588
|
+
writeFileSync(ledgerPath, JSON.stringify(led, null, 2) + "\n");
|
|
589
|
+
console.log(`no ledger found — recorded ${led.total} existing finding(s) as the baseline.`);
|
|
590
|
+
console.log(`wrote gyde-allowance.json\n`);
|
|
591
|
+
console.log(LEDGER_NOTE);
|
|
592
|
+
console.log("\nThis run recorded a baseline rather than judging one. It is not a pass.");
|
|
593
|
+
writeEvidence(root, result, { ok: false, why: "recorded a baseline; nothing was judged" });
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let ledger;
|
|
598
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, "utf8")); }
|
|
599
|
+
catch (e) {
|
|
600
|
+
console.error(`gyde-allowance.json could not be parsed: ${e.message}`);
|
|
601
|
+
writeEvidence(root, result, { ok: false, why: `the ledger could not be parsed: ${e.message}` });
|
|
602
|
+
return 1;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const verdict = gate([{ name: "design-system", findings: result.findings, ledger }]);
|
|
606
|
+
console.log(formatGate(verdict));
|
|
607
|
+
|
|
608
|
+
// G-99. No gate and no verdict change — the ratchet was already correct for a
|
|
609
|
+
// migration. This subtracts two numbers the ledger has carried since G-52, so
|
|
610
|
+
// that "not started" and "not visible from here" stop reading the same.
|
|
611
|
+
const migrating = design.migrating || [];
|
|
612
|
+
if (migrating.length) {
|
|
613
|
+
console.log("");
|
|
614
|
+
console.log(formatMigration(migrationProgress(result.findings, ledger, { migrating })));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// G-68. Written, not just reported. An adoption that stays in stdout is
|
|
618
|
+
// adopted again on the next run, so the rule reports forever and ratchets
|
|
619
|
+
// never — which is the to-do-list-nobody-reads failure LEDGER_NOTE names.
|
|
620
|
+
if (verdict.adopted.length) {
|
|
621
|
+
writeFileSync(ledgerPath, JSON.stringify(
|
|
622
|
+
adopt(ledger, verdict.adopted, { rules: result.rulesRun }), null, 2) + "\n");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// G-67. Deliberately NOT a ledger finding. A crossing is a runtime defect
|
|
626
|
+
// rather than style debt, so allowancing one would record "this renders
|
|
627
|
+
// undefined props, and that is fine for now" as a committed number. It is
|
|
628
|
+
// gated on the product's own template version instead — see BLOCKS_FROM.
|
|
629
|
+
// Discovered once and shared by every check below. Calling `discover` per
|
|
630
|
+
// check re-walks the workspace and, more importantly, lets two checks in the
|
|
631
|
+
// same run disagree about which packages exist.
|
|
632
|
+
const ws = discover(root);
|
|
633
|
+
const cb = checkClientBoundary(root, { packages: ws.packages });
|
|
634
|
+
const cbBlocks = clientBoundaryBlocks(readManifest(root)?.version);
|
|
635
|
+
if (cb.crossings.length || cb.unresolved.length) {
|
|
636
|
+
console.log("");
|
|
637
|
+
console.log(formatClientBoundary(cb, { blocking: cb.crossings.length ? cbBlocks : null }));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* G-101. The mandates, enforced on the same schedule as the client boundary.
|
|
642
|
+
*
|
|
643
|
+
* Each is reported wherever it is measured and blocks only from the template
|
|
644
|
+
* version in the SCHEDULE, which the product takes by running `upgrade`. The
|
|
645
|
+
* schedule is printed whenever anything on it is advisory, so a consumer can
|
|
646
|
+
* see what taking the next version costs before they take it — the question
|
|
647
|
+
* a changelog answers badly and a generated table answers exactly.
|
|
648
|
+
*/
|
|
649
|
+
const templateVersion = readManifest(root)?.version ?? null;
|
|
650
|
+
|
|
651
|
+
const tw = detectTailwind(root, {
|
|
652
|
+
packages: ws.packages,
|
|
653
|
+
utilityClasses: result.tailwindClasses,
|
|
654
|
+
cssDirectives: result.tailwindCssDirectives,
|
|
655
|
+
});
|
|
656
|
+
const sx = checkStyleX(root, { packages: ws.packages, renders: summarise(ws).carryUI });
|
|
657
|
+
|
|
658
|
+
const breaches = [
|
|
659
|
+
{ rule: "client-boundary", n: cb.crossings.length, what: `${cb.crossings.length} value(s) crossing the "use client" boundary` },
|
|
660
|
+
{ rule: "tailwind-present", n: tw.present ? tw.dependencies.length + tw.configs.length : 0, what: "Tailwind is installed" },
|
|
661
|
+
{ rule: "tailwind-in-css", n: result.tailwindCssDirectives, what: `${result.tailwindCssDirectives} Tailwind directive(s) in stylesheets` },
|
|
662
|
+
{ rule: "styling-layer", n: sx.unknown ? 0 : sx.missing.length + sx.unwired.length + sx.competing.length, what: "the styling layer is not StyleX, or is not wired" },
|
|
663
|
+
].filter((b) => b.n > 0);
|
|
664
|
+
|
|
665
|
+
const blocking = breaches.filter((b) => blocksAt(b.rule, templateVersion));
|
|
666
|
+
|
|
667
|
+
if (breaches.length) {
|
|
668
|
+
console.log("");
|
|
669
|
+
console.log(formatSchedule(templateVersion));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const ok = verdict.ok && blocking.length === 0;
|
|
673
|
+
const why = !verdict.ok ? "new findings since the ledger"
|
|
674
|
+
: blocking.length ? blocking.map((b) => b.what).join("; ")
|
|
675
|
+
: "held";
|
|
676
|
+
|
|
677
|
+
writeEvidence(root, result, {
|
|
678
|
+
ok, why,
|
|
679
|
+
clientBoundary: { crossings: cb.crossings.length, unresolved: cb.unresolved.length, blocking: cbBlocks },
|
|
680
|
+
enforcement: {
|
|
681
|
+
templateVersion,
|
|
682
|
+
blocking: blocking.map((b) => b.rule),
|
|
683
|
+
advisory: breaches.filter((b) => !blocksAt(b.rule, templateVersion)).map((b) => b.rule),
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
return ok ? 0 : 1;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function main(argv) {
|
|
691
|
+
const [cmd, pathArg] = argv;
|
|
692
|
+
if (!cmd || ["-h", "--help", "help"].includes(cmd)) {
|
|
693
|
+
console.log("gyde design <scan|plan|init|gate|tokens> [path]");
|
|
694
|
+
console.log(" scan measure a repository as it is");
|
|
695
|
+
console.log(" plan say what scaffolding would be emitted; write nothing");
|
|
696
|
+
console.log(" init write exactly what plan described; never overwrites");
|
|
697
|
+
console.log(" gate fail on anything new since the committed ledger");
|
|
698
|
+
console.log(" upgrade take a new template version; never merges a conflict");
|
|
699
|
+
console.log(" (--dry-run to see the decisions and write nothing)");
|
|
700
|
+
console.log(" tokens print the generated stylesheet for the seed dictionary");
|
|
701
|
+
return 0;
|
|
702
|
+
}
|
|
703
|
+
if (cmd === "tokens") {
|
|
704
|
+
const problems = validate(SEED);
|
|
705
|
+
if (problems.length) { console.error(problems.join("\n")); return 1; }
|
|
706
|
+
console.log(generateCss(SEED));
|
|
707
|
+
return 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const root = resolve(pathArg || ".");
|
|
711
|
+
if (!existsSync(root)) { console.error(`no such path: ${root}`); return 1; }
|
|
712
|
+
const config = readConfig(root);
|
|
713
|
+
|
|
714
|
+
if (cmd === "scan") return cmdScan(root, config);
|
|
715
|
+
if (cmd === "plan") return cmdPlan(root, config);
|
|
716
|
+
if (cmd === "init") return cmdInit(root, config);
|
|
717
|
+
if (cmd === "gate") return cmdGate(root, config);
|
|
718
|
+
if (cmd === "upgrade") return cmdUpgrade(root, config, { dryRun: argv.includes("--dry-run") });
|
|
719
|
+
console.error(`unknown command: ${cmd}`);
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
process.exitCode = main(process.argv.slice(2));
|