create-kedge 0.10.0 → 0.11.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 +49 -0
- package/package.json +1 -1
- package/src/create.mjs +89 -16
- package/template.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# create-kedge
|
|
2
|
+
|
|
3
|
+
A new [Kedge](https://www.npmjs.com/package/@kedge/cli) store: an open-source (MIT)
|
|
4
|
+
headless commerce backend that runs on Cloudflare Workers and D1, in your own
|
|
5
|
+
Cloudflare account.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
mkdir my-store && cd my-store
|
|
9
|
+
npm create kedge@latest
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run it in an empty folder, or name a new one: `npm create kedge@latest my-store`.
|
|
13
|
+
The store's name comes from the folder (`--name` chooses another). It needs Node 24
|
|
14
|
+
or newer and pnpm 10 or newer (`npm install --global pnpm`), and checks both
|
|
15
|
+
before it writes anything.
|
|
16
|
+
|
|
17
|
+
## With a coding agent
|
|
18
|
+
|
|
19
|
+
Give your agent this page and ask: *"Make me a Kedge store and place a test
|
|
20
|
+
order."* The store it makes carries the `kedge-start` skill (in `.claude/skills/`
|
|
21
|
+
and `.agents/skills/`), which holds the steps below. A command that needs you
|
|
22
|
+
stops with exit code 3 and one line starting `for you:`; your agent passes it on.
|
|
23
|
+
|
|
24
|
+
## The first run
|
|
25
|
+
|
|
26
|
+
**On this machine** (no Cloudflare account needed):
|
|
27
|
+
|
|
28
|
+
1. `pnpm kedge check --fast`
|
|
29
|
+
2. `pnpm kedge dev` (keep it running): the store at `http://localhost:8787`, its
|
|
30
|
+
admin at `/app/`.
|
|
31
|
+
3. `pnpm kedge staff invite-owner you@example.com`: open the link it prints, set a
|
|
32
|
+
password and an authenticator, and you're the store's Owner.
|
|
33
|
+
4. `pnpm kedge seed --sample apparel --url http://localhost:8787`
|
|
34
|
+
5. `pnpm kedge smoke --url http://localhost:8787`: a test order, placed.
|
|
35
|
+
|
|
36
|
+
**On Cloudflare** (a Workers Paid account):
|
|
37
|
+
|
|
38
|
+
1. Commit the store: `git add -A && git commit -m "A new Kedge store"`.
|
|
39
|
+
2. Give the CLI credentials: `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` in
|
|
40
|
+
`.env.local`, or `npx wrangler login`. `pnpm kedge doctor --remote` checks them.
|
|
41
|
+
3. `pnpm kedge deploy --jurisdiction <eu|us|fedramp|none>`: where the store's data
|
|
42
|
+
lives, chosen once.
|
|
43
|
+
4. `pnpm kedge staff invite-owner you@example.com --remote`: your invite.
|
|
44
|
+
5. `pnpm kedge seed --sample apparel --url <store> --live` and
|
|
45
|
+
`pnpm kedge smoke --url <store> --live`: sample products and a test order on the
|
|
46
|
+
live store.
|
|
47
|
+
|
|
48
|
+
Every command takes `--json` for a single result object on stdout. The installed
|
|
49
|
+
`@kedge/docs` holds the guides for extending the store, matched to its version.
|
package/package.json
CHANGED
package/src/create.mjs
CHANGED
|
@@ -7,8 +7,26 @@ import { randomBytes } from "node:crypto";
|
|
|
7
7
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Kedge's rule for a store's name, not Cloudflare's: with its six-character
|
|
12
|
+
* suffix it becomes the Worker's, the databases' and the queue's names, and a
|
|
13
|
+
* preview's Worker adds more, all inside Cloudflare's 63 characters for a
|
|
14
|
+
* workers.dev name (letters, digits and dashes, no dash first or last:
|
|
15
|
+
* https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#limitations).
|
|
16
|
+
*/
|
|
11
17
|
export const NAME = /^[a-z][a-z0-9-]{0,38}[a-z0-9]$/;
|
|
18
|
+
|
|
19
|
+
/** A store name from any folder's name (p0.10.1 §2): always one that meets NAME, so a folder is never refused. */
|
|
20
|
+
export function storeNameOf(folder) {
|
|
21
|
+
let s = folder
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "");
|
|
25
|
+
if (!s) return "my-store";
|
|
26
|
+
if (!/^[a-z]/.test(s)) s = `store-${s}`;
|
|
27
|
+
s = s.slice(0, 40).replace(/-+$/, "");
|
|
28
|
+
return s.length < 2 ? "my-store" : s;
|
|
29
|
+
}
|
|
12
30
|
/** The starter's placeholder for the store's Cloudflare names. */
|
|
13
31
|
export const PLACEHOLDER = "my-store";
|
|
14
32
|
|
|
@@ -62,6 +80,31 @@ export function render(template, { name, suffix: sfx, packs }) {
|
|
|
62
80
|
return files;
|
|
63
81
|
}
|
|
64
82
|
|
|
83
|
+
/** The folder's entries, none when it doesn't exist yet. */
|
|
84
|
+
const existing = (dir) => (existsSync(dir) ? readdirSync(dir) : []);
|
|
85
|
+
|
|
86
|
+
/** The top-level names the template writes. */
|
|
87
|
+
const ownedTop = (files) => new Set(Object.keys(files).map((p) => p.split("/")[0]));
|
|
88
|
+
|
|
89
|
+
/** An existing .gitignore with each of the starter's lines it lacks appended, its own lines as they were. */
|
|
90
|
+
/** Where the CLI and kedge dev write credentials and keys: never tracked (p0.10 §4, §6.2). */
|
|
91
|
+
const CREDENTIAL_PATHS = [".env.local", ".env.development.local", ".dev.vars", ".kedge/state.db"];
|
|
92
|
+
|
|
93
|
+
export function mergeIgnore(mine, starter) {
|
|
94
|
+
const lines = mine.split("\n").map((l) => l.trim());
|
|
95
|
+
// A rule counts as present only if no exception follows it: `.env*` then `!.env.local` would commit
|
|
96
|
+
// the credentials the starter's `.env*` keeps out (codex code r4), so such a rule is appended again, last.
|
|
97
|
+
const holds = (rule) => {
|
|
98
|
+
const at = lines.lastIndexOf(rule);
|
|
99
|
+
return at >= 0 && !lines.slice(at + 1).some((l) => l.startsWith("!"));
|
|
100
|
+
};
|
|
101
|
+
const add = starter
|
|
102
|
+
.split("\n")
|
|
103
|
+
.filter((l) => l.trim() && (l.trim().startsWith("#") ? !lines.includes(l.trim()) : !holds(l.trim())));
|
|
104
|
+
if (!add.length) return mine;
|
|
105
|
+
return `${mine.replace(/\n*$/, "\n")}# Kedge's starter (create-kedge):\n${add.join("\n")}\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
65
108
|
export function write(dir, files) {
|
|
66
109
|
for (const [path, text] of Object.entries(files)) {
|
|
67
110
|
if (isAbsolute(path) || path.split("/").includes("..")) throw new Error(`template path ${path} escapes the store`);
|
|
@@ -78,7 +121,12 @@ export function write(dir, files) {
|
|
|
78
121
|
* clean-machine run is what proves these; every package's `engines.node` says the same.
|
|
79
122
|
*/
|
|
80
123
|
export const MIN_NODE = 24;
|
|
81
|
-
|
|
124
|
+
/**
|
|
125
|
+
* pnpm 10 runs the store's pinned pnpm (`packageManager`, pnpm@11.11.0 in the
|
|
126
|
+
* starter) by itself: measured 2026-11-01, `npx pnpm@10 --version` in such a
|
|
127
|
+
* folder answers 11.11.0 (p0.10.1 §1, 3).
|
|
128
|
+
*/
|
|
129
|
+
export const MIN_PNPM = 10;
|
|
82
130
|
|
|
83
131
|
/** What this machine lacks, with the one command that fixes it; null when nothing. */
|
|
84
132
|
export function environmentProblem({ node, pnpm, install }) {
|
|
@@ -86,9 +134,10 @@ export function environmentProblem({ node, pnpm, install }) {
|
|
|
86
134
|
if (major(node) < MIN_NODE)
|
|
87
135
|
return `Kedge needs Node ${MIN_NODE} or newer, and this is ${node}: install it from https://nodejs.org (or \`nvm install ${MIN_NODE}\`), then run this again`;
|
|
88
136
|
if (!install) return null;
|
|
89
|
-
if (!pnpm)
|
|
137
|
+
if (!pnpm)
|
|
138
|
+
return `Kedge stores install with pnpm, which isn't on the path: run \`npm install --global pnpm\`, then run this again`;
|
|
90
139
|
if (major(pnpm) < MIN_PNPM)
|
|
91
|
-
return `Kedge needs pnpm ${MIN_PNPM} or newer, and this is ${pnpm}: run \`npm install --global pnpm
|
|
140
|
+
return `Kedge needs pnpm ${MIN_PNPM} or newer, and this is ${pnpm}: run \`npm install --global pnpm\`, then run this again`;
|
|
92
141
|
return null;
|
|
93
142
|
}
|
|
94
143
|
|
|
@@ -104,11 +153,8 @@ export function main(
|
|
|
104
153
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
105
154
|
};
|
|
106
155
|
const positional = argv.filter((a, i) => !a.startsWith("--") && !["--name", "--from-packs"].includes(argv[i - 1]));
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
error(USAGE);
|
|
110
|
-
return 2;
|
|
111
|
-
}
|
|
156
|
+
// No directory: the folder it's run in, as `npm create kedge@latest` alone leaves it (p0.10.1 §1, 1).
|
|
157
|
+
const target = positional[0] ?? ".";
|
|
112
158
|
// Before anything is written: a half-made store on a machine that can't install it helps no one.
|
|
113
159
|
const pnpm = flags.has("--no-install") ? null : run("pnpm", ["--version"], { encoding: "utf8" });
|
|
114
160
|
const problem = environmentProblem({
|
|
@@ -121,11 +167,9 @@ export function main(
|
|
|
121
167
|
return 1;
|
|
122
168
|
}
|
|
123
169
|
const dir = resolve(target);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
const name = value("--name") ?? basename(dir).toLowerCase();
|
|
170
|
+
const chosen = value("--name");
|
|
171
|
+
const name = chosen ?? storeNameOf(basename(dir));
|
|
172
|
+
if (!chosen) log(`store name ${name}, from the folder; --name to choose another`);
|
|
129
173
|
const fromPacks = value("--from-packs");
|
|
130
174
|
const t = JSON.parse(readFileSync(template, "utf8"));
|
|
131
175
|
let files;
|
|
@@ -135,10 +179,39 @@ export function main(
|
|
|
135
179
|
error(e.message);
|
|
136
180
|
return 1;
|
|
137
181
|
}
|
|
182
|
+
// A workspace's own dot-entries (.git, .salve, .vscode) may already be here: a platform like salve
|
|
183
|
+
// loads AGENTS.md and skills only from its root (p0.11 row 11). Anything the template writes may not.
|
|
184
|
+
const clash = existing(dir).filter((e) => !e.startsWith(".") || (e !== ".gitignore" && ownedTop(files).has(e)));
|
|
185
|
+
if (clash.length) {
|
|
186
|
+
error(
|
|
187
|
+
`${dir} already holds ${clash.join(", ")}: run it in an empty folder, or one holding only its own dot-files, or name a new one: npm create kedge@latest my-store`,
|
|
188
|
+
);
|
|
189
|
+
return positional[0] === undefined ? 2 : 1;
|
|
190
|
+
}
|
|
138
191
|
mkdirSync(dir, { recursive: true });
|
|
192
|
+
const ignore = join(dir, ".gitignore");
|
|
193
|
+
if (existsSync(ignore) && files[".gitignore"] !== undefined)
|
|
194
|
+
files[".gitignore"] = mergeIgnore(readFileSync(ignore, "utf8"), files[".gitignore"]);
|
|
139
195
|
write(dir, files);
|
|
140
196
|
log(`created ${name} in ${dir} (Kedge ${t.version})`);
|
|
141
|
-
|
|
197
|
+
// Inside a repository already (a workspace's), the store is part of it: no repository inside another (p0.11 row 12).
|
|
198
|
+
const inRepo =
|
|
199
|
+
String(run("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, encoding: "utf8" })?.stdout ?? "").trim() ===
|
|
200
|
+
"true";
|
|
201
|
+
if (!flags.has("--no-git") && !inRepo) run("git", ["init", "-q"], { cwd: dir, stdio: "inherit" });
|
|
202
|
+
// Whatever else the repository's ignore files say, the credentials the CLI saves stay out of Git.
|
|
203
|
+
if (inRepo || !flags.has("--no-git")) {
|
|
204
|
+
const kept = CREDENTIAL_PATHS.filter(
|
|
205
|
+
// 1 is "not ignored"; no git, or no repository (128), is no answer.
|
|
206
|
+
(p) => run("git", ["check-ignore", "-q", p], { cwd: dir, stdio: "ignore" })?.status === 1,
|
|
207
|
+
);
|
|
208
|
+
if (kept.length) {
|
|
209
|
+
error(
|
|
210
|
+
`Git here wouldn't ignore ${kept.join(", ")}, where the CLI saves credentials and keys: add them to .gitignore (an exception such as !.env.local overrides the starter's rule) before anything is committed`,
|
|
211
|
+
);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
142
215
|
if (!flags.has("--no-install")) {
|
|
143
216
|
const r = run("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
|
|
144
217
|
if (r.status !== 0) {
|
|
@@ -153,6 +226,6 @@ export function main(
|
|
|
153
226
|
return g.status ?? 1;
|
|
154
227
|
}
|
|
155
228
|
}
|
|
156
|
-
log(`next: cd ${target} && pnpm kedge check`);
|
|
229
|
+
log(`next: ${target === "." ? "" : `cd ${target} && `}pnpm kedge check`);
|
|
157
230
|
return 0;
|
|
158
231
|
}
|