@getvitops/create 0.8.0-alpha.0 → 0.8.1

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.
@@ -1,4 +1,5 @@
1
1
  import react from '@astrojs/react';
2
+ import { cloudflareEmail } from '@emdash-cms/cloudflare/plugins';
2
3
  import vitops from '@getvitops/astro';
3
4
  import { resolveOrganizationName } from '@getvitops/astro/routes';
4
5
  import { vitopsEmdash, vitopsHosting } from '@getvitops/emdash';
@@ -35,8 +36,9 @@ export default defineConfig({
35
36
  emdash({
36
37
  database,
37
38
  storage,
38
- // Vitops design-system blocks (image compare, copy snippet, banner,
39
- // disclosure, carousel) in the editor slash menu. Default 'integration'
39
+ // Vitops design-system blocks (link/button, image compare, copy snippet,
40
+ // banner, disclosure, accordion, hero, carousel, gallery, cards, marquee) in the
41
+ // editor slash menu. Narrow them with `vitopsEmdash({ blocks: { exclude } })`. Default 'integration'
40
42
  // script delivery: Layout.astro renders <Head /> from @getvitops/astro,
41
43
  // which emits the web-component runtime tags.
42
44
  //
@@ -46,7 +48,24 @@ export default defineConfig({
46
48
  // a VITOPS_GITHUB_TOKEN secret are set (see SETUP.html) — see
47
49
  // AGENTS.md's "EmDash plugin as the design-system editing surface".
48
50
  // Until then the live half still works; only the sync half is skipped.
49
- plugins: [vitopsEmdash({ designSystemEditor: { config } })],
51
+ //
52
+ // cloudflareEmail sends magic-link sign-in, invites and account recovery
53
+ // through Cloudflare Email Sending (the EMAIL binding in wrangler.jsonc).
54
+ // Magic link is how an editor gets into a site whose database was copied
55
+ // from another origin: a passkey only works at the origin it was
56
+ // registered on, so a dev database refreshed from prod
57
+ // (scripts/refresh-dev.mjs) would otherwise lock everyone out of dev. The
58
+ // cloned users already exist, so the same email signs in on both origins.
59
+ // `from` must be on a domain onboarded with Cloudflare Email Sending —
60
+ // `npx vitops email setup send.<your-domain>` — and is usually the
61
+ // subdomain, not the apex (see AGENTS.md's "Sender-domain onboarding").
62
+ // Without it, magic link answers "Email is not configured".
63
+ plugins: [
64
+ vitopsEmdash({ designSystemEditor: { config } }),
65
+ cloudflareEmail({
66
+ from: { email: 'cms@send.my-emdash-site.example.com', name: organizationName },
67
+ }),
68
+ ],
50
69
  // 1.0's runtime migration mode defaults to 'auto' (apply pending core
51
70
  // migrations on first request) — fine for local dev, but a deploy
52
71
  // should have already migrated before traffic arrives. 'check' 503s a
@@ -20,22 +20,23 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@astrojs/cloudflare": "^14.0.0",
23
- "@astrojs/react": "^6.0.0",
24
- "@emdash-cms/cloudflare": "^1.0.1",
23
+ "@astrojs/react": "^7.1.2",
24
+ "@emdash-cms/cloudflare": "^1.4.0",
25
25
  "@getvitops/astro": "latest",
26
26
  "@getvitops/core": "latest",
27
27
  "@getvitops/emdash": "latest",
28
28
  "@tailwindcss/vite": "^4.3.3",
29
- "astro": "^7.1.2",
30
- "emdash": "^1.0.1",
31
- "react": "^19.2.4",
32
- "react-dom": "^19.2.4",
29
+ "astro": "^7.4.4",
30
+ "emdash": "^1.4.0",
31
+ "react": "^19.3.1",
32
+ "react-dom": "^19.3.1",
33
33
  "tailwindcss": "^4.3.3"
34
34
  },
35
35
  "devDependencies": {
36
- "@astrojs/check": "^0.9.7",
36
+ "@astrojs/check": "^0.9.12",
37
37
  "@cloudflare/workers-types": "^4.20260305.1",
38
38
  "@getvitops/cli": "latest",
39
+ "@iconify-json/fa7-solid": "^1.2.5",
39
40
  "typescript": "^6.0.3",
40
41
  "vite": "latest",
41
42
  "vite-plus": "latest",
@@ -44,11 +45,11 @@
44
45
  "devEngines": {
45
46
  "runtime": {
46
47
  "name": "node",
47
- "version": "24.18.0",
48
+ "version": "24",
48
49
  "onFail": "download"
49
50
  }
50
51
  },
51
- "packageManager": "pnpm@11.17.0",
52
+ "packageManager": "pnpm@12.16.1",
52
53
  "emdash": {
53
54
  "seed": "seed/seed.json"
54
55
  }
@@ -0,0 +1,183 @@
1
+ ---
2
+ lockfileVersion: '9.0'
3
+
4
+ importers:
5
+
6
+ .:
7
+ configDependencies: {}
8
+ packageManagerDependencies:
9
+ '@pnpm/exe':
10
+ specifier: 12.16.1
11
+ version: 12.16.1
12
+ pnpm:
13
+ specifier: 12.16.1
14
+ version: 12.16.1
15
+
16
+ packages:
17
+
18
+ '@pnpm/exe.android-arm64@12.16.1':
19
+ resolution: {integrity: sha512-PKf78Ew+i0CBYf0Xc+eMcqQiqyBT+u8XCVg1F5roc98/6wWfdYlWhrUMmEBiKWaJ3JqHxrtt8l1NwZ7qfJt2/Q==}
20
+ cpu: [arm64]
21
+ os: [android]
22
+
23
+ '@pnpm/exe.android-x64@12.16.1':
24
+ resolution: {integrity: sha512-xdu7gMbB98pdvhwMeA6612Of515FDlRUtfA/1f7nWPZfPLnEdSRBO2u+1N0XBtQ/ennhPH/lrxB1gC6gnR/cdg==}
25
+ cpu: [x64]
26
+ os: [android]
27
+
28
+ '@pnpm/exe.darwin-arm64@12.16.1':
29
+ resolution: {integrity: sha512-pVbmIIbhixiLvADLu1WA3TaW8FZy/D72R1De27WbbFx8/2mxvnsTyFVm4fO9kjpjMY7RzJd48CiQ96p8fO3ebQ==}
30
+ cpu: [arm64]
31
+ os: [darwin]
32
+
33
+ '@pnpm/exe.darwin-x64@12.16.1':
34
+ resolution: {integrity: sha512-xZvQBFqcZjO7vFKTK3XsJpqY6lnBL9EWSLCzVUPq3kZ94+D96gwJCa76xxGNK8Cj32f4eeM91Qg+yNRR4sRpDw==}
35
+ cpu: [x64]
36
+ os: [darwin]
37
+
38
+ '@pnpm/exe.freebsd-x64@12.16.1':
39
+ resolution: {integrity: sha512-zTtunfi9mBhRXiXEJYfwiJj0tk7MgTSfqDSRh+Az9FZ9THazVIiQiVNS8jpcsftDOqu7LKNffQdjmgwHDThfUg==}
40
+ cpu: [x64]
41
+ os: [freebsd]
42
+
43
+ '@pnpm/exe.linux-arm64-musl@12.16.1':
44
+ resolution: {integrity: sha512-BJ3mOmcmQgcUo92Ee/WfEOt1pRiv3y2MMAkhEiYHd85UiPpHTfFhv+SpCbALmfExzXk3UlaRtMYwHlkQymc4VA==}
45
+ cpu: [arm64]
46
+ os: [linux]
47
+ libc: [musl]
48
+
49
+ '@pnpm/exe.linux-arm64@12.16.1':
50
+ resolution: {integrity: sha512-JaRd5gNxwdNGBBkrZFt8bknrK4YmHVJBW0JMhUQ0fJ0dEYp6C2GT9ExaCqOO8EwYEyqM+jpKgJIK+8WFaJy3ew==}
51
+ cpu: [arm64]
52
+ os: [linux]
53
+ libc: [glibc]
54
+
55
+ '@pnpm/exe.linux-ppc64@12.16.1':
56
+ resolution: {integrity: sha512-FnSV0Jv40S1cMv7vFNWwvu0X567Lec4L4HCRead3CFcD6zQDNbGw81s8N5bcKPahtPD9ApVuhsbmFCx4rw6RMw==}
57
+ cpu: [ppc64]
58
+ os: [linux]
59
+ libc: [glibc]
60
+
61
+ '@pnpm/exe.linux-riscv64@12.16.1':
62
+ resolution: {integrity: sha512-C1Sww4FN5IttD4atRRhAKmQtSGWVp04ZAKNxO8pfu2bkRPwOW+7v+2DH1Xs1W+kZYYpO86BYDvUcadefF+AgaQ==}
63
+ cpu: [riscv64]
64
+ os: [linux]
65
+ libc: [glibc]
66
+
67
+ '@pnpm/exe.linux-s390x@12.16.1':
68
+ resolution: {integrity: sha512-kn2fautsTcxIMODoovQVqZyfpDRdNgGkoeVqNdrKqbeBBvrEl+fXLkmZiq5xDht1LMr4sYiCNcsMfNMT6JPFmg==}
69
+ cpu: [s390x]
70
+ os: [linux]
71
+ libc: [glibc]
72
+
73
+ '@pnpm/exe.linux-x64-musl@12.16.1':
74
+ resolution: {integrity: sha512-PM6ccHCOe/O9SsTflTJTYWfwogJZa6EmUvpnwZgiPguJBddh20wDQKgxUPXf48rDsPm9vMsIij0uHswwwbXL8g==}
75
+ cpu: [x64]
76
+ os: [linux]
77
+ libc: [musl]
78
+
79
+ '@pnpm/exe.linux-x64@12.16.1':
80
+ resolution: {integrity: sha512-RK//8ZLcXGW8A2k8bQOXZDBN1Qu92zN4dYnG1bzj1zoQ1fkZ6jNzZ1AvXtsfGVma795CJtvoaF5bLXf4l/FIUw==}
81
+ cpu: [x64]
82
+ os: [linux]
83
+ libc: [glibc]
84
+
85
+ '@pnpm/exe.win32-arm64@12.16.1':
86
+ resolution: {integrity: sha512-Ox3kNNVxtGw6pK7SVejmeOUgHhUTZgAM7NQQ6cU7frPw3ix9+j7d2MGbRg3fyEFAaKWq8ZAZLTQGcDyLnAu+qA==}
87
+ cpu: [arm64]
88
+ os: [win32]
89
+
90
+ '@pnpm/exe.win32-x64@12.16.1':
91
+ resolution: {integrity: sha512-k+FhRJ0ttphFoFIVB1Sk4v76YJAulafmA/Ly57cysesUhhIzVlApeOShUz0jXubKx317TThlEfpQrukldnqORA==}
92
+ cpu: [x64]
93
+ os: [win32]
94
+
95
+ '@pnpm/exe@12.16.1':
96
+ resolution: {integrity: sha512-Opsh2R7IuFY3ekd0C282Tmzgr5hiSlKMOv05/3dJXUrrp5NbmjC1Cm5PCFsa8xsiXY7z4oSkI4Qof1LjTkgAJg==}
97
+ engines: {node: '>=18'}
98
+ hasBin: true
99
+
100
+ pnpm@12.16.1:
101
+ resolution: {integrity: sha512-CR0d7nknV1Zhsl2rPCrjHK/BxU1rce1qZgDPDByxBDSkR+rhQBXxsj4ua+3u1535SpMuIWTSgyKA3OP7MvZW0Q==}
102
+ engines: {node: '>=18'}
103
+ hasBin: true
104
+
105
+ snapshots:
106
+
107
+ '@pnpm/exe.android-arm64@12.16.1':
108
+ optional: true
109
+
110
+ '@pnpm/exe.android-x64@12.16.1':
111
+ optional: true
112
+
113
+ '@pnpm/exe.darwin-arm64@12.16.1':
114
+ optional: true
115
+
116
+ '@pnpm/exe.darwin-x64@12.16.1':
117
+ optional: true
118
+
119
+ '@pnpm/exe.freebsd-x64@12.16.1':
120
+ optional: true
121
+
122
+ '@pnpm/exe.linux-arm64-musl@12.16.1':
123
+ optional: true
124
+
125
+ '@pnpm/exe.linux-arm64@12.16.1':
126
+ optional: true
127
+
128
+ '@pnpm/exe.linux-ppc64@12.16.1':
129
+ optional: true
130
+
131
+ '@pnpm/exe.linux-riscv64@12.16.1':
132
+ optional: true
133
+
134
+ '@pnpm/exe.linux-s390x@12.16.1':
135
+ optional: true
136
+
137
+ '@pnpm/exe.linux-x64-musl@12.16.1':
138
+ optional: true
139
+
140
+ '@pnpm/exe.linux-x64@12.16.1':
141
+ optional: true
142
+
143
+ '@pnpm/exe.win32-arm64@12.16.1':
144
+ optional: true
145
+
146
+ '@pnpm/exe.win32-x64@12.16.1':
147
+ optional: true
148
+
149
+ '@pnpm/exe@12.16.1':
150
+ optionalDependencies:
151
+ '@pnpm/exe.android-arm64': 12.16.1
152
+ '@pnpm/exe.android-x64': 12.16.1
153
+ '@pnpm/exe.darwin-arm64': 12.16.1
154
+ '@pnpm/exe.darwin-x64': 12.16.1
155
+ '@pnpm/exe.freebsd-x64': 12.16.1
156
+ '@pnpm/exe.linux-arm64': 12.16.1
157
+ '@pnpm/exe.linux-arm64-musl': 12.16.1
158
+ '@pnpm/exe.linux-ppc64': 12.16.1
159
+ '@pnpm/exe.linux-riscv64': 12.16.1
160
+ '@pnpm/exe.linux-s390x': 12.16.1
161
+ '@pnpm/exe.linux-x64': 12.16.1
162
+ '@pnpm/exe.linux-x64-musl': 12.16.1
163
+ '@pnpm/exe.win32-arm64': 12.16.1
164
+ '@pnpm/exe.win32-x64': 12.16.1
165
+
166
+ pnpm@12.16.1:
167
+ optionalDependencies:
168
+ '@pnpm/exe.android-arm64': 12.16.1
169
+ '@pnpm/exe.android-x64': 12.16.1
170
+ '@pnpm/exe.darwin-arm64': 12.16.1
171
+ '@pnpm/exe.darwin-x64': 12.16.1
172
+ '@pnpm/exe.freebsd-x64': 12.16.1
173
+ '@pnpm/exe.linux-arm64': 12.16.1
174
+ '@pnpm/exe.linux-arm64-musl': 12.16.1
175
+ '@pnpm/exe.linux-ppc64': 12.16.1
176
+ '@pnpm/exe.linux-riscv64': 12.16.1
177
+ '@pnpm/exe.linux-s390x': 12.16.1
178
+ '@pnpm/exe.linux-x64': 12.16.1
179
+ '@pnpm/exe.linux-x64-musl': 12.16.1
180
+ '@pnpm/exe.win32-arm64': 12.16.1
181
+ '@pnpm/exe.win32-x64': 12.16.1
182
+
183
+ ---
@@ -8,7 +8,7 @@ allowBuilds:
8
8
 
9
9
  # Supply-chain hardening: cooldown on brand-new releases (toolchain exempt)
10
10
  # and no non-registry transitive sources.
11
- minimumReleaseAge: 1440
11
+ minimumReleaseAge: 4320
12
12
  minimumReleaseAgeExclude:
13
13
  - '@getvitops/*'
14
14
  - emdash
@@ -0,0 +1,10 @@
1
+ # Cloudflare Workers static assets apply this file to every asset response.
2
+ #
3
+ # A baseline that cannot break a page. No Content-Security-Policy on purpose: <Head /> emits
4
+ # inline scripts (the consent stub, the colour-scheme bootstrap), so a strict policy needs
5
+ # per-site nonces or hashes — add one deliberately once you know which third-party tags you load.
6
+ /*
7
+ X-Content-Type-Options: nosniff
8
+ Referrer-Policy: strict-origin-when-cross-origin
9
+ Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
10
+ X-Frame-Options: SAMEORIGIN
@@ -66,6 +66,12 @@ cfg.r2_buckets = (cfg.r2_buckets ?? []).map((r) =>
66
66
  r.binding === 'MEDIA' ? { ...r, bucket_name: `${r.bucket_name}-dev` } : r,
67
67
  );
68
68
 
69
+ // No cron on dev. Dev's database is a copy of prod (scripts/refresh-dev.mjs),
70
+ // so it carries prod's scheduled publishes and plugin jobs; leaving the trigger
71
+ // on would run them a second time against the copy. The cost: scheduled
72
+ // publishing can't be exercised on dev.
73
+ delete cfg.triggers;
74
+
69
75
  // Flat config — no nested environments.
70
76
  delete cfg.env;
71
77
 
@@ -0,0 +1,182 @@
1
+ // Refreshes the DEV D1 database (and, optionally, the dev media bucket) from
2
+ // PRODUCTION. One direction only: prod is the source of truth for content, dev
3
+ // is a disposable copy used to rehearse code and schema changes against real
4
+ // data (EmDash's own "rehearse changes on a preview environment" pattern).
5
+ //
6
+ // node scripts/refresh-dev.mjs # wipe dev D1, import prod's export
7
+ // node scripts/refresh-dev.mjs --dry # run every check, change nothing
8
+ //
9
+ // Names come from wrangler.jsonc, the same way scripts/build-dev-wrangler.mjs
10
+ // derives them, so the two can't disagree about which resources are "dev":
11
+ //
12
+ // D1 <database_name> → <database_name>-dev (id from $DEV_D1_ID)
13
+ // R2 <bucket_name> → <bucket_name>-dev
14
+ //
15
+ // SAFETY, in the order it is enforced:
16
+ // 1. $DEV_D1_ID must be set, and `wrangler d1 info <name>-dev` must report
17
+ // that same id. A name that resolves to a different database than the id
18
+ // we were told about means one of the two is stale.
19
+ // 2. The dev id must differ from prod's. This script DROPS every table in
20
+ // the target, so pointing it at prod would erase production — the exact
21
+ // thing the dev/prod split exists to prevent.
22
+ // Both checks run before the first write, and `--dry` stops after them.
23
+ //
24
+ // Not copied: anything outside D1/R2. Sessions live in the Astro session store
25
+ // (KV), not D1, so nothing is carried over there. The preview-link secret IS in
26
+ // D1 — set EMDASH_PREVIEW_SECRET on the dev worker so prod preview links don't
27
+ // verify on dev (see SETUP.html).
28
+ //
29
+ // Media is copied only when R2 S3 credentials are present, because wrangler has
30
+ // no bucket-to-bucket sync and rclone needs the S3 API:
31
+ // R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, CLOUDFLARE_ACCOUNT_ID
32
+ // Without them the database is refreshed and media is left alone, with a note —
33
+ // dev then shows broken images for anything uploaded since the last copy.
34
+ //
35
+ // KNOWN LIMIT: `wrangler d1 export` does not support virtual tables, so an FTS5
36
+ // search table breaks the export. EmDash rebuilds its search index on the first
37
+ // search call, so if an export ever fails on one, drop it from prod's schema
38
+ // dump rather than working around it here.
39
+
40
+ import { execFileSync } from 'node:child_process';
41
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
42
+ import { tmpdir } from 'node:os';
43
+ import { join } from 'node:path';
44
+ import { pathToFileURL } from 'node:url';
45
+
46
+ /**
47
+ * Pull the D1 database and R2 bucket names out of a wrangler.jsonc source.
48
+ * Regex rather than a JSONC parser: the template carries no parser, the file
49
+ * is ours, and these two keys are the only thing read. `//` comment lines are
50
+ * stripped first so a commented-out example block can't be picked up.
51
+ */
52
+ export function parseBindingNames(jsonc) {
53
+ const code = jsonc
54
+ .split('\n')
55
+ .filter((line) => !line.trim().startsWith('//'))
56
+ .join('\n');
57
+ const database = /"database_name"\s*:\s*"([^"]+)"/.exec(code)?.[1];
58
+ const bucket = /"bucket_name"\s*:\s*"([^"]+)"/.exec(code)?.[1];
59
+ return { database, bucket };
60
+ }
61
+
62
+ /**
63
+ * Throws unless it is safe to wipe `devId`'s database. Pure, so the one check
64
+ * that stands between this script and production is asserted in a test.
65
+ */
66
+ export function assertSafeTarget({ devName, devId, devReportedId, prodId }) {
67
+ if (!devId) throw new Error('DEV_D1_ID is not set.');
68
+ if (!devName.endsWith('-dev')) throw new Error(`Refusing: "${devName}" is not a -dev database.`);
69
+ if (devReportedId !== devId) {
70
+ throw new Error(
71
+ `DEV_D1_ID (${devId}) does not match "${devName}" (${devReportedId}). One of them is stale.`,
72
+ );
73
+ }
74
+ if (devId === prodId) throw new Error('Refusing: the dev database id is the production id.');
75
+ }
76
+
77
+ /**
78
+ * SQL that empties a database: views, then virtual tables, then ordinary
79
+ * tables. `rows` is `{ name, type, sql }` from sqlite_master. A virtual table
80
+ * owns shadow tables named `<virtual>_*`, which SQLite refuses to drop on their
81
+ * own and removes with the parent, so those are skipped.
82
+ */
83
+ export function dropStatements(rows) {
84
+ const virtual = rows.filter((r) => /^CREATE VIRTUAL TABLE/i.test(r.sql ?? ''));
85
+ const isShadow = (name) => virtual.some((v) => name.startsWith(`${v.name}_`));
86
+ const tables = rows.filter(
87
+ (r) => r.type === 'table' && !virtual.includes(r) && !isShadow(r.name),
88
+ );
89
+ const q = (name) => `"${name.replaceAll('"', '""')}"`;
90
+ return [
91
+ 'PRAGMA defer_foreign_keys = true;',
92
+ ...rows.filter((r) => r.type === 'view').map((r) => `DROP VIEW IF EXISTS ${q(r.name)};`),
93
+ ...virtual.map((r) => `DROP TABLE IF EXISTS ${q(r.name)};`),
94
+ ...tables.map((r) => `DROP TABLE IF EXISTS ${q(r.name)};`),
95
+ ];
96
+ }
97
+
98
+ const wrangler = (args, opts = {}) =>
99
+ execFileSync('pnpm', ['exec', 'wrangler', ...args], { encoding: 'utf8', ...opts });
100
+
101
+ const d1Id = (name) => JSON.parse(wrangler(['d1', 'info', name, '--json'])).uuid;
102
+
103
+ function main() {
104
+ const dry = process.argv.includes('--dry');
105
+ const wranglerConfig = 'wrangler.jsonc';
106
+ if (!existsSync(wranglerConfig))
107
+ throw new Error(`Run from the project root (no ${wranglerConfig}).`);
108
+
109
+ const { database, bucket } = parseBindingNames(readFileSync(wranglerConfig, 'utf8'));
110
+ if (!database) throw new Error(`No D1 database_name found in ${wranglerConfig}.`);
111
+ const devDatabase = `${database}-dev`;
112
+
113
+ assertSafeTarget({
114
+ devName: devDatabase,
115
+ devId: process.env.DEV_D1_ID,
116
+ devReportedId: d1Id(devDatabase),
117
+ prodId: d1Id(database),
118
+ });
119
+ console.log(`OK to refresh ${devDatabase} from ${database}.`);
120
+
121
+ const dir = mkdtempSync(join(tmpdir(), 'refresh-dev-'));
122
+ const dump = join(dir, 'prod.sql');
123
+ const wipe = join(dir, 'wipe.sql');
124
+
125
+ const master = JSON.parse(
126
+ wrangler([
127
+ 'd1',
128
+ 'execute',
129
+ devDatabase,
130
+ '--remote',
131
+ '--json',
132
+ '--command',
133
+ "SELECT name, type, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' AND name NOT LIKE '\\_cf\\_%' ESCAPE '\\' AND type IN ('table','view')",
134
+ ]),
135
+ )[0].results;
136
+ writeFileSync(wipe, `${dropStatements(master).join('\n')}\n`);
137
+
138
+ if (dry) {
139
+ console.log(`--dry: would drop ${master.length} objects from ${devDatabase} and import prod.`);
140
+ console.log(
141
+ bucket ? `--dry: would copy ${bucket} → ${bucket}-dev.` : '--dry: no R2 bucket found.',
142
+ );
143
+ return;
144
+ }
145
+
146
+ wrangler(['d1', 'export', database, '--remote', '--output', dump], { stdio: 'inherit' });
147
+ wrangler(['d1', 'execute', devDatabase, '--remote', '--file', wipe], { stdio: 'inherit' });
148
+ wrangler(['d1', 'execute', devDatabase, '--remote', '--file', dump], { stdio: 'inherit' });
149
+ console.log(`Imported ${database} into ${devDatabase}.`);
150
+
151
+ const { R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, CLOUDFLARE_ACCOUNT_ID } = process.env;
152
+ if (!bucket) return;
153
+ if (!R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !CLOUDFLARE_ACCOUNT_ID) {
154
+ console.log(
155
+ `Skipped media: R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY not set, ${bucket}-dev left as is.`,
156
+ );
157
+ return;
158
+ }
159
+ // Credentials ride rclone's env-var config, not the command line, so the
160
+ // secret never appears in a process listing.
161
+ const env = {
162
+ ...process.env,
163
+ RCLONE_CONFIG_R2_TYPE: 's3',
164
+ RCLONE_CONFIG_R2_PROVIDER: 'Cloudflare',
165
+ RCLONE_CONFIG_R2_ENDPOINT: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
166
+ RCLONE_CONFIG_R2_ACCESS_KEY_ID: R2_ACCESS_KEY_ID,
167
+ RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: R2_SECRET_ACCESS_KEY,
168
+ };
169
+ // `sync`, not `copy`: media deleted from prod should disappear from dev too.
170
+ execFileSync('rclone', ['sync', `R2:${bucket}`, `R2:${bucket}-dev`], { env, stdio: 'inherit' });
171
+ console.log(`Synced ${bucket} → ${bucket}-dev.`);
172
+ }
173
+
174
+ // Importable for tests without running; executed when invoked as a script.
175
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
176
+ try {
177
+ main();
178
+ } catch (err) {
179
+ console.error(`ERROR: ${err.message}`);
180
+ process.exit(1);
181
+ }
182
+ }
@@ -36,16 +36,32 @@
36
36
  "name": "primary",
37
37
  "label": "Primary Navigation",
38
38
  "items": [
39
- { "type": "custom", "label": "Home", "url": "/" },
40
- { "type": "custom", "label": "About", "url": "/about" }
39
+ {
40
+ "type": "custom",
41
+ "label": "Home",
42
+ "url": "/"
43
+ },
44
+ {
45
+ "type": "custom",
46
+ "label": "About",
47
+ "url": "/about"
48
+ }
41
49
  ]
42
50
  },
43
51
  {
44
52
  "name": "footer",
45
53
  "label": "Footer",
46
54
  "items": [
47
- { "type": "custom", "label": "Terms & Conditions", "url": "/terms" },
48
- { "type": "custom", "label": "Privacy Policy", "url": "/privacy" }
55
+ {
56
+ "type": "custom",
57
+ "label": "Terms & Conditions",
58
+ "url": "/terms"
59
+ },
60
+ {
61
+ "type": "custom",
62
+ "label": "Privacy Policy",
63
+ "url": "/privacy"
64
+ }
49
65
  ]
50
66
  }
51
67
  ],
