@kedge/cli 0.10.0 → 0.10.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.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @kedge/cli
2
+
3
+ `kedge`, the command line of a [Kedge](https://www.npmjs.com/package/create-kedge)
4
+ store. A new store comes from `npm create kedge@latest`; this package is one of
5
+ its dependencies, run as `pnpm kedge <command>`.
6
+
7
+ | Command | Does |
8
+ |---|---|
9
+ | `kedge dev` | The store on this machine, its admin at `/app/` |
10
+ | `kedge check` | Typecheck, `generate --check`, `doctor`, then the store's tests |
11
+ | `kedge doctor [--remote]` | The store's checks; `--remote` adds its Cloudflare credentials and resources |
12
+ | `kedge generate` | Compose migrations and the lock from core, plugins and `local/` |
13
+ | `kedge preview` | An isolated copy on Cloudflare, with `test/scenarios` run against it |
14
+ | `kedge deploy` | Production: migrations first, then the Worker, under one recorded operation |
15
+ | `kedge staff invite-owner <email>` | The first Owner's invite link (`--remote` for Cloudflare) |
16
+ | `kedge keys create … --save` | A storefront or secret key, saved to the store's env file |
17
+ | `kedge seed --sample apparel --url <url>` | Sample products |
18
+ | `kedge smoke --url <url>` | A test order, placed; run again, the same order |
19
+ | `kedge upgrade --to <version>` | Every `@kedge` package to a release, with its codemods |
20
+
21
+ **For agents.** Exit `0` done, `1` refused or failed (the reason on stderr), `2`
22
+ usage, `3` a person must act: the last line of stderr starts `for you:` and is one
23
+ sentence to pass on. Run the same command again after they do; every command is
24
+ safe to repeat. With `--json`, stdout is one object, `{"ok": true, ...}`,
25
+ `{"ok": false, "error"}` or `{"ok": false, "handoff"}`.
26
+
27
+ Cloudflare credentials come from the environment, then the store's `.env.local`,
28
+ then `wrangler login`; `CLOUDFLARE_API_BASE_URL` is honoured for every call.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.10.0",
3
- "upgradesFrom": "0.9.0",
2
+ "version": "0.10.1",
3
+ "upgradesFrom": "0.10.0",
4
4
  "doStateFormat": 1,
5
5
  "migrations": {
6
6
  "0000_schema.sql": {
@@ -143,7 +143,8 @@
143
143
  "packages": {
144
144
  "@kedge/core": {
145
145
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
146
- "package.json": "fe58d717c5be02f0eaaebef065ddd26fb60697d5cea7d804a88270d8d917c53e",
146
+ "README.md": "5cffa13dcc33f4c3c39adb020a8c589722a88ed904033de1f16385a8a3e67236",
147
+ "package.json": "9593f2661eb8c3b199da8260de0439e85a6a5bbd53c5f79904df19a9767b76f3",
147
148
  "src/access/audit.ts": "374bf28051543e2cfc3a92f6dd3b27525562eb76d12d0628951da855323be120",
148
149
  "src/access/context.ts": "bf4d50e4473ebe99aea6e6d673b0006595bb5bcf47e9f34c094fa4d9c6fea5e7",
149
150
  "src/access/permissions.ts": "0e71620cf6dcddd414bdba4634986366f6c37c8d94f419c4fdd289131b5771da",
@@ -190,6 +191,7 @@
190
191
  },
191
192
  "@kedge/db": {
192
193
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
194
+ "README.md": "02526e2c786cd6e421e6bbd3689cac5fe8641578092baf07902cd65d6d102569",
193
195
  "migrations/0000_schema.sql": "31d99f65263f43480cc25e249197a6555e33166e28785cf0859b7cfd2bed53ad",
194
196
  "migrations/0001_triggers.sql": "61f5a9a0aec224dc5b69778a4143f3476d5c814350979bfbbb9547269bd688f7",
195
197
  "migrations/0002_stock_bounds.sql": "4e4b63d22aacceb196a2a54caadee9619d392b83b2eb82a3396871f0538d914b",
@@ -221,7 +223,7 @@
221
223
  "migrations/0028_tracks_entities.sql": "bbdad5c12570bc528abd5b294fa4277b780940fcf773ea67403e9b2324321c12",
222
224
  "migrations/0029_cli_session.sql": "a883b970c2820a84ac3e0c65530681df488ecc0546ebf29a43bc9a5778e8c1f9",
223
225
  "migrations/0030_credential_ref.sql": "b06cee4958d222a93d0af1b6a57e260beb98a92a9a1b2ba4c7a895fc40b79201",
224
- "package.json": "2d61ec5e5da2667b39cab8d57ef3a224c601aed3c9c44bd3d72a14451983f5a9",
226
+ "package.json": "311c0d8fa2fb339c4e8bda31d005e68233c5fd1f83ab116af5921e9cccd55792",
225
227
  "src/access-sql.ts": "51481f129f4614ba770218526048042aa0d118274ce7a737e0584037ea59dff0",
226
228
  "src/access.ts": "933e0fcaed1d0aca624df7556f3aab7368aa33793619b29e3ad10f610842db44",
227
229
  "src/admin.ts": "694d508e2bb51eda9134d163b049240688d5fdcd6783a66e2ae18bf9da21d140",
@@ -262,7 +264,8 @@
262
264
  },
263
265
  "@kedge/server": {
264
266
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
265
- "package.json": "1e943bb847224dd1df5d310d51aaa221e71dcc0b1df9c8ce76028c8f0d7ca048",
267
+ "README.md": "1fb1bcd08cc11851accb343d6df724383706cf9324d58785d097431476524b26",
268
+ "package.json": "44dc6665b926a512f039e1b58b22530ba2300d39761e9adc7766f3e41b5832e2",
266
269
  "src/access.ts": "224341803cb2448ab9458c4757035abc7344c0748e261ffcf18a6b072c3ebfa0",
267
270
  "src/admin-routes.ts": "9c1f13283f1b11709c0d27512205d9ead59a9042a4df36fa1bec344184ad7d0e",
268
271
  "src/admin-shell.ts": "3b94e14e3cd8303fd7f3153a65ce7c3339203b58f74656c40839587ca996a7c3",
@@ -284,7 +287,8 @@
284
287
  },
285
288
  "@kedge/runtime-cf": {
286
289
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
287
- "package.json": "8f28ac1819e6e1ae515b499709e5d0770abf11ebe0a004d6d78aad93d28a3aa1",
290
+ "README.md": "b678c333e82364c5e9349bb1aa4f64d1479678893b471ddc0c4ea41b769adef9",
291
+ "package.json": "0ee37754d1a8d0d663927c7504193befbc302c0ff63e0609eb6729e9371860db",
288
292
  "src/admin.ts": "2c005857fe2928b4e82777590f13f7a83e26edfa30b24f5463ee0c80932b2ca0",
289
293
  "src/cart-do.ts": "d7be874e440d41b716a96607341b82b0992a9da1656456322fca3634f6be8529",
290
294
  "src/clock-do.ts": "8d68b0ba01d16763fe7d13feee71d315bbc1ebbee7e5f8694a9d186c4ccab3a5",
@@ -310,12 +314,14 @@
310
314
  },
311
315
  "@kedge/plugin-sdk": {
312
316
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
313
- "package.json": "ff1cf9398e25997dda431c9d671a89b95142ff1263ec4fd5be1078039bbf19ab",
317
+ "README.md": "685a9eede5493ce08535b0525cf30b1290d48ba9e75b3662239fb5b6e08d3baa",
318
+ "package.json": "c0ed2f482fdac69c049eb76eee53f5ee1ad9918dbd33714515e09a91ac214fd0",
314
319
  "src/index.ts": "cb2aa938e2065f6ca5f01a470b421ec0ca20599f149114051823e2510a76d11b"
315
320
  },
316
321
  "@kedge/testing": {
317
322
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
318
- "package.json": "83a2a8ea4c49397908896974b716799f861992814b26cc67b1cf060ef08acb1b",
323
+ "README.md": "94da183c9b4fdb9617dbe93cfb169b81ce4bd6ed7c9a364d6d6ad3e62578d9cc",
324
+ "package.json": "1cd6b1b4d32fb197fc6a11ba1c7531e20e56cfb58245cb70a38937579442a77e",
319
325
  "src/budgets.ts": "4ffb780a7543d1d2c7c7e5dca7a2d4cdb363e3b0ee2801a928d928da8088e525",
320
326
  "src/http.ts": "59c4bb9d3df6885333103a05cddde0d03cc70008a416c99905422fbead034ee7",
321
327
  "src/index.ts": "c3705394753e6c6ecf3abfd19a3b1e672c61c33bda4ff8f2d54ece10f506b5cd",
@@ -327,6 +333,7 @@
327
333
  },
328
334
  "@kedge/docs": {
329
335
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
336
+ "README.md": "e131075a2958e014a2c2dcaa33882d04deeadedd52d2f692a04fe7d1af669905",
330
337
  "extending/admin.md": "4514c38097dc5442b1ec6761c337689dd1f0c76b1320184c1417c4bb34704464",
331
338
  "extending/contributions.md": "2f970bd64f1e131a408c494e867060907e8d2c83b9f89efd02ae4618aa49be5b",
332
339
  "extending/custom-fields.md": "ccdd0d83b8c54d72992b43cad3a7742c42b65009b863b44c71006ad7255fb900",
@@ -339,15 +346,16 @@
339
346
  "extending/timers.md": "6da7fc0a8eee314df7158df16e5670d194842d5de452032ba8be414b5dac4e7e",
340
347
  "extending/tracks.md": "3d85cd54a5060c35fe70b3bbea848c16ffd0a4e2e21e590b104408690a721ac6",
341
348
  "index.json": "4d0f56f56cd470c78e93c9b26ce251ee89fdb624dfd20febb1fc411b398182e9",
342
- "package.json": "db16291c1918694a3a932643e6b5fb30fa74f6cc3ad4f73a6d3ea7cbc8279022",
349
+ "package.json": "df6d4c464cc5256ad388a943d959d4792373379148fdf235f25a5e759bcfc7a9",
343
350
  "reference/api.md": "114830d766ab3b81a5181f7b07b10012ac5ee15898c87fa9515c71cec60ec374",
344
351
  "reference/errors.md": "c4fa0dcb4d79d5a4888d7fc3ee525a045b21d3cb3ac158f5172dbb02880979f2",
345
352
  "skills/kedge-extend/SKILL.md": "7ce215445e99d87e401b7f2669c184f3fa263b75aabced2cdf5eefb398043013",
346
353
  "skills/kedge-plugin/SKILL.md": "21af3f112cfcdf2a1cd2eb8e9148a0558a4aebf29b1a057cb5ece09af8116d99",
347
354
  "skills/kedge-preview-deploy/SKILL.md": "4b794f84b8f639aff97f4d5e28c44ec8b44de00f6b7b5dc25500dff722891480",
348
- "skills/kedge-start/SKILL.md": "6f37eebbed97f6a2f6be16a682eead0a540eef1df2e2b8af4e715bf0d9531b42",
355
+ "skills/kedge-start/SKILL.md": "04eed3cc12d66eaf1531b26671b0e90d44fd4eb81b394bb00d3e6a4e8c97e3c5",
349
356
  "skills/kedge-upgrade/SKILL.md": "97cba305676a22696d6d4e25d2c5e823b47fc0be04b12813d17b9dfd3c5a6c2e",
350
357
  "upgrading/0.10.0.md": "169efc96cdf80af167df6607c0db4d977e29c135858ba0c61f53d50d9facc676",
358
+ "upgrading/0.10.1.md": "d47ddc49f1724e0f3be55a7136d75cbcd8f0bf1128cc7ed2aa6e17cf884ac893",
351
359
  "upgrading/0.8.0.md": "b0ddd26993048ea1fdd0e76fb2707060cc4032ffb199a691278020361be95cf8",
352
360
  "upgrading/0.9.0.md": "d632d6143636351289684dd51dd51eb0be3e76975c4a42d65acdcee04549d21d",
353
361
  "upgrading/codemods/add-admin-assets.mjs": "fa5d069a3489889dcc095b309020d0e773004b3a39544457747491963492f0a1",
@@ -355,55 +363,57 @@
355
363
  },
356
364
  "@kedge/admin": {
357
365
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
366
+ "README.md": "1ce1a6d2a16995f6342c9bcc40ddbac0e2bc35cdcdf1ad24efe28aa5b85d79a3",
358
367
  "dist/_headers": "b1d979b2fdf27af6c0f41fdaee420f2a2b192f28dc855fec7491f8ffa685c9ba",
359
- "dist/app/assets/account-BAdVy9E2.js": "8966c18f8606ea9f795c87ffe2023748f9134e0c96277e4a687ade3d92c189e2",
368
+ "dist/app/assets/account-pLgh_XLx.js": "cee500d077ab53d4607358d76d2f3d12e5b8f3fe0950023b104bbd32094e55d4",
360
369
  "dist/app/assets/card-CH9fCW35.js": "995ee7034e88909a04b07d559a7f520fb08df647e960606f54f5ab88c7201a3f",
361
370
  "dist/app/assets/display-CLAPxMXW.js": "4f4e029a0f62756c966c2da476c9f62b7bc59b521c84fa2af86b0f1e2ce51729",
362
- "dist/app/assets/home-BCCZ6txc.js": "17737d164554aadbaacb71fc2387825e89eb68b19976282d83675df1657cbbc0",
371
+ "dist/app/assets/home-DrmfIxH8.js": "87154c23f458a95e797eca4189411a42b5ba6640a6fdc23270130c429940be87",
363
372
  "dist/app/assets/index-BA_54h5V.css": "0d7763f516591b37628f3bd5cad733d786783e78a092ff4ad74f0a4045394c73",
364
- "dist/app/assets/index-kv11NdsY.js": "84102186c2125beaa6e9f08cffca1e807cadacb1c4d237c0e5a9d67a11312ed5",
365
- "dist/app/assets/invite-C4IwtpZf.js": "8ff169820e79cfb17e15151dcf46c4325bcb4e70fb9be55214372533dbf987bb",
366
- "dist/app/assets/resource-detail-CRWJVEL8.js": "3566481e229730047d08c9ca3c398560e785774c311122ddc8557cf363a97b8b",
367
- "dist/app/assets/resource-form-DulAO-yd.js": "8b8b89fb78e2dcd53471756fbf7a55c6d877e1f93ed46db506f5f6e6f42c48f9",
368
- "dist/app/assets/resource-list-wxhtKedG.js": "f5a452d17f88de461d5b29e2db4b0c9e375dcfa8af34fe6d9a4775ee999f9c66",
369
- "dist/app/assets/setup-CBpGZMzZ.js": "b80589ba2411dcfe36e69b82b725ce7124e644c64fa48ba5f5acf9cd5afb1202",
370
- "dist/app/assets/use-command-CVgCpnG6.js": "e882a35ab8186a3e959c1bda20c6dc5e7a8538b34ba8da1c4d51f3f6118b85cc",
371
- "dist/app/assets/work-lists-CKGWNjSC.js": "1258c82ea08da8dcf4b2a585dda65cb07bdde37d10b1a9326708d711a1b7caad",
373
+ "dist/app/assets/index-CJIaoAxE.js": "e99daee039bd6873e4cf73eea6dce911eb01578926279223f1b538ef3302da74",
374
+ "dist/app/assets/invite-DtAspOzb.js": "aa238d84ae5c5c16b95337e5056ac1f59d37f87e2645f949ced15b94e892f205",
375
+ "dist/app/assets/resource-detail-Du1RdgVZ.js": "ebf6435a529b8675c118335900725e2ea91ec5125c52f3dd4c6dc278cf2c2f10",
376
+ "dist/app/assets/resource-form-EmoEfjrj.js": "1f8826d42e5fa918f0b23754f773a3307b37e178441d3b02885dfba7e5fff08e",
377
+ "dist/app/assets/resource-list-VLCxpKZl.js": "8b42f06d949b02f28dd30a2511b189b69f564b90ba803984015d348f80078287",
378
+ "dist/app/assets/setup-BiUpBePR.js": "d45e13f5eced59f064eb86e9dcb78661163f780d22c99770f0b1094fb4619e1b",
379
+ "dist/app/assets/use-command-CB6a5Dop.js": "de5ed878a74f8086d8db82597f0bae2fcd5181d06123834596bc03ec9600e042",
380
+ "dist/app/assets/work-lists-C3wqs0gp.js": "4b3a49ed9b62997fe0f9320b26c97b6b5fbb36bd46abb46a40b751b07a513f2d",
372
381
  "dist/app/boot.js": "3099141f8935b0c2f15a041f205a673c977a9c8bfc7aa30f23a44b44a69bdb1d",
373
- "dist/app/build.json": "4a5afa95433c107b6ab2ffdc0837e45fb6aae06eb79d48593d266010ec1e7a10",
374
- "dist/app/index.html": "7cf61261460b8ca9cb9731996b8ceffd74dbe0ec2399d5786c2196a199a3908d",
382
+ "dist/app/build.json": "ee4c28bdd6ba1b787b8f22d07f962cb81ceedba95e8f034138715b4088e8663c",
383
+ "dist/app/index.html": "7ede70cce6abcff8c1225ce961885a540206462300c5897a23f3cba80c7ee8db",
375
384
  "dist/app/licenses.txt": "7a2bb91cbbc5e4201b72d4d95b2a4d7458f42e80a0d05b01398c9e93f5764f62",
376
- "package.json": "ac905bafbb0da5e7d0a8acbd59ac66ee5da567579e7c31be282bfc9c881e25fc",
385
+ "package.json": "742f602ed4dce5f800505a9e9e9996240ab19462182c1213c96e5e0db54e922f",
377
386
  "slots.json": "3252f255e1628537056317d74be5fd2db2fb7e283cd6e8d3f2d49e6dad4b15e5"
378
387
  },
379
388
  "@kedge/cli": {
380
389
  "LICENSE": "5068e060c465b99dee8b5d7ccfecc51f395affd63d6b9651a2d31e71e3a0d9fb",
390
+ "README.md": "642be70aa940d01572dc5e91482353b3ecb9c24d4558f3014a4b93fb62a909fd",
381
391
  "bin/kedge.mjs": "8279a888842a6ee6c0ae9e26fd4380b97892648ca94979fc69d505ff8af6aa9f",
382
- "package.json": "bf3232f8449a5417d2fd9e5819a083a7a1ba8d0451712a495a6724c57355aff8",
392
+ "package.json": "318bca59c7980a066581995a1f4cd22d59fbb652dd6603f7a45bb88453030618",
383
393
  "src/cf.ts": "af5a7e281f628f07c46426c819da35b96d2662f275a196073b511966aa1786e6",
384
- "src/check.ts": "3199e0e2e1be899f0216580eb8c4bbe0faf4e403da2970e8a060e9751d71dcb5",
394
+ "src/check.ts": "273ff4b59d717aea33e166e4d88775f190fa826cc3432642a2ccfa2cad12ddd6",
385
395
  "src/cli-session.ts": "2e666d1e64f1f3997b5923dc8803e8ddb84cc05c61593d22cbae19b148e69768",
386
396
  "src/code-lint.ts": "a696fb8ce00a10da783c1d514a1afa1dd9306a9e04819ae36580c592b651a544",
387
- "src/commands.ts": "cd4aeab6821608e17717d08063d9dcbdcd56d6696f3fde10adbe462bbe73ad1d",
397
+ "src/commands.ts": "3f1d6ba7b1c68aa6125eb409c172b50688c1b77d8306b24c655f37487f008cf6",
388
398
  "src/compose.ts": "bc67b22f814e54c0c27db88d776b135d3c7b26b55702f678e1f0253aaf50c943",
389
- "src/d1-batch.ts": "45bda667f5c77118d0004dff95516cb242a16456763fa57465ee3cc6895fac35",
399
+ "src/d1-batch.ts": "dd4dd69778ccf47970e9fc80637b6655ccd94bffa96a6ff7de3dfa92dd668662",
390
400
  "src/deploy.ts": "574c4338231bdc1cc7bfd2024e2e37689b2d75b2a3b822889a46a1fb9bba25e8",
391
401
  "src/doctor.ts": "17db6b809e9e71ddb10bee6aa009c1f86beff3b5476629ec4ff8626e8c776b9f",
392
402
  "src/fork.ts": "f3d9878f58098a563bf2abc76f9e3207514e81779d144831d2e0259c8f455656",
393
403
  "src/index.ts": "1f1427679dce8ce228bbce9fcc9290382dd2027c5f2c25bf6a712de74d9793ab",
394
404
  "src/invariants.ts": "177446f4144571345c6872f816a076dc88f5dc92dc0503281456f3a2164229ae",
395
- "src/keys-save.ts": "bb1fc660f20a0f0d2369e5c79d70723aae191e7c34a9f26a952be3d57f5d48b3",
396
- "src/local-state.ts": "abc1b6414bc8a8603ce52acdab3906c4aab84b000f9bf82ee5e722447b0f4dcd",
397
- "src/main.ts": "076ce997163de82b929249701eb0019d3f70829eb61d0075cd8ab572554cb03c",
405
+ "src/keys-save.ts": "682e27974af1e0aba6127e70f73b99a848dae7baf31a6db1584d68d64f027581",
406
+ "src/local-state.ts": "bcfc69f6eb1b3ca134f428e27fe7dd38340755a0a37565587b5c88dfd99cae83",
407
+ "src/main.ts": "f0b3e2c4b30f9b1a525f5be092a31c6c6435b6be16c14124e379343e52ccfbbd",
398
408
  "src/manifest.ts": "9d52eafd735f9e3d8978588f2b9fb712e17d0e78106170fdb8cd373f39920c87",
399
409
  "src/n1.ts": "17b26eb4114dba819282c44c67ef8bf5fbfcc854674133bee5d87bd250ebdc39",
400
410
  "src/ops.ts": "9eab0317f1f6b480b07498621f9d6b47fd8a22f24e417b83555b7fe0a4ef5649",
401
411
  "src/order.ts": "faba43ade99bd2f966cbc40c3c5d780e6f47a8df06c3e45cc16629ac82d5407e",
402
- "src/output.ts": "1f257884d714eb51470697add21e135c418b1466eddd838ebbfb06eeb855cbd4",
412
+ "src/output.ts": "d599d8752695efc8b56919914f2fb7fc27b5f420d0c5f5d6df03b8416a404184",
403
413
  "src/plugin-lint.ts": "b716e02ee5a2ebaac1ef0bee709d5417fa800aa7ab2b1d3ded1607739091584d",
404
- "src/preview.ts": "0ebdf3364c9f20097794aec38731dd263f30654e451128eda5ca4c5f3ed7529f",
414
+ "src/preview.ts": "48e83edc7282053eef9be43644c96d885499605ff5305ac6c0c0adc10a171022",
405
415
  "src/schema.ts": "d0b2b8b1bbb7a9ea1a53e092563ac62f0e98c8c1b27a5172d977e2f1c63453d1",
406
- "src/smoke.ts": "13c245e4bf54760137ab3997c3b2825caad162aea47e6687fd7897d526af31cd",
416
+ "src/smoke.ts": "dd722d21d319db8c75a826f799721ae371761497bb3524bf980249a05d94940c",
407
417
  "src/sqlite-verify.ts": "1181960dfbbf5b94bd376c024a69d71aaa76bd69c6b92359713acf665e89c3b5",
408
418
  "src/store-api.ts": "72a7ee732ba8b531c703df5880d701a305a9c1552a5a3fa631c8ea464d72c8ff",
409
419
  "src/store.ts": "fcee4e1838b289c0a5875f63fd83610e46173edd8d189827ce6aa94723b97635",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kedge/cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,10 +28,10 @@
28
28
  "yaml": "2.9.1",
29
29
  "@babel/parser": "7.29.9",
30
30
  "wrangler": "4.152.0",
31
- "@kedge/core": "0.10.0",
32
- "@kedge/db": "0.10.0",
33
- "@kedge/testing": "0.10.0",
34
- "@kedge/runtime-cf": "0.10.0"
31
+ "@kedge/core": "0.10.1",
32
+ "@kedge/db": "0.10.1",
33
+ "@kedge/testing": "0.10.1",
34
+ "@kedge/runtime-cf": "0.10.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "24.19.2",
package/src/check.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
7
  import { join } from "node:path";
8
- import { childStdio } from "./output";
8
+ import { childFailed, childStdio } from "./output";
9
9
 
10
10
  export interface CheckInput {
11
11
  cwd: string;
@@ -22,7 +22,9 @@ const runBin = (bin: string, args: string[], cwd: string): number => {
22
22
  console.error(`${bin} isn't installed in this store: run pnpm install`);
23
23
  return 1;
24
24
  }
25
- return spawnSync(path, args, { cwd, stdio: childStdio() }).status ?? 1;
25
+ const r = spawnSync(path, args, { cwd, stdio: childStdio(), maxBuffer: 1 << 26 });
26
+ childFailed(r);
27
+ return r.status ?? 1;
26
28
  };
27
29
 
28
30
  export async function check(input: CheckInput): Promise<number> {
package/src/commands.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // acting on production.
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { existsSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
9
+ import { createServer } from "node:net";
9
10
  import { join, resolve } from "node:path";
10
11
  import { pathToFileURL } from "node:url";
11
12
  import { isPermission, type KedgeConfig } from "@kedge/core";
@@ -41,7 +42,7 @@ import { childStdio, emit } from "./output";
41
42
  import { needsLive, smoke } from "./smoke";
42
43
  import { git, storeNames } from "./store";
43
44
  import { storeRequest } from "./store-api";
44
- import { resolveTarget, type StoreTarget } from "./target";
45
+ import { devPort, resolveTarget, type StoreTarget } from "./target";
45
46
  import { compileStoreZone } from "./tz";
46
47
 
47
48
  export interface CommandDeps {
@@ -302,14 +303,8 @@ function adminUrl(cwd: string, url: string): string | null {
302
303
  }
303
304
 
304
305
  export async function migrateCommand(args: readonly string[], d: CommandDeps): Promise<number> {
305
- if (!args.includes("--remote")) {
306
- // Local: the dev database, through Wrangler's own bookkeeping.
307
- const r = spawnSync(process.execPath, [wranglerBin(), "d1", "migrations", "apply", "DB", "--local"], {
308
- cwd: d.cwd,
309
- stdio: childStdio(),
310
- });
311
- return r.status ?? 1;
312
- }
306
+ // Local: the dev database, through Wrangler's own bookkeeping.
307
+ if (!args.includes("--remote")) return migrateLocal(d);
313
308
  return refusals(d, async () => (await deployer(d)).migrateRemote());
314
309
  }
315
310
 
@@ -743,7 +738,41 @@ export function devArgs(cwd: string): string[] {
743
738
  return id ? ["--define", `KEDGE_ADMIN_BUILD:${JSON.stringify(id)}`] : [];
744
739
  }
745
740
 
746
- export function devCommand(d: CommandDeps): number {
741
+ /**
742
+ * The local database's migrations, through Wrangler's own bookkeeping, in one
743
+ * line: Wrangler prints its whole status table again after each migration,
744
+ * thousands of lines for a new store (p0.10.1 §1, 8). Its full output is shown
745
+ * only when it fails.
746
+ */
747
+ export function migrateLocal(d: Pick<CommandDeps, "cwd" | "log" | "error">): number {
748
+ const r = spawnSync(process.execPath, [wranglerBin(), "d1", "migrations", "apply", "DB", "--local"], {
749
+ cwd: d.cwd,
750
+ encoding: "utf8",
751
+ env: { ...process.env, WRANGLER_SEND_METRICS: "false" },
752
+ maxBuffer: 1 << 26,
753
+ });
754
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
755
+ if (r.status !== 0) {
756
+ (d.error ?? console.error)(out.trim().split("\n").slice(-40).join("\n"));
757
+ return r.status ?? 1;
758
+ }
759
+ const n = new Set(out.match(/\b\d{4}_[a-z0-9_]+\.sql\b/g) ?? []).size;
760
+ (d.log ?? console.log)(
761
+ n ? `applied ${n} migration${n === 1 ? "" : "s"} to the local database` : "the local database is up to date",
762
+ );
763
+ return 0;
764
+ }
765
+
766
+ /** Whether something already listens on the port: Wrangler would otherwise move to another, silently. */
767
+ function portTaken(port: number): Promise<boolean> {
768
+ return new Promise((resolve) => {
769
+ const server = createServer();
770
+ server.once("error", () => resolve(true));
771
+ server.listen(port, "127.0.0.1", () => server.close(() => resolve(false)));
772
+ });
773
+ }
774
+
775
+ export async function devCommand(d: CommandDeps): Promise<number> {
747
776
  const vars = join(d.cwd, ".dev.vars");
748
777
  if (!existsSync(vars)) {
749
778
  const lines = [
@@ -759,11 +788,16 @@ export function devCommand(d: CommandDeps): number {
759
788
  });
760
789
  (d.log ?? console.log)("wrote .dev.vars with fresh local secrets");
761
790
  }
762
- const migrated = spawnSync(process.execPath, [wranglerBin(), "d1", "migrations", "apply", "DB", "--local"], {
763
- cwd: d.cwd,
764
- stdio: "inherit",
765
- });
766
- if (migrated.status !== 0) return migrated.status ?? 1;
791
+ // One port for dev, its invite link and the CLI's local target (p0.10.1 §3): taken is a refusal, never a move.
792
+ const port = devPort(d.cwd);
793
+ if (await portTaken(port)) {
794
+ (d.error ?? console.error)(
795
+ `port ${port} is in use: stop what's on it, or set "dev": { "port": <another> } in wrangler.jsonc`,
796
+ );
797
+ return 1;
798
+ }
799
+ const migrated = migrateLocal(d);
800
+ if (migrated !== 0) return migrated;
767
801
  let extra: string[];
768
802
  try {
769
803
  extra = devArgs(d.cwd);
@@ -771,7 +805,20 @@ export function devCommand(d: CommandDeps): number {
771
805
  (d.log ?? console.log)((e as Error).message);
772
806
  return 1;
773
807
  }
774
- return spawnSync(process.execPath, [wranglerBin(), "dev", ...extra], { cwd: d.cwd, stdio: "inherit" }).status ?? 0;
808
+ const origin = `http://localhost:${port}`;
809
+ // Where the store and its admin are, and how a person signs in to a new store's admin (p0.10.1 §1, 5).
810
+ (d.log ?? console.log)(
811
+ [
812
+ `the store: ${origin} its admin: ${origin}/app/`,
813
+ "no one can sign in to the admin until there's an Owner: pnpm kedge staff invite-owner <your email>",
814
+ ].join("\n"),
815
+ );
816
+ return (
817
+ spawnSync(process.execPath, [wranglerBin(), "dev", "--port", String(port), ...extra], {
818
+ cwd: d.cwd,
819
+ stdio: "inherit",
820
+ }).status ?? 0
821
+ );
775
822
  }
776
823
 
777
824
  export const removeIfExists = (p: string) => rmSync(p, { recursive: true, force: true });
package/src/d1-batch.ts CHANGED
@@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import type { CliBatch } from "@kedge/db";
9
9
  import { wranglerBin } from "./cf";
10
- import { childStdio } from "./output";
10
+ import { childFailed, childStdio } from "./output";
11
11
 
12
12
  /** Runs one CLI batch against the store's D1; tests replace it. */
13
13
  export type Executor = (
@@ -56,8 +56,10 @@ export const wranglerExecute: Executor = (b, opts) => {
56
56
  cwd: opts.cwd,
57
57
  stdio: childStdio(),
58
58
  env: bootstrapEnv(process.env, dir, opts.env),
59
+ maxBuffer: 1 << 26,
59
60
  },
60
61
  );
62
+ childFailed(r);
61
63
  return r.status ?? 1;
62
64
  } finally {
63
65
  // The file holds no secret (only digests), but it needn't outlive the run.
package/src/keys-save.ts CHANGED
@@ -11,7 +11,7 @@ import type { DatabaseSync } from "node:sqlite";
11
11
  import { NeedsPerson } from "./cf";
12
12
  import { OLDER_KEDGE, type SessionDeps, withCliSession } from "./cli-session";
13
13
  import { DeployRefused } from "./deploy";
14
- import { type RefRecord, readEnvLocal, saveEnv, stateDb, withStoreLock } from "./local-state";
14
+ import { ENV_FILE, type RefRecord, readEnvLocal, saveEnv, stateDb, withStoreLock } from "./local-state";
15
15
  import { storeRequest } from "./store-api";
16
16
  import type { StoreTarget } from "./target";
17
17
 
@@ -85,7 +85,10 @@ async function save(db: DatabaseSync, target: StoreTarget, wanted: Wanted, d: Sa
85
85
  // digest, never by a label beside it: a key with no such ref, or one for another store, would be sent
86
86
  // to the wrong store or relabelled as this one's (codex code r11, r13). So any such key, of either
87
87
  // kind, is the person's to remove before anything is saved beside it.
88
- const env = readEnvLocal(d.cwd);
88
+ moveLocalBundle(db, d);
89
+ // Each store's keys in its own file (p0.10.1 §4): kedge dev's apart from Cloudflare's.
90
+ const envFile = target.kind === "local" ? ENV_FILE.local : ENV_FILE.production;
91
+ const env = readEnvLocal(d.cwd, envFile);
89
92
  const all = db.prepare("SELECT * FROM credential_ref").all() as unknown as RefRecord[];
90
93
  const foreign: string[] = [];
91
94
  for (const name of [ENV.publishable, ENV.secret]) {
@@ -104,7 +107,7 @@ async function save(db: DatabaseSync, target: StoreTarget, wanted: Wanted, d: Sa
104
107
  if (sf && !all.some((r) => r.store === store && r.storefront_id === sf)) foreign.push(ENV.storefront);
105
108
  if (foreign.length)
106
109
  throw new NeedsPerson(
107
- `.env.local holds ${foreign.join(", ")} that this checkout didn't make for this store: remove ${foreign.length === 1 ? "that line" : "those lines"}, or save this store's keys in its own checkout, then run this again`,
110
+ `${envFile} holds ${foreign.join(", ")} that this checkout didn't make for this store: remove ${foreign.length === 1 ? "that line" : "those lines"}, or save this store's keys in its own checkout, then run this again`,
108
111
  );
109
112
  const saved = env[envName];
110
113
  if (saved && (await works(target, wanted, saved, d))) {
@@ -172,7 +175,7 @@ async function save(db: DatabaseSync, target: StoreTarget, wanted: Wanted, d: Sa
172
175
  // 4. Saved before anything else happens, then marked saved.
173
176
  const values: Record<string, string> = { [ENV.store]: store, [envName]: made.key };
174
177
  if (made.storefront_id) values[ENV.storefront] = made.storefront_id;
175
- saveEnv(d.cwd, values);
178
+ saveEnv(d.cwd, values, envFile);
176
179
  set(ref, { state: "saved" });
177
180
  d.log(`saved ${Object.keys(values).join(", ")} to .env.local`);
178
181
  return values;
@@ -254,6 +257,48 @@ export function badOrigins(origins: readonly string[]): string[] {
254
257
  });
255
258
  }
256
259
 
260
+ /**
261
+ * A 0.10.0 checkout saved `kedge dev`'s keys in `.env.local`, where they'd block
262
+ * Cloudflare's (p0.10.1 §4). Under the lock, before anything else, the bundle
263
+ * proven local moves to `.env.development.local`: each key whose digest matches
264
+ * a ref recorded for a `local/…` store, the storefront a local ref recorded, and
265
+ * a `local/…` store marker. Nothing unproven moves. The destination is written
266
+ * first and read back, then only names it now holds are removed from
267
+ * `.env.local`; a run stopped between the two finishes the removal next time.
268
+ * A destination already holding one of those names with another value is a handoff.
269
+ */
270
+ function moveLocalBundle(db: DatabaseSync, d: SaveDeps): void {
271
+ const prod = readEnvLocal(d.cwd, ENV_FILE.production);
272
+ const refs = (db.prepare("SELECT * FROM credential_ref").all() as unknown as RefRecord[]).filter((r) =>
273
+ r.store.startsWith("local/"),
274
+ );
275
+ const moving: Record<string, string> = {};
276
+ for (const name of [ENV.publishable, ENV.secret]) {
277
+ const value = prod[name];
278
+ if (value && refs.some((r) => r.env_name === name && r.key_digest === keyDigest(value))) moving[name] = value;
279
+ }
280
+ const sf = prod[ENV.storefront];
281
+ if (sf && refs.some((r) => r.storefront_id === sf)) moving[ENV.storefront] = sf;
282
+ if (prod[ENV.store]?.startsWith("local/")) moving[ENV.store] = prod[ENV.store] as string;
283
+ const names = Object.keys(moving);
284
+ if (!names.length) return;
285
+ const dev = readEnvLocal(d.cwd, ENV_FILE.local);
286
+ const clash = names.filter((n) => dev[n] !== undefined && dev[n] !== moving[n]);
287
+ if (clash.length)
288
+ throw new NeedsPerson(
289
+ `${ENV_FILE.production} holds kedge dev's ${clash.join(", ")}, and ${ENV_FILE.local} holds others: keep one, remove the other line, then run this again`,
290
+ );
291
+ saveEnv(d.cwd, moving, ENV_FILE.local);
292
+ const landed = readEnvLocal(d.cwd, ENV_FILE.local);
293
+ saveEnv(
294
+ d.cwd,
295
+ {},
296
+ ENV_FILE.production,
297
+ names.filter((n) => landed[n] === moving[n]),
298
+ );
299
+ d.log(`moved kedge dev's ${names.join(", ")} from ${ENV_FILE.production} to ${ENV_FILE.local}`);
300
+ }
301
+
257
302
  /**
258
303
  * Whether a saved key still does its job (§6.2): a publishable key passes the
259
304
  * storefront gate; a secret key holds its grants. Only a definite answer
@@ -77,24 +77,36 @@ export function stateDb(cwd: string): DatabaseSync {
77
77
  return db;
78
78
  }
79
79
 
80
+ /** Where a store's saved keys go (p0.10.1 §4): `kedge dev`'s apart from Cloudflare's, so neither blocks the other. */
81
+ export const ENV_FILE = { production: ".env.local", local: ".env.development.local" } as const;
82
+
80
83
  /**
81
- * Writes `values` into `.env.local`, keeping every other line: a temporary
84
+ * Rewrites an env file, keeping every line it doesn't touch: `values` set
85
+ * (replaced in place, or added at the end) and `remove` dropped. A temporary
82
86
  * file in the same directory, mode 0600, renamed over it, so a crash leaves
83
87
  * the old file or the new one, never half of either.
84
88
  */
85
- export function saveEnv(cwd: string, values: Record<string, string>): void {
86
- const file = join(cwd, ".env.local");
89
+ export function saveEnv(
90
+ cwd: string,
91
+ values: Record<string, string>,
92
+ name: string = ENV_FILE.production,
93
+ remove: readonly string[] = [],
94
+ ): void {
95
+ const file = join(cwd, name);
87
96
  const lines = existsSync(file) ? readFileSync(file, "utf8").split("\n") : [];
88
97
  const left = new Set(Object.keys(values));
89
- const out = lines.map((line) => {
90
- const name = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
91
- if (!name || !(name in values)) return line;
92
- left.delete(name);
93
- return `${name}=${values[name]}`;
98
+ const drop = new Set(remove);
99
+ const out = lines.flatMap((line) => {
100
+ const key = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
101
+ if (!key) return [line];
102
+ if (drop.has(key)) return [];
103
+ if (!(key in values)) return [line];
104
+ left.delete(key);
105
+ return [`${key}=${values[key]}`];
94
106
  });
95
107
  while (out.length && out.at(-1) === "") out.pop();
96
- for (const name of left) out.push(`${name}=${values[name]}`);
97
- const tmp = join(cwd, `.env.local.${process.pid}.tmp`);
108
+ for (const key of left) out.push(`${key}=${values[key]}`);
109
+ const tmp = join(cwd, `${name}.${process.pid}.tmp`);
98
110
  try {
99
111
  writeFileSync(tmp, `${out.join("\n")}\n`, { mode: 0o600 });
100
112
  renameSync(tmp, file);
@@ -103,8 +115,8 @@ export function saveEnv(cwd: string, values: Record<string, string>): void {
103
115
  }
104
116
  }
105
117
 
106
- /** `.env.local`'s values, as the credential resolver reads them. */
107
- export function readEnvLocal(cwd: string): Record<string, string> {
108
- const file = join(cwd, ".env.local");
118
+ /** An env file's values (`.env.local` by default), as the credential resolver reads them. */
119
+ export function readEnvLocal(cwd: string, name: string = ENV_FILE.production): Record<string, string> {
120
+ const file = join(cwd, name);
109
121
  return existsSync(file) ? (parseEnv(readFileSync(file, "utf8")) as Record<string, string>) : {};
110
122
  }
package/src/main.ts CHANGED
@@ -118,7 +118,15 @@ async function inviteOrigin(
118
118
  ): Promise<string | null> {
119
119
  const url = flag(args, "--url");
120
120
  if (url) return new URL(url).origin;
121
- if (!remote) return null;
121
+ // Local: kedge dev's own origin, the port it binds (p0.10.1 §1, 5; §3).
122
+ if (!remote) {
123
+ try {
124
+ const { devPort } = await import("./target");
125
+ return `http://localhost:${devPort(cwd)}`;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
122
130
  try {
123
131
  const worker = storeNames(cwd).names.worker;
124
132
  return `https://${worker}.${await (cf ?? (await cloudflareFor(env, cwd))).workersSubdomain()}.workers.dev`;
@@ -372,8 +380,17 @@ async function run(argv: string[], cwd: string, deps: RunDeps & { json: boolean
372
380
  else results.push(...(await doctorRemote({ cwd, cf, lock, logpushJobs: () => cf.logpushJobs() })));
373
381
  }
374
382
  }
375
- console.log(flags.includes("--json") ? JSON.stringify({ checks: results }, null, 2) : formatReport(results));
376
383
  const handoff = results.find((r) => r.handoff)?.handoff;
384
+ // The same envelope as every --json command (p0.10.1 §1, 7): ok, the checks, and a handoff when there is one.
385
+ console.log(
386
+ flags.includes("--json")
387
+ ? JSON.stringify({
388
+ ok: !results.some((r) => r.status === "fail"),
389
+ checks: results,
390
+ ...(handoff ? { handoff } : {}),
391
+ })
392
+ : formatReport(results),
393
+ );
377
394
  if (handoff) {
378
395
  console.error(`for you: ${handoff}`);
379
396
  return 3;
@@ -463,7 +480,8 @@ export async function main(argv: string[], cwd: string, deps: RunDeps = {}): Pro
463
480
  const f = failureOf(e);
464
481
  if (!f) throw e;
465
482
  console.error(f.line);
466
- if (json) (deps.emit ?? emit)(f.result);
483
+ // doctor keeps its own --json report, and a failure past it is still one object (p0.10.1, codex code r20).
484
+ if (json || (command === "doctor" && flags.includes("--json"))) (deps.emit ?? emit)(f.result);
467
485
  return f.code;
468
486
  }
469
487
  });
package/src/output.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  // `--json` (p0.10 §5): stdout carries one object when the command ends, and
2
- // nothing else, so an agent parses it whole. Progress still reaches a person,
3
- // on stderr: console.log is pointed there for the command's run, and every
4
- // child the CLI starts (Wrangler, the store's checks and scenarios) writes its
5
- // stdout there too, since a child's line on stdout would break the object.
6
- import type { StdioOptions } from "node:child_process";
2
+ // nothing else, so an agent parses it whole. stderr carries only what the
3
+ // agent must read: a refusal, a handoff's `for you:` line, or a child's failure.
4
+ // Progress is dropped, and every child the CLI starts (Wrangler, the store's
5
+ // checks and scenarios) is captured and shown, its last lines on stderr, only
6
+ // when it fails: an agent's tool shows both streams as one, so anything chatty
7
+ // there reads as part of the answer (p0.10.1 §1, 7).
8
+ import type { SpawnSyncReturns, StdioOptions } from "node:child_process";
7
9
 
8
10
  let reserved = false;
9
11
 
@@ -13,8 +15,8 @@ export async function withJsonOutput<T>(on: boolean, work: () => Promise<T>): Pr
13
15
  const log = console.log;
14
16
  const info = console.info;
15
17
  reserved = true;
16
- console.log = console.error;
17
- console.info = console.error;
18
+ console.log = () => {};
19
+ console.info = () => {};
18
20
  try {
19
21
  return await work();
20
22
  } finally {
@@ -29,7 +31,14 @@ export function emit(result: Record<string, unknown>): void {
29
31
  process.stdout.write(`${JSON.stringify(result)}\n`);
30
32
  }
31
33
 
32
- /** A child's stdio: inherited, except that its stdout goes to stderr while stdout is reserved. */
34
+ /** A child's stdio: inherited, or under `--json` captured, for `childFailed` to show if it fails. */
33
35
  export function childStdio(): StdioOptions {
34
- return reserved ? ["inherit", 2, "inherit"] : "inherit";
36
+ return reserved ? ["ignore", "pipe", "pipe"] : "inherit";
37
+ }
38
+
39
+ /** Under `--json`, a captured child that failed shows its last lines on stderr; inherited output was already seen. */
40
+ export function childFailed(r: Pick<SpawnSyncReturns<string | Buffer>, "status" | "stdout" | "stderr">): void {
41
+ if (!reserved || r.status === 0) return;
42
+ const out = `${r.stdout?.toString() ?? ""}${r.stderr?.toString() ?? ""}`.trim();
43
+ if (out) process.stderr.write(`${out.split("\n").slice(-40).join("\n")}\n`);
35
44
  }
package/src/preview.ts CHANGED
@@ -33,7 +33,7 @@ import type { Lock } from "./compose";
33
33
  import { CORE_SECRETS, DeployRefused, openOps, stageAssets } from "./deploy";
34
34
  import { assetsShape, declaredClasses, readWrangler } from "./doctor";
35
35
  import { LostOwnership, OperationHeld, type OpsStore } from "./ops";
36
- import { childStdio } from "./output";
36
+ import { childFailed, childStdio } from "./output";
37
37
  import { hashTree, type StoreNames, sha256, type WranglerJson } from "./store";
38
38
  import { readTar, type TarEntry, TarRefused, writeTar } from "./tar";
39
39
 
@@ -1018,7 +1018,9 @@ export function runScenarios(cwd: string, credentials: StoreCredentials, env: No
1018
1018
  cwd,
1019
1019
  stdio: childStdio(),
1020
1020
  env: { ...env, KEDGE_PREVIEW_CREDENTIALS: JSON.stringify(credentials) },
1021
+ maxBuffer: 1 << 26,
1021
1022
  });
1023
+ childFailed(r);
1022
1024
  return r.status ?? 1;
1023
1025
  } finally {
1024
1026
  rmSync(tmp, { recursive: true, force: true });
package/src/smoke.ts CHANGED
@@ -47,11 +47,25 @@ export interface SmokeResult {
47
47
  status: string;
48
48
  }
49
49
 
50
- const table = (db: DatabaseSync) =>
51
- db.exec(`CREATE TABLE IF NOT EXISTS smoke_run (
52
- name TEXT PRIMARY KEY, store TEXT NOT NULL, storefront_id TEXT NOT NULL,
50
+ const COLUMNS = `store TEXT NOT NULL, name TEXT NOT NULL, storefront_id TEXT NOT NULL,
53
51
  cart_id TEXT, cart_token TEXT, completion TEXT, order_id TEXT, order_token TEXT, number TEXT, abandoned TEXT,
54
- created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`);
52
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL`;
53
+
54
+ /**
55
+ * Runs keyed by store and name (p0.10.1 §5): `first` on kedge dev and `first` on
56
+ * Cloudflare are two runs. A 0.10.0 checkout's `smoke_run`, keyed by name alone,
57
+ * is copied in whole the first time, every recorded moment with it, so a run it
58
+ * stopped resumes; the old table is left as it was.
59
+ */
60
+ function table(db: DatabaseSync) {
61
+ db.exec(`CREATE TABLE IF NOT EXISTS smoke_runs (${COLUMNS}, PRIMARY KEY (store, name))`);
62
+ const legacy = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'smoke_run'").get();
63
+ if (legacy)
64
+ db.exec(`INSERT OR IGNORE INTO smoke_runs (store, name, storefront_id, cart_id, cart_token, completion, order_id,
65
+ order_token, number, abandoned, created_at, updated_at)
66
+ SELECT store, name, storefront_id, cart_id, cart_token, completion, order_id, order_token, number, abandoned,
67
+ created_at, updated_at FROM smoke_run`);
68
+ }
55
69
 
56
70
  /** The smoke run `name` on `target`: resumed, or answered again once it has its number. */
57
71
  export async function smoke(target: StoreTarget, name: string, d: SmokeDeps): Promise<SmokeResult> {
@@ -69,20 +83,18 @@ export async function smoke(target: StoreTarget, name: string, d: SmokeDeps): Pr
69
83
  async function run(db: DatabaseSync, target: StoreTarget, name: string, d: SmokeDeps): Promise<SmokeResult> {
70
84
  const now = d.now ?? Date.now;
71
85
  const store = identityOf(target);
72
- const get = () => db.prepare("SELECT * FROM smoke_run WHERE name = ?").get(name) as unknown as Run | undefined;
86
+ const get = () =>
87
+ db.prepare("SELECT * FROM smoke_runs WHERE store = ? AND name = ?").get(store, name) as unknown as Run | undefined;
73
88
  const set = (a: Partial<Run>) => {
74
89
  const cols = Object.keys(a);
75
- db.prepare(`UPDATE smoke_run SET ${cols.map((c) => `${c} = ?`).join(", ")}, updated_at = ? WHERE name = ?`).run(
76
- ...cols.map((c) => (a as Record<string, string | null>)[c] ?? null),
77
- now(),
78
- name,
79
- );
90
+ db.prepare(
91
+ `UPDATE smoke_runs SET ${cols.map((c) => `${c} = ?`).join(", ")}, updated_at = ? WHERE store = ? AND name = ?`,
92
+ ).run(...cols.map((c) => (a as Record<string, string | null>)[c] ?? null), now(), store, name);
80
93
  };
81
94
 
82
95
  let r = get();
83
96
  // A finished run answers its order again, sending nothing: a retried command can't place a second one.
84
97
  if (r?.number && r.order_id) {
85
- if (r.store !== store) throw new DeployRefused(`smoke run ${name} belongs to another store (${r.store})`);
86
98
  return { run: name, order_id: r.order_id, number: r.number, status: "open" };
87
99
  }
88
100
  if (r?.abandoned)
@@ -106,19 +118,15 @@ async function run(db: DatabaseSync, target: StoreTarget, name: string, d: Smoke
106
118
 
107
119
  if (!r) {
108
120
  const t = now();
109
- db.prepare("INSERT INTO smoke_run (name, store, storefront_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(
110
- name,
111
- store,
112
- storefront,
113
- t,
114
- t,
115
- );
121
+ db.prepare(
122
+ "INSERT INTO smoke_runs (store, name, storefront_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
123
+ ).run(store, name, storefront, t, t);
116
124
  r = get() as Run;
117
125
  }
118
- // A cart admits only its own storefront's key: a record for another store or storefront is refused, not reused.
119
- if (r.store !== store || r.storefront_id !== storefront)
126
+ // A cart admits only its own storefront's key: a record for another storefront is refused, not reused.
127
+ if (r.storefront_id !== storefront)
120
128
  throw new DeployRefused(
121
- `smoke run ${name} was made for ${r.store === store ? `storefront ${r.storefront_id}` : r.store}: start another with --run <new name>`,
129
+ `smoke run ${name} was made for storefront ${r.storefront_id}: start another with --run <new name>`,
122
130
  );
123
131
 
124
132
  const shop = { "x-kedge-key": key };