@codepress/design-kit 0.2.2 → 0.4.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/README.md +127 -15
- package/package.json +1 -1
- package/schema/design-kit.config.schema.json +11 -1
- package/skills/design-kit-setup/SKILL.md +14 -10
- package/skills/storybook-setup/SKILL.md +389 -0
- package/src/capture-stories.mjs +1 -1
- package/src/ci.mjs +463 -0
- package/src/cli.mjs +52 -2
- package/src/init.mjs +224 -41
- package/src/templates/design-kit-skill.md +16 -5
package/src/ci.mjs
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CI side of the workflow `design-kit init` writes:
|
|
3
|
+
*
|
|
4
|
+
* - `design-kit ci-check` decides whether a run builds the kit. It builds
|
|
5
|
+
* only when a path in the config's `watch` list has changed since the
|
|
6
|
+
* newest kit's commit (git tree and blob ids, so no file is read), when
|
|
7
|
+
* there is no kit, when the newest kit is close to expiring, or when the
|
|
8
|
+
* run asked for it (`--force`, `--package`).
|
|
9
|
+
* - `design-kit ci-cleanup` deletes the older kits on the branch once a new
|
|
10
|
+
* one is uploaded. A failed delete is a warning, never a failure.
|
|
11
|
+
*
|
|
12
|
+
* Where kits are stored is behind two functions, `findNewestKit` and
|
|
13
|
+
* `deleteOlderKits` (GitHub Actions artifacts, through `gh`), so another
|
|
14
|
+
* store changes only those and the workflow's upload step.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { appendFileSync } from "node:fs";
|
|
18
|
+
import { posix } from "node:path";
|
|
19
|
+
|
|
20
|
+
import { CONFIG_NAME, ConfigError, loadConfig } from "./config.mjs";
|
|
21
|
+
|
|
22
|
+
export const ARTIFACT = "design-kit";
|
|
23
|
+
/** Rebuild a kit this old even with no UI change, so it never expires. */
|
|
24
|
+
export const DEFAULT_MAX_AGE_DAYS = 75;
|
|
25
|
+
/** What `watch` means when a config has none: the whole repository. */
|
|
26
|
+
export const DEFAULT_WATCH = ["."];
|
|
27
|
+
const DAY_MS = 86_400_000;
|
|
28
|
+
const PAGES = 5;
|
|
29
|
+
|
|
30
|
+
// ── storage: GitHub Actions artifacts ───────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** Runs `gh` and returns its stdout. */
|
|
33
|
+
export function runGh(args, { env = process.env } = {}) {
|
|
34
|
+
return execFileSync("gh", args, {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
env,
|
|
37
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Every `design-kit` artifact of the repository, newest first. */
|
|
43
|
+
function listKits({ repo, gh }) {
|
|
44
|
+
const artifacts = [];
|
|
45
|
+
for (let page = 1; page <= PAGES; page += 1) {
|
|
46
|
+
const body = JSON.parse(
|
|
47
|
+
gh([
|
|
48
|
+
"api",
|
|
49
|
+
`repos/${repo}/actions/artifacts?name=${ARTIFACT}&per_page=100&page=${page}`,
|
|
50
|
+
]),
|
|
51
|
+
);
|
|
52
|
+
const batch = body?.artifacts ?? [];
|
|
53
|
+
artifacts.push(...batch);
|
|
54
|
+
if (batch.length < 100) break;
|
|
55
|
+
}
|
|
56
|
+
return artifacts
|
|
57
|
+
.filter((artifact) => artifact.name === ARTIFACT)
|
|
58
|
+
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The newest unexpired kit on `branch`: `{id, sha, createdAt, runId}`, or
|
|
63
|
+
* null when there is none. Throws when the store cannot be read.
|
|
64
|
+
*/
|
|
65
|
+
export function findNewestKit({ repo, branch, gh = runGh }) {
|
|
66
|
+
const kit = listKits({ repo, gh }).find(
|
|
67
|
+
(artifact) =>
|
|
68
|
+
!artifact.expired && artifact.workflow_run?.head_branch === branch,
|
|
69
|
+
);
|
|
70
|
+
if (!kit?.workflow_run?.head_sha) return null;
|
|
71
|
+
return {
|
|
72
|
+
id: String(kit.id),
|
|
73
|
+
sha: kit.workflow_run.head_sha,
|
|
74
|
+
createdAt: kit.created_at,
|
|
75
|
+
runId: kit.workflow_run.id ? String(kit.workflow_run.id) : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deletes the kits on `branch` older than the one `keep` names (an artifact
|
|
81
|
+
* id). Returns `{deleted: [ids], failed: [{id, error}], keptFound}`; never
|
|
82
|
+
* throws for a single failed delete. When the kept kit is not in the listing
|
|
83
|
+
* (not listed yet, or on another branch), nothing is deleted: "older" has
|
|
84
|
+
* nothing to be measured against.
|
|
85
|
+
*/
|
|
86
|
+
export function deleteOlderKits({ repo, branch, keep, gh = runGh }) {
|
|
87
|
+
const kits = listKits({ repo, gh }).filter(
|
|
88
|
+
(artifact) => artifact.workflow_run?.head_branch === branch,
|
|
89
|
+
);
|
|
90
|
+
const kept = kits.find((artifact) => String(artifact.id) === String(keep));
|
|
91
|
+
const deleted = [];
|
|
92
|
+
const failed = [];
|
|
93
|
+
if (!kept) return { deleted, failed, keptFound: false };
|
|
94
|
+
for (const artifact of kits) {
|
|
95
|
+
if (String(artifact.id) === String(keep)) continue;
|
|
96
|
+
// Only kits uploaded before the one just kept: a newer one, or one
|
|
97
|
+
// uploaded in the same instant, is never removed.
|
|
98
|
+
if (!(Date.parse(artifact.created_at) < Date.parse(kept.created_at)))
|
|
99
|
+
continue;
|
|
100
|
+
try {
|
|
101
|
+
gh([
|
|
102
|
+
"api",
|
|
103
|
+
"--method",
|
|
104
|
+
"DELETE",
|
|
105
|
+
`repos/${repo}/actions/artifacts/${artifact.id}`,
|
|
106
|
+
]);
|
|
107
|
+
deleted.push(String(artifact.id));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
failed.push({ id: String(artifact.id), error: errorText(error) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { deleted, failed, keptFound: Boolean(kept) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function errorText(error) {
|
|
116
|
+
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
|
117
|
+
return (stderr || error?.message || String(error)).split("\n")[0];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── git: ids of the watched paths ───────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function git(cwd, args, env = process.env) {
|
|
123
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
env,
|
|
126
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
127
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A `watch` entry as git names it: forward slashes, `.`/`..` segments and
|
|
133
|
+
* repeated or trailing slashes resolved, "" for the repository root.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeWatchPath(path) {
|
|
136
|
+
const normalized = posix.normalize(String(path).replace(/\\/g, "/"));
|
|
137
|
+
return normalized.replace(/^\.\/?$/, "").replace(/\/+$/, "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `{path: id}` for each watched path at `rev`: the git tree id of a folder,
|
|
142
|
+
* the blob id of a file, or "missing" when the path does not exist there.
|
|
143
|
+
* Each path is listed on its own (`git ls-tree <rev> -- <path>`), so
|
|
144
|
+
* overlapping entries such as `src` and `src/theme.css` cannot hide each
|
|
145
|
+
* other. A path that is absent lists nothing; a tree git cannot read throws,
|
|
146
|
+
* so the run builds instead of mistaking it for an absent path. Only trees
|
|
147
|
+
* are read, so it works in a blobless (`--filter=blob:none`) clone.
|
|
148
|
+
*/
|
|
149
|
+
export function watchedIds(repoDir, rev, paths) {
|
|
150
|
+
const ids = {};
|
|
151
|
+
for (const path of paths) {
|
|
152
|
+
const key = normalizeWatchPath(path);
|
|
153
|
+
if (key === "") {
|
|
154
|
+
ids[path] = git(repoDir, [
|
|
155
|
+
"rev-parse",
|
|
156
|
+
"--verify",
|
|
157
|
+
`${rev}^{tree}`,
|
|
158
|
+
]).trim();
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const out = git(repoDir, ["ls-tree", "-z", "--full-tree", rev, "--", key]);
|
|
162
|
+
let id = "missing";
|
|
163
|
+
for (const entry of out.split("\0").filter(Boolean)) {
|
|
164
|
+
const tab = entry.indexOf("\t");
|
|
165
|
+
if (entry.slice(tab + 1) === key) id = entry.slice(0, tab).split(" ")[2];
|
|
166
|
+
}
|
|
167
|
+
ids[path] = id;
|
|
168
|
+
}
|
|
169
|
+
return ids;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** True when `sha` is a commit in this clone. */
|
|
173
|
+
function hasCommit(repoDir, sha) {
|
|
174
|
+
try {
|
|
175
|
+
// No lazy fetch from a partial clone's remote: one shallow fetch below
|
|
176
|
+
// brings the commit and its trees together.
|
|
177
|
+
git(repoDir, ["cat-file", "-e", `${sha}^{commit}`], {
|
|
178
|
+
...process.env,
|
|
179
|
+
GIT_NO_LAZY_FETCH: "1",
|
|
180
|
+
});
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Makes `sha` available: already here, or a shallow, blobless fetch. */
|
|
188
|
+
export function ensureCommit(repoDir, sha) {
|
|
189
|
+
if (hasCommit(repoDir, sha)) return;
|
|
190
|
+
git(repoDir, [
|
|
191
|
+
"fetch",
|
|
192
|
+
"--quiet",
|
|
193
|
+
"--no-tags",
|
|
194
|
+
"--depth=1",
|
|
195
|
+
"--filter=blob:none",
|
|
196
|
+
"origin",
|
|
197
|
+
sha,
|
|
198
|
+
]);
|
|
199
|
+
if (!hasCommit(repoDir, sha)) throw new Error(`${sha} is not a commit`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── the decision ────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
const short = (sha) => String(sha).slice(0, 7);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The reasons to build that need no git: `force`, an explicit package, no
|
|
208
|
+
* kit, or a kit close to expiring. Null when none applies.
|
|
209
|
+
*/
|
|
210
|
+
function buildWithoutComparing({
|
|
211
|
+
force,
|
|
212
|
+
packageSpec,
|
|
213
|
+
kit,
|
|
214
|
+
lookupError,
|
|
215
|
+
now,
|
|
216
|
+
maxAgeDays,
|
|
217
|
+
}) {
|
|
218
|
+
if (force) return { build: true, reason: "forced (the force input is set)" };
|
|
219
|
+
if (packageSpec)
|
|
220
|
+
return {
|
|
221
|
+
build: true,
|
|
222
|
+
reason: `an explicit package was given (${packageSpec})`,
|
|
223
|
+
};
|
|
224
|
+
if (!kit)
|
|
225
|
+
return {
|
|
226
|
+
build: true,
|
|
227
|
+
reason: lookupError
|
|
228
|
+
? `could not look up the newest kit (${lookupError}), so building`
|
|
229
|
+
: "no kit yet on this branch",
|
|
230
|
+
};
|
|
231
|
+
const ageDays = (now - Date.parse(kit.createdAt)) / DAY_MS;
|
|
232
|
+
if (!(ageDays < maxAgeDays))
|
|
233
|
+
return {
|
|
234
|
+
build: true,
|
|
235
|
+
reason: Number.isFinite(ageDays)
|
|
236
|
+
? `refresh before expiry: the newest kit (${short(kit.sha)}) is ${Math.floor(ageDays)} days old, and the limit is ${maxAgeDays}`
|
|
237
|
+
: `refresh before expiry: the newest kit (${short(kit.sha)}) has no usable date`,
|
|
238
|
+
};
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Whether to build, and why. Pure: every input is already looked up.
|
|
244
|
+
* `kit` is `{sha, createdAt}` or null; `before` and `after` are
|
|
245
|
+
* `watchedIds` at the kit's commit and at HEAD, unless `fetchError` says
|
|
246
|
+
* why they could not be read.
|
|
247
|
+
*/
|
|
248
|
+
export function decide({
|
|
249
|
+
force = false,
|
|
250
|
+
packageSpec = "",
|
|
251
|
+
kit = null,
|
|
252
|
+
lookupError,
|
|
253
|
+
now = Date.now(),
|
|
254
|
+
maxAgeDays = DEFAULT_MAX_AGE_DAYS,
|
|
255
|
+
fetchError,
|
|
256
|
+
before = {},
|
|
257
|
+
after = {},
|
|
258
|
+
}) {
|
|
259
|
+
const early = buildWithoutComparing({
|
|
260
|
+
force,
|
|
261
|
+
packageSpec,
|
|
262
|
+
kit,
|
|
263
|
+
lookupError,
|
|
264
|
+
now,
|
|
265
|
+
maxAgeDays,
|
|
266
|
+
});
|
|
267
|
+
if (early) return early;
|
|
268
|
+
if (fetchError)
|
|
269
|
+
return {
|
|
270
|
+
build: true,
|
|
271
|
+
reason: `could not read the kit's commit ${short(kit.sha)} (${fetchError}), so building`,
|
|
272
|
+
};
|
|
273
|
+
const paths = [...new Set([...Object.keys(before), ...Object.keys(after)])];
|
|
274
|
+
const changed = paths.filter((path) => before[path] !== after[path]);
|
|
275
|
+
if (changed.length)
|
|
276
|
+
return { build: true, reason: `changed: ${changed.join(", ")}` };
|
|
277
|
+
return {
|
|
278
|
+
build: false,
|
|
279
|
+
reason: `watched files unchanged since ${short(kit.sha)}`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Looks everything up and decides. `kit` given (`{sha, createdAt}`, or null
|
|
285
|
+
* for "no kit") skips the store lookup; otherwise `findKit()` is called.
|
|
286
|
+
* `head` is the commit being built (default HEAD). Returns
|
|
287
|
+
* `{build, reason, kit}`.
|
|
288
|
+
*/
|
|
289
|
+
export function ciCheck({
|
|
290
|
+
repoDir,
|
|
291
|
+
head = "HEAD",
|
|
292
|
+
watch = DEFAULT_WATCH,
|
|
293
|
+
force = false,
|
|
294
|
+
packageSpec = "",
|
|
295
|
+
kit,
|
|
296
|
+
findKit,
|
|
297
|
+
now = Date.now(),
|
|
298
|
+
maxAgeDays = DEFAULT_MAX_AGE_DAYS,
|
|
299
|
+
}) {
|
|
300
|
+
const inputs = { force, packageSpec, now, maxAgeDays };
|
|
301
|
+
let lookupError;
|
|
302
|
+
if (kit === undefined && !force && !packageSpec) {
|
|
303
|
+
try {
|
|
304
|
+
kit = findKit();
|
|
305
|
+
} catch (error) {
|
|
306
|
+
kit = null;
|
|
307
|
+
lookupError = errorText(error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
kit ??= null;
|
|
311
|
+
const early = buildWithoutComparing({ ...inputs, kit, lookupError });
|
|
312
|
+
if (early) return { ...early, kit };
|
|
313
|
+
let fetchError;
|
|
314
|
+
let before;
|
|
315
|
+
let after;
|
|
316
|
+
try {
|
|
317
|
+
ensureCommit(repoDir, kit.sha);
|
|
318
|
+
before = watchedIds(repoDir, kit.sha, watch);
|
|
319
|
+
after = watchedIds(repoDir, head, watch);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
fetchError = errorText(error);
|
|
322
|
+
}
|
|
323
|
+
const missing = after
|
|
324
|
+
? Object.keys(after).filter((path) => after[path] === "missing")
|
|
325
|
+
: [];
|
|
326
|
+
return {
|
|
327
|
+
...decide({ ...inputs, kit, before, after, fetchError }),
|
|
328
|
+
kit,
|
|
329
|
+
missing,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── the commands ────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
function maxAgeFrom(value, env) {
|
|
336
|
+
const raw = value ?? env.DESIGN_KIT_MAX_AGE_DAYS;
|
|
337
|
+
if (raw === undefined || raw === "") return DEFAULT_MAX_AGE_DAYS;
|
|
338
|
+
const days = Number(raw);
|
|
339
|
+
if (!Number.isFinite(days) || days < 0)
|
|
340
|
+
throw new ConfigError(
|
|
341
|
+
`--max-age-days (or DESIGN_KIT_MAX_AGE_DAYS) takes a number of days, got ${raw}`,
|
|
342
|
+
);
|
|
343
|
+
return days;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function repoAndBranch(options, env) {
|
|
347
|
+
const repo = options.repo ?? env.GITHUB_REPOSITORY;
|
|
348
|
+
const branch = options.branch ?? env.GITHUB_REF_NAME;
|
|
349
|
+
if (!repo || !branch)
|
|
350
|
+
throw new ConfigError(
|
|
351
|
+
"--repo <owner/name> and --branch <name> are needed outside GitHub Actions",
|
|
352
|
+
);
|
|
353
|
+
return { repo, branch };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function emit(values, env) {
|
|
357
|
+
// One line per value: a newline (from a dispatch input in the reason)
|
|
358
|
+
// must not add a second `build=` line to $GITHUB_OUTPUT.
|
|
359
|
+
const lines = Object.entries(values).map(
|
|
360
|
+
([key, value]) => `${key}=${String(value).replace(/[\r\n]+/g, " ")}`,
|
|
361
|
+
);
|
|
362
|
+
for (const line of lines) console.log(line);
|
|
363
|
+
if (env.GITHUB_OUTPUT)
|
|
364
|
+
appendFileSync(env.GITHUB_OUTPUT, `${lines.join("\n")}\n`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function summary(text, env) {
|
|
368
|
+
if (env.GITHUB_STEP_SUMMARY)
|
|
369
|
+
appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}\n`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* `design-kit ci-check`: prints `build=true|false` and `reason=…` (and
|
|
374
|
+
* writes them to $GITHUB_OUTPUT and the step summary when set).
|
|
375
|
+
*/
|
|
376
|
+
export function ciCheckCommand(options, { env = process.env, gh, now } = {}) {
|
|
377
|
+
const config = loadConfig(options.config);
|
|
378
|
+
const watch = config.raw.watch?.length ? config.raw.watch : DEFAULT_WATCH;
|
|
379
|
+
if ((options.since === undefined) !== (options["kit-created"] === undefined))
|
|
380
|
+
throw new ConfigError("--since and --kit-created go together");
|
|
381
|
+
const kit =
|
|
382
|
+
options.since !== undefined
|
|
383
|
+
? { sha: options.since, createdAt: options["kit-created"] }
|
|
384
|
+
: options["no-kit"]
|
|
385
|
+
? null
|
|
386
|
+
: undefined;
|
|
387
|
+
if (kit?.createdAt && Number.isNaN(Date.parse(kit.createdAt)))
|
|
388
|
+
throw new ConfigError(
|
|
389
|
+
`--kit-created takes an ISO 8601 date, got ${kit.createdAt}`,
|
|
390
|
+
);
|
|
391
|
+
const repoDir = git(config.configDir, [
|
|
392
|
+
"rev-parse",
|
|
393
|
+
"--show-toplevel",
|
|
394
|
+
]).trim();
|
|
395
|
+
const result = ciCheck({
|
|
396
|
+
repoDir,
|
|
397
|
+
watch,
|
|
398
|
+
force: Boolean(options.force),
|
|
399
|
+
packageSpec: options.package ?? "",
|
|
400
|
+
kit,
|
|
401
|
+
findKit: () =>
|
|
402
|
+
findNewestKit({
|
|
403
|
+
...repoAndBranch(options, env),
|
|
404
|
+
gh: gh ?? ((args) => runGh(args, { env })),
|
|
405
|
+
}),
|
|
406
|
+
now: now ?? Date.now(),
|
|
407
|
+
maxAgeDays: maxAgeFrom(options["max-age-days"], env),
|
|
408
|
+
});
|
|
409
|
+
if (result.missing?.length)
|
|
410
|
+
console.log(
|
|
411
|
+
`::warning title=Design kit::These watch paths do not exist at this commit, so they never trigger a rebuild: ${result.missing.join(", ")}. Fix or remove them in ${CONFIG_NAME}.`,
|
|
412
|
+
);
|
|
413
|
+
emit(
|
|
414
|
+
{
|
|
415
|
+
build: String(result.build),
|
|
416
|
+
reason: result.reason,
|
|
417
|
+
"kit-sha": result.kit?.sha ?? "",
|
|
418
|
+
},
|
|
419
|
+
env,
|
|
420
|
+
);
|
|
421
|
+
summary(
|
|
422
|
+
`Design kit: ${result.build ? "building" : "nothing to build"}, ${result.reason}.`,
|
|
423
|
+
env,
|
|
424
|
+
);
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `design-kit ci-cleanup --keep <artifact id>`: deletes the branch's older
|
|
430
|
+
* kits. Always exits 0; each failed delete is a workflow warning.
|
|
431
|
+
*/
|
|
432
|
+
export function ciCleanupCommand(options, { env = process.env, gh } = {}) {
|
|
433
|
+
if (!options.keep) throw new ConfigError("--keep <artifact id> is required");
|
|
434
|
+
const { repo, branch } = repoAndBranch(options, env);
|
|
435
|
+
let result;
|
|
436
|
+
try {
|
|
437
|
+
result = deleteOlderKits({
|
|
438
|
+
repo,
|
|
439
|
+
branch,
|
|
440
|
+
keep: options.keep,
|
|
441
|
+
gh: gh ?? ((args) => runGh(args, { env })),
|
|
442
|
+
});
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.log(
|
|
445
|
+
`::warning title=Design kit::Could not list the older kits, so none were deleted: ${errorText(error)}`,
|
|
446
|
+
);
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
if (!result.keptFound) {
|
|
450
|
+
console.log(
|
|
451
|
+
`::warning title=Design kit::The kit just uploaded (${options.keep}) is not listed on ${branch} yet, so no older kit was deleted. The next build deletes them.`,
|
|
452
|
+
);
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
for (const { id, error } of result.failed)
|
|
456
|
+
console.log(
|
|
457
|
+
`::warning title=Design kit::Could not delete the older kit ${id}: ${error}`,
|
|
458
|
+
);
|
|
459
|
+
const text = `Design kit: deleted ${result.deleted.length} older kit${result.deleted.length === 1 ? "" : "s"} on ${branch}${result.failed.length ? `; ${result.failed.length} could not be deleted` : ""}.`;
|
|
460
|
+
console.log(text);
|
|
461
|
+
summary(text, env);
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -17,12 +17,19 @@ const SETUP_GUIDE = new URL(
|
|
|
17
17
|
import.meta.url,
|
|
18
18
|
);
|
|
19
19
|
|
|
20
|
+
/** The `storybook-setup` skill, printed by `design-kit storybook-guide`. */
|
|
21
|
+
const STORYBOOK_GUIDE = new URL(
|
|
22
|
+
"../skills/storybook-setup/SKILL.md",
|
|
23
|
+
import.meta.url,
|
|
24
|
+
);
|
|
25
|
+
|
|
20
26
|
const USAGE = `usage: design-kit <command> [options]
|
|
21
27
|
|
|
22
28
|
init set a kit up in this repository: the config, a daily
|
|
23
29
|
GitHub Actions build, and a skill for its agents
|
|
24
|
-
[--root <dir>] [--dry-run] [--force]
|
|
25
|
-
|
|
30
|
+
[--root <dir>] [--dry-run] [--force] [--workflow-only]
|
|
31
|
+
--workflow-only --force rewrites just the workflow
|
|
32
|
+
capture stories capture every story of a Storybook 7-9 static build
|
|
26
33
|
[--config <file>] [--out <dir>] [--storybook-dir <dir>]
|
|
27
34
|
[--only <id,...> | --only-file <file>] [--base <previous kit>]
|
|
28
35
|
capture pages capture the configured pages of a running app
|
|
@@ -33,8 +40,20 @@ const USAGE = `usage: design-kit <command> [options]
|
|
|
33
40
|
[--kit <dir>] default: DESIGN_KIT_DIR, the config's "out", ./design-kit
|
|
34
41
|
validate check design-kit.config.json against the v1 schema
|
|
35
42
|
[--config <file>]
|
|
43
|
+
ci-check in CI: print build=true|false and the reason. Builds
|
|
44
|
+
when a "watch" path changed since the newest kit, there
|
|
45
|
+
is no kit, it is DESIGN_KIT_MAX_AGE_DAYS (75) old, or
|
|
46
|
+
--force or --package is given
|
|
47
|
+
[--config <file>] [--since <sha> --kit-created <iso> | --no-kit]
|
|
48
|
+
[--force] [--package <spec>] [--max-age-days <n>]
|
|
49
|
+
[--repo <owner/name>] [--branch <name>]
|
|
50
|
+
ci-cleanup in CI: delete the branch's kits older than --keep
|
|
51
|
+
--keep <artifact id> [--repo <owner/name>] [--branch <name>]
|
|
36
52
|
setup-guide print the guide for setting a kit up in a repository as
|
|
37
53
|
one reviewable pull request (an agent skill)
|
|
54
|
+
storybook-guide print the guide for adding Storybook, with stories for
|
|
55
|
+
every existing component, as one reviewable pull request
|
|
56
|
+
(an agent skill)
|
|
38
57
|
|
|
39
58
|
The config is the nearest design-kit.config.json at or above the working
|
|
40
59
|
directory unless --config names one. Exit codes: 0 done, 1 a capture could
|
|
@@ -170,19 +189,50 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
170
189
|
root: "value",
|
|
171
190
|
"dry-run": "flag",
|
|
172
191
|
force: "flag",
|
|
192
|
+
"workflow-only": "flag",
|
|
173
193
|
});
|
|
174
194
|
const { init } = await import("./init.mjs");
|
|
175
195
|
return init({
|
|
176
196
|
root: options.root ?? ".",
|
|
177
197
|
dryRun: Boolean(options["dry-run"]),
|
|
178
198
|
force: Boolean(options.force),
|
|
199
|
+
workflowOnly: Boolean(options["workflow-only"]),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (command === "ci-check") {
|
|
203
|
+
const options = parse(argv.slice(1), {
|
|
204
|
+
config: "value",
|
|
205
|
+
since: "value",
|
|
206
|
+
"kit-created": "value",
|
|
207
|
+
"no-kit": "flag",
|
|
208
|
+
force: "flag",
|
|
209
|
+
package: "value",
|
|
210
|
+
"max-age-days": "value",
|
|
211
|
+
repo: "value",
|
|
212
|
+
branch: "value",
|
|
179
213
|
});
|
|
214
|
+
const { ciCheckCommand } = await import("./ci.mjs");
|
|
215
|
+
return ciCheckCommand(options, { env });
|
|
216
|
+
}
|
|
217
|
+
if (command === "ci-cleanup") {
|
|
218
|
+
const options = parse(argv.slice(1), {
|
|
219
|
+
keep: "value",
|
|
220
|
+
repo: "value",
|
|
221
|
+
branch: "value",
|
|
222
|
+
});
|
|
223
|
+
const { ciCleanupCommand } = await import("./ci.mjs");
|
|
224
|
+
return ciCleanupCommand(options, { env });
|
|
180
225
|
}
|
|
181
226
|
if (command === "setup-guide") {
|
|
182
227
|
parse(argv.slice(1), {});
|
|
183
228
|
process.stdout.write(readFileSync(SETUP_GUIDE, "utf8"));
|
|
184
229
|
return 0;
|
|
185
230
|
}
|
|
231
|
+
if (command === "storybook-guide") {
|
|
232
|
+
parse(argv.slice(1), {});
|
|
233
|
+
process.stdout.write(readFileSync(STORYBOOK_GUIDE, "utf8"));
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
186
236
|
if (command === "validate") {
|
|
187
237
|
const options = parse(argv.slice(1), { config: "value" });
|
|
188
238
|
const config = loadConfig(options.config);
|