@@ -86,6 +102,84 @@
86
102
  }
87
103
  ]
88
104
  },
105
+ {
106
+ "_type": "vitops.hero",
107
+ "_key": "demo-hero",
108
+ "eyebrow": "Vitops blocks",
109
+ "heading": "Build pages from design-system patterns",
110
+ "body": "Every block below renders a framework pattern, so it follows your tokens and works without JavaScript.",
111
+ "primaryLabel": "Open the admin",
112
+ "primaryHref": "/_emdash/admin",
113
+ "secondaryLabel": "Docs",
114
+ "secondaryHref": "https://docs.vitops.ca",
115
+ "layout": "centered"
116
+ },
117
+ {
118
+ "_type": "vitops.cards",
119
+ "_key": "demo-cards",
120
+ "columns": "3",
121
+ "cards": [
122
+ {
123
+ "_key": "c1",
124
+ "title": "Tokens",
125
+ "body": "Colour, type and spacing come from site.json.",
126
+ "href": "https://docs.vitops.ca",
127
+ "linkLabel": "Read more"
128
+ },
129
+ {
130
+ "_key": "c2",
131
+ "title": "Patterns",
132
+ "body": "Cards, carousels and dialogs share one vocabulary."
133
+ },
134
+ {
135
+ "_key": "c3",
136
+ "title": "Accessible",
137
+ "body": "Semantic HTML first, enhanced when JavaScript loads."
138
+ }
139
+ ]
140
+ },
141
+ {
142
+ "_type": "vitops.accordion",
143
+ "_key": "demo-accordion",
144
+ "exclusive": true,
145
+ "items": [
146
+ {
147
+ "_key": "a1",
148
+ "summary": "What is a block?",
149
+ "body": "A reusable section an editor inserts from the slash menu.",
150
+ "open": true
151
+ },
152
+ {
153
+ "_key": "a2",
154
+ "summary": "Can I add my own?",
155
+ "body": "Yes. Pass blocks.extra to the plugin and map a component for it."
156
+ }
157
+ ]
158
+ },
159
+ {
160
+ "_type": "vitops.marquee",
161
+ "_key": "demo-marquee",
162
+ "pauseOnHover": true,
163
+ "reverse": false,
164
+ "items": [
165
+ {
166
+ "_key": "m1",
167
+ "text": "Fast"
168
+ },
169
+ {
170
+ "_key": "m2",
171
+ "text": "Accessible"
172
+ },
173
+ {
174
+ "_key": "m3",
175
+ "text": "Editable"
176
+ },
177
+ {
178
+ "_key": "m4",
179
+ "text": "Yours"
180
+ }
181
+ ]
182
+ },
89
183
  {
90
184
  "_type": "vitops.banner",
91
185
  "_key": "welcome-banner",
@@ -161,7 +255,12 @@
161
255
  "style": "h2",
162
256
  "markDefs": [],
163
257
  "children": [
164
- { "_type": "span", "_key": "terms-use-h-1", "text": "Use of the site", "marks": [] }
258
+ {
259
+ "_type": "span",
260
+ "_key": "terms-use-h-1",
261
+ "text": "Use of the site",
262
+ "marks": []
263
+ }
165
264
  ]
166
265
  },
