create-zerodirs 0.1.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 +85 -0
- package/dist/cli/args.js +128 -0
- package/dist/cli/prompts.js +148 -0
- package/dist/core/answers.js +162 -0
- package/dist/core/cloudflare.js +88 -0
- package/dist/core/dataset.js +88 -0
- package/dist/core/env-files.js +45 -0
- package/dist/core/exec.js +47 -0
- package/dist/core/files.js +151 -0
- package/dist/core/setup-md.js +118 -0
- package/dist/core/site-config.js +175 -0
- package/dist/core/template.js +66 -0
- package/dist/core/wrangler-config.js +76 -0
- package/dist/create.js +247 -0
- package/dist/index.js +39 -0
- package/package.json +37 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting the template onto disk.
|
|
3
|
+
*
|
|
4
|
+
* Three sources, in the order they are tried:
|
|
5
|
+
*
|
|
6
|
+
* 1. `--template-dir <path>` / `ZERODIRS_TEMPLATE_DIR` — copy a local
|
|
7
|
+
* directory. This is not only a test hook: it is how CI and this repository
|
|
8
|
+
* exercise the whole flow today, because the free release tarball does not
|
|
9
|
+
* exist until the release tag is cut.
|
|
10
|
+
* 2. free edition — the `zerodirs/zerodirs-oss` release tarball, via giget.
|
|
11
|
+
* 3. `--pro` — `git clone --depth 1` of the private starter the buyer has been
|
|
12
|
+
* given access to. Their own git credentials do the authentication; this
|
|
13
|
+
* CLI never sees a licence key or a token.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, statSync } from 'node:fs';
|
|
16
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
17
|
+
import { copyTemplate } from './files.js';
|
|
18
|
+
import { run } from './exec.js';
|
|
19
|
+
export const FREE_REPO = 'zerodirs/zerodirs-oss';
|
|
20
|
+
export const PRO_REPO = 'zerodirs/zerodirs';
|
|
21
|
+
/** `--template-dir` wins over everything, including `--pro`. */
|
|
22
|
+
export function resolveTemplateDir(option, env, cwd) {
|
|
23
|
+
const value = option ?? env;
|
|
24
|
+
if (!value)
|
|
25
|
+
return undefined;
|
|
26
|
+
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
27
|
+
}
|
|
28
|
+
export async function fetchTemplate(request) {
|
|
29
|
+
if (request.templateDir) {
|
|
30
|
+
const dir = request.templateDir;
|
|
31
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
32
|
+
throw new Error(`--template-dir: ${dir} is not a directory`);
|
|
33
|
+
if (!existsSync(resolve(dir, 'package.json')))
|
|
34
|
+
throw new Error(`--template-dir: ${dir} has no package.json — that is not a ZeroDirs template`);
|
|
35
|
+
const copied = copyTemplate(dir, request.target);
|
|
36
|
+
return { source: `${dir} (local)`, files: copied.files };
|
|
37
|
+
}
|
|
38
|
+
if (request.edition === 'free')
|
|
39
|
+
return fetchFreeTarball(request);
|
|
40
|
+
return cloneProRepo(request);
|
|
41
|
+
}
|
|
42
|
+
async function fetchFreeTarball(request) {
|
|
43
|
+
const source = `github:${FREE_REPO}${request.ref ? `#${request.ref}` : ''}`;
|
|
44
|
+
try {
|
|
45
|
+
const { downloadTemplate } = await import('giget');
|
|
46
|
+
const result = await downloadTemplate(source, { dir: request.target, force: true, preferOffline: false });
|
|
47
|
+
return { source: `${source} (${result.source ?? 'tarball'})`, files: -1 };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`could not download the free template from ${source}: ${error.message}\n` +
|
|
51
|
+
`If the release has not been published yet, point at a local copy instead:\n` +
|
|
52
|
+
` create-zerodirs <dir> --template-dir <path-to-zerodirs>/.derived/free`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function cloneProRepo(request) {
|
|
56
|
+
const url = `https://github.com/${PRO_REPO}.git`;
|
|
57
|
+
const args = ['clone', '--depth', '1', ...(request.ref ? ['--branch', request.ref] : []), url, request.target];
|
|
58
|
+
const result = await run('git', args, { cwd: request.cwd });
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
throw new Error(`could not clone ${url}: ${result.message}\n` +
|
|
61
|
+
`The paid starter is a private repository — make sure your git credentials can read it (this CLI never handles a licence key).\n` +
|
|
62
|
+
`To use a copy you already have:\n` +
|
|
63
|
+
` create-zerodirs <dir> --pro --template-dir <path-to-starter>`);
|
|
64
|
+
}
|
|
65
|
+
return { source: `${url} (git clone --depth 1)`, files: -1 };
|
|
66
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wrangler.jsonc` rewriting.
|
|
3
|
+
*
|
|
4
|
+
* JSONC, not JSON: the starter's file is more comment than config — why `main`
|
|
5
|
+
* points at `src/worker.ts`, why `triggers` has to be present, and the whole
|
|
6
|
+
* `send_email` block that explains when the binding becomes legal. Parsing and
|
|
7
|
+
* re-serialising would throw all of that away, so the edits are applied with
|
|
8
|
+
* `jsonc-parser`, which rewrites one value at a time and leaves the surrounding
|
|
9
|
+
* bytes (comments, tabs, key order) untouched.
|
|
10
|
+
*
|
|
11
|
+
* Pure string -> string, like `rewriteSiteConfig()`.
|
|
12
|
+
*/
|
|
13
|
+
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
14
|
+
/** The starter's file is tab-indented; `modify` re-indents only what it inserts. */
|
|
15
|
+
const FORMATTING = { insertSpaces: false, tabSize: 1, eol: '\n' };
|
|
16
|
+
/** Reads the file's current values without disturbing it. Throws on malformed JSONC. */
|
|
17
|
+
export function parseWranglerConfig(source) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const value = parse(source, errors, { allowTrailingComma: true });
|
|
20
|
+
if (errors.length)
|
|
21
|
+
throw new Error(`wrangler.jsonc did not parse (${errors.length} error(s) at offset ${errors[0]?.offset})`);
|
|
22
|
+
return value ?? {};
|
|
23
|
+
}
|
|
24
|
+
function edit(source, path, value) {
|
|
25
|
+
return applyEdits(source, modify(source, path, value, { formattingOptions: FORMATTING }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Applies the buyer's identity to `wrangler.jsonc`.
|
|
29
|
+
*
|
|
30
|
+
* - `name` becomes the Worker name, which is also the `*.workers.dev` subdomain.
|
|
31
|
+
* - `d1_databases[0].database_name` / `r2_buckets[0].bucket_name` become the
|
|
32
|
+
* names the CLI (or SETUP.md) creates. `database_id` is emptied rather than
|
|
33
|
+
* carried over: the template's id points at somebody else's database, and
|
|
34
|
+
* `wrangler d1 create --update-config` fills the real one in.
|
|
35
|
+
* - `routes` gets the buyer's custom domain, or is dropped when there is none.
|
|
36
|
+
* A route naming a domain the buyer does not own fails their first deploy.
|
|
37
|
+
* - `send_email` is only added when the config asks for it (R8) — the binding
|
|
38
|
+
* is invalid until Email Routing is enabled on the zone, and an invalid
|
|
39
|
+
* binding fails the deploy rather than degrading.
|
|
40
|
+
*/
|
|
41
|
+
export function rewriteWranglerConfig(source, edits) {
|
|
42
|
+
const current = parseWranglerConfig(source);
|
|
43
|
+
let out = source;
|
|
44
|
+
out = edit(out, ['name'], edits.name);
|
|
45
|
+
if (Array.isArray(current.d1_databases) && current.d1_databases.length > 0) {
|
|
46
|
+
out = edit(out, ['d1_databases', 0, 'database_name'], edits.databaseName);
|
|
47
|
+
if (current.d1_databases[0] && 'database_id' in current.d1_databases[0]) {
|
|
48
|
+
out = edit(out, ['d1_databases', 0, 'database_id'], '');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(current.r2_buckets) && current.r2_buckets.length > 0) {
|
|
52
|
+
out = edit(out, ['r2_buckets', 0, 'bucket_name'], edits.bucketName);
|
|
53
|
+
}
|
|
54
|
+
if (edits.domain) {
|
|
55
|
+
out = edit(out, ['routes'], [{ pattern: edits.domain, custom_domain: true }]);
|
|
56
|
+
}
|
|
57
|
+
else if (current.routes !== undefined) {
|
|
58
|
+
out = edit(out, ['routes'], undefined);
|
|
59
|
+
}
|
|
60
|
+
if (edits.sendEmail && current.send_email === undefined) {
|
|
61
|
+
out = edit(out, ['send_email'], [{ name: 'EMAIL', remote: true }]);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Removes a binding array so `wrangler <resource> create --binding … --update-config`
|
|
67
|
+
* can write a complete, correct block back — id included. Used immediately
|
|
68
|
+
* before a create call, and only then: the rewritten placeholder above is what
|
|
69
|
+
* a project without created resources keeps.
|
|
70
|
+
*/
|
|
71
|
+
export function dropBinding(source, key) {
|
|
72
|
+
const current = parseWranglerConfig(source);
|
|
73
|
+
if (current[key] === undefined)
|
|
74
|
+
return source;
|
|
75
|
+
return edit(source, [key], undefined);
|
|
76
|
+
}
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flow, in the order design.md §11.1 lists it: check the environment, get
|
|
3
|
+
* the template, ask, write, (optionally) create resources, initialise locally,
|
|
4
|
+
* then print what is left and write it to `SETUP.md`.
|
|
5
|
+
*
|
|
6
|
+
* The last step is the one worth naming: the CLI prints `pnpm dev`, it does not
|
|
7
|
+
* run it. A scaffolder that ends by seizing the terminal with a dev server is a
|
|
8
|
+
* scaffolder you cannot use from a script.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
11
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
12
|
+
import { detectPackageManager, installCommand, runPrefix } from './cli/args.js';
|
|
13
|
+
import { CancelledError, ask, askDataset, prompts as p } from './cli/prompts.js';
|
|
14
|
+
import { planProject } from './core/answers.js';
|
|
15
|
+
import { createD1, createR2, WRANGLER_AUTH_DOCS, whoami, wranglerFor } from './core/cloudflare.js';
|
|
16
|
+
import { listDatasets, readDataset } from './core/dataset.js';
|
|
17
|
+
import { renderEnvFile, randomSecret } from './core/env-files.js';
|
|
18
|
+
import { run } from './core/exec.js';
|
|
19
|
+
import { isEmptyDir, loadDatasetContent, write } from './core/files.js';
|
|
20
|
+
import { renderSetupMd } from './core/setup-md.js';
|
|
21
|
+
import { rewriteSiteConfig } from './core/site-config.js';
|
|
22
|
+
import { fetchTemplate, resolveTemplateDir } from './core/template.js';
|
|
23
|
+
import { rewriteWranglerConfig } from './core/wrangler-config.js';
|
|
24
|
+
export async function create(options, environment) {
|
|
25
|
+
const { cwd, env } = environment;
|
|
26
|
+
const edition = options.pro ? 'pro' : 'free';
|
|
27
|
+
const manager = options.packageManager ?? detectPackageManager(env['npm_config_user_agent']);
|
|
28
|
+
const templateDir = resolveTemplateDir(options.templateDir, env['ZERODIRS_TEMPLATE_DIR'], cwd);
|
|
29
|
+
p.intro(`create-zerodirs · ${edition === 'pro' ? 'paid starter' : 'free edition'}`);
|
|
30
|
+
// 1. Where -------------------------------------------------------------
|
|
31
|
+
const dirName = options.dir ??
|
|
32
|
+
(options.yes
|
|
33
|
+
? 'my-directory'
|
|
34
|
+
: (await p.text({ message: 'Where should the project go?', placeholder: './my-directory', defaultValue: './my-directory' })));
|
|
35
|
+
if (typeof dirName !== 'string' || p.isCancel(dirName))
|
|
36
|
+
throw new CancelledError();
|
|
37
|
+
const target = isAbsolute(dirName) ? dirName : resolve(cwd, dirName);
|
|
38
|
+
if (!isEmptyDir(target)) {
|
|
39
|
+
p.cancel(`${target} already has files in it. Pick an empty directory — this CLI never overwrites an existing project.`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const createdTarget = !existsSync(target);
|
|
43
|
+
mkdirSync(target, { recursive: true });
|
|
44
|
+
try {
|
|
45
|
+
// 2. Template --------------------------------------------------------
|
|
46
|
+
const spinner = p.spinner();
|
|
47
|
+
spinner.start(templateDir ? 'Copying template' : edition === 'pro' ? 'Cloning the starter' : 'Downloading the template');
|
|
48
|
+
let templateSource;
|
|
49
|
+
try {
|
|
50
|
+
const fetched = await fetchTemplate({ edition, ...(templateDir ? { templateDir } : {}), target, cwd });
|
|
51
|
+
templateSource = fetched.source;
|
|
52
|
+
spinner.stop(`Template: ${fetched.source}`);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
spinner.stop('Could not get the template');
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
// A clone would otherwise leave the project pointing at somebody else's
|
|
59
|
+
// remote. The CLI does not run `git init` either — that is the buyer's call.
|
|
60
|
+
rmSync(join(target, '.git'), { recursive: true, force: true });
|
|
61
|
+
// 3. Questions -------------------------------------------------------
|
|
62
|
+
const datasetNames = listDatasets(target);
|
|
63
|
+
const askContext = {
|
|
64
|
+
options,
|
|
65
|
+
edition: edition,
|
|
66
|
+
projectDir: dirName.replace(/^\.\//, ''),
|
|
67
|
+
datasets: datasetNames.map((name) => {
|
|
68
|
+
const dataset = readDataset(target, name);
|
|
69
|
+
return { name, meta: dataset.meta, listings: dataset.listingCount };
|
|
70
|
+
}),
|
|
71
|
+
cloudflareAllowed: options.cloudflare,
|
|
72
|
+
};
|
|
73
|
+
const datasetName = await askDataset(askContext);
|
|
74
|
+
const dataset = readDataset(target, datasetName);
|
|
75
|
+
const answers = await ask(askContext, datasetName);
|
|
76
|
+
const plan = planProject(answers, dataset);
|
|
77
|
+
// 4. Write -----------------------------------------------------------
|
|
78
|
+
writeProjectFiles(target, plan);
|
|
79
|
+
const content = loadDatasetContent(target, dataset.dir);
|
|
80
|
+
// 5. Cloudflare resources -------------------------------------------
|
|
81
|
+
let resourcesCreated = false;
|
|
82
|
+
const notes = [];
|
|
83
|
+
if (answers.createResources && options.cloudflare) {
|
|
84
|
+
resourcesCreated = await provisionResources(target, plan, notes);
|
|
85
|
+
}
|
|
86
|
+
// 6. Local initialisation --------------------------------------------
|
|
87
|
+
let installed = false;
|
|
88
|
+
if (options.install) {
|
|
89
|
+
const installSpinner = p.spinner();
|
|
90
|
+
installSpinner.start(`${installCommand(manager)}`);
|
|
91
|
+
const result = await run(manager, ['install'], { cwd: target, timeout: 900_000 });
|
|
92
|
+
installed = result.ok;
|
|
93
|
+
if (result.ok)
|
|
94
|
+
installSpinner.stop('Dependencies installed');
|
|
95
|
+
else {
|
|
96
|
+
installSpinner.stop('Dependencies were not installed');
|
|
97
|
+
notes.push(`${installCommand(manager)} failed (${result.message}) — run it yourself in the project.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (installed && resourcesCreated)
|
|
101
|
+
await initialiseLocalDatabase(target, manager, plan, notes);
|
|
102
|
+
// 7. What is left ----------------------------------------------------
|
|
103
|
+
const setup = renderSetupMd({
|
|
104
|
+
plan,
|
|
105
|
+
runPrefix: runPrefix(manager),
|
|
106
|
+
installCommand: installCommand(manager),
|
|
107
|
+
dataset: datasetName,
|
|
108
|
+
datasetListings: content.listings,
|
|
109
|
+
resourcesCreated,
|
|
110
|
+
installed,
|
|
111
|
+
templateSource,
|
|
112
|
+
});
|
|
113
|
+
write(join(target, 'SETUP.md'), setup);
|
|
114
|
+
p.note([
|
|
115
|
+
`${content.listings} listings, ${content.blog} blog posts, ${content.logos} logos from "${datasetName}"`,
|
|
116
|
+
`site.kind: ${plan.identity.kind} · /${plan.identity.listingBase}/ · seo.jsonLd.listing: ${plan.identity.jsonLd}`,
|
|
117
|
+
`${plan.siteConfig.categories.length} categories, ${plan.siteConfig.tags.length} tags`,
|
|
118
|
+
...notes,
|
|
119
|
+
].join('\n'), 'Project');
|
|
120
|
+
const relative = dirName.startsWith('.') || isAbsolute(dirName) ? dirName : `./${dirName}`;
|
|
121
|
+
p.outro([
|
|
122
|
+
'Next:',
|
|
123
|
+
` cd ${relative}`,
|
|
124
|
+
installed ? null : ` ${installCommand(manager)}`,
|
|
125
|
+
` ${runPrefix(manager)} dev`,
|
|
126
|
+
'',
|
|
127
|
+
'SETUP.md lists everything this CLI deliberately left to you.',
|
|
128
|
+
]
|
|
129
|
+
.filter((line) => line !== null)
|
|
130
|
+
.join('\n'));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Nothing reached the "project is written" line, so whatever is in the
|
|
135
|
+
// directory is a half-copied template. Leaving it behind would only make
|
|
136
|
+
// the next attempt fail on the non-empty check.
|
|
137
|
+
if (createdTarget)
|
|
138
|
+
rmSync(target, { recursive: true, force: true });
|
|
139
|
+
if (error instanceof CancelledError) {
|
|
140
|
+
p.cancel('Cancelled — nothing was kept.');
|
|
141
|
+
return 130;
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Writing
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
function writeProjectFiles(target, plan) {
|
|
150
|
+
const siteConfigPath = join(target, 'site.config.ts');
|
|
151
|
+
if (!existsSync(siteConfigPath))
|
|
152
|
+
throw new Error('the template has no site.config.ts — that is not a ZeroDirs template');
|
|
153
|
+
write(siteConfigPath, rewriteSiteConfig(readFileSync(siteConfigPath, 'utf8'), plan.siteConfig));
|
|
154
|
+
const wranglerPath = join(target, 'wrangler.jsonc');
|
|
155
|
+
if (existsSync(wranglerPath))
|
|
156
|
+
write(wranglerPath, rewriteWranglerConfig(readFileSync(wranglerPath, 'utf8'), plan.wrangler));
|
|
157
|
+
// `.dev.vars` and `.env` are generated from the template's own example files,
|
|
158
|
+
// so their key sets stay equal to it by construction (§11.6: `wrangler types`
|
|
159
|
+
// derives `Env` from `.dev.vars`).
|
|
160
|
+
const devVarsExample = join(target, '.dev.vars.example');
|
|
161
|
+
if (existsSync(devVarsExample)) {
|
|
162
|
+
write(join(target, '.dev.vars'), renderEnvFile(readFileSync(devVarsExample, 'utf8'), { ADMIN_SECRET: randomSecret(), TOKEN_SECRET: randomSecret() }, { blankUnlisted: true }));
|
|
163
|
+
}
|
|
164
|
+
const envExample = join(target, '.env.example');
|
|
165
|
+
if (existsSync(envExample)) {
|
|
166
|
+
write(join(target, '.env'), renderEnvFile(readFileSync(envExample, 'utf8'), { LISTINGS_SOURCE: 'files' }));
|
|
167
|
+
}
|
|
168
|
+
renameProject(join(target, 'package.json'), plan.workerName);
|
|
169
|
+
}
|
|
170
|
+
/** The template's package is `@zerodirs/starter`; the buyer's is theirs. */
|
|
171
|
+
function renameProject(path, name) {
|
|
172
|
+
if (!existsSync(path))
|
|
173
|
+
return;
|
|
174
|
+
const source = readFileSync(path, 'utf8');
|
|
175
|
+
const parsed = JSON.parse(source);
|
|
176
|
+
parsed['name'] = name;
|
|
177
|
+
write(path, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Cloudflare
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
async function provisionResources(target, plan, notes) {
|
|
183
|
+
const spinner = p.spinner();
|
|
184
|
+
spinner.start('Checking the Cloudflare account');
|
|
185
|
+
const account = await whoami(target);
|
|
186
|
+
if (!account.signedIn) {
|
|
187
|
+
spinner.stop('Not signed in — no resources created');
|
|
188
|
+
notes.push(`wrangler could not identify an account, so no resources were created. Authenticate it (${WRANGLER_AUTH_DOCS}) and run the commands in SETUP.md.`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
spinner.stop(`Cloudflare: ${account.message}`);
|
|
192
|
+
spinner.start(`Creating D1 database ${plan.databaseName}`);
|
|
193
|
+
const d1 = await createD1(target, plan.databaseName);
|
|
194
|
+
if (!d1.ok) {
|
|
195
|
+
spinner.stop(`D1 database not created`);
|
|
196
|
+
notes.push(d1.hint ?? `wrangler d1 create failed: ${d1.reason}`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
spinner.stop(`D1 database ${plan.databaseName} created and bound as DB`);
|
|
200
|
+
spinner.start(`Creating R2 bucket ${plan.bucketName}`);
|
|
201
|
+
const r2 = await createR2(target, plan.bucketName);
|
|
202
|
+
if (!r2.ok) {
|
|
203
|
+
spinner.stop('R2 bucket not created');
|
|
204
|
+
notes.push(r2.hint ?? `wrangler r2 bucket create failed: ${r2.reason}`);
|
|
205
|
+
// The database exists, so the project is usable; only media uploads are not.
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
spinner.stop(`R2 bucket ${plan.bucketName} created and bound as MEDIA`);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Local only. `db:migrate:local` and `db:seed:local` both talk to the miniflare
|
|
213
|
+
* database under `.wrangler/`; nothing here reaches the buyer's account.
|
|
214
|
+
*/
|
|
215
|
+
async function initialiseLocalDatabase(target, manager, plan, notes) {
|
|
216
|
+
const steps = [
|
|
217
|
+
{ label: 'wrangler types', command: wranglerFor(target).command, args: [...wranglerFor(target).prefix, 'types'] },
|
|
218
|
+
{ label: 'db:migrate:local', command: manager, args: [...runArgs(manager), 'db:migrate:local'] },
|
|
219
|
+
{ label: 'db:seed:local', command: manager, args: [...runArgs(manager), 'db:seed:local', '--dataset', plan.answers.dataset] },
|
|
220
|
+
];
|
|
221
|
+
for (const step of steps) {
|
|
222
|
+
const spinner = p.spinner();
|
|
223
|
+
spinner.start(step.label);
|
|
224
|
+
const result = await run(step.command, step.args, { cwd: target, timeout: 600_000 });
|
|
225
|
+
if (result.ok)
|
|
226
|
+
spinner.stop(step.label);
|
|
227
|
+
else {
|
|
228
|
+
spinner.stop(`${step.label} failed`);
|
|
229
|
+
notes.push(`${step.label} failed (${result.message}) — SETUP.md has the command.`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function runArgs(manager) {
|
|
235
|
+
return manager === 'npm' ? ['run'] : manager === 'yarn' ? [] : ['run'];
|
|
236
|
+
}
|
|
237
|
+
/** Exposed for the `--version` line. */
|
|
238
|
+
export function readOwnVersion(packageJsonPath) {
|
|
239
|
+
try {
|
|
240
|
+
if (!existsSync(packageJsonPath) || !statSync(packageJsonPath).isFile())
|
|
241
|
+
return '0.0.0';
|
|
242
|
+
return String(JSON.parse(readFileSync(packageJsonPath, 'utf8')).version ?? '0.0.0');
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return '0.0.0';
|
|
246
|
+
}
|
|
247
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Entry point. Deliberately thin: it checks the Node version *before* importing
|
|
4
|
+
* anything else, because a CLI whose first output is a SyntaxError from inside
|
|
5
|
+
* a dependency has told the user nothing. The real flow is a dynamic import
|
|
6
|
+
* that only happens once the runtime is known to be new enough.
|
|
7
|
+
*/
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { nodeVersionError, parseArgs, USAGE } from './cli/args.js';
|
|
11
|
+
async function main() {
|
|
12
|
+
const versionProblem = nodeVersionError(process.versions.node);
|
|
13
|
+
if (versionProblem) {
|
|
14
|
+
console.error(versionProblem);
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
18
|
+
if (!parsed.ok) {
|
|
19
|
+
console.error(parsed.message);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const { create, readOwnVersion } = await import('./create.js');
|
|
23
|
+
if (parsed.options.help) {
|
|
24
|
+
console.log(USAGE);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (parsed.options.version) {
|
|
28
|
+
console.log(readOwnVersion(join(dirname(fileURLToPath(import.meta.url)), '../package.json')));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
return create(parsed.options, { cwd: process.cwd(), env: process.env });
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
process.exitCode = await main();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error(`\ncreate-zerodirs: ${error.message ?? String(error)}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-zerodirs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a ZeroDirs directory site: pick a dataset, answer a few questions, get a project that builds.",
|
|
5
|
+
"keywords": ["zerodirs", "directory", "astro", "cloudflare", "starter"],
|
|
6
|
+
"homepage": "https://docs.zerodirs.com/",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"email": "hi@zerodirs.com"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-zerodirs": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.18"
|
|
17
|
+
},
|
|
18
|
+
"files": ["dist", "README.md"],
|
|
19
|
+
"packageManager": "pnpm@10.12.4",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"check": "tsc -p tsconfig.build.json --noEmit",
|
|
23
|
+
"test": "tsc -p tsconfig.build.json && vitest run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "1.7.0",
|
|
27
|
+
"execa": "10.0.1",
|
|
28
|
+
"giget": "3.3.1",
|
|
29
|
+
"jsonc-parser": "3.3.1",
|
|
30
|
+
"magicast": "0.5.4"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^24.13.3",
|
|
34
|
+
"typescript": "^5.9.0",
|
|
35
|
+
"vitest": "4.1.11"
|
|
36
|
+
}
|
|
37
|
+
}
|