@codepress/design-kit 0.8.0 → 0.10.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 +146 -7
- package/package.json +3 -2
- package/schema/design-kit.config.schema.json +54 -0
- package/src/capture-pages.mjs +1 -0
- package/src/ci.mjs +76 -113
- package/src/cli.mjs +37 -4
- package/src/config.mjs +40 -0
- package/src/init.mjs +263 -73
- package/src/kit.mjs +347 -25
- package/src/storage/bucket.mjs +237 -0
- package/src/storage/codepress.mjs +363 -0
- package/src/storage/common.mjs +125 -0
- package/src/storage/github.mjs +175 -0
- package/src/storage/index.mjs +149 -0
- package/src/storage/zip.mjs +249 -0
- package/src/templates/design-kit-skill.md +18 -18
package/src/config.mjs
CHANGED
|
@@ -194,6 +194,7 @@ export function resolveConfig(raw, path) {
|
|
|
194
194
|
errors.push(`pages.pages[${index}].name: "${page.name}" is used twice`);
|
|
195
195
|
names.add(page?.name);
|
|
196
196
|
});
|
|
197
|
+
if (!errors.length && raw.storage) errors.push(...storageErrors(raw.storage));
|
|
197
198
|
if (errors.length) {
|
|
198
199
|
throw new ConfigError(
|
|
199
200
|
`${path}:\n${errors.map((error) => ` ${error}`).join("\n")}`,
|
|
@@ -208,6 +209,7 @@ export function resolveConfig(raw, path) {
|
|
|
208
209
|
configDir,
|
|
209
210
|
raw,
|
|
210
211
|
out: at(raw.out),
|
|
212
|
+
storage: raw.storage ? { ...raw.storage } : { type: "github" },
|
|
211
213
|
theme: raw.theme ? normalizeTheme(raw.theme) : undefined,
|
|
212
214
|
};
|
|
213
215
|
if (raw.stories) {
|
|
@@ -264,6 +266,44 @@ export function resolveConfig(raw, path) {
|
|
|
264
266
|
return config;
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
/** The keys each storage type takes, and which of them it needs. */
|
|
270
|
+
const STORAGE_KEYS = {
|
|
271
|
+
github: { allowed: [], required: [] },
|
|
272
|
+
codepress: { allowed: ["url", "apiUrl"], required: [] },
|
|
273
|
+
s3: {
|
|
274
|
+
allowed: ["bucket", "prefix", "region", "endpoint"],
|
|
275
|
+
required: ["bucket"],
|
|
276
|
+
},
|
|
277
|
+
r2: {
|
|
278
|
+
allowed: ["accountId", "bucket", "prefix"],
|
|
279
|
+
required: ["accountId", "bucket"],
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** What the schema cannot say about `storage`: the keys each type takes. */
|
|
284
|
+
function storageErrors(storage) {
|
|
285
|
+
const keys = STORAGE_KEYS[storage.type];
|
|
286
|
+
if (!keys) return [];
|
|
287
|
+
const errors = [];
|
|
288
|
+
for (const key of keys.required)
|
|
289
|
+
if (!(key in storage))
|
|
290
|
+
errors.push(`storage.${key}: is required for ${storage.type} storage`);
|
|
291
|
+
for (const key of Object.keys(storage)) {
|
|
292
|
+
if (key === "type") continue;
|
|
293
|
+
if (!keys.allowed.includes(key))
|
|
294
|
+
errors.push(
|
|
295
|
+
`storage.${key}: not used by ${storage.type} storage${key === "endpoint" && storage.type === "r2" ? " (r2 derives it from accountId)" : ""}`,
|
|
296
|
+
);
|
|
297
|
+
else if (/^TODO/.test(String(storage[key])))
|
|
298
|
+
errors.push(`storage.${key}: still ${storage[key]}`);
|
|
299
|
+
else if (key === "accountId" && !/^[A-Za-z0-9]+$/.test(storage[key]))
|
|
300
|
+
errors.push(
|
|
301
|
+
"storage.accountId: must be your Cloudflare account id, letters and digits only",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return errors;
|
|
305
|
+
}
|
|
306
|
+
|
|
267
307
|
/** The theme as the manifest records it, with its defaults filled in. */
|
|
268
308
|
export function normalizeTheme(theme) {
|
|
269
309
|
const attribute = theme.attribute ?? "class";
|
package/src/init.mjs
CHANGED
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
23
23
|
|
|
24
24
|
import { DEFAULT_MAX_AGE_DAYS } from "./ci.mjs";
|
|
25
|
-
import { CONFIG_NAME } from "./config.mjs";
|
|
25
|
+
import { CONFIG_NAME, ConfigError } from "./config.mjs";
|
|
26
|
+
import { STORAGE_TYPES } from "./storage/index.mjs";
|
|
26
27
|
|
|
27
28
|
const PACKAGE = JSON.parse(
|
|
28
29
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
@@ -661,12 +662,19 @@ export function watchDefaults(found) {
|
|
|
661
662
|
);
|
|
662
663
|
}
|
|
663
664
|
|
|
664
|
-
/**
|
|
665
|
-
|
|
665
|
+
/** The config's $schema URL for this version of the package. */
|
|
666
|
+
const SCHEMA_URL = `https://unpkg.com/${PACKAGE.name}@${PACKAGE.version}/schema/design-kit.config.schema.json`;
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* design-kit.config.json, as an object. `storage` (a type) adds the
|
|
670
|
+
* storage key; without it the kit is a GitHub Actions artifact.
|
|
671
|
+
*/
|
|
672
|
+
export function buildConfig(found, storage) {
|
|
666
673
|
const config = {
|
|
667
|
-
$schema:
|
|
674
|
+
$schema: SCHEMA_URL,
|
|
668
675
|
version: 1,
|
|
669
676
|
out: "design-kit",
|
|
677
|
+
...(storage ? { storage: storageConfig(storage) } : {}),
|
|
670
678
|
watch: watchDefaults(found),
|
|
671
679
|
theme: {
|
|
672
680
|
attribute: found.theme.attribute,
|
|
@@ -706,8 +714,115 @@ export function buildConfig(found) {
|
|
|
706
714
|
return config;
|
|
707
715
|
}
|
|
708
716
|
|
|
709
|
-
/**
|
|
710
|
-
|
|
717
|
+
/** The upload-artifact step and the cleanup job of `github` storage. */
|
|
718
|
+
const ARTIFACT_STEPS = (kitDir) => ` - id: upload
|
|
719
|
+
if: hashFiles('${kitDir}/manifest.json') != ''
|
|
720
|
+
uses: actions/upload-artifact@v5
|
|
721
|
+
with:
|
|
722
|
+
name: design-kit
|
|
723
|
+
path: ${kitDir}
|
|
724
|
+
# The most a public repository allows. A private repository may
|
|
725
|
+
# raise it to 400 (and DESIGN_KIT_MAX_AGE_DAYS with it).
|
|
726
|
+
retention-days: ${RETENTION_DAYS}
|
|
727
|
+
if-no-files-found: error
|
|
728
|
+
|
|
729
|
+
# Deletes this branch's older kits once the new one is uploaded. The only
|
|
730
|
+
# job that may delete artifacts; a failed delete is a warning.
|
|
731
|
+
cleanup:
|
|
732
|
+
name: Delete older kits
|
|
733
|
+
needs: build
|
|
734
|
+
if: needs.build.outputs.artifact-id != ''
|
|
735
|
+
runs-on: ubuntu-latest
|
|
736
|
+
timeout-minutes: 5
|
|
737
|
+
permissions:
|
|
738
|
+
actions: write
|
|
739
|
+
steps:
|
|
740
|
+
- uses: actions/setup-node@v5
|
|
741
|
+
with:
|
|
742
|
+
node-version: "22"
|
|
743
|
+
|
|
744
|
+
- continue-on-error: true
|
|
745
|
+
env:
|
|
746
|
+
GH_TOKEN: \${{ github.token }}
|
|
747
|
+
KEEP: \${{ needs.build.outputs.artifact-id }}
|
|
748
|
+
run: npx --yes -p "$DESIGN_KIT_PACKAGE" design-kit ci-cleanup --keep "$KEEP"
|
|
749
|
+
`;
|
|
750
|
+
|
|
751
|
+
/** The storage objects `init --storage <type>` writes. */
|
|
752
|
+
export function storageConfig(type) {
|
|
753
|
+
if (type === "s3")
|
|
754
|
+
return {
|
|
755
|
+
type,
|
|
756
|
+
bucket: `${TODO}: your S3 bucket's name`,
|
|
757
|
+
region: `${TODO}: the bucket's AWS region, such as us-east-1`,
|
|
758
|
+
};
|
|
759
|
+
if (type === "r2")
|
|
760
|
+
return {
|
|
761
|
+
type,
|
|
762
|
+
accountId: `${TODO}: your Cloudflare account id`,
|
|
763
|
+
bucket: `${TODO}: your R2 bucket's name`,
|
|
764
|
+
};
|
|
765
|
+
return { type };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* What differs between the storage types in the workflow: where the kit
|
|
770
|
+
* goes, the secrets the check and upload steps read, and the upload step.
|
|
771
|
+
*/
|
|
772
|
+
function workflowStorage(type, kitDir) {
|
|
773
|
+
const uploadStep = (name, env) => ` - name: ${name}
|
|
774
|
+
if: hashFiles('${kitDir}/manifest.json') != ''
|
|
775
|
+
working-directory: \${{ runner.temp }}/design-kit-cli${env}
|
|
776
|
+
run: design-kit upload --config "$CONFIG" --dir "$GITHUB_WORKSPACE/${kitDir}"
|
|
777
|
+
`;
|
|
778
|
+
const awsEnv = (id, secret) => `
|
|
779
|
+
AWS_ACCESS_KEY_ID: \${{ secrets.${id} }}
|
|
780
|
+
AWS_SECRET_ACCESS_KEY: \${{ secrets.${secret} }}`;
|
|
781
|
+
if (type === "codepress")
|
|
782
|
+
return {
|
|
783
|
+
where:
|
|
784
|
+
"to CodePress, signed in with this workflow's GitHub OIDC token (no\n# secret)",
|
|
785
|
+
checkEnv: `
|
|
786
|
+
# Optional. Without a CodePress API key the check reads the newest
|
|
787
|
+
# kit with this job's GitHub OIDC token; if the read fails, it builds.
|
|
788
|
+
CODEPRESS_API_KEY: \${{ secrets.CODEPRESS_API_KEY }}`,
|
|
789
|
+
checkPermissions: "\n id-token: write",
|
|
790
|
+
buildPermissions: `
|
|
791
|
+
# id-token: write lets design-kit upload sign in to CodePress with
|
|
792
|
+
# GitHub's OIDC token. Only this job and the check get it.
|
|
793
|
+
permissions:
|
|
794
|
+
contents: read
|
|
795
|
+
id-token: write`,
|
|
796
|
+
upload: uploadStep("Upload the kit to CodePress", ""),
|
|
797
|
+
};
|
|
798
|
+
if (type === "s3")
|
|
799
|
+
return {
|
|
800
|
+
where:
|
|
801
|
+
"to your S3 bucket (the AWS_ACCESS_KEY_ID and\n# AWS_SECRET_ACCESS_KEY repository secrets)",
|
|
802
|
+
checkEnv: awsEnv("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"),
|
|
803
|
+
buildPermissions: "",
|
|
804
|
+
upload: uploadStep(
|
|
805
|
+
"Upload the kit to S3",
|
|
806
|
+
`\n env:${awsEnv("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")}`,
|
|
807
|
+
),
|
|
808
|
+
};
|
|
809
|
+
return {
|
|
810
|
+
where:
|
|
811
|
+
"to your Cloudflare R2 bucket (an R2 API token's\n# R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY repository secrets)",
|
|
812
|
+
checkEnv: awsEnv("R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"),
|
|
813
|
+
buildPermissions: "",
|
|
814
|
+
upload: uploadStep(
|
|
815
|
+
"Upload the kit to R2",
|
|
816
|
+
`\n env:${awsEnv("R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY")}`,
|
|
817
|
+
),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* .github/workflows/design-kit.yml, as text. `storage` is the config's
|
|
823
|
+
* storage object; without one (or with github) the kit is an artifact.
|
|
824
|
+
*/
|
|
825
|
+
export function buildWorkflow(found, storage = { type: "github" }) {
|
|
711
826
|
const pm = found.packageManager;
|
|
712
827
|
const rel = (path) => posix(relative(found.repoRoot, path)) || ".";
|
|
713
828
|
const projectDir = rel(found.project);
|
|
@@ -737,6 +852,10 @@ export function buildWorkflow(found) {
|
|
|
737
852
|
` - uses: actions/setup-node@v5\n with:\n ${node}${cache}${cachePath}`,
|
|
738
853
|
);
|
|
739
854
|
|
|
855
|
+
const artifact = (storage?.type ?? "github") === "github";
|
|
856
|
+
const store = artifact
|
|
857
|
+
? null
|
|
858
|
+
: workflowStorage(storage.type, at("design-kit"));
|
|
740
859
|
const storiesStep = found.storybook
|
|
741
860
|
? `
|
|
742
861
|
- name: Build Storybook and capture stories
|
|
@@ -749,13 +868,18 @@ export function buildWorkflow(found) {
|
|
|
749
868
|
|
|
750
869
|
# Captures this repository's design kit: ${found.storybook ? "every Storybook story and " : ""}the pages
|
|
751
870
|
# listed in ${at(CONFIG_NAME)}, as static HTML with the CSS and fonts they
|
|
752
|
-
# load.
|
|
753
|
-
|
|
871
|
+
# load. ${
|
|
872
|
+
artifact
|
|
873
|
+
? `It uploads the result as the \`design-kit\` artifact, which agents
|
|
874
|
+
# download to draw mockups from the app's real markup.`
|
|
875
|
+
: `It uploads the result ${store.where} with \`design-kit upload\`,
|
|
876
|
+
# and agents fetch it with \`design-kit fetch\` to draw mockups from the
|
|
877
|
+
# app's real markup.`
|
|
878
|
+
}
|
|
754
879
|
#
|
|
755
880
|
# It runs daily, but builds only when a path in the config's "watch" list
|
|
756
881
|
# changed since the newest kit's commit, when there is no kit yet, or when
|
|
757
|
-
# the newest kit is DESIGN_KIT_MAX_AGE_DAYS old, so a kit never expires
|
|
758
|
-
# unreplaced. After an upload it deletes the branch's older kits.
|
|
882
|
+
# the newest kit is DESIGN_KIT_MAX_AGE_DAYS old${artifact ? ", so a kit never expires\n# unreplaced. After an upload it deletes the branch's older kits." : "."}
|
|
759
883
|
#
|
|
760
884
|
# Written by \`design-kit init\` (${PACKAGE.name}); \`init --workflow-only
|
|
761
885
|
# --force\` rewrites just this file. Edit it freely.
|
|
@@ -785,9 +909,13 @@ env:
|
|
|
785
909
|
DESIGN_KIT_PACKAGE: "${PACKAGE.name}@${PACKAGE.version}"
|
|
786
910
|
PLAYWRIGHT_PACKAGE: "@playwright/test@${playwrightPin()}"
|
|
787
911
|
CONFIG: \${{ github.workspace }}/${at(CONFIG_NAME)}
|
|
788
|
-
# Rebuild a kit this many days old even with no UI change
|
|
912
|
+
# Rebuild a kit this many days old even with no UI change.${
|
|
913
|
+
artifact
|
|
914
|
+
? ` Kits are kept
|
|
789
915
|
# ${RETENTION_DAYS} days (retention-days below), so this refreshes the kit before it
|
|
790
|
-
# expires
|
|
916
|
+
# expires.`
|
|
917
|
+
: ""
|
|
918
|
+
}
|
|
791
919
|
DESIGN_KIT_MAX_AGE_DAYS: "${DEFAULT_MAX_AGE_DAYS}"
|
|
792
920
|
|
|
793
921
|
jobs:
|
|
@@ -796,8 +924,7 @@ jobs:
|
|
|
796
924
|
runs-on: ubuntu-latest
|
|
797
925
|
timeout-minutes: 10
|
|
798
926
|
permissions:
|
|
799
|
-
contents: read
|
|
800
|
-
actions: read
|
|
927
|
+
contents: read${artifact ? "\n actions: read" : (store.checkPermissions ?? "")}
|
|
801
928
|
outputs:
|
|
802
929
|
build: \${{ steps.check.outputs.build }}
|
|
803
930
|
steps:
|
|
@@ -816,8 +943,7 @@ jobs:
|
|
|
816
943
|
node-version: "22"
|
|
817
944
|
|
|
818
945
|
- id: check
|
|
819
|
-
env:
|
|
820
|
-
GH_TOKEN: \${{ github.token }}
|
|
946
|
+
env:${artifact ? "\n GH_TOKEN: ${{ github.token }}" : store.checkEnv}
|
|
821
947
|
FORCE: \${{ inputs.force || 'false' }}
|
|
822
948
|
PACKAGE_INPUT: \${{ inputs.package || '' }}
|
|
823
949
|
run: |
|
|
@@ -831,9 +957,13 @@ jobs:
|
|
|
831
957
|
needs: check
|
|
832
958
|
if: needs.check.outputs.build == 'true'
|
|
833
959
|
runs-on: ubuntu-latest
|
|
834
|
-
timeout-minutes: 60
|
|
960
|
+
timeout-minutes: 60${
|
|
961
|
+
artifact
|
|
962
|
+
? `
|
|
835
963
|
outputs:
|
|
836
|
-
artifact-id: \${{ steps.upload.outputs.artifact-id }}
|
|
964
|
+
artifact-id: \${{ steps.upload.outputs.artifact-id }}`
|
|
965
|
+
: store.buildPermissions
|
|
966
|
+
}
|
|
837
967
|
steps:
|
|
838
968
|
- uses: actions/checkout@v5
|
|
839
969
|
with:
|
|
@@ -881,42 +1011,34 @@ ${storiesStep}
|
|
|
881
1011
|
run: |
|
|
882
1012
|
echo "::warning title=Design kit::Nothing to capture yet, so no kit was uploaded: ${at(CONFIG_NAME)} has no stories and its pages still have ${TODO} settings. Fill in the pages (or add Storybook) and run this workflow again."
|
|
883
1013
|
|
|
884
|
-
|
|
885
|
-
if: hashFiles('${at("design-kit")}/manifest.json') != ''
|
|
886
|
-
uses: actions/upload-artifact@v5
|
|
887
|
-
with:
|
|
888
|
-
name: design-kit
|
|
889
|
-
path: ${at("design-kit")}
|
|
890
|
-
# The most a public repository allows. A private repository may
|
|
891
|
-
# raise it to 400 (and DESIGN_KIT_MAX_AGE_DAYS with it).
|
|
892
|
-
retention-days: ${RETENTION_DAYS}
|
|
893
|
-
if-no-files-found: error
|
|
894
|
-
|
|
895
|
-
# Deletes this branch's older kits once the new one is uploaded. The only
|
|
896
|
-
# job that may delete artifacts; a failed delete is a warning.
|
|
897
|
-
cleanup:
|
|
898
|
-
name: Delete older kits
|
|
899
|
-
needs: build
|
|
900
|
-
if: needs.build.outputs.artifact-id != ''
|
|
901
|
-
runs-on: ubuntu-latest
|
|
902
|
-
timeout-minutes: 5
|
|
903
|
-
permissions:
|
|
904
|
-
actions: write
|
|
905
|
-
steps:
|
|
906
|
-
- uses: actions/setup-node@v5
|
|
907
|
-
with:
|
|
908
|
-
node-version: "22"
|
|
909
|
-
|
|
910
|
-
- continue-on-error: true
|
|
911
|
-
env:
|
|
912
|
-
GH_TOKEN: \${{ github.token }}
|
|
913
|
-
KEEP: \${{ needs.build.outputs.artifact-id }}
|
|
914
|
-
run: npx --yes -p "$DESIGN_KIT_PACKAGE" design-kit ci-cleanup --keep "$KEEP"
|
|
915
|
-
`;
|
|
1014
|
+
${artifact ? ARTIFACT_STEPS(at("design-kit")) : store.upload}`;
|
|
916
1015
|
}
|
|
917
1016
|
|
|
1017
|
+
/** Where the skill says the kit is, and how a reader signs in, per type. */
|
|
1018
|
+
const SKILL_STORAGE = {
|
|
1019
|
+
github: {
|
|
1020
|
+
where:
|
|
1021
|
+
"uploads the `design-kit` artifact (kept 90 days; older kits are\ndeleted).",
|
|
1022
|
+
auth: "The kit is a GitHub Actions artifact, so `fetch` uses the `gh` CLI, signed\nin with read access to this repository.",
|
|
1023
|
+
},
|
|
1024
|
+
codepress: {
|
|
1025
|
+
where:
|
|
1026
|
+
"uploads the kit to CodePress, which keeps the newest kit of each\nbranch.",
|
|
1027
|
+
auth: "The kit is stored in CodePress, so `fetch` needs a CodePress API key of an\norganization that connected this repository: set `CODEPRESS_API_KEY`, or\npass `--token-file <path>` naming a file that holds the key (the key alone,\nor a `CODEPRESS_API_KEY=` line). Without either it exits 1 and says so; a\nkey of another organization gets `forbidden`.",
|
|
1028
|
+
},
|
|
1029
|
+
s3: {
|
|
1030
|
+
where: "uploads the kit to this repository's own S3 bucket.",
|
|
1031
|
+
auth: "The kit is in this repository's S3 bucket, so `fetch` runs the `aws` CLI\nwith credentials that may read it (`AWS_ACCESS_KEY_ID` and\n`AWS_SECRET_ACCESS_KEY`, or an AWS profile).",
|
|
1032
|
+
},
|
|
1033
|
+
r2: {
|
|
1034
|
+
where: "uploads the kit to this repository's own Cloudflare R2 bucket.",
|
|
1035
|
+
auth: "The kit is in this repository's Cloudflare R2 bucket, so `fetch` runs the\n`aws` CLI with an R2 API token's keys as `AWS_ACCESS_KEY_ID` and\n`AWS_SECRET_ACCESS_KEY`.",
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
|
|
918
1039
|
/** .claude/skills/design-kit/SKILL.md, as text. */
|
|
919
|
-
export function buildSkill(found) {
|
|
1040
|
+
export function buildSkill(found, storage = { type: "github" }) {
|
|
1041
|
+
const where = SKILL_STORAGE[storage?.type] ?? SKILL_STORAGE.github;
|
|
920
1042
|
const pm = found.packageManager;
|
|
921
1043
|
const projectDir = posix(relative(found.repoRoot, found.project)) || ".";
|
|
922
1044
|
const cli = `npx --yes -p ${PACKAGE.name}@${PACKAGE.version} -p @playwright/test@${playwrightPin()} design-kit`;
|
|
@@ -928,6 +1050,9 @@ export function buildSkill(found) {
|
|
|
928
1050
|
? `$DK capture stories --config "$CONFIG" --out "$KIT" # builds Storybook first`
|
|
929
1051
|
: `# (no Storybook: this kit has pages only)`,
|
|
930
1052
|
PACKAGE: PACKAGE.name,
|
|
1053
|
+
FETCH_CLI: `npx --yes -p ${PACKAGE.name}@${PACKAGE.version} design-kit`,
|
|
1054
|
+
STORAGE_WHERE: where.where,
|
|
1055
|
+
STORAGE_AUTH: where.auth,
|
|
931
1056
|
INSTALL: installCommand(pm),
|
|
932
1057
|
// The capture drives the project's own Playwright when it has one.
|
|
933
1058
|
PLAYWRIGHT_INSTALL: found.playwright
|
|
@@ -961,18 +1086,23 @@ function instructionsFile(repoRoot) {
|
|
|
961
1086
|
* The plan: every file init writes, with its content, and the notes for the
|
|
962
1087
|
* summary. Nothing is written here.
|
|
963
1088
|
*/
|
|
964
|
-
export function planInit(found) {
|
|
1089
|
+
export function planInit(found, storage) {
|
|
965
1090
|
const at = (path) => join(found.repoRoot, path);
|
|
966
|
-
const
|
|
1091
|
+
const config = buildConfig(found, storage);
|
|
1092
|
+
const skill = buildSkill(found, config.storage);
|
|
967
1093
|
const instructions = instructionsFile(found.repoRoot);
|
|
968
1094
|
const instructionsText = readText(at(instructions));
|
|
969
1095
|
const writes = [
|
|
970
1096
|
{
|
|
971
1097
|
path: join(found.project, CONFIG_NAME),
|
|
972
1098
|
kind: "file",
|
|
973
|
-
content: `${JSON.stringify(
|
|
1099
|
+
content: `${JSON.stringify(config, null, 2)}\n`,
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
path: at(WORKFLOW_PATH),
|
|
1103
|
+
kind: "file",
|
|
1104
|
+
content: buildWorkflow(found, config.storage),
|
|
974
1105
|
},
|
|
975
|
-
{ path: at(WORKFLOW_PATH), kind: "file", content: buildWorkflow(found) },
|
|
976
1106
|
{ path: at(`${SKILL_DIR}/SKILL.md`), kind: "file", content: skill },
|
|
977
1107
|
agentsSkillMode(found.repoRoot) === "symlink"
|
|
978
1108
|
? {
|
|
@@ -1064,8 +1194,26 @@ function describe(found) {
|
|
|
1064
1194
|
];
|
|
1065
1195
|
}
|
|
1066
1196
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1197
|
+
/** What is left to do for a storage type, for the summary. */
|
|
1198
|
+
function storageTodo(storage) {
|
|
1199
|
+
const type = storage?.type;
|
|
1200
|
+
if (type === "codepress")
|
|
1201
|
+
return [
|
|
1202
|
+
`Storage: the kit uploads to CodePress with this workflow's GitHub OIDC token, so no secret is needed. Connect this repository in CodePress (https://codepress.dev) first. The daily check reads the newest kit with the same OIDC token (or a CODEPRESS_API_KEY repository secret, if you add one) and builds when it cannot; agents and scripts read with CODEPRESS_API_KEY or --token-file.`,
|
|
1203
|
+
];
|
|
1204
|
+
if (type === "s3")
|
|
1205
|
+
return [
|
|
1206
|
+
`Storage: fill in storage.bucket and storage.region in ${CONFIG_NAME}, and add the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY repository secrets for a user that may read and write the bucket.`,
|
|
1207
|
+
];
|
|
1208
|
+
if (type === "r2")
|
|
1209
|
+
return [
|
|
1210
|
+
`Storage: fill in storage.accountId and storage.bucket in ${CONFIG_NAME}, create an R2 API token with Object Read & Write on the bucket, and add its keys as the R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY repository secrets.`,
|
|
1211
|
+
];
|
|
1212
|
+
return [];
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function todo(found, storage) {
|
|
1216
|
+
const items = [...storageTodo(storage)];
|
|
1069
1217
|
const sb = found.storybook;
|
|
1070
1218
|
if (!sb)
|
|
1071
1219
|
items.push(
|
|
@@ -1095,7 +1243,7 @@ function todo(found) {
|
|
|
1095
1243
|
return items;
|
|
1096
1244
|
}
|
|
1097
1245
|
|
|
1098
|
-
function summary(found, plan, { dryRun, workflowOnly, notes }) {
|
|
1246
|
+
function summary(found, plan, { dryRun, workflowOnly, notes, storage }) {
|
|
1099
1247
|
const rel = (path) => posix(relative(found.repoRoot, path)) || ".";
|
|
1100
1248
|
const lines = [
|
|
1101
1249
|
`design-kit init${workflowOnly ? " --workflow-only" : ""}${dryRun ? " --dry-run" : ""}: ${found.project}`,
|
|
@@ -1121,7 +1269,7 @@ function summary(found, plan, { dryRun, workflowOnly, notes }) {
|
|
|
1121
1269
|
lines.push(` ${rel(write.path)}${note}`);
|
|
1122
1270
|
}
|
|
1123
1271
|
lines.push("", "Still to decide");
|
|
1124
|
-
for (const item of notes ?? todo(found)) lines.push(` - ${item}`);
|
|
1272
|
+
for (const item of notes ?? todo(found, storage)) lines.push(` - ${item}`);
|
|
1125
1273
|
return lines.join("\n");
|
|
1126
1274
|
}
|
|
1127
1275
|
|
|
@@ -1129,10 +1277,13 @@ function summary(found, plan, { dryRun, workflowOnly, notes }) {
|
|
|
1129
1277
|
|
|
1130
1278
|
/**
|
|
1131
1279
|
* `init --workflow-only`: rewrites .github/workflows/design-kit.yml from
|
|
1132
|
-
* this version
|
|
1133
|
-
* `
|
|
1280
|
+
* this version, for the config's storage. The config is left alone, unless
|
|
1281
|
+
* `storage` (a type) is given: then its storage key is set and its $schema
|
|
1282
|
+
* moved to this version, in the same run as the workflow's new pin, since
|
|
1283
|
+
* older versions reject the key. When the config has no `watch` list, the
|
|
1284
|
+
* summary prints the one init would write.
|
|
1134
1285
|
*/
|
|
1135
|
-
function initWorkflowOnly(found, { dryRun, force, log }) {
|
|
1286
|
+
function initWorkflowOnly(found, { dryRun, force, log, storage }) {
|
|
1136
1287
|
const configFile = join(found.project, CONFIG_NAME);
|
|
1137
1288
|
if (!exists(configFile))
|
|
1138
1289
|
throw new InitError(
|
|
@@ -1143,12 +1294,39 @@ function initWorkflowOnly(found, { dryRun, force, log }) {
|
|
|
1143
1294
|
throw new InitError(
|
|
1144
1295
|
`${WORKFLOW_PATH} already exists. Nothing was written; pass --force with --workflow-only to replace it.`,
|
|
1145
1296
|
);
|
|
1146
|
-
const plan = {
|
|
1147
|
-
writes: [{ path, kind: "file", content: buildWorkflow(found) }],
|
|
1148
|
-
};
|
|
1149
|
-
if (!dryRun) apply(plan.writes[0]);
|
|
1150
|
-
const notes = [];
|
|
1151
1297
|
const raw = readJson(configFile);
|
|
1298
|
+
if (storage && !raw)
|
|
1299
|
+
throw new InitError(
|
|
1300
|
+
`${configFile} is not valid JSON, so its storage key cannot be set. Fix it first.`,
|
|
1301
|
+
);
|
|
1302
|
+
const writes = [];
|
|
1303
|
+
let configStorage = raw?.storage;
|
|
1304
|
+
if (storage) {
|
|
1305
|
+
configStorage =
|
|
1306
|
+
raw.storage?.type === storage ? raw.storage : storageConfig(storage);
|
|
1307
|
+
// The key goes after "out", where init writes it; every other key and
|
|
1308
|
+
// its order stay as they are.
|
|
1309
|
+
const updated = {};
|
|
1310
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1311
|
+
if (key === "storage") continue;
|
|
1312
|
+
updated[key] = key === "$schema" ? SCHEMA_URL : value;
|
|
1313
|
+
if (key === "out") updated.storage = configStorage;
|
|
1314
|
+
}
|
|
1315
|
+
if (!("storage" in updated)) updated.storage = configStorage;
|
|
1316
|
+
writes.push({
|
|
1317
|
+
path: configFile,
|
|
1318
|
+
kind: "file",
|
|
1319
|
+
content: `${JSON.stringify(updated, null, 2)}\n`,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
writes.push({
|
|
1323
|
+
path,
|
|
1324
|
+
kind: "file",
|
|
1325
|
+
content: buildWorkflow(found, configStorage),
|
|
1326
|
+
});
|
|
1327
|
+
const plan = { writes };
|
|
1328
|
+
if (!dryRun) for (const write of writes) apply(write);
|
|
1329
|
+
const notes = [...(storage ? storageTodo(configStorage) : [])];
|
|
1152
1330
|
if (!Array.isArray(raw?.watch) || !raw.watch.length)
|
|
1153
1331
|
notes.push(
|
|
1154
1332
|
`${CONFIG_NAME} has no "watch" list, so the kit rebuilds after any commit. To rebuild only when the UI changes, add:\n "watch": ${JSON.stringify(watchDefaults(found))}`,
|
|
@@ -1161,24 +1339,31 @@ function initWorkflowOnly(found, { dryRun, force, log }) {
|
|
|
1161
1339
|
/**
|
|
1162
1340
|
* Runs init. Options: `root` (the project folder, default the working
|
|
1163
1341
|
* directory), `dryRun`, `force`, `workflowOnly` (write just the workflow, for
|
|
1164
|
-
* a repository set up by an older version), `
|
|
1165
|
-
*
|
|
1342
|
+
* a repository set up by an older version), `storage` (github, codepress,
|
|
1343
|
+
* s3 or r2: where the kit is stored), `log`. Returns the exit code: 0 done,
|
|
1344
|
+
* 1 already set up or another file in the way.
|
|
1166
1345
|
*/
|
|
1167
1346
|
export function init({
|
|
1168
1347
|
root = ".",
|
|
1169
1348
|
dryRun = false,
|
|
1170
1349
|
force = false,
|
|
1171
1350
|
workflowOnly = false,
|
|
1351
|
+
storage,
|
|
1172
1352
|
log = console.log,
|
|
1173
1353
|
} = {}) {
|
|
1354
|
+
if (storage !== undefined && !STORAGE_TYPES.includes(storage))
|
|
1355
|
+
throw new ConfigError(
|
|
1356
|
+
`--storage takes ${STORAGE_TYPES.join(", ")}, got ${storage}`,
|
|
1357
|
+
);
|
|
1174
1358
|
const found = detectProject(root);
|
|
1175
|
-
if (workflowOnly)
|
|
1176
|
-
|
|
1359
|
+
if (workflowOnly)
|
|
1360
|
+
return initWorkflowOnly(found, { dryRun, force, log, storage });
|
|
1361
|
+
const plan = planInit(found, storage);
|
|
1177
1362
|
if (!force) {
|
|
1178
1363
|
const config = join(found.project, CONFIG_NAME);
|
|
1179
1364
|
if (exists(config))
|
|
1180
1365
|
throw new InitError(
|
|
1181
|
-
`already set up: ${config} exists. Nothing was written; pass --force to replace the files init writes.`,
|
|
1366
|
+
`already set up: ${config} exists. Nothing was written; pass --force to replace the files init writes${storage ? `, or run init --workflow-only --force --storage ${storage} to set where the kit is stored and rewrite only the workflow` : ""}.`,
|
|
1182
1367
|
);
|
|
1183
1368
|
const inTheWay = plan.writes
|
|
1184
1369
|
.filter(
|
|
@@ -1199,7 +1384,12 @@ export function init({
|
|
|
1199
1384
|
for (const write of plan.writes)
|
|
1200
1385
|
if (write.kind !== "unchanged") apply(write);
|
|
1201
1386
|
}
|
|
1202
|
-
log(
|
|
1387
|
+
log(
|
|
1388
|
+
summary(found, plan, {
|
|
1389
|
+
dryRun,
|
|
1390
|
+
storage: storage && storageConfig(storage),
|
|
1391
|
+
}),
|
|
1392
|
+
);
|
|
1203
1393
|
if (!dryRun) log(`\n${CALL_TO_ACTION}`);
|
|
1204
1394
|
return 0;
|
|
1205
1395
|
}
|