@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.
- package/LICENSE +21 -0
- package/package.json +72 -0
- package/scripts/templateManifest.ts +49 -0
- package/scripts/tsconfig.json +26 -0
- package/scripts/vendorTemplate.ts +84 -0
- package/scripts/verifyPack.ts +88 -0
- package/src/audit/cliAudit.ts +406 -0
- package/src/bin.ts +111 -0
- package/src/capabilities/add.ts +288 -0
- package/src/capabilities/addBootstrap.ts +275 -0
- package/src/capabilities/catalog.ts +175 -0
- package/src/capabilities/compose.ts +39 -0
- package/src/capabilities/configConstants.ts +74 -0
- package/src/capabilities/configImports.ts +397 -0
- package/src/capabilities/eject.ts +331 -0
- package/src/capabilities/emailProvisioner.ts +346 -0
- package/src/capabilities/entitlementGap.ts +70 -0
- package/src/capabilities/entryExports.ts +162 -0
- package/src/capabilities/flow.ts +550 -0
- package/src/capabilities/hostRegistry.ts +368 -0
- package/src/capabilities/loadFailure.ts +208 -0
- package/src/capabilities/manifests.ts +238 -0
- package/src/capabilities/mediaProvisioner.ts +471 -0
- package/src/capabilities/mintSecrets.ts +306 -0
- package/src/capabilities/paymentsProvisioner.ts +207 -0
- package/src/capabilities/prerequisites.ts +168 -0
- package/src/capabilities/r2Bucket.ts +113 -0
- package/src/capabilities/reconcile.ts +1483 -0
- package/src/capabilities/remove.ts +597 -0
- package/src/capabilities/requiredOptions.ts +92 -0
- package/src/capabilities/rotateSecrets.ts +305 -0
- package/src/capabilities/secrets.ts +178 -0
- package/src/capabilities/secretsDispatcher.ts +29 -0
- package/src/capabilities/secretsProvisioner.ts +389 -0
- package/src/capabilities/storageProvisioner.ts +414 -0
- package/src/capabilities/supportProvisioner.ts +515 -0
- package/src/capabilities/testersLoader.ts +52 -0
- package/src/capabilities/testersProvisioner.ts +236 -0
- package/src/capabilities/turnstileProvisioner.ts +347 -0
- package/src/capabilities/vectorProvisioner.ts +260 -0
- package/src/ci/fileModes.ts +223 -0
- package/src/ci/sourceFiles.ts +200 -0
- package/src/ci/workflowDrivers.ts +524 -0
- package/src/cloudflare/accountAnswer.ts +110 -0
- package/src/cloudflare/config.ts +685 -0
- package/src/cloudflare/storeId.ts +129 -0
- package/src/commands/add.ts +372 -0
- package/src/commands/alias.ts +205 -0
- package/src/commands/dashboard.ts +651 -0
- package/src/commands/deploy.ts +150 -0
- package/src/commands/dev.ts +37 -0
- package/src/commands/doctor.ts +2059 -0
- package/src/commands/email.ts +425 -0
- package/src/commands/env.ts +155 -0
- package/src/commands/feature.ts +359 -0
- package/src/commands/init.ts +538 -0
- package/src/commands/media.ts +303 -0
- package/src/commands/migrate.ts +129 -0
- package/src/commands/payments.ts +336 -0
- package/src/commands/provision.ts +368 -0
- package/src/commands/remove.ts +151 -0
- package/src/commands/secrets.ts +652 -0
- package/src/commands/seed.ts +229 -0
- package/src/commands/storage.ts +309 -0
- package/src/commands/support.ts +331 -0
- package/src/commands/testers.ts +1020 -0
- package/src/commands/token.ts +364 -0
- package/src/commands/turnstile.ts +271 -0
- package/src/commands/ui.ts +222 -0
- package/src/commands/upgrade.ts +517 -0
- package/src/commands/vector.ts +390 -0
- package/src/commands/worker.ts +295 -0
- package/src/dashboard/api.ts +323 -0
- package/src/dashboard/connect.ts +758 -0
- package/src/dashboard/contract.ts +289 -0
- package/src/dashboard/grant.ts +124 -0
- package/src/dashboard/registry.ts +519 -0
- package/src/dashboard/resolveTarget.ts +119 -0
- package/src/dev/delivery.ts +174 -0
- package/src/dev/devLogin.ts +155 -0
- package/src/dev/devLoginTargets.ts +91 -0
- package/src/dev/env.ts +206 -0
- package/src/dev/hostWorkers.ts +290 -0
- package/src/dev/keys.ts +111 -0
- package/src/dev/logging.ts +87 -0
- package/src/dev/openUrl.ts +75 -0
- package/src/dev/orchestrator.ts +1014 -0
- package/src/dev/ports.ts +220 -0
- package/src/dev/readyWatch.ts +142 -0
- package/src/dev/state.ts +90 -0
- package/src/devSecrets/bootstrapVars.ts +265 -0
- package/src/devSecrets/devVars.ts +240 -0
- package/src/devSecrets/edit.ts +256 -0
- package/src/devSecrets/file.ts +277 -0
- package/src/devSecrets/generate.ts +428 -0
- package/src/devSecrets/location.ts +80 -0
- package/src/devSecrets/mode.ts +71 -0
- package/src/devSecrets/records.ts +30 -0
- package/src/devSecrets/report.ts +99 -0
- package/src/devSecrets/seed.ts +344 -0
- package/src/devSecrets/store.ts +262 -0
- package/src/devSecrets/targets.ts +204 -0
- package/src/dispatch.ts +147 -0
- package/src/docs/catalog.ts +246 -0
- package/src/docs/writeCatalog.ts +45 -0
- package/src/doctor/cloudflare.ts +287 -0
- package/src/doctor/devPreferences.ts +155 -0
- package/src/doctor/devSecrets.ts +464 -0
- package/src/doctor/devVars.ts +414 -0
- package/src/doctor/devVarsLocal.ts +138 -0
- package/src/doctor/environments.ts +155 -0
- package/src/doctor/health.ts +354 -0
- package/src/doctor/localDelivery.ts +91 -0
- package/src/doctor/portsRegistry.ts +252 -0
- package/src/doctor/projectName.ts +584 -0
- package/src/doctor/secretBindings.ts +166 -0
- package/src/doctor/settings.ts +274 -0
- package/src/doctor/settingsSources.ts +202 -0
- package/src/doctor/workerName.ts +174 -0
- package/src/doctor/wranglerVars.ts +33 -0
- package/src/feature/bindings.ts +93 -0
- package/src/feature/create.ts +179 -0
- package/src/feature/destroy.ts +160 -0
- package/src/feature/devConfig.ts +201 -0
- package/src/feature/identity.ts +100 -0
- package/src/feature/manifest.ts +132 -0
- package/src/feature/ports.ts +615 -0
- package/src/feature/provision.ts +362 -0
- package/src/feature/sync.ts +148 -0
- package/src/feature/worktree.ts +282 -0
- package/src/help/groups.ts +47 -0
- package/src/help/rootUsage.ts +135 -0
- package/src/main.ts +73 -0
- package/src/migrations/ledger.ts +129 -0
- package/src/migrations/registry.ts +47 -0
- package/src/migrations/run.ts +1066 -0
- package/src/notifier/check.ts +129 -0
- package/src/notifier/installer.ts +48 -0
- package/src/notifier/notify.ts +152 -0
- package/src/notifier/state.ts +248 -0
- package/src/notifier/version.ts +59 -0
- package/src/platform/editor.ts +333 -0
- package/src/platform/rc.ts +118 -0
- package/src/platform/shell.ts +83 -0
- package/src/project/appBindings.ts +184 -0
- package/src/project/appWorkflows.ts +266 -0
- package/src/project/applyDomains.ts +166 -0
- package/src/project/askDomains.ts +220 -0
- package/src/project/atomic.ts +466 -0
- package/src/project/bindingEntries.ts +425 -0
- package/src/project/config.ts +701 -0
- package/src/project/dashboard.ts +118 -0
- package/src/project/deploy.ts +364 -0
- package/src/project/devVars.ts +113 -0
- package/src/project/domainPrompt.ts +191 -0
- package/src/project/domains.ts +386 -0
- package/src/project/envInventory.ts +356 -0
- package/src/project/environment.ts +125 -0
- package/src/project/extensions.ts +69 -0
- package/src/project/jsonc.ts +289 -0
- package/src/project/packageManager.ts +238 -0
- package/src/project/readOptionalFile.ts +342 -0
- package/src/project/rollback.ts +145 -0
- package/src/project/scaffold.ts +1088 -0
- package/src/project/templateFiles.ts +53 -0
- package/src/project/verifyDeploy.ts +230 -0
- package/src/project/versionMetadata.ts +77 -0
- package/src/project/workerAddress.ts +176 -0
- package/src/project/workerCommand.ts +564 -0
- package/src/project/workerIdentity.ts +50 -0
- package/src/project/workerManifest.ts +135 -0
- package/src/project/workerScaffold.ts +289 -0
- package/src/project/workerScope.ts +394 -0
- package/src/project/workers.ts +86 -0
- package/src/project/workflows.ts +281 -0
- package/src/project/wrangler.ts +168 -0
- package/src/provision/confirm.ts +86 -0
- package/src/provision/environment.ts +407 -0
- package/src/provision/featureConfig.ts +98 -0
- package/src/provision/mode.ts +62 -0
- package/src/provision/pendingSecrets.ts +96 -0
- package/src/provision/resources.ts +126 -0
- package/src/provision/secretBindings.ts +149 -0
- package/src/provision/store.ts +33 -0
- package/src/provision/unprovisioned.ts +114 -0
- package/src/provision/wranglerEnv.ts +220 -0
- package/src/rootFlags.ts +48 -0
- package/src/seed/drivers.ts +423 -0
- package/src/seed/media.ts +187 -0
- package/src/seed/plan.ts +137 -0
- package/src/seed/prepare.ts +224 -0
- package/src/seed/registry.ts +25 -0
- package/src/seed/run.ts +793 -0
- package/src/seed/safety.ts +206 -0
- package/src/terminal/logger.ts +42 -0
- package/src/terminal/output.ts +64 -0
- package/src/terminal/style.ts +132 -0
- package/src/test-utils/doctorHarness.ts +190 -0
- package/src/test-utils/migrateHarness.ts +126 -0
- package/src/test-utils/seedHarness.ts +173 -0
- package/src/test-utils/tempRepo.ts +45 -0
- package/src/tokens/config.ts +16 -0
- package/src/tokens/engine.ts +345 -0
- package/src/tokens/mintedTokens.ts +233 -0
- package/src/tokens/sinks.ts +84 -0
- package/src/ui/flow.ts +451 -0
- package/src/ui/react.ts +112 -0
- package/src/ui/routeAllowlist.ts +208 -0
- package/src/ui/scaffold.ts +113 -0
- package/src/ui/screenStyles.ts +127 -0
- package/src/ui/stubs.ts +135 -0
- package/src/ui/templates.ts +52 -0
- package/src/ui/wire.ts +311 -0
- package/src/ui/workerUi.ts +172 -0
- package/templates/starter/.dev.secrets.example.jsonc +43 -0
- package/templates/starter/.dev.vars.example +30 -0
- package/templates/starter/apps/api/package.json +22 -0
- package/templates/starter/apps/api/pithy.config.ts +65 -0
- package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
- package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
- package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
- package/templates/starter/apps/api/src/index.ts +8 -0
- package/templates/starter/apps/api/tsconfig.json +26 -0
- package/templates/starter/apps/api/wrangler.jsonc +68 -0
- package/templates/starter/biome.template.jsonc +75 -0
- package/templates/starter/gitignore +37 -0
- package/templates/starter/package.json +28 -0
- package/templates/starter/pithy.config.ts +67 -0
- package/templates/starter/plugins/no-console.grit +25 -0
- package/templates/starter/plugins/no-process-io.grit +25 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/tsconfig.tools.json +30 -0
- package/templates/starter/vitest.config.ts +124 -0
- package/templates/starter/vitest.workers.config.ts +26 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { type Capability, defineCapability } from "@pithy-sh/core/src/capability/capability";
|
|
8
|
+
import { InternalError } from "@pithy-sh/core/src/error/pithyError";
|
|
9
|
+
import type { Migration } from "kysely/migration";
|
|
10
|
+
import { afterEach, beforeEach } from "vitest";
|
|
11
|
+
import { type MigrationFanOutOptions, readProjectLedger, type WorkerScope } from "../migrations/run";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared scaffolding for the `migrateProject` suites, extracted so those suites can live in more
|
|
15
|
+
* than one file.
|
|
16
|
+
*
|
|
17
|
+
* Splitting them is a speed decision, not a taste one. Vitest parallelizes across test *files* and
|
|
18
|
+
* runs the tests inside one file in sequence, and every `migrateProject` call spawns its own
|
|
19
|
+
* Miniflare — so a single file holding all of them serializes ~30 workerd boots. Measured before
|
|
20
|
+
* the split: `migrations/run.test.ts` took 21.1s, of which the `fan-out over workers` group alone
|
|
21
|
+
* was 11.8s, and it set the floor for the whole package's 28.0s node suite. One file per heavy
|
|
22
|
+
* group lets those groups run at the same time.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The pending half of {@link readProjectLedger}, for the suites that assert how far a schema is behind.
|
|
27
|
+
* The other half — a migration the ledger records and the project no longer declares — has its own
|
|
28
|
+
* suite in `migrations/ledger.test.ts`, because it is a refusal rather than a number.
|
|
29
|
+
*/
|
|
30
|
+
export const pendingFrom = async (options: MigrationFanOutOptions): Promise<number> => {
|
|
31
|
+
const ledger = await readProjectLedger(options);
|
|
32
|
+
// These suites migrate databases they created a moment ago, so every one of them answers. A `partial`
|
|
33
|
+
// here is the harness reporting a sum with a hole in it, and asserting a number against that would be
|
|
34
|
+
// the #371 defect wearing a test's clothes — so it fails loudly instead (#371).
|
|
35
|
+
if (ledger.state !== "read") {
|
|
36
|
+
throw new InternalError({
|
|
37
|
+
message: "The migration harness could not read every database's ledger.",
|
|
38
|
+
action: "Check the fixture's wrangler.jsonc bindings — a database in scope did not answer.",
|
|
39
|
+
detail: `readProjectLedger returned state ${ledger.state}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return ledger.pending;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Creates a one-column table — the smallest migration that proves `up`/`down` ran. */
|
|
46
|
+
export const createTable = (name: string): Migration => ({
|
|
47
|
+
up: async (db) => {
|
|
48
|
+
await db.schema
|
|
49
|
+
.createTable(name)
|
|
50
|
+
.addColumn("id", "integer", (c) => c.primaryKey().autoIncrement())
|
|
51
|
+
.execute();
|
|
52
|
+
},
|
|
53
|
+
down: async (db) => {
|
|
54
|
+
await db.schema.dropTable(name).execute();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** The `things` table migration, used by the default `app` capability. */
|
|
59
|
+
export const createThings = createTable("things");
|
|
60
|
+
|
|
61
|
+
/** A capability with one database on `DB` — the project's baseline registry. */
|
|
62
|
+
export function appCapability(): Capability {
|
|
63
|
+
return defineCapability({
|
|
64
|
+
name: "app",
|
|
65
|
+
requiredBindings: [],
|
|
66
|
+
databases: {
|
|
67
|
+
app: { binding: "DB", tables: {}, migrations: { "0001_things": createThings }, migrationOrder: 1000 },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A second capability, on its own binding by default — the other worker's registry. */
|
|
73
|
+
export function multiplayerCapability(binding = "COLLAB_DB"): Capability {
|
|
74
|
+
return defineCapability({
|
|
75
|
+
name: "multiplayer",
|
|
76
|
+
requiredBindings: [],
|
|
77
|
+
databases: {
|
|
78
|
+
collab: {
|
|
79
|
+
binding,
|
|
80
|
+
tables: {},
|
|
81
|
+
migrations: { "0001_rooms": createTable("rooms") },
|
|
82
|
+
migrationOrder: 500,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A throwaway project directory and the workers inside it, rebuilt per test. */
|
|
89
|
+
export interface MigrateHarness {
|
|
90
|
+
/** The project root. A getter: `beforeEach` makes a new temp directory for every test. */
|
|
91
|
+
readonly projectDir: string;
|
|
92
|
+
/** The project's one worker, `apps/api`, composing `capabilities`. */
|
|
93
|
+
api(capabilities: Capability[]): WorkerScope;
|
|
94
|
+
/** A second worker, `apps/<name>`, so the fan-out has something to fan out over. */
|
|
95
|
+
worker(name: string, capabilities: Capability[]): Promise<WorkerScope>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Registers the per-test temp project and returns the accessors the suites use. Call it once at
|
|
100
|
+
* `describe` scope; `projectDir` is a getter because each test gets a fresh directory, so a plain
|
|
101
|
+
* value captured at import would go stale after the first test.
|
|
102
|
+
*/
|
|
103
|
+
export function migrateHarness(): MigrateHarness {
|
|
104
|
+
let dir = "";
|
|
105
|
+
beforeEach(async () => {
|
|
106
|
+
dir = await mkdtemp(join(tmpdir(), "pithy-migrate-"));
|
|
107
|
+
await mkdir(join(dir, "apps", "api"), { recursive: true });
|
|
108
|
+
});
|
|
109
|
+
afterEach(async () => {
|
|
110
|
+
await rm(dir, { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
get projectDir(): string {
|
|
115
|
+
return dir;
|
|
116
|
+
},
|
|
117
|
+
api(capabilities: Capability[]): WorkerScope {
|
|
118
|
+
return { name: "api", dir: join(dir, "apps", "api"), capabilities };
|
|
119
|
+
},
|
|
120
|
+
async worker(name: string, capabilities: Capability[]): Promise<WorkerScope> {
|
|
121
|
+
const workerDir = join(dir, "apps", name);
|
|
122
|
+
await mkdir(workerDir, { recursive: true });
|
|
123
|
+
return { name, dir: workerDir, capabilities };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { D1Database, KVNamespace } from "@cloudflare/workers-types";
|
|
8
|
+
import { type Capability, defineCapability } from "@pithy-sh/core/src/capability/capability";
|
|
9
|
+
import { d1SeedGroup, defineSeed, kvSeedGroup } from "@pithy-sh/core/src/seed/seed";
|
|
10
|
+
import type { Migration } from "kysely/migration";
|
|
11
|
+
import { Miniflare } from "miniflare";
|
|
12
|
+
import { afterEach, beforeEach } from "vitest";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import type { WorkerScope } from "../migrations/run";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shared scaffolding for the `seedProject` suites, extracted so those suites can live in more than
|
|
18
|
+
* one file.
|
|
19
|
+
*
|
|
20
|
+
* Splitting them is a speed decision, not a taste one. Vitest parallelizes across test *files* and
|
|
21
|
+
* runs the tests inside one file in sequence, and every `seedProject`/`migrateProject` call spawns
|
|
22
|
+
* its own Miniflare — so a single file holding all of them serializes dozens of workerd boots.
|
|
23
|
+
* Measured before the split: `seed/run.test.ts` took 25.1s, of which `--redo` was 10.1s and
|
|
24
|
+
* `fan-out over workers` 9.6s, and it set the floor for the whole package's 28.0s node suite.
|
|
25
|
+
*
|
|
26
|
+
* See `test-utils/migrateHarness.ts` for the same treatment of the migration suites.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A tiny two-column table — the D1 seed target. */
|
|
30
|
+
export const Things = z.object({
|
|
31
|
+
id: z.number().int().describe("Auto-increment primary key."),
|
|
32
|
+
name: z.string().describe("The thing's name."),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** The migration that creates the `things` table so a seed has somewhere to land. */
|
|
36
|
+
export const createThings: Migration = {
|
|
37
|
+
up: async (db) => {
|
|
38
|
+
await db.schema
|
|
39
|
+
.createTable("things")
|
|
40
|
+
.addColumn("id", "integer", (c) => c.primaryKey().autoIncrement())
|
|
41
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
42
|
+
.execute();
|
|
43
|
+
},
|
|
44
|
+
down: async (db) => {
|
|
45
|
+
await db.schema.dropTable("things").execute();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A typed KV store — the KV seed target. */
|
|
50
|
+
export const notesStore = {
|
|
51
|
+
prefix: "notes",
|
|
52
|
+
key: z.object({ slug: z.string().describe("The note's slug segment.") }),
|
|
53
|
+
value: z.object({ body: z.string().describe("The note's body.") }),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The one worker's `wrangler.jsonc` for a local run: a D1 on `DB`, a KV on `CACHE`. */
|
|
57
|
+
export const localWrangler = {
|
|
58
|
+
d1_databases: [{ binding: "DB", database_id: "db-local" }],
|
|
59
|
+
kv_namespaces: [{ binding: "CACHE", id: "cache-local" }],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** A capability that both declares the `things` table / `notes` store and seeds them. */
|
|
63
|
+
export function dataCapability(
|
|
64
|
+
environments: readonly string[] = ["dev", "staging"],
|
|
65
|
+
rows: readonly { id: number; name: string }[] = [
|
|
66
|
+
{ id: 1, name: "one" },
|
|
67
|
+
{ id: 2, name: "two" },
|
|
68
|
+
],
|
|
69
|
+
): Capability {
|
|
70
|
+
return defineCapability({
|
|
71
|
+
name: "app",
|
|
72
|
+
requiredBindings: [],
|
|
73
|
+
databases: {
|
|
74
|
+
app: {
|
|
75
|
+
binding: "DB",
|
|
76
|
+
tables: { things: Things },
|
|
77
|
+
migrations: { "0001_things": createThings },
|
|
78
|
+
migrationOrder: 1000,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
kvNamespaces: { cache: { binding: "CACHE", stores: { notes: notesStore } } },
|
|
82
|
+
seeds: [
|
|
83
|
+
defineSeed({
|
|
84
|
+
name: "demo",
|
|
85
|
+
order: 1000,
|
|
86
|
+
environments,
|
|
87
|
+
d1: [d1SeedGroup("app", "things", Things, rows)],
|
|
88
|
+
kv: [kvSeedGroup("cache", "notes", notesStore, [{ key: { slug: "a" }, value: { body: "hello" } }])],
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A fresh Miniflare over a seed run's persisted state, for reading back what the seed wrote. */
|
|
95
|
+
export interface LocalStores {
|
|
96
|
+
/** The `DB` database, as the seeder wrote it. */
|
|
97
|
+
d1: D1Database;
|
|
98
|
+
/** The `CACHE` namespace, as the seeder wrote it. */
|
|
99
|
+
kv: KVNamespace;
|
|
100
|
+
/** Shuts the reader down — always call it, or the workerd process outlives the test. */
|
|
101
|
+
dispose: () => Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A throwaway project directory and the workers inside it, rebuilt per test. */
|
|
105
|
+
export interface SeedHarness {
|
|
106
|
+
/** The project root. A getter: `beforeEach` makes a new temp directory for every test. */
|
|
107
|
+
readonly projectDir: string;
|
|
108
|
+
/** The one worker's directory — `apps/api`, under the project root. Also a getter. */
|
|
109
|
+
readonly workerDir: string;
|
|
110
|
+
/** The project's one worker, composing `capabilities`. */
|
|
111
|
+
api(capabilities: Capability[]): WorkerScope;
|
|
112
|
+
/** A second worker in `apps/<name>`, sharing the project root's local stores. */
|
|
113
|
+
worker(name: string, capabilities: Capability[], config?: unknown): Promise<WorkerScope>;
|
|
114
|
+
/** Writes the one worker's `wrangler.jsonc`. */
|
|
115
|
+
writeWrangler(config: unknown): Promise<void>;
|
|
116
|
+
/** Opens a fresh Miniflare over the same persisted state to read back what a seed wrote. */
|
|
117
|
+
openLocal(): Promise<LocalStores>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Registers the per-test temp project and returns the accessors the suites use. Call it once at
|
|
122
|
+
* `describe` scope; the directories are getters because each test gets fresh ones, so a plain value
|
|
123
|
+
* captured at import would go stale after the first test.
|
|
124
|
+
*/
|
|
125
|
+
export function seedHarness(): SeedHarness {
|
|
126
|
+
let dir = "";
|
|
127
|
+
let workerDir = "";
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
dir = await mkdtemp(join(tmpdir(), "pithy-seed-run-"));
|
|
130
|
+
workerDir = join(dir, "apps", "api");
|
|
131
|
+
await mkdir(workerDir, { recursive: true });
|
|
132
|
+
});
|
|
133
|
+
afterEach(async () => {
|
|
134
|
+
await rm(dir, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
get projectDir(): string {
|
|
139
|
+
return dir;
|
|
140
|
+
},
|
|
141
|
+
get workerDir(): string {
|
|
142
|
+
return workerDir;
|
|
143
|
+
},
|
|
144
|
+
api(capabilities: Capability[]): WorkerScope {
|
|
145
|
+
return { name: "api", dir: workerDir, capabilities };
|
|
146
|
+
},
|
|
147
|
+
async worker(name: string, capabilities: Capability[], config?: unknown): Promise<WorkerScope> {
|
|
148
|
+
const target = join(dir, "apps", name);
|
|
149
|
+
await mkdir(target, { recursive: true });
|
|
150
|
+
await writeFile(join(target, "wrangler.jsonc"), JSON.stringify(config ?? localWrangler));
|
|
151
|
+
return { name, dir: target, capabilities };
|
|
152
|
+
},
|
|
153
|
+
async writeWrangler(config: unknown): Promise<void> {
|
|
154
|
+
await writeFile(join(workerDir, "wrangler.jsonc"), JSON.stringify(config));
|
|
155
|
+
},
|
|
156
|
+
async openLocal(): Promise<LocalStores> {
|
|
157
|
+
const state = join(dir, ".wrangler", "state", "v3");
|
|
158
|
+
const mf = new Miniflare({
|
|
159
|
+
modules: true,
|
|
160
|
+
script: "export default {};",
|
|
161
|
+
d1Databases: { DB: "db-local" },
|
|
162
|
+
kvNamespaces: { CACHE: "cache-local" },
|
|
163
|
+
d1Persist: join(state, "d1"),
|
|
164
|
+
kvPersist: join(state, "kv"),
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
d1: (await mf.getD1Database("DB")) as unknown as D1Database,
|
|
168
|
+
kv: (await mf.getKVNamespace("CACHE")) as unknown as KVNamespace,
|
|
169
|
+
dispose: () => mf.dispose(),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { rm } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The two halves of "a test made a git repository in a temp directory and then deleted it", which is a
|
|
8
|
+
* race unless both are handled.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a repo is not an ordinary directory to delete
|
|
11
|
+
*
|
|
12
|
+
* `git commit` can leave work running after it exits. Auto-maintenance — `gc --auto`, and the
|
|
13
|
+
* commit-graph and `pack-refs` steps under it — is spawned in the background and detached, so the
|
|
14
|
+
* command returns while a second process is still writing inside `.git`. A recursive delete that starts
|
|
15
|
+
* in that window unlinks a directory's contents, then finds a file back in it, and `rmdir` fails:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* Error: ENOTEMPTY: directory not empty, rmdir '/tmp/pithy-pack-ke283n/checkout/.git/info'
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* It reproduces on a loaded machine and essentially never on an idle one, which is why it arrived from
|
|
22
|
+
* CI rather than from anybody's laptop.
|
|
23
|
+
*
|
|
24
|
+
* ## Both halves, because either alone is a coin flip
|
|
25
|
+
*
|
|
26
|
+
* {@link GIT_NO_MAINTENANCE} removes the cause: with `gc.auto=0` and `maintenance.auto=false` there is no
|
|
27
|
+
* second process to race. {@link removeTempDir} survives it anyway — Node's `rm` retries exactly the
|
|
28
|
+
* errno family this produces (`ENOTEMPTY`, `EBUSY`, `EPERM`, `EMFILE`), and it defaults to **no** retries.
|
|
29
|
+
*
|
|
30
|
+
* Configuration is passed per invocation rather than written into the repo, so it holds for `init`
|
|
31
|
+
* itself. A `git config` line lands after the repository already exists, which is one command too late.
|
|
32
|
+
*/
|
|
33
|
+
export const GIT_NO_MAINTENANCE = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"] as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Delete a temp directory, tolerating a writer that has not finished.
|
|
37
|
+
*
|
|
38
|
+
* `force` so a directory already gone is not an error — a test that cleans up twice, or one whose
|
|
39
|
+
* fixture never got created, should not fail in teardown. The retries are the part that matters: ten
|
|
40
|
+
* attempts over roughly a second, which is far longer than any background git step needs and still short
|
|
41
|
+
* enough that a genuinely stuck delete fails the suite rather than hanging it.
|
|
42
|
+
*/
|
|
43
|
+
export async function removeTempDir(path: string): Promise<void> {
|
|
44
|
+
await rm(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { ProfileOverride } from "@pithy-sh/cloudflare/src/tokens/profiles";
|
|
5
|
+
import type { ProjectConfig } from "../project/config";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the engine's profile-override resolver from `pithy.config.ts`'s `tokens.overrides`. A profile
|
|
9
|
+
* with no configured override resolves to `undefined` (its predefined defaults stand). This is the
|
|
10
|
+
* config half of overriding a profile; a `--permission` / `--store` flag is the per-mint half and
|
|
11
|
+
* wins over this.
|
|
12
|
+
*/
|
|
13
|
+
export function tokenOverrideResolver(config: ProjectConfig): (profile: string) => ProfileOverride | undefined {
|
|
14
|
+
const overrides = config.tokens?.overrides ?? {};
|
|
15
|
+
return (profile) => overrides[profile];
|
|
16
|
+
}
|