@getvitops/create 0.2.0 → 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/README.md +6 -0
- package/package.json +1 -1
- package/templates/emdash/.claude/settings.json +3 -0
- package/templates/emdash/AGENTS.md +7 -0
- package/templates/emdash/README.md +33 -8
- package/templates/emdash/_gitignore +3 -0
- package/templates/emdash/astro.config.mjs +13 -6
- package/templates/emdash/package.json +4 -2
- package/templates/emdash/scripts/init-hosting.mjs +150 -0
package/README.md
CHANGED
|
@@ -43,6 +43,12 @@ with every relevant `@getvitops/*` package pre-wired:
|
|
|
43
43
|
- **GitHub bootstrap** — `pnpm run init:github` creates a same-named GitHub
|
|
44
44
|
repo via the `gh` CLI, pushes `main` + `dev`, and makes `dev` the default
|
|
45
45
|
branch (idempotent; `--dry-run` / `--public` flags).
|
|
46
|
+
- **Hosting seam** — astro.config resolves its adapter + EmDash
|
|
47
|
+
database/storage through `vitopsHosting()` from `@getvitops/emdash`
|
|
48
|
+
(`cloudflare` default; `node` for VPS/docker/k8s after installing
|
|
49
|
+
`@astrojs/node` + `better-sqlite3`), and `pnpm run init:hosting` provisions
|
|
50
|
+
the platform (cloudflare: dev D1 + R2 created via wrangler, `DEV_D1_ID` /
|
|
51
|
+
`CLOUDFLARE_ACCOUNT_ID` repo secrets set via `gh`; idempotent, `--dry-run`).
|
|
46
52
|
- **dev → main promotion flow** — GitHub Actions deploying `dev` to an
|
|
47
53
|
isolated `-dev` worker (`dev.<domain>` with a custom domain; own D1/R2 via
|
|
48
54
|
`scripts/build-dev-wrangler.mjs`, which hard-fails without `DEV_D1_ID`) and
|
package/package.json
CHANGED
|
@@ -22,6 +22,11 @@ styled by the Vitops design system.
|
|
|
22
22
|
full generated class-vocabulary reference to this file.
|
|
23
23
|
- The Worker cron trigger in `wrangler.jsonc` drives scheduled publishing —
|
|
24
24
|
don't remove it.
|
|
25
|
+
- Hosting (adapter + EmDash database/storage) comes from `vitopsHosting()`
|
|
26
|
+
(`@getvitops/emdash`) in astro.config.mjs — never hard-code an adapter or
|
|
27
|
+
d1/r2/sqlite there. Switch targets via `vitopsHosting({ target })` or the
|
|
28
|
+
`HOSTING` env var (see README "Hosting"); the `node` target needs
|
|
29
|
+
`@astrojs/node` + `better-sqlite3` installed first.
|
|
25
30
|
|
|
26
31
|
## Branch model
|
|
27
32
|
|
|
@@ -49,4 +54,6 @@ searching them over pre-trained knowledge for Astro/EmDash questions.
|
|
|
49
54
|
- `pnpm deploy:dev` — build + deploy the isolated dev worker (needs `DEV_D1_ID`)
|
|
50
55
|
- `pnpm run init:github` — one-time: create + push the GitHub repo (main + dev,
|
|
51
56
|
dev default). Only run when the user asks to set up GitHub.
|
|
57
|
+
- `pnpm run init:hosting` — one-time: provision the hosting platform (dev
|
|
58
|
+
D1/R2 + repo secrets on cloudflare). Only run when the user asks for it.
|
|
52
59
|
- `npx vitops validate design-system.json` — schema-check the design config
|
|
@@ -74,14 +74,15 @@ One-time setup:
|
|
|
74
74
|
pushes `main` + `dev`, and makes `dev` the default branch. Idempotent;
|
|
75
75
|
`-- --dry-run` previews. Then branch-protect `main` (require the promotion
|
|
76
76
|
PR + review).
|
|
77
|
-
2.
|
|
78
|
-
`wrangler
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
2. `pnpm run init:hosting` — provisions the hosting platform (after
|
|
78
|
+
`npx wrangler login`): creates the isolated dev D1 + R2 and sets the
|
|
79
|
+
`DEV_D1_ID` / `CLOUDFLARE_ACCOUNT_ID` repo secrets. Idempotent;
|
|
80
|
+
`-- --dry-run` previews.
|
|
81
|
+
3. Create a Cloudflare API token (Workers + D1 + R2 edit —
|
|
82
|
+
<https://dash.cloudflare.com/profile/api-tokens>; tokens can't be created
|
|
83
|
+
via CLI) and `gh secret set CLOUDFLARE_API_TOKEN`. Optionally
|
|
84
|
+
`PROMOTE_TOKEN` (a PAT allowed to bypass protection) for hands-off
|
|
85
|
+
promotion. The first workflow runs fail until the secrets exist.
|
|
85
86
|
|
|
86
87
|
`pnpm deploy:dev` reproduces the dev deploy locally: it rewrites the
|
|
87
88
|
adapter-generated wrangler config (`scripts/build-dev-wrangler.mjs`) so the
|
|
@@ -89,6 +90,30 @@ dev worker never touches prod data — it hard-fails without `DEV_D1_ID`,
|
|
|
89
90
|
on purpose. Schema/content changes live in the EmDash database, not git, so
|
|
90
91
|
promote them separately (via the prod admin or its MCP server).
|
|
91
92
|
|
|
93
|
+
## Hosting
|
|
94
|
+
|
|
95
|
+
The adapter and EmDash database/storage come from one call in
|
|
96
|
+
`astro.config.mjs` — `vitopsHosting()` from `@getvitops/emdash` — so hosting
|
|
97
|
+
is a configurable dimension of the project, not a rewrite:
|
|
98
|
+
|
|
99
|
+
- **`cloudflare`** (default) — Workers + D1 + R2. `wrangler.jsonc`,
|
|
100
|
+
`src/worker.ts` (fetch + scheduled handlers), and the deploy workflows
|
|
101
|
+
belong to this target.
|
|
102
|
+
- **`node`** — `@astrojs/node` (standalone) + SQLite file + local uploads,
|
|
103
|
+
for a VPS / docker-compose / k8s host. Not installed by default; to switch:
|
|
104
|
+
`pnpm add @astrojs/node better-sqlite3`, then `vitopsHosting({ target:
|
|
105
|
+
'node' })` (or set `HOSTING=node` — the env var wins). Run the built site
|
|
106
|
+
with `node ./dist/server/entry.mjs`; data lives in `./data/` (gitignored) —
|
|
107
|
+
persist it as a volume. Scheduled publishing runs in-process on Node, so
|
|
108
|
+
the worker/cron pieces aren't needed. For production, move storage to
|
|
109
|
+
S3-compatible and/or the database to Postgres via `vitopsHosting({ node:
|
|
110
|
+
{ database, storage } })`. CI deploy for Node hosts is TBD — the shipped
|
|
111
|
+
workflows are Cloudflare-specific.
|
|
112
|
+
|
|
113
|
+
Moving an existing site between targets (either direction) is a data
|
|
114
|
+
migration: content lives in the database (D1 export / EmDash seed round-trip)
|
|
115
|
+
and media in the storage backend (bucket/directory copy).
|
|
116
|
+
|
|
92
117
|
## AI tooling
|
|
93
118
|
|
|
94
119
|
- **MCP servers** (`.mcp.json`): the Astro docs (`mcp.docs.astro.build`) and
|
|
@@ -12,6 +12,9 @@ worker-configuration.d.ts
|
|
|
12
12
|
# wrangler local state (local D1/R2 data lives here during `astro dev`)
|
|
13
13
|
.wrangler/
|
|
14
14
|
|
|
15
|
+
# node hosting target: SQLite db + local uploads
|
|
16
|
+
data/
|
|
17
|
+
|
|
15
18
|
# generated by the getvitops() integration on dev/build
|
|
16
19
|
src/styles/
|
|
17
20
|
public/vitops/
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import cloudflare from '@astrojs/cloudflare';
|
|
2
1
|
import react from '@astrojs/react';
|
|
3
|
-
import { d1, r2 } from '@emdash-cms/cloudflare';
|
|
4
2
|
import getvitops from '@getvitops/astro';
|
|
5
|
-
import { vitopsEmdash } from '@getvitops/emdash';
|
|
3
|
+
import { vitopsEmdash, vitopsHosting } from '@getvitops/emdash';
|
|
6
4
|
import { defineConfig } from 'astro/config';
|
|
7
5
|
import emdash from 'emdash/astro';
|
|
8
6
|
|
|
7
|
+
// Hosting seam: adapter + EmDash database/storage for the configured target.
|
|
8
|
+
// Default 'cloudflare' (Workers + D1 + R2 — see wrangler.jsonc / src/worker.ts).
|
|
9
|
+
// To move to a Node host (VPS / docker-compose / k8s) later:
|
|
10
|
+
// pnpm add @astrojs/node better-sqlite3
|
|
11
|
+
// then pass { target: 'node' } here (or set the HOSTING env var, which wins).
|
|
12
|
+
// Content lives in the database and media in storage, so switching hosts is a
|
|
13
|
+
// data migration, not a rewrite — see README "Hosting".
|
|
14
|
+
const { adapter, database, storage } = await vitopsHosting();
|
|
15
|
+
|
|
9
16
|
export default defineConfig({
|
|
10
17
|
output: 'server',
|
|
11
|
-
adapter
|
|
18
|
+
adapter,
|
|
12
19
|
image: {
|
|
13
20
|
layout: 'constrained',
|
|
14
21
|
responsiveStyles: true,
|
|
@@ -17,8 +24,8 @@ export default defineConfig({
|
|
|
17
24
|
// The EmDash admin UI (/_emdash/admin) is a React app.
|
|
18
25
|
react(),
|
|
19
26
|
emdash({
|
|
20
|
-
database
|
|
21
|
-
storage
|
|
27
|
+
database,
|
|
28
|
+
storage,
|
|
22
29
|
// Vitops design-system blocks (image compare, copy snippet, banner,
|
|
23
30
|
// disclosure, carousel) in the editor slash menu. Default 'integration'
|
|
24
31
|
// script delivery: Layout.astro renders <Head /> from @getvitops/astro,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"deploy": "astro build && wrangler deploy",
|
|
11
11
|
"deploy:dev": "astro build && node scripts/build-dev-wrangler.mjs && wrangler deploy --config dist/server/wrangler.dev.json",
|
|
12
12
|
"init:github": "node scripts/init-github.mjs",
|
|
13
|
+
"init:hosting": "node scripts/init-hosting.mjs",
|
|
13
14
|
"typecheck": "astro check",
|
|
14
15
|
"build:css": "vitops generate -i design-system.json -f tailwind -o src/styles",
|
|
15
16
|
"prepare": "vp config"
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"@astrojs/react": "^6.0.0",
|
|
20
21
|
"@emdash-cms/cloudflare": "^0.31.0",
|
|
21
22
|
"@getvitops/astro": "^0.4.0",
|
|
22
|
-
"@getvitops/emdash": "^0.
|
|
23
|
+
"@getvitops/emdash": "^0.2.0",
|
|
23
24
|
"astro": "^7.1.2",
|
|
24
25
|
"emdash": "^0.31.0",
|
|
25
26
|
"react": "^19.2.4",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"@cloudflare/workers-types": "^4.20260305.1",
|
|
31
32
|
"@getvitops/cli": "^0.4.0",
|
|
32
33
|
"typescript": "^6.0.3",
|
|
34
|
+
"vite": "catalog:",
|
|
33
35
|
"vite-plus": "catalog:",
|
|
34
36
|
"wrangler": "^4.111.0"
|
|
35
37
|
},
|
|
@@ -40,7 +42,7 @@
|
|
|
40
42
|
"onFail": "download"
|
|
41
43
|
}
|
|
42
44
|
},
|
|
43
|
-
"packageManager": "pnpm@11.
|
|
45
|
+
"packageManager": "pnpm@11.17.0",
|
|
44
46
|
"emdash": {
|
|
45
47
|
"seed": "seed/seed.json"
|
|
46
48
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// One-time hosting-platform setup for the target configured in
|
|
2
|
+
// astro.config.mjs (vitopsHosting from @getvitops/emdash).
|
|
3
|
+
//
|
|
4
|
+
// pnpm run init:hosting # cloudflare (the default target)
|
|
5
|
+
// pnpm run init:hosting -- --dry-run
|
|
6
|
+
// pnpm run init:hosting -- --target node
|
|
7
|
+
//
|
|
8
|
+
// cloudflare: creates the isolated DEV resources (D1 database + R2 bucket —
|
|
9
|
+
// prod ones are auto-provisioned by wrangler on the first deploy) and, when a
|
|
10
|
+
// GitHub repo is wired up (see init-github.mjs), sets the repo secrets the
|
|
11
|
+
// deploy workflows need. The Cloudflare API token cannot be created via CLI,
|
|
12
|
+
// so that one secret stays manual.
|
|
13
|
+
//
|
|
14
|
+
// Idempotent: safe to re-run — existing resources and secrets are just
|
|
15
|
+
// updated/skipped. Requires wrangler auth (`npx wrangler login`).
|
|
16
|
+
|
|
17
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const dryRun = args.includes('--dry-run');
|
|
22
|
+
const targetFlag = args[args.indexOf('--target') + 1];
|
|
23
|
+
const target = (args.includes('--target') && targetFlag) || process.env.HOSTING || 'cloudflare';
|
|
24
|
+
|
|
25
|
+
const name = JSON.parse(readFileSync('package.json', 'utf8')).name.replace(/^@[^/]+\//, '');
|
|
26
|
+
|
|
27
|
+
function quiet(cmd) {
|
|
28
|
+
try {
|
|
29
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(cmd, { capture = false } = {}) {
|
|
36
|
+
if (dryRun) {
|
|
37
|
+
console.log(`[dry-run] ${cmd}`);
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
console.log(`$ ${cmd}`);
|
|
41
|
+
try {
|
|
42
|
+
if (capture) {
|
|
43
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] });
|
|
44
|
+
process.stdout.write(out);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
48
|
+
return '';
|
|
49
|
+
} catch {
|
|
50
|
+
console.error(
|
|
51
|
+
`\nERROR: \`${cmd}\` failed (see output above). Fix and re-run — the script is idempotent.`,
|
|
52
|
+
);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (target === 'node') {
|
|
58
|
+
console.log(`Hosting target 'node' has no cloud resources to provision. To switch this
|
|
59
|
+
project to a Node host (VPS / docker-compose / k8s):
|
|
60
|
+
|
|
61
|
+
1. pnpm add @astrojs/node better-sqlite3
|
|
62
|
+
2. In astro.config.mjs: vitopsHosting({ target: 'node' }) (or set HOSTING=node)
|
|
63
|
+
3. Build and run: pnpm build && node ./dist/server/entry.mjs
|
|
64
|
+
Data lives in ./data/ (SQLite db + uploads) — persist it as a volume.
|
|
65
|
+
4. CI deploy for VPS/docker/k8s is not scaffolded yet — the deploy-*.yml
|
|
66
|
+
workflows are Cloudflare-specific; replace or disable them.
|
|
67
|
+
|
|
68
|
+
For production on Node, move storage to S3-compatible and/or the database to
|
|
69
|
+
Postgres via vitopsHosting({ node: { database, storage } }) — see the
|
|
70
|
+
@getvitops/emdash README.`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (target !== 'cloudflare') {
|
|
75
|
+
console.error(`ERROR: unknown hosting target '${target}'. Valid targets: cloudflare, node.`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Preflight: wrangler auth ------------------------------------------------
|
|
80
|
+
const whoami = spawnSync('npx', ['wrangler', 'whoami'], { encoding: 'utf8' });
|
|
81
|
+
if (whoami.status !== 0) {
|
|
82
|
+
console.error('ERROR: wrangler is not logged in. Run `npx wrangler login` first.');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
// "Account Name | <32-hex account id>" table row in `wrangler whoami` output.
|
|
86
|
+
const accountId = (whoami.stdout.match(/\b([0-9a-f]{32})\b/) ?? [])[1] ?? null;
|
|
87
|
+
|
|
88
|
+
// --- Dev D1 database ---------------------------------------------------------
|
|
89
|
+
const devDb = `${name}-dev`;
|
|
90
|
+
let devDbId = null;
|
|
91
|
+
const d1List = quiet('npx wrangler d1 list --json');
|
|
92
|
+
if (d1List) {
|
|
93
|
+
devDbId = JSON.parse(d1List).find((d) => d.name === devDb)?.uuid ?? null;
|
|
94
|
+
}
|
|
95
|
+
if (devDbId) {
|
|
96
|
+
console.log(`D1 database ${devDb} already exists (${devDbId}).`);
|
|
97
|
+
} else {
|
|
98
|
+
const out = run(`npx wrangler d1 create ${devDb}`, { capture: true });
|
|
99
|
+
devDbId = (out.match(/"database_id":\s*"([0-9a-f-]{36})"/) ?? [])[1] ?? null;
|
|
100
|
+
if (!dryRun && !devDbId) {
|
|
101
|
+
// Output format drifts across wrangler versions — the list is authoritative.
|
|
102
|
+
const relist = quiet('npx wrangler d1 list --json');
|
|
103
|
+
devDbId = relist ? (JSON.parse(relist).find((d) => d.name === devDb)?.uuid ?? null) : null;
|
|
104
|
+
}
|
|
105
|
+
if (!dryRun && !devDbId) {
|
|
106
|
+
console.error('ERROR: could not determine the database_id of the created dev database.');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Dev R2 bucket -----------------------------------------------------------
|
|
112
|
+
const devBucket = `${name}-media-dev`;
|
|
113
|
+
const bucketList = quiet('npx wrangler r2 bucket list');
|
|
114
|
+
if (bucketList && new RegExp(`name:\\s+${devBucket}\\s*$`, 'm').test(bucketList)) {
|
|
115
|
+
console.log(`R2 bucket ${devBucket} already exists.`);
|
|
116
|
+
} else {
|
|
117
|
+
run(`npx wrangler r2 bucket create ${devBucket}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- GitHub repo secrets (when the repo is wired up) -------------------------
|
|
121
|
+
const hasGh = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8' }).status === 0;
|
|
122
|
+
const hasOrigin = quiet('git remote get-url origin') !== null;
|
|
123
|
+
if (hasGh && hasOrigin) {
|
|
124
|
+
if (devDbId) run(`gh secret set DEV_D1_ID --body ${devDbId}`);
|
|
125
|
+
if (accountId) run(`gh secret set CLOUDFLARE_ACCOUNT_ID --body ${accountId}`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(
|
|
128
|
+
'\nSkipping GitHub secrets (no gh auth or no origin remote — run init:github first).',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`
|
|
133
|
+
Cloudflare dev environment ready:
|
|
134
|
+
D1: ${devDb}${devDbId ? ` (${devDbId})` : ''}
|
|
135
|
+
R2: ${devBucket}
|
|
136
|
+
${
|
|
137
|
+
hasGh && hasOrigin
|
|
138
|
+
? ''
|
|
139
|
+
: ` Set repo secrets once GitHub is wired up:
|
|
140
|
+
gh secret set DEV_D1_ID --body ${devDbId ?? '<id>'}
|
|
141
|
+
gh secret set CLOUDFLARE_ACCOUNT_ID --body ${accountId ?? '<account id>'}
|
|
142
|
+
`
|
|
143
|
+
}
|
|
144
|
+
Still manual (CLI can't create API tokens):
|
|
145
|
+
1. Create a token with Workers + D1 + R2 edit permissions:
|
|
146
|
+
https://dash.cloudflare.com/profile/api-tokens
|
|
147
|
+
2. gh secret set CLOUDFLARE_API_TOKEN
|
|
148
|
+
|
|
149
|
+
Prod D1/R2 are provisioned automatically by wrangler on the first \`pnpm deploy\`.
|
|
150
|
+
Local \`pnpm deploy:dev\` runs need: export DEV_D1_ID=${devDbId ?? '<id>'}`);
|