@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
@@ -0,0 +1,390 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { CloudflareClients } from "@pithy-sh/cloudflare/src/client/clients";
7
+ import { CloudflareWorkflowsClient } from "@pithy-sh/cloudflare/src/workflows/workflowsClient";
8
+ import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
9
+ import { defineCommand } from "citty";
10
+ import { parse } from "comment-json";
11
+ import { createRemoteCliAudit } from "../audit/cliAudit";
12
+ import { CloudflareVectorProvisioner, loadVector, type VectorModule } from "../capabilities/vectorProvisioner";
13
+ import { type CloudflareAccountSelection, cloudflareEnv } from "../cloudflare/config";
14
+ import { type AppVectorizeBinding, applyAppBindings, appWorkflowBindings } from "../project/appBindings";
15
+ import { loadProject, projectCloudflareAccount, requireProjectName } from "../project/config";
16
+ import { ENV_ARG, requireEnvironment } from "../project/environment";
17
+ import { projectCapabilities, resolveWorkers } from "../project/workerScope";
18
+ import { readWranglerConfig, writeWranglerConfig } from "../project/wrangler";
19
+ import { assertResetConfirmed, resetConfirmPhrase } from "../seed/safety";
20
+ import { formatDone, formatJsonLine, withErrorReporting } from "../terminal/output";
21
+
22
+ /**
23
+ * `pithy vector provision | reset | reprocess` — the three commands that stand a search index up, rebuild
24
+ * it, and re-embed it.
25
+ *
26
+ * All three are `--env`-targeted, and `dev` is a real environment here rather than a local one: Cloudflare
27
+ * ships no local emulation for Vectorize, so a dev search reaches a real remote index. That is also why the
28
+ * capability's bindings are declared `remote`.
29
+ *
30
+ * `reset` carries the reset gate from `docs/commands/seed.md` — the same gate `pithy seed --redo` uses, imported
31
+ * rather than re-implemented. `--yes` does not unlock it: `--yes` means "yes, this is not dev", and it was
32
+ * designed to authorize additive writes. A reset deletes every vector in an index.
33
+ */
34
+
35
+ /**
36
+ * The Cloudflare credentials this command provisions with, for **the account the project belongs to**.
37
+ *
38
+ * The account is a parameter rather than an ambient, so this cannot resolve before something has
39
+ * established which account the project is for (#206).
40
+ */
41
+ function loadCloudflareCreds(account: CloudflareAccountSelection | null): { accountId: string; apiToken: string } {
42
+ const vars = cloudflareEnv({ account });
43
+ const accountId = vars.CLOUDFLARE_ACCOUNT_ID ?? "";
44
+ const apiToken = vars.CLOUDFLARE_API_TOKEN ?? "";
45
+ if (!accountId || !apiToken) {
46
+ throw new ValidationError({
47
+ message: "Cloudflare credentials are missing.",
48
+ action: "Run pithy init to record CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, or export them.",
49
+ });
50
+ }
51
+ return { accountId, apiToken };
52
+ }
53
+
54
+ /** Load the vector capability's resolved config from `pithy.config.ts`. */
55
+ async function loadVectorConfig(projectDir: string) {
56
+ const { isVectorCapability } = await loadVector();
57
+ // Capabilities live in each Worker's `apps/<name>/pithy.config.ts`; provisioning is one
58
+ // project-wide decision, so the first Worker composing this capability provides it.
59
+ const capability = (await resolveWorkers({ projectDir }).then(projectCapabilities)).find(isVectorCapability);
60
+ if (!capability) {
61
+ throw new ValidationError({
62
+ message: "The vector capability is not configured.",
63
+ action: "Add `vector({ indexes: { ... } })` to pithy.config.ts (run `pithy add vector`).",
64
+ });
65
+ }
66
+ return capability.vectorConfig;
67
+ }
68
+
69
+ /** The `wrangler.jsonc` slice the app database id is read from. */
70
+ interface WranglerAppConfig {
71
+ d1_databases?: { binding: string; database_id?: string }[];
72
+ env?: Record<string, { d1_databases?: { binding: string; database_id?: string }[] } | undefined>;
73
+ }
74
+
75
+ /**
76
+ * Resolve the app database id for an environment. `dev` reads the top-level bindings; every other
77
+ * environment reads its own `env.<name>` stanza — the same rule the audit target resolver uses.
78
+ */
79
+ function buildResolveEnv(projectDir: string): (env: string) => Promise<{ appDatabaseId: string }> {
80
+ return async (env) => {
81
+ const config = parse(await readFile(join(projectDir, "wrangler.jsonc"), "utf8")) as unknown as WranglerAppConfig;
82
+ const stanza = env === "dev" ? config : config.env?.[env];
83
+ const appDatabaseId = stanza?.d1_databases?.find((database) => database.binding === "DB")?.database_id;
84
+ if (!appDatabaseId) {
85
+ throw new ValidationError({
86
+ message: `wrangler.jsonc has no DB database_id for ${env}.`,
87
+ action: `Provision the ${env} app database and set its id on the DB binding — the corpus lives there.`,
88
+ });
89
+ }
90
+ return { appDatabaseId };
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Build the live provisioner for one environment, and resolve the project name every one of these
96
+ * commands needs. An index is *found by name and reused*, so the name must be the same on every run:
97
+ * `requireProjectName` refuses to guess where `resolveProjectName` would differ between checkouts.
98
+ */
99
+ async function buildProvisioner(projectDir: string, env: string) {
100
+ // The name first, before the credentials: both are local checks, and a config that cannot name the
101
+ // project is not a Cloudflare problem to report as one.
102
+ const project = requireProjectName(await loadProject(projectDir));
103
+ const { accountId, apiToken } = loadCloudflareCreds(await projectCloudflareAccount(projectDir));
104
+ const config = await loadVectorConfig(projectDir);
105
+ return {
106
+ project,
107
+ config,
108
+ env,
109
+ provisioner: new CloudflareVectorProvisioner({
110
+ cf: new CloudflareClients({ accountId, apiToken }),
111
+ project,
112
+ accountId,
113
+ apiToken,
114
+ config,
115
+ resolveEnv: buildResolveEnv(projectDir),
116
+ workflows: new CloudflareWorkflowsClient({ accountId, apiToken }),
117
+ }),
118
+ accountId,
119
+ apiToken,
120
+ };
121
+ }
122
+
123
+ /** The `wrangler.jsonc` slice the provisioning record is written into. */
124
+ interface WranglerVarsConfig {
125
+ vars?: Record<string, string>;
126
+ env?: Record<string, { vars?: Record<string, string> } | undefined>;
127
+ }
128
+
129
+ /**
130
+ * Write what provisioning observed into the app's `wrangler.jsonc`, as the `VECTOR_PROVISIONED` var for this
131
+ * environment. The Worker compares its declared filterable fields against it at boot and refuses to serve on
132
+ * drift — which is the only way an adopter who edits a metadata schema and deploys without re-provisioning
133
+ * hears about it, because Vectorize answers such a filter with partial results and no error.
134
+ *
135
+ * `dev` writes the top-level `vars`; every other environment writes its own `env.<name>` stanza — the same
136
+ * rule the app database id is read by, above. The write is comment-preserving and idempotent: re-running
137
+ * provision with nothing changed rewrites the identical string.
138
+ */
139
+ async function recordProvisioned(
140
+ projectDir: string,
141
+ env: string,
142
+ result: Awaited<ReturnType<VectorModule["provisionVector"]>>,
143
+ ): Promise<void> {
144
+ const { toProvisionRecord, VECTOR_PROVISIONED_VAR } = await loadVector();
145
+ const value = JSON.stringify(toProvisionRecord(result));
146
+
147
+ const config = (await readWranglerConfig(projectDir)) as WranglerVarsConfig;
148
+ if (env === "dev") {
149
+ config.vars ??= {};
150
+ config.vars[VECTOR_PROVISIONED_VAR] = value;
151
+ } else {
152
+ config.env ??= {};
153
+ config.env[env] ??= {};
154
+ const stanza = config.env[env];
155
+ if (stanza) {
156
+ stanza.vars ??= {};
157
+ stanza.vars[VECTOR_PROVISIONED_VAR] = value;
158
+ }
159
+ }
160
+ await writeWranglerConfig(projectDir, config);
161
+ }
162
+
163
+ /**
164
+ * Write this environment's `vectorize` and `workflows` bindings into the app's `wrangler.jsonc`.
165
+ *
166
+ * `pithy add vector` cannot write either one: wrangler requires an `index_name` on a `vectorize` entry
167
+ * and a `name` + `class_name` on a `workflows` entry, and both values are provisioning outputs. So the
168
+ * entries arrive here, complete, once the index exists and the reprocess worker is deployed —
169
+ * `capabilities/add.ts` documents the other half of the contract.
170
+ */
171
+ async function recordBindings(
172
+ projectDir: string,
173
+ project: string,
174
+ env: string,
175
+ config: Awaited<ReturnType<typeof loadVectorConfig>>,
176
+ result: Awaited<ReturnType<VectorModule["provisionVector"]>>,
177
+ ): Promise<void> {
178
+ const { vectorWorkflowRegistry, VECTOR_CAPABILITY } = await loadVector();
179
+ const vectorize: AppVectorizeBinding[] = result.indexes.flatMap((entry) => {
180
+ const binding = config.indexes[entry.index]?.binding;
181
+ // Vectorize has no local emulation, so the binding reaches the real index even in `wrangler dev`.
182
+ return binding ? [{ binding, index_name: entry.indexName, remote: true }] : [];
183
+ });
184
+
185
+ await applyAppBindings(projectDir, env, {
186
+ vectorize,
187
+ workflows: appWorkflowBindings(vectorWorkflowRegistry, { project, capability: VECTOR_CAPABILITY, env }),
188
+ });
189
+ }
190
+
191
+ /** Parse a `--filter` argument. A malformed filter fails here rather than inside a running Workflow. */
192
+ function parseFilter(raw: string | undefined): Record<string, unknown> | undefined {
193
+ if (!raw) return undefined;
194
+ let decoded: unknown;
195
+ try {
196
+ decoded = JSON.parse(raw);
197
+ } catch {
198
+ throw new ValidationError({
199
+ message: "--filter is not valid JSON.",
200
+ action: 'Pass a metadata filter object, e.g. --filter \'{"ownerId":"ada"}\'.',
201
+ });
202
+ }
203
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
204
+ throw new ValidationError({
205
+ message: "--filter must be a JSON object.",
206
+ action: 'Pass a metadata filter object, e.g. --filter \'{"ownerId":"ada"}\'.',
207
+ });
208
+ }
209
+ return decoded as Record<string, unknown>;
210
+ }
211
+
212
+ const provision = defineCommand({
213
+ meta: {
214
+ name: "provision",
215
+ description: "Create each configured index and its metadata indexes, then deploy the reprocess worker",
216
+ },
217
+ args: {
218
+ env: ENV_ARG,
219
+ json: { type: "boolean", default: false, description: "Machine-readable output" },
220
+ },
221
+ run: ({ args }) =>
222
+ withErrorReporting(args.json, async () => {
223
+ const env = requireEnvironment(args.env);
224
+ const projectDir = process.cwd();
225
+ const { provisionVector } = await loadVector();
226
+ const { provisioner, project, config } = await buildProvisioner(projectDir, env);
227
+
228
+ const result = await provisionVector(provisioner, { project, config, env });
229
+ // Last, and only on success: the record is the Worker's evidence that these metadata indexes exist,
230
+ // so it must never claim more than provisioning actually got done.
231
+ await recordProvisioned(projectDir, env, result);
232
+ await recordBindings(projectDir, project, env, config, result);
233
+
234
+ if (args.json) {
235
+ process.stdout.write(`${formatJsonLine({ command: "vector provision", ...result })}\n`);
236
+ return;
237
+ }
238
+ for (const entry of result.indexes) {
239
+ const created = entry.created.length > 0 ? `, ${entry.created.length} metadata index(es) created` : "";
240
+ process.stdout.write(`${entry.index}: ${entry.indexName} ready${created}.\n`);
241
+ for (const extra of entry.extra) {
242
+ process.stdout.write(` ${extra.propertyName} is indexed but not declared. It still costs a slot.\n`);
243
+ }
244
+ }
245
+ process.stdout.write(`${formatDone()}\n`);
246
+ }),
247
+ });
248
+
249
+ const reset = defineCommand({
250
+ meta: {
251
+ name: "reset",
252
+ description: "DESTRUCTIVE: delete each index, rebuild it, and re-embed the corpus from D1",
253
+ },
254
+ args: {
255
+ env: ENV_ARG,
256
+ "confirm-reset": {
257
+ type: "string",
258
+ description: 'Unlock a non-dev reset non-interactively: "yes, i really want to reset <env>"',
259
+ },
260
+ json: { type: "boolean", default: false, description: "Machine-readable output" },
261
+ },
262
+ run: ({ args }) =>
263
+ withErrorReporting(args.json, async () => {
264
+ const env = requireEnvironment(args.env);
265
+ const projectDir = process.cwd();
266
+ const interactive = !args.json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
267
+
268
+ // The gate first, before a single Cloudflare call. `--yes` deliberately does not appear here.
269
+ await assertResetConfirmed({
270
+ env,
271
+ json: args.json,
272
+ ...(args["confirm-reset"] !== undefined ? { confirmReset: args["confirm-reset"] } : {}),
273
+ ...(interactive ? { prompt: resetPrompt(env) } : {}),
274
+ });
275
+
276
+ const { resetVector } = await loadVector();
277
+ const { provisioner, project, config, accountId, apiToken } = await buildProvisioner(projectDir, env);
278
+
279
+ // A reset destroys an environment's entire search index, so it is audited at `critical` — and on `dev`
280
+ // it is audited not at all, because a dev reset changes nothing shared.
281
+ const audit = await createRemoteCliAudit({
282
+ projectDir,
283
+ env,
284
+ capabilities: await resolveWorkers({ projectDir }).then(projectCapabilities),
285
+ clients: new CloudflareClients({ accountId, apiToken }),
286
+ apiToken,
287
+ });
288
+ const event = {
289
+ action: "vector/index_reset" as const,
290
+ severity: "critical" as const,
291
+ resourceType: "cf_vectorize_index" as const,
292
+ resourceId: env,
293
+ metadata: { indexes: Object.keys(config.indexes) },
294
+ };
295
+
296
+ let result: Awaited<ReturnType<typeof resetVector>>;
297
+ try {
298
+ result = await resetVector(provisioner, { project, config, env });
299
+ } catch (error) {
300
+ // Truthful: recorded as it happened, never as it was intended.
301
+ await audit({ ...event, outcome: "failure" });
302
+ throw error;
303
+ }
304
+ await audit({ ...event, outcome: "success" });
305
+ // A reset rebuilds every index, so the record it left behind is stale by definition. Rewrite it.
306
+ await recordProvisioned(projectDir, env, result);
307
+
308
+ if (args.json) {
309
+ process.stdout.write(`${formatJsonLine({ command: "vector reset", ...result })}\n`);
310
+ return;
311
+ }
312
+ process.stdout.write(`DESTRUCTIVE. Every vector in ${env} was deleted and rebuilt from the corpus.\n`);
313
+ for (const entry of result.indexes) {
314
+ process.stdout.write(`${entry.index}: ${entry.indexName} rebuilt and re-embedded.\n`);
315
+ }
316
+ process.stdout.write(`${formatDone()}\n`);
317
+ }),
318
+ });
319
+
320
+ const reprocess = defineCommand({
321
+ meta: { name: "reprocess", description: "Re-embed an index's documents through the reprocess Workflow" },
322
+ args: {
323
+ env: ENV_ARG,
324
+ index: { type: "string", description: "The index to re-embed, as named in pithy.config.ts" },
325
+ all: {
326
+ type: "boolean",
327
+ default: false,
328
+ description: "Re-embed every document, not only the ones whose model differs from config",
329
+ },
330
+ filter: { type: "string", description: 'Narrow the run to matching documents, as JSON: \'{"ownerId":"ada"}\'' },
331
+ json: { type: "boolean", default: false, description: "Machine-readable output" },
332
+ },
333
+ run: ({ args }) =>
334
+ withErrorReporting(args.json, async () => {
335
+ const env = requireEnvironment(args.env);
336
+ const projectDir = process.cwd();
337
+ const { provisioner, config } = await buildProvisioner(projectDir, env);
338
+ const filter = parseFilter(args.filter);
339
+
340
+ // No `--index` means every configured index, which is what "re-embed after a model change" usually is.
341
+ const indexes = args.index ? [args.index] : Object.keys(config.indexes);
342
+ for (const index of indexes) {
343
+ if (!config.indexes[index]) {
344
+ throw new ValidationError({
345
+ message: `No index named \`${index}\` is configured.`,
346
+ action: `Pick one of: ${Object.keys(config.indexes).join(", ")}.`,
347
+ });
348
+ }
349
+ }
350
+
351
+ const runs: { index: string; report: unknown }[] = [];
352
+ for (const index of indexes) {
353
+ runs.push({
354
+ index,
355
+ report: await provisioner.reprocess(env, index, {
356
+ all: args.all,
357
+ ...(filter ? { filter } : {}),
358
+ }),
359
+ });
360
+ }
361
+
362
+ if (args.json) {
363
+ process.stdout.write(`${formatJsonLine({ command: "vector reprocess", env, runs })}\n`);
364
+ return;
365
+ }
366
+ for (const run of runs) {
367
+ const report = run.report as { reembedded?: number; scanned?: number } | null;
368
+ process.stdout.write(
369
+ `${run.index}: ${report?.reembedded ?? 0} re-embedded of ${report?.scanned ?? 0} scanned.\n`,
370
+ );
371
+ }
372
+ process.stdout.write(`${formatDone()}\n`);
373
+ }),
374
+ });
375
+
376
+ /** The interactive reset prompt: an `@clack/prompts` text field asking for the exact, environment-named phrase. */
377
+ function resetPrompt(env: string): () => Promise<string> {
378
+ return async () => {
379
+ const { isCancel, text } = await import("@clack/prompts");
380
+ const answer = await text({
381
+ message: `DESTRUCTIVE: this deletes every vector in ${env}. Type "${resetConfirmPhrase(env)}" to confirm:`,
382
+ });
383
+ return isCancel(answer) ? "" : answer;
384
+ };
385
+ }
386
+
387
+ export default defineCommand({
388
+ meta: { name: "vector", description: "Provision, reset, and re-embed the vector indexes" },
389
+ subCommands: { provision, reset, reprocess },
390
+ });