@pithy-sh/cli 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.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +72 -0
  3. package/scripts/templateManifest.ts +49 -0
  4. package/scripts/tsconfig.json +26 -0
  5. package/scripts/vendorTemplate.ts +84 -0
  6. package/scripts/verifyPack.ts +88 -0
  7. package/src/audit/cliAudit.ts +406 -0
  8. package/src/bin.ts +111 -0
  9. package/src/capabilities/add.ts +288 -0
  10. package/src/capabilities/addBootstrap.ts +275 -0
  11. package/src/capabilities/catalog.ts +175 -0
  12. package/src/capabilities/compose.ts +39 -0
  13. package/src/capabilities/configConstants.ts +74 -0
  14. package/src/capabilities/configImports.ts +397 -0
  15. package/src/capabilities/eject.ts +331 -0
  16. package/src/capabilities/emailProvisioner.ts +346 -0
  17. package/src/capabilities/entitlementGap.ts +70 -0
  18. package/src/capabilities/entryExports.ts +162 -0
  19. package/src/capabilities/flow.ts +550 -0
  20. package/src/capabilities/hostRegistry.ts +368 -0
  21. package/src/capabilities/loadFailure.ts +208 -0
  22. package/src/capabilities/manifests.ts +238 -0
  23. package/src/capabilities/mediaProvisioner.ts +471 -0
  24. package/src/capabilities/mintSecrets.ts +306 -0
  25. package/src/capabilities/paymentsProvisioner.ts +207 -0
  26. package/src/capabilities/prerequisites.ts +168 -0
  27. package/src/capabilities/r2Bucket.ts +113 -0
  28. package/src/capabilities/reconcile.ts +1483 -0
  29. package/src/capabilities/remove.ts +597 -0
  30. package/src/capabilities/requiredOptions.ts +92 -0
  31. package/src/capabilities/rotateSecrets.ts +305 -0
  32. package/src/capabilities/secrets.ts +178 -0
  33. package/src/capabilities/secretsDispatcher.ts +29 -0
  34. package/src/capabilities/secretsProvisioner.ts +389 -0
  35. package/src/capabilities/storageProvisioner.ts +414 -0
  36. package/src/capabilities/supportProvisioner.ts +515 -0
  37. package/src/capabilities/testersLoader.ts +52 -0
  38. package/src/capabilities/testersProvisioner.ts +236 -0
  39. package/src/capabilities/turnstileProvisioner.ts +347 -0
  40. package/src/capabilities/vectorProvisioner.ts +260 -0
  41. package/src/ci/fileModes.ts +223 -0
  42. package/src/ci/sourceFiles.ts +200 -0
  43. package/src/ci/workflowDrivers.ts +524 -0
  44. package/src/cloudflare/accountAnswer.ts +110 -0
  45. package/src/cloudflare/config.ts +685 -0
  46. package/src/cloudflare/storeId.ts +129 -0
  47. package/src/commands/add.ts +372 -0
  48. package/src/commands/alias.ts +205 -0
  49. package/src/commands/dashboard.ts +651 -0
  50. package/src/commands/deploy.ts +150 -0
  51. package/src/commands/dev.ts +37 -0
  52. package/src/commands/doctor.ts +2059 -0
  53. package/src/commands/email.ts +425 -0
  54. package/src/commands/env.ts +155 -0
  55. package/src/commands/feature.ts +359 -0
  56. package/src/commands/init.ts +538 -0
  57. package/src/commands/media.ts +303 -0
  58. package/src/commands/migrate.ts +129 -0
  59. package/src/commands/payments.ts +336 -0
  60. package/src/commands/provision.ts +368 -0
  61. package/src/commands/remove.ts +151 -0
  62. package/src/commands/secrets.ts +652 -0
  63. package/src/commands/seed.ts +229 -0
  64. package/src/commands/storage.ts +309 -0
  65. package/src/commands/support.ts +331 -0
  66. package/src/commands/testers.ts +1020 -0
  67. package/src/commands/token.ts +364 -0
  68. package/src/commands/turnstile.ts +271 -0
  69. package/src/commands/ui.ts +222 -0
  70. package/src/commands/upgrade.ts +517 -0
  71. package/src/commands/vector.ts +390 -0
  72. package/src/commands/worker.ts +295 -0
  73. package/src/dashboard/api.ts +323 -0
  74. package/src/dashboard/connect.ts +758 -0
  75. package/src/dashboard/contract.ts +289 -0
  76. package/src/dashboard/grant.ts +124 -0
  77. package/src/dashboard/registry.ts +519 -0
  78. package/src/dashboard/resolveTarget.ts +119 -0
  79. package/src/dev/delivery.ts +174 -0
  80. package/src/dev/devLogin.ts +155 -0
  81. package/src/dev/devLoginTargets.ts +91 -0
  82. package/src/dev/env.ts +206 -0
  83. package/src/dev/hostWorkers.ts +290 -0
  84. package/src/dev/keys.ts +111 -0
  85. package/src/dev/logging.ts +87 -0
  86. package/src/dev/openUrl.ts +75 -0
  87. package/src/dev/orchestrator.ts +1014 -0
  88. package/src/dev/ports.ts +220 -0
  89. package/src/dev/readyWatch.ts +142 -0
  90. package/src/dev/state.ts +90 -0
  91. package/src/devSecrets/bootstrapVars.ts +265 -0
  92. package/src/devSecrets/devVars.ts +240 -0
  93. package/src/devSecrets/edit.ts +256 -0
  94. package/src/devSecrets/file.ts +277 -0
  95. package/src/devSecrets/generate.ts +428 -0
  96. package/src/devSecrets/location.ts +80 -0
  97. package/src/devSecrets/mode.ts +71 -0
  98. package/src/devSecrets/records.ts +30 -0
  99. package/src/devSecrets/report.ts +99 -0
  100. package/src/devSecrets/seed.ts +344 -0
  101. package/src/devSecrets/store.ts +262 -0
  102. package/src/devSecrets/targets.ts +204 -0
  103. package/src/dispatch.ts +147 -0
  104. package/src/docs/catalog.ts +246 -0
  105. package/src/docs/writeCatalog.ts +45 -0
  106. package/src/doctor/cloudflare.ts +287 -0
  107. package/src/doctor/devPreferences.ts +155 -0
  108. package/src/doctor/devSecrets.ts +464 -0
  109. package/src/doctor/devVars.ts +414 -0
  110. package/src/doctor/devVarsLocal.ts +138 -0
  111. package/src/doctor/environments.ts +155 -0
  112. package/src/doctor/health.ts +354 -0
  113. package/src/doctor/localDelivery.ts +91 -0
  114. package/src/doctor/portsRegistry.ts +252 -0
  115. package/src/doctor/projectName.ts +584 -0
  116. package/src/doctor/secretBindings.ts +166 -0
  117. package/src/doctor/settings.ts +274 -0
  118. package/src/doctor/settingsSources.ts +202 -0
  119. package/src/doctor/workerName.ts +174 -0
  120. package/src/doctor/wranglerVars.ts +33 -0
  121. package/src/feature/bindings.ts +93 -0
  122. package/src/feature/create.ts +179 -0
  123. package/src/feature/destroy.ts +160 -0
  124. package/src/feature/devConfig.ts +201 -0
  125. package/src/feature/identity.ts +100 -0
  126. package/src/feature/manifest.ts +132 -0
  127. package/src/feature/ports.ts +615 -0
  128. package/src/feature/provision.ts +362 -0
  129. package/src/feature/sync.ts +148 -0
  130. package/src/feature/worktree.ts +282 -0
  131. package/src/help/groups.ts +47 -0
  132. package/src/help/rootUsage.ts +135 -0
  133. package/src/main.ts +73 -0
  134. package/src/migrations/ledger.ts +129 -0
  135. package/src/migrations/registry.ts +47 -0
  136. package/src/migrations/run.ts +1066 -0
  137. package/src/notifier/check.ts +129 -0
  138. package/src/notifier/installer.ts +48 -0
  139. package/src/notifier/notify.ts +152 -0
  140. package/src/notifier/state.ts +248 -0
  141. package/src/notifier/version.ts +59 -0
  142. package/src/platform/editor.ts +333 -0
  143. package/src/platform/rc.ts +118 -0
  144. package/src/platform/shell.ts +83 -0
  145. package/src/project/appBindings.ts +184 -0
  146. package/src/project/appWorkflows.ts +266 -0
  147. package/src/project/applyDomains.ts +166 -0
  148. package/src/project/askDomains.ts +220 -0
  149. package/src/project/atomic.ts +466 -0
  150. package/src/project/bindingEntries.ts +425 -0
  151. package/src/project/config.ts +701 -0
  152. package/src/project/dashboard.ts +118 -0
  153. package/src/project/deploy.ts +364 -0
  154. package/src/project/devVars.ts +113 -0
  155. package/src/project/domainPrompt.ts +191 -0
  156. package/src/project/domains.ts +386 -0
  157. package/src/project/envInventory.ts +356 -0
  158. package/src/project/environment.ts +125 -0
  159. package/src/project/extensions.ts +69 -0
  160. package/src/project/jsonc.ts +289 -0
  161. package/src/project/packageManager.ts +238 -0
  162. package/src/project/readOptionalFile.ts +342 -0
  163. package/src/project/rollback.ts +145 -0
  164. package/src/project/scaffold.ts +1088 -0
  165. package/src/project/templateFiles.ts +53 -0
  166. package/src/project/verifyDeploy.ts +230 -0
  167. package/src/project/versionMetadata.ts +77 -0
  168. package/src/project/workerAddress.ts +176 -0
  169. package/src/project/workerCommand.ts +564 -0
  170. package/src/project/workerIdentity.ts +50 -0
  171. package/src/project/workerManifest.ts +135 -0
  172. package/src/project/workerScaffold.ts +289 -0
  173. package/src/project/workerScope.ts +394 -0
  174. package/src/project/workers.ts +86 -0
  175. package/src/project/workflows.ts +281 -0
  176. package/src/project/wrangler.ts +168 -0
  177. package/src/provision/confirm.ts +86 -0
  178. package/src/provision/environment.ts +407 -0
  179. package/src/provision/featureConfig.ts +98 -0
  180. package/src/provision/mode.ts +62 -0
  181. package/src/provision/pendingSecrets.ts +96 -0
  182. package/src/provision/resources.ts +126 -0
  183. package/src/provision/secretBindings.ts +149 -0
  184. package/src/provision/store.ts +33 -0
  185. package/src/provision/unprovisioned.ts +114 -0
  186. package/src/provision/wranglerEnv.ts +220 -0
  187. package/src/rootFlags.ts +48 -0
  188. package/src/seed/drivers.ts +423 -0
  189. package/src/seed/media.ts +187 -0
  190. package/src/seed/plan.ts +137 -0
  191. package/src/seed/prepare.ts +224 -0
  192. package/src/seed/registry.ts +25 -0
  193. package/src/seed/run.ts +793 -0
  194. package/src/seed/safety.ts +206 -0
  195. package/src/terminal/logger.ts +42 -0
  196. package/src/terminal/output.ts +64 -0
  197. package/src/terminal/style.ts +132 -0
  198. package/src/test-utils/doctorHarness.ts +190 -0
  199. package/src/test-utils/migrateHarness.ts +126 -0
  200. package/src/test-utils/seedHarness.ts +173 -0
  201. package/src/test-utils/tempRepo.ts +45 -0
  202. package/src/tokens/config.ts +16 -0
  203. package/src/tokens/engine.ts +345 -0
  204. package/src/tokens/mintedTokens.ts +233 -0
  205. package/src/tokens/sinks.ts +84 -0
  206. package/src/ui/flow.ts +451 -0
  207. package/src/ui/react.ts +112 -0
  208. package/src/ui/routeAllowlist.ts +208 -0
  209. package/src/ui/scaffold.ts +113 -0
  210. package/src/ui/screenStyles.ts +127 -0
  211. package/src/ui/stubs.ts +135 -0
  212. package/src/ui/templates.ts +52 -0
  213. package/src/ui/wire.ts +311 -0
  214. package/src/ui/workerUi.ts +172 -0
  215. package/templates/starter/.dev.secrets.example.jsonc +43 -0
  216. package/templates/starter/.dev.vars.example +30 -0
  217. package/templates/starter/apps/api/package.json +22 -0
  218. package/templates/starter/apps/api/pithy.config.ts +65 -0
  219. package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
  220. package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
  221. package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
  222. package/templates/starter/apps/api/src/index.ts +8 -0
  223. package/templates/starter/apps/api/tsconfig.json +26 -0
  224. package/templates/starter/apps/api/wrangler.jsonc +68 -0
  225. package/templates/starter/biome.template.jsonc +75 -0
  226. package/templates/starter/gitignore +37 -0
  227. package/templates/starter/package.json +28 -0
  228. package/templates/starter/pithy.config.ts +67 -0
  229. package/templates/starter/plugins/no-console.grit +25 -0
  230. package/templates/starter/plugins/no-process-io.grit +25 -0
  231. package/templates/starter/tsconfig.json +14 -0
  232. package/templates/starter/tsconfig.tools.json +30 -0
  233. package/templates/starter/vitest.config.ts +124 -0
  234. package/templates/starter/vitest.workers.config.ts +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pithy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@pithy-sh/cli",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/pithy-sh/pithy.git",