167
266
  {
@@ -23,8 +23,8 @@
23
23
  }
24
24
  },
25
25
  "roles": {
26
- "neutral": "ink",
27
- "surface": "ink",
26
+ "neutral": { "hue": "ink", "kind": "surface" },
27
+ "surface": { "hue": "ink", "kind": "surface" },
28
28
  "ui-primary": "brand",
29
29
  "brand-primary": "brand",
30
30
  "info": "sky",
@@ -17,11 +17,17 @@ const { entry: page } = slug ? await getEmDashEntry('pages', slug) : { entry: nu
17
17
  if (!page) {
18
18
  return Astro.rewrite('/404');
19
19
  }
20
+
21
+ // `page.data` is typed `unknown` until `.emdash/types.ts` is generated from a
22
+ // running site (`pnpm dev`, then `emdash types`), so a fresh scaffold would fail
23
+ // `astro check`; the cast keeps it passing both before and after, where the
24
+ // generated type is already `string`.
25
+ const title = page.data.title as string | undefined;
20
26
  ---
21
27
 
22
- <Layout title={page.data.title}>
28
+ <Layout title={title}>
23
29
  <article class="rhythm">
24
- <h1>{page.data.title}</h1>
30
+ <h1>{title}</h1>
25
31
  {page.data.content && <PortableText value={page.data.content} />}
26
32
  </article>
27
33
  </Layout>