8
+ "directory": "packages/cli"
9
+ },
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "bin": {
15
+ "pithy": "./src/bin.ts"
16
+ },
17
+ "exports": {
18
+ "./src/*": "./src/*.ts"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "scripts",
23
+ "templates",
24
+ "!src/**/*.test.*"
25
+ ],
26
+ "scripts": {
27
+ "prepack": "bun scripts/vendorTemplate.ts",
28
+ "postpack": "bun scripts/vendorTemplate.ts --clean",
29
+ "pack:verify": "bun scripts/verifyPack.ts",
30
+ "build": "tsc -p tsconfig.json --noEmit false --outDir dist",
31
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.contract.json && tsc -p scripts/tsconfig.json && tsc -p tsconfig.ciScripts.json",
32
+ "test": "vitest run",
33
+ "test:node": "vitest run",
34
+ "test:integration": "vitest run --config vitest.integration.config.ts",
35
+ "clean": "rm -rf dist .turbo",
36
+ "reset": "bun run clean && rm -rf node_modules"
37
+ },
38
+ "dependencies": {
39
+ "@clack/prompts": "^1.7.0",
40
+ "@cloudflare/workers-types": "^5.20260729.1",
41
+ "@pithy-sh/cloudflare": "workspace:*",
42
+ "@pithy-sh/core": "workspace:*",
43
+ "@pithy-sh/email": "workspace:*",
44
+ "@pithy-sh/secrets": "workspace:*",
45
+ "@pithy-sh/turnstile": "workspace:*",
46
+ "@pithy-sh/ui-react": "workspace:*",
47
+ "citty": "^0.2.2",
48
+ "comment-json": "^5.0.0",
49
+ "kysely": "^0.29.0",
50
+ "miniflare": "^4.20260722.1",
51
+ "picocolors": "^1.1.1"
52
+ },
53
+ "devDependencies": {
54
+ "@pithy-sh/audit": "workspace:*",
55
+ "@pithy-sh/auth": "workspace:*",
56
+ "@pithy-sh/i18n": "workspace:*",
57
+ "@pithy-sh/media": "workspace:*",
58
+ "@pithy-sh/payments": "workspace:*",
59
+ "@pithy-sh/storage": "workspace:*",
60
+ "@pithy-sh/support": "workspace:*",
61
+ "@pithy-sh/testers": "workspace:*",
62
+ "@pithy-sh/tsconfig": "workspace:*",
63
+ "@pithy-sh/vector": "workspace:*",
64
+ "@types/node": "^22.15.0",
65
+ "@vitest/coverage-v8": "^4.1.0",
66
+ "rolldown": "^1.2.4",
67
+ "typescript": "^7.0.2",
68
+ "vitest": "^4.1.0",
69
+ "wrangler": "^4.115.0",
70
+ "zod": "^4.0.0"
71
+ }
72
+ }
@@ -0,0 +1,49 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * What the published `@pithy-sh/cli` is allowed to carry from the starter template, and where it comes from.
6
+ *
7
+ * Side-effect free, because two scripts need the same answer: `vendorTemplate.ts` copies the allowlist in,
8
+ * `verifyPack.ts` checks the tarball against it. One definition, or the check agrees with the copy by
9
+ * coincidence.
10
+ */
11
+
12
+ import { dirname, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { committedFiles } from "../src/project/templateFiles";
15
+
16
+ /** `packages/cli`. */
17
+ export const CLI_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
18
+
19
+ /** The repo root's starter — the single copy. Present in a checkout, absent in an installed package. */
20
+ export const SOURCE = resolve(CLI_DIR, "../../templates/starter");
21
+
22
+ /** Where `prepack` puts it, and the only path `files` carries. */
23
+ export const TEMPLATES_DIR = resolve(CLI_DIR, "templates");
24
+
25
+ /** The template's path inside the package, and inside the tarball under `package/`. */
26
+ export const VENDORED = "templates/starter";
27
+
28
+ /**
29
+ * The starter's committed files, relative to `source`, sorted. **The allowlist.**
30
+ *
31
+ * {@link committedFiles} is the allowlist itself, and it lives in `src/` because `pithy init` reads it
32
+ * too: the packer decides what a published tarball carries, and the scaffold decides what an adopter's
33
+ * new project carries, and both were reading the same directory by different rules. `.dev.vars` got past
34
+ * `files` into the tarball (#145) and past `cp` into an adopter's project (#152) — one defect, two
35
+ * readers, and fixing the packer alone left the worse half open.
36
+ *
37
+ * What is not shared is the answer to "no index". Here it is fatal: a tarball built from a working tree
38
+ * is exactly what the allowlist exists to prevent, so a pack outside a checkout must stop. `pithy init`
39
+ * carries on, because an installed CLI has a vendored copy and no `.git` to ask.
40
+ */
41
+ export function templateFiles(source: string): string[] {
42
+ const files = committedFiles(source);
43
+ if (files === null) {
44
+ throw new Error(
45
+ `Could not read the git index at ${source}, or nothing under it is committed. Pack from a checkout, with git available.`,
46
+ );
47
+ }
48
+ return files;
49
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ // A third program, for the same reason `tsconfig.contract.json` is a second one: these files are
3
+ // outside `src`, so the package's own tsconfig — `rootDir: "src"` — cannot include them without
4
+ // moving its emit root. They were therefore in no program at all, and the script that decides
5
+ // whether a published CLI can run its first command was never typechecked. `bun` ran it anyway.
6
+ //
7
+ // Node only. Nothing here executes in a Worker; it runs on the machine doing the packing.
8
+ //
9
+ // `rootDir` is the package, not this directory, so one module under `src` can be shared with the
10
+ // scaffold that reads the same starter directory the packer does — `src/project/templateFiles.ts`.
11
+ // The rule it holds is "what git has committed here", and duplicating it is how the packer and
12
+ // `pithy init` came to disagree about `.dev.vars`.
13
+ //
14
+ // Only that one file crosses, and only in this direction. `src` roots at `src`, so a module there
15
+ // importing a script is `TS6059`; a script importing `src` is fine as long as the module it reaches
16
+ // has no project imports of its own. That one has none — `node:child_process` and `node:path` — so
17
+ // nothing of the CLI's graph, and none of its Workers types, arrives in this program behind it.
18
+ // `include` still names only this directory: the shared module joins through the import, as a leaf.
19
+ "extends": "@pithy-sh/tsconfig/base.json",
20
+ "compilerOptions": {
21
+ "rootDir": "..",
22
+ "lib": ["ES2022"],
23
+ "types": ["node"]
24
+ },
25
+ "include": ["**/*.ts"]
26
+ }
@@ -0,0 +1,84 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * Vendor the starter template into the package for publication, and take it out again after.
6
+ *
7
+ * `templates/starter` lives at the repo root — one copy, read by the CLI and by the tests that hold
8
+ * the template to the kit's rules. That path is outside `packages/cli`, so `npm pack` never saw it and
9
+ * a published `@pithy-sh/cli` shipped no template at all. `prepack` copies it in, `postpack --clean`
10
+ * removes it, and `files` in the manifest carries it into the tarball. The copy is never committed:
11
+ * two copies of the starter is the failure mode this avoids, not the one it introduces.
12
+ *
13
+ * What gets copied is the allowlist in `templateManifest.ts`, file by file — never the directory. A
14
+ * recursive copy published the maintainer's working tree, `.dev.vars` included.
15
+ *
16
+ * Run by the packer, so it fails loudly. A silent no-op here publishes a CLI whose first command cannot
17
+ * run. `verifyPack.ts` is the post-condition on the artefact, for the packs this hook never sees.
18
+ */
19
+
20
+ import { copyFile, lstat, mkdir, readdir, rm, stat } from "node:fs/promises";
21
+ import { dirname, join, relative, sep } from "node:path";
22
+ import { SOURCE, TEMPLATES_DIR, templateFiles } from "./templateManifest";
23
+
24
+ const TARGET = join(TEMPLATES_DIR, "starter");
25
+
26
+ /** Every file under `dir`, relative to it, sorted, with `/` separators. */
27
+ async function walk(dir: string): Promise<string[]> {
28
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
29
+ return entries
30
+ .filter((entry) => entry.isFile())
31
+ .map((entry) => relative(dir, join(entry.parentPath, entry.name)).split(sep).join("/"))
32
+ .sort();
33
+ }
34
+
35
+ async function vendor(): Promise<void> {
36
+ const files = templateFiles(SOURCE);
37
+ await rm(TEMPLATES_DIR, { recursive: true, force: true });
38
+ try {
39
+ for (const file of files) {
40
+ const from = join(SOURCE, file);
41
+ // Regular files only. A symlink in the tarball resolves inside the adopter's `node_modules`, and a
42
+ // committed one aimed out of the template would copy whatever it points at.
43
+ if (!(await lstat(from)).isFile()) throw new Error(`${from} is not a regular file. The template ships files.`);
44
+ const to = join(TARGET, file);
45
+ await mkdir(dirname(to), { recursive: true });
46
+ await copyFile(from, to);
47
+ }
48
+ // The post-condition, checked rather than assumed: what landed is the allowlist, exactly.
49
+ const copied = await walk(TARGET);
50
+ if (copied.join("\n") !== files.join("\n")) {
51
+ throw new Error(`Vendored ${TARGET} does not match the git index:\n got ${copied.join(", ")}`);
52
+ }
53
+ // stderr, not stdout: `npm pack --json` splices a script's stdout into the JSON it prints.
54
+ process.stderr.write(`Vendored ${files.length} committed template files into ${TARGET}\n`);
55
+ } catch (error) {
56
+ // A half-copied `templates/` outlives the failed pack, is gitignored, and then shadows the repo root
57
+ // for every later run in this checkout. Leave nothing.
58
+ await rm(TEMPLATES_DIR, { recursive: true, force: true });
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * True in a checkout, false in an installed package.
65
+ *
66
+ * `files` ships `scripts/` now, because a published manifest naming `prepack` and `postpack` has to carry
67
+ * what they run. So both hooks can fire inside somebody's `node_modules` — where the repo-root template
68
+ * does not exist and `templates/starter` is already in place. There, both are no-ops. The clean hook
69
+ * especially: deleting the template out of an installed package is the defect this file exists to prevent.
70
+ */
71
+ async function inCheckout(): Promise<boolean> {
72
+ return (await stat(SOURCE).catch(() => null))?.isDirectory() === true;
73
+ }
74
+
75
+ if (!(await inCheckout())) {
76
+ if (!(await stat(join(TARGET, "package.json")).catch(() => null))?.isFile()) {
77
+ throw new Error(`No starter template at ${SOURCE} and none vendored at ${TARGET}. This install is broken.`);
78
+ }
79
+ process.stderr.write(`Template already in place at ${TARGET}. Nothing to do.\n`);
80
+ } else if (process.argv.includes("--clean")) {
81
+ await rm(TEMPLATES_DIR, { recursive: true, force: true });
82
+ } else {
83
+ await vendor();
84
+ }
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * Hold a `@pithy-sh/cli` tarball to what it must contain, and fail the pack when it does not.
6
+ *
7
+ * `files` does not fail on a missing path. Pack with scripts disabled — `npm pack --ignore-scripts`, or
8
+ * any publish path that turns lifecycle hooks off — and `prepack` never vendors the starter, `templates`
9
+ * matches nothing, and the tarball ships with no template at all: silently, exit 0. That is a straight
10
+ * regression to the defect this whole mechanism was built to close, and the mechanism itself could not
11
+ * detect it, because the only check lived inside the hook that did not run.
12
+ *
13
+ * This is that missing post-condition, and it sits on the artefact rather than on the hook. Nothing our
14
+ * manifest declares can execute during a pack that refuses to execute scripts — so the check has to be a
15
+ * separate step, run by CI on every commit and by whatever publishes, on the exact file it is about to
16
+ * push. `bun run pack:verify` packs and checks; `bun run pack:verify <tarball>` checks one already built.
17
+ *
18
+ * packages/cli $ bun run pack:verify
19
+ * packages/cli $ bun run pack:verify ../../pithy-sh-cli-1.2.3.tgz
20
+ */
21
+
22
+ import { execFileSync } from "node:child_process";
23
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { join } from "node:path";
26
+ import { CLI_DIR, SOURCE, templateFiles, VENDORED } from "./templateManifest";
27
+
28
+ /** Everything the tarball holds, relative to the package root, `/`-separated and sorted. */
29
+ function entries(tarball: string): string[] {
30
+ const listed = execFileSync("tar", ["-tzf", tarball], { encoding: "utf8" });
31
+ return listed
32
+ .split("\n")
33
+ .filter((line) => line.length > 0 && !line.endsWith("/"))
34
+ .map((line) => line.replace(/^package\//, ""))
35
+ .sort();
36
+ }
37
+
38
+ function verify(tarball: string): string[] {
39
+ const packed = entries(tarball);
40
+ const expected = templateFiles(SOURCE).map((file) => `${VENDORED}/${file}`);
41
+ const faults: string[] = [];
42
+
43
+ const shipped = packed.filter((file) => file.startsWith("templates/"));
44
+ const missing = expected.filter((file) => !shipped.includes(file));
45
+ const extra = shipped.filter((file) => !expected.includes(file));
46
+ if (missing.length > 0) {
47
+ faults.push(`${missing.length} template file(s) missing — pithy init cannot run:\n ${missing.join("\n ")}`);
48
+ }
49
+ // The security half. A tarball is published once and cannot be unpublished.
50
+ if (extra.length > 0) {
51
+ faults.push(`${extra.length} file(s) under templates/ that are not committed:\n ${extra.join("\n ")}`);
52
+ }
53
+
54
+ const tests = packed.filter((file) => file.startsWith("src/") && file.endsWith(".test.ts"));
55
+ if (tests.length > 0) faults.push(`${tests.length} of the CLI's own test file(s) shipped.`);
56
+
57
+ // `prepack` and `postpack` run wherever the package sits, an adopter's `node_modules` included.
58
+ const referenced = new Set<string>();
59
+ const manifest = JSON.parse(readFileSync(join(CLI_DIR, "package.json"), "utf8")) as {
60
+ scripts?: Record<string, string>;
61
+ };
62
+ for (const command of Object.values(manifest.scripts ?? {})) {
63
+ for (const match of command.matchAll(/(?:^|\s)(scripts\/[\w./-]+\.ts)/g)) referenced.add(match[1] as string);
64
+ }
65
+ const absent = [...referenced].filter((file) => !packed.includes(file)).sort();
66
+ if (absent.length > 0)
67
+ faults.push(`the manifest runs script(s) the tarball does not carry:\n ${absent.join("\n ")}`);
68
+
69
+ return faults;
70
+ }
71
+
72
+ const given = process.argv[2];
73
+ const workDir = mkdtempSync(join(tmpdir(), "pithy-verify-pack-"));
74
+ try {
75
+ let tarball = given;
76
+ if (tarball === undefined) {
77
+ tarball = join(workDir, "cli.tgz");
78
+ execFileSync("bun", ["pm", "pack", "--quiet", "--filename", tarball], { cwd: CLI_DIR, stdio: "inherit" });
79
+ }
80
+ const faults = verify(tarball);
81
+ if (faults.length > 0) {
82
+ process.stderr.write(`${tarball} is not publishable.\n\n${faults.join("\n\n")}\n`);
83
+ process.exit(1);
84
+ }
85
+ process.stderr.write(`${tarball} checks out.\n`);
86
+ } finally {
87
+ rmSync(workDir, { recursive: true, force: true });
88
+ }