create-svc 0.1.23 → 0.1.25

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 (36) hide show
  1. package/package.json +3 -1
  2. package/src/post-scaffold.test.ts +10 -10
  3. package/src/post-scaffold.ts +4 -9
  4. package/src/scaffold.test.ts +46 -101
  5. package/src/scaffold.ts +10 -3
  6. package/{templates/shared/scripts → src/service-runtime}/authctl.ts +3 -3
  7. package/{templates/shared/scripts → src/service-runtime}/cloudrun/cleanup.ts +1 -1
  8. package/{templates/shared/scripts → src/service-runtime}/cloudrun/cli.ts +1 -1
  9. package/src/service-runtime/cloudrun/config.ts +55 -0
  10. package/src/service-runtime/runtime.ts +8 -0
  11. package/{templates/targets/workers/scripts → src/service-runtime}/workers/cli.ts +7 -6
  12. package/src/service.test.ts +0 -2
  13. package/src/service.ts +13 -19
  14. package/templates/shared/README.md +1 -1
  15. package/templates/shared/service.config.ts +31 -0
  16. package/templates/targets/workers/Makefile +1 -1
  17. package/templates/targets/workers/package.json +8 -11
  18. package/templates/variants/bun-connectrpc/Makefile +1 -1
  19. package/templates/variants/bun-connectrpc/package.json +7 -10
  20. package/templates/variants/bun-hono/Makefile +1 -1
  21. package/templates/variants/bun-hono/package.json +7 -10
  22. package/templates/variants/go-chi/Dockerfile +1 -1
  23. package/templates/variants/go-chi/Makefile +1 -1
  24. package/templates/variants/go-chi/go.mod +30 -1
  25. package/templates/variants/go-chi/go.sum +146 -0
  26. package/templates/variants/go-chi/package.json +7 -10
  27. package/templates/variants/go-connectrpc/Dockerfile +1 -1
  28. package/templates/variants/go-connectrpc/Makefile +1 -1
  29. package/templates/variants/go-connectrpc/go.mod +30 -2
  30. package/templates/variants/go-connectrpc/go.sum +150 -0
  31. package/templates/variants/go-connectrpc/package.json +7 -10
  32. package/templates/shared/scripts/cloudrun/config.ts +0 -62
  33. /package/{templates/shared/scripts → src/service-runtime}/cloudrun/bootstrap.ts +0 -0
  34. /package/{templates/shared/scripts → src/service-runtime}/cloudrun/deploy.ts +0 -0
  35. /package/{templates/shared/scripts → src/service-runtime}/cloudrun/lib.ts +0 -0
  36. /package/{templates/shared/scripts → src/service-runtime}/cloudrun/neon.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  ],
44
44
  "packageManager": "bun@1.3.2",
45
45
  "devDependencies": {
46
+ "@types/pg": "^8.16.0",
46
47
  "@types/bun": "latest"
47
48
  },
48
49
  "peerDependencies": {
@@ -51,6 +52,7 @@
51
52
  "dependencies": {
52
53
  "@clack/prompts": "^1.4.0",
53
54
  "@neondatabase/api-client": "^2.7.1",
55
+ "pg": "^8.16.3",
54
56
  "picocolors": "^1.1.1"
55
57
  }
56
58
  }
@@ -4,23 +4,23 @@ import { buildDeploymentVerificationCommands, buildPostScaffoldCommands } from "
4
4
  describe("buildPostScaffoldCommands", () => {
5
5
  test("runs create and deploy for HTTP services", () => {
6
6
  expect(buildPostScaffoldCommands({ framework: "hono" })).toEqual([
7
- { command: "bun", args: ["./scripts/cloudrun/cli.ts", "create"] },
8
- { command: "bun", args: ["./scripts/cloudrun/cli.ts", "deploy"] },
7
+ { command: "service", args: ["create"] },
8
+ { command: "service", args: ["deploy"] },
9
9
  ]);
10
10
  });
11
11
 
12
12
  test("builds SDK artifacts before create and deploy for ConnectRPC services", () => {
13
13
  expect(buildPostScaffoldCommands({ framework: "connectrpc" })).toEqual([
14
- { command: "bun", args: ["./scripts/cloudrun/cli.ts", "sdk", "build"] },
15
- { command: "bun", args: ["./scripts/cloudrun/cli.ts", "create"] },
16
- { command: "bun", args: ["./scripts/cloudrun/cli.ts", "deploy"] },
14
+ { command: "service", args: ["sdk", "build"] },
15
+ { command: "service", args: ["create"] },
16
+ { command: "service", args: ["deploy"] },
17
17
  ]);
18
18
  });
19
19
 
20
20
  test("uses the workers service CLI for workers services", () => {
21
21
  expect(buildPostScaffoldCommands({ target: "workers", framework: "hono" })).toEqual([
22
- { command: "bun", args: ["./scripts/workers/cli.ts", "create"] },
23
- { command: "bun", args: ["./scripts/workers/cli.ts", "deploy"] },
22
+ { command: "service", args: ["create"] },
23
+ { command: "service", args: ["deploy"] },
24
24
  ]);
25
25
  });
26
26
  });
@@ -34,7 +34,7 @@ describe("buildDeploymentVerificationCommands", () => {
34
34
  command: "sh",
35
35
  args: [
36
36
  "-c",
37
- 'TOKEN="$(bun ./scripts/cloudrun/cli.ts auth token)" && curl --fail --show-error --silent -H "Authorization: Bearer $TOKEN" "https://api.launch.anmho.com/v1/admin/waitlist?limit=1"',
37
+ 'TOKEN="$(service auth token)" && curl --fail --show-error --silent -H "Authorization: Bearer $TOKEN" "https://api.launch.anmho.com/v1/admin/waitlist?limit=1"',
38
38
  ],
39
39
  },
40
40
  ]);
@@ -64,7 +64,7 @@ describe("buildDeploymentVerificationCommands", () => {
64
64
  command: "sh",
65
65
  args: [
66
66
  "-c",
67
- 'TOKEN="$(bun ./scripts/cloudrun/cli.ts auth token)" && grpcurl -H "Authorization: Bearer $TOKEN" -d \'{"limit":1}\' -proto protos/waitlist/v1/waitlist.proto "api.launch.anmho.com:443" waitlist.v1.WaitlistService/ListWaitlistEntries',
67
+ 'TOKEN="$(service auth token)" && grpcurl -H "Authorization: Bearer $TOKEN" -d \'{"limit":1}\' -proto protos/waitlist/v1/waitlist.proto "api.launch.anmho.com:443" waitlist.v1.WaitlistService/ListWaitlistEntries',
68
68
  ],
69
69
  });
70
70
  });
@@ -81,7 +81,7 @@ describe("buildDeploymentVerificationCommands", () => {
81
81
  command: "sh",
82
82
  args: [
83
83
  "-c",
84
- 'TOKEN="$(bun ./scripts/workers/cli.ts auth token)" && curl --fail --show-error --silent -H "Authorization: Bearer $TOKEN" "https://api.launch.anmho.com/v1/admin/waitlist?limit=1"',
84
+ 'TOKEN="$(service auth token)" && curl --fail --show-error --silent -H "Authorization: Bearer $TOKEN" "https://api.launch.anmho.com/v1/admin/waitlist?limit=1"',
85
85
  ],
86
86
  });
87
87
  });
@@ -59,7 +59,7 @@ export function buildDeploymentVerificationCommands(
59
59
  Partial<Pick<ScaffoldConfig, "target" | "serviceName" | "gcpProject" | "region">>
60
60
  ): PostScaffoldCommand[] {
61
61
  const origin = verificationOrigin(config);
62
- const tokenCommand = `TOKEN="$(bun ${serviceCliPath(config)} auth token)"`;
62
+ const tokenCommand = 'TOKEN="$(service auth token)"';
63
63
  return [
64
64
  shellVerificationCommand(`curl --fail --show-error --silent "${origin}/"`),
65
65
  shellVerificationCommand(`curl --fail --show-error --silent "${origin}/readyz"`),
@@ -148,18 +148,13 @@ function verificationHost(
148
148
  export function buildPostScaffoldCommands(
149
149
  config: Pick<ScaffoldConfig, "framework"> & Partial<Pick<ScaffoldConfig, "target">>
150
150
  ): PostScaffoldCommand[] {
151
- const serviceCli = serviceCliPath(config);
152
151
  return [
153
- ...(config.target !== "workers" && config.framework === "connectrpc" ? [{ command: "bun", args: [serviceCli, "sdk", "build"] }] : []),
154
- { command: "bun", args: [serviceCli, "create"] },
155
- { command: "bun", args: [serviceCli, "deploy"] },
152
+ ...(config.target !== "workers" && config.framework === "connectrpc" ? [{ command: "service", args: ["sdk", "build"] }] : []),
153
+ { command: "service", args: ["create"] },
154
+ { command: "service", args: ["deploy"] },
156
155
  ];
157
156
  }
158
157
 
159
- function serviceCliPath(config: Partial<Pick<ScaffoldConfig, "target">>) {
160
- return config.target === "workers" ? "./scripts/workers/cli.ts" : "./scripts/cloudrun/cli.ts";
161
- }
162
-
163
158
  function installProjectDependencies(cwd: string) {
164
159
  requireCommand("bun");
165
160
  run("bun", ["install"], { cwd });
@@ -54,63 +54,31 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
54
54
  })
55
55
  );
56
56
 
57
- const configScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "config.ts")).text();
58
57
  const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
59
58
  expect(serviceConfig).toContain('service_id: "dns-api"');
60
59
  expect(serviceConfig).toContain('target: "cloudrun"');
60
+ expect(serviceConfig).toContain('profile: "microservice"');
61
+ expect(serviceConfig).toContain('domain: "waitlist"');
62
+ expect(serviceConfig).toContain('kind: "microservice"');
63
+ expect(serviceConfig).toContain(`runtime: "${variant.runtime}"`);
64
+ expect(serviceConfig).toContain(`framework: "${variant.framework}"`);
61
65
  expect(serviceConfig).toContain('module: "buf.build/anmho/dns-api"');
62
66
  expect(serviceConfig).toContain('cloudflare_vault_path: "prod/providers/cloudflare"');
63
67
  expect(serviceConfig).toContain('issuer: "https://auth.anmho.com/api/auth"');
64
68
  expect(serviceConfig).toContain('audience: "api://dns-api"');
65
69
  expect(serviceConfig).toContain('vault_path_prefix: "prod/apps/dns-api/server/oauth-clients"');
66
70
  expect(serviceConfig).toContain('api_key_secret_name: "dns-api-temporal-api-key"');
67
- expect(configScript).toContain('profile: "microservice"');
68
- expect(configScript).toContain('domain: "waitlist"');
69
- expect(configScript).toContain('kind: "microservice"');
70
- expect(configScript).toContain(`runtime: "${variant.runtime}"`);
71
- expect(configScript).toContain(`framework: "${variant.framework}"`);
72
- expect(configScript).toContain('mode: "create_new"');
73
- expect(configScript).toContain('quotaProjectId: "anmho-infra-prod"');
74
- expect(configScript).toContain('issuer: "https://auth.anmho.com/api/auth"');
75
- expect(configScript).toContain('audience: "api://dns-api"');
76
- expect(configScript).toContain('jwksUrl: "https://auth.anmho.com/api/auth/jwks"');
77
- expect(configScript).toContain('apiKeySecretName: "dns-api-temporal-api-key"');
78
- expect(configScript).toContain('projectId: ""');
79
- expect(configScript).toContain('baseBranchId: ""');
80
- expect(configScript).toContain('baseBranchName: "main"');
81
- expect(configScript).toContain('previewBranchPrefix: "dns-api-pr"');
82
- expect(configScript).toContain('hostname: "api.dns-api.anmho.com"');
83
- expect(configScript).toContain('cloudflareVaultPath: "prod/providers/cloudflare"');
84
- expect(configScript).not.toContain("github:");
85
- expect(configScript).not.toContain("attachmentBucket");
86
-
87
- const deployScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
88
- expect(deployScript).toContain('--billing-project", config.project.quotaProjectId');
89
- expect(deployScript).toContain('projectMode === "use_existing"');
90
- expect(deployScript).toContain("serviceDomain");
91
- expect(deployScript).toContain("ensureProductionDomainMapping");
92
- expect(deployScript).toContain("ensureCloudflareDnsRecord");
93
- expect(deployScript).toContain("gcloudWithRetry");
94
- expect(deployScript).toContain("CLOUDFLARE_API_TOKEN");
95
- expect(deployScript).toContain('"domain-mappings",');
96
- expect(deployScript).toContain('"--region",');
97
- expect(deployScript).toContain("assertProductionDomainAvailable");
98
- expect(deployScript).toContain("assertServiceNameAvailable");
99
- expect(deployScript).not.toContain("ensureStorageBucket");
100
-
71
+ expect(serviceConfig).toContain('project_mode: "create_new"');
72
+ expect(serviceConfig).toContain('quota_project_id: "anmho-infra-prod"');
73
+ expect(serviceConfig).toContain('jwks_url: "https://auth.anmho.com/api/auth/jwks"');
74
+ expect(serviceConfig).toContain('project_id: ""');
75
+ expect(serviceConfig).toContain('base_branch_id: ""');
76
+ expect(serviceConfig).toContain('base_branch_name: "main"');
77
+ expect(serviceConfig).toContain('preview_branch_prefix: "dns-api-pr"');
78
+ expect(serviceConfig).toContain('hostname: "api.dns-api.anmho.com"');
79
+ expect(serviceConfig).not.toContain("github:");
80
+ expect(serviceConfig).not.toContain("attachmentBucket");
101
81
  expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "integrations.ts")).exists()).toBeFalse();
102
- const destroyScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cleanup.ts")).text();
103
- expect(destroyScript).toContain("assertOwnedResource");
104
- expect(destroyScript).toContain("Planning resources to destroy");
105
- expect(destroyScript).toContain("Resources selected for destroy");
106
- expect(destroyScript).toContain("Destroy cannot continue until resource discovery succeeds");
107
- expect(destroyScript).toContain("deleteAuthResourceServer");
108
- expect(destroyScript).toContain("deleteGrafanaResources");
109
- expect(destroyScript).toContain('gcx", ["resources", "delete"');
110
- expect(destroyScript).toContain("config.temporal.apiKeySecretName");
111
- const neonScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "neon.ts")).text();
112
- expect(neonScript).toContain("assertDatabaseOwned");
113
- expect(neonScript).toContain("assertDisposableBranchName");
114
82
  const seedScript = await Bun.file(join(generatedRoot, "scripts", "seed.ts")).text();
115
83
  expect(seedScript).toContain("SEED_PROD=true");
116
84
  expect(seedScript).toContain("waitlist_entries");
@@ -168,14 +136,19 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
168
136
 
169
137
  if (variant.runtime === "go") {
170
138
  const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
139
+ const goSumExists = await Bun.file(join(generatedRoot, "go.sum")).exists();
171
140
  const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
172
141
  expect(goMod).toContain("module github.com/anmho/dns-api");
173
142
  expect(goMod).not.toContain("module example.com/dns-api");
143
+ expect(goSumExists).toBeTrue();
144
+ const dockerfile = await Bun.file(join(generatedRoot, "Dockerfile")).text();
145
+ expect(dockerfile).toContain("COPY go.mod go.sum ./");
174
146
  expect(packageJson).toContain('"dev": "make dev"');
175
- expect(packageJson).toContain('"migrate": "make migrate"');
176
- expect(packageJson).toContain('"create": "bun run ./scripts/cloudrun/cli.ts create"');
177
- expect(packageJson).toContain('"deploy": "bun run ./scripts/cloudrun/cli.ts deploy"');
178
- expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
147
+ expect(packageJson).toContain('"service": "service"');
148
+ expect(packageJson).toContain('"migrate": "service migrate"');
149
+ expect(packageJson).toContain('"create": "service create"');
150
+ expect(packageJson).toContain('"deploy": "service deploy"');
151
+ expect(packageJson).toContain('"destroy": "service destroy"');
179
152
 
180
153
  const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
181
154
  expect(mainGo).toContain("github.com/anmho/dns-api");
@@ -211,45 +184,26 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
211
184
  const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
212
185
  expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
213
186
  expect(packageJson).toContain("@temporalio/worker");
214
- expect(packageJson).toContain('"service": "./scripts/cloudrun/cli.ts"');
215
187
  expect(packageJson).toContain('"dev": "bun run ./scripts/dev.ts bun run ./src/index.ts"');
216
188
  expect(packageJson).toContain('"gen": "bun run ./scripts/codegen.ts"');
217
- expect(packageJson).toContain('"create": "bun run ./scripts/cloudrun/cli.ts create"');
218
- expect(packageJson).toContain('"deploy": "bun run ./scripts/cloudrun/cli.ts deploy"');
219
- expect(packageJson).toContain('"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"');
220
- expect(packageJson).toContain('"auth": "bun run ./scripts/cloudrun/cli.ts auth"');
221
- expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
222
- const serviceCli = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).text();
223
- expect(serviceCli).toContain("service <command> [args]");
224
- expect(serviceCli).toContain("Provision auth, database, migrations, and first deploy");
225
- expect(serviceCli).toContain("assertServiceNameAvailable(config.serviceName)");
226
- expect(serviceCli).toContain("ensureAuthResourceServer");
227
- expect(serviceCli).toContain("ensureAuthClient");
228
- expect(serviceCli).toContain("auth token");
229
- expect(serviceCli).toContain('["resources", "push", "--path", "./grafana"]');
230
- const cloudrunLib = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
231
- expect(cloudrunLib).toContain("resolveTemporalRuntimeConfig");
232
- expect(cloudrunLib).toContain("TEMPORAL_API_KEY_ENV");
233
- expect(cloudrunLib).toContain("value === undefined");
234
-
235
- const authctlScript = await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).text();
236
- expect(authctlScript).toContain("authctl");
237
- expect(authctlScript).toContain("resource-servers");
238
- expect(authctlScript).toContain("clients");
239
- expect(authctlScript).toContain("defaultClientTargetArgs");
240
- expect(authctlScript).toContain("ensureAuthClient");
241
- expect(authctlScript).toContain("mintAuthToken");
242
- expect(authctlScript).toContain("clientVaultPath");
243
- expect(authctlScript).toContain("deleteAuthResourceServer");
244
- expect(authctlScript).toContain("readAuthctlAccessVaultField");
245
- expect(authctlScript).toContain("prod/apps/auth/authctl/cloudflare-access");
246
- expect(authctlScript).toContain('existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl")');
247
- expect(authctlScript).not.toContain('defaultAuthResourceServerArgs(), "--yes", "--json"');
189
+ expect(packageJson).toContain('"service": "service"');
190
+ expect(packageJson).toContain('"migrate": "service migrate"');
191
+ expect(packageJson).toContain('"create": "service create"');
192
+ expect(packageJson).toContain('"deploy": "service deploy"');
193
+ expect(packageJson).toContain('"dashboards": "service dashboards"');
194
+ expect(packageJson).toContain('"auth": "service auth"');
195
+ expect(packageJson).toContain('"destroy": "service destroy"');
196
+ expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
197
+ expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeFalse();
198
+ const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
199
+ expect(serviceConfig).toContain('service_id: "dns-api"');
200
+ expect(serviceConfig).toContain('project_id: "anmho-dns-api"');
201
+ expect(serviceConfig).toContain('database_name: "dns_api"');
248
202
  const authScript = await Bun.file(join(generatedRoot, "src", "auth.ts")).text();
249
203
  expect(authScript).toContain('"Ed25519"');
250
204
 
251
205
  const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
252
- expect(makefile).toContain("npx --no-install service");
206
+ expect(makefile).toContain("SERVICE := service");
253
207
  expect(makefile).toContain("dashboards:");
254
208
  expect(makefile).toContain("auth:");
255
209
  expect(makefile).toContain("bun run dev");
@@ -346,9 +300,9 @@ test("scaffolds the workers target with wrangler lifecycle commands", async () =
346
300
 
347
301
  const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
348
302
  expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
349
- expect(packageJson).toContain('"service": "./scripts/workers/cli.ts"');
350
303
  expect(packageJson).toContain('"dev": "wrangler dev"');
351
- expect(packageJson).toContain('"auth": "bun run ./scripts/workers/cli.ts auth"');
304
+ expect(packageJson).toContain('"service": "service"');
305
+ expect(packageJson).toContain('"auth": "service auth"');
352
306
  expect(packageJson).toContain('"wrangler"');
353
307
  expect(packageJson).toContain('"pg"');
354
308
 
@@ -370,28 +324,19 @@ test("scaffolds the workers target with wrangler lifecycle commands", async () =
370
324
  expect(readme).toContain("Cloudflare Workers");
371
325
  expect(readme).toContain("Hyperdrive binding for Neon-backed Postgres persistence");
372
326
  expect(readme).not.toContain("Cloud Run");
373
- const workerCli = await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).text();
374
- expect(workerCli).toContain("hyperdrive");
375
- expect(workerCli).toContain('["resources", "push", "--path", "./grafana"]');
376
- expect(workerCli).toContain("ensureAuthResourceServer");
377
- expect(workerCli).toContain("ensureAuthClient");
378
- expect(workerCli).toContain("auth token");
379
- expect(workerCli).toContain("Workers database schema applied");
380
- expect(workerCli).toContain("create table if not exists waitlist_entries");
381
- expect(workerCli).toContain("DATABASE_URL or NEON_API_KEY is required to provision the Hyperdrive binding");
382
- expect(workerCli).toContain("createProjectBranchDatabase");
383
- expect(workerCli).toContain("deleteNeonDatabase");
384
- expect(workerCli).toContain("deleteGrafanaResources");
385
- expect(workerCli).toContain("hyperdrive\", \"delete");
327
+ const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
328
+ expect(serviceConfig).toContain('target: "workers"');
329
+ expect(serviceConfig).toContain('hostname: "api.dns-api.anmho.com"');
330
+ expect(serviceConfig).toContain('database_name: "dns_api"');
386
331
  const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
387
332
  expect(makefile).toContain('no generated code for workers');
388
333
  expect(makefile).toContain("auth:");
389
334
  expect(makefile).not.toContain("scripts/codegen.ts");
390
335
 
391
- expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeTrue();
336
+ expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeFalse();
392
337
  expect(await Bun.file(join(generatedRoot, "src", "auth.ts")).exists()).toBeTrue();
393
338
  expect(await Bun.file(join(generatedRoot, "src", "storage.ts")).exists()).toBeTrue();
394
- expect(await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).exists()).toBeTrue();
339
+ expect(await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).exists()).toBeFalse();
395
340
  expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
396
341
  expect(await Bun.file(join(generatedRoot, "scripts", "dev.ts")).exists()).toBeFalse();
397
342
  expect(await Bun.file(join(generatedRoot, "scripts", "ensure-local-db.ts")).exists()).toBeFalse();
package/src/scaffold.ts CHANGED
@@ -77,6 +77,14 @@ export async function scaffoldProject(config: ScaffoldConfig) {
77
77
  }
78
78
 
79
79
  function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "variant" | "target", relativePath: string) {
80
+ if (
81
+ relativePath === "scripts/authctl.ts" ||
82
+ relativePath.startsWith("scripts/cloudrun/") ||
83
+ relativePath.startsWith("scripts/workers/")
84
+ ) {
85
+ return true;
86
+ }
87
+
80
88
  if (target === "workers") {
81
89
  if (templateKind === "target") {
82
90
  return false;
@@ -94,8 +102,7 @@ function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "var
94
102
  relativePath === "scripts/local-docker.ts" ||
95
103
  relativePath === "scripts/local-env.ts" ||
96
104
  relativePath === "scripts/seed.ts" ||
97
- relativePath === "scripts/wait-for-db.ts" ||
98
- relativePath.startsWith("scripts/cloudrun/")
105
+ relativePath === "scripts/wait-for-db.ts"
99
106
  );
100
107
  }
101
108
 
@@ -110,7 +117,7 @@ function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "var
110
117
  );
111
118
  }
112
119
 
113
- return relativePath.startsWith("scripts/workers/") || relativePath === "wrangler.toml";
120
+ return relativePath === "wrangler.toml";
114
121
  }
115
122
 
116
123
  async function ensureTargetDirectory(targetDir: string) {
@@ -1,5 +1,5 @@
1
- import serviceConfig from "../service.config";
2
1
  import { existsSync } from "node:fs";
2
+ import { serviceConfig } from "./runtime";
3
3
 
4
4
  type CommandResult = {
5
5
  success: boolean;
@@ -47,7 +47,7 @@ export function defaultAuthResourceServerArgs() {
47
47
  auth.resource_server.audience,
48
48
  "--stage",
49
49
  serviceConfig.stage_default,
50
- ...auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope]),
50
+ ...(auth.resource_server.default_scopes as string[]).flatMap((scope: string) => ["--scope", scope]),
51
51
  ];
52
52
  }
53
53
 
@@ -244,7 +244,7 @@ function defaultClientTargetArgs(rest: string[]) {
244
244
  const hasScope = hasFlag(rest, "--scope");
245
245
  return [
246
246
  ...(hasResourceServer ? [] : ["--resource-server", serviceConfig.auth.resource_server.id]),
247
- ...(hasScope ? [] : serviceConfig.auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope])),
247
+ ...(hasScope ? [] : (serviceConfig.auth.resource_server.default_scopes as string[]).flatMap((scope: string) => ["--scope", scope])),
248
248
  ];
249
249
  }
250
250
 
@@ -151,7 +151,7 @@ function planProductionDomainMapping(plan: DestroyPlan) {
151
151
  return;
152
152
  }
153
153
 
154
- const routeName = mapping.spec?.routeName;
154
+ const routeName = mapping.spec?.routeName ?? "";
155
155
  if (routeName !== config.serviceName) {
156
156
  plan.blockers.push(`${config.domain.hostname} maps to ${routeName || "an unknown service"}; refusing to delete ambiguous DNS mapping`);
157
157
  return;
@@ -23,7 +23,7 @@ import {
23
23
  serviceOrigin,
24
24
  } from "./lib";
25
25
 
26
- async function main(argv = Bun.argv.slice(2)) {
26
+ export async function main(argv = Bun.argv.slice(2)) {
27
27
  const [command, ...rest] = argv;
28
28
 
29
29
  if (!command || command === "--help" || command === "-h" || command === "help") {
@@ -0,0 +1,55 @@
1
+ import { serviceConfig } from "../runtime";
2
+
3
+ const cloudrun = serviceConfig.cloudrun;
4
+ const dns = serviceConfig.dns;
5
+ const neon = serviceConfig.neon;
6
+
7
+ export const config = {
8
+ serviceName: serviceConfig.service_id,
9
+ profile: serviceConfig.profile,
10
+ example: serviceConfig.example,
11
+ runtime: serviceConfig.runtime,
12
+ framework: serviceConfig.framework,
13
+ region: cloudrun.region,
14
+ artifactRepository: cloudrun.artifact_repository,
15
+ runtimeServiceAccount: cloudrun.service_account,
16
+ project: {
17
+ mode: cloudrun.project_mode,
18
+ id: cloudrun.project_id,
19
+ name: cloudrun.project_name,
20
+ createIfMissing: cloudrun.create_if_missing,
21
+ billingAccount: cloudrun.billing_account,
22
+ quotaProjectId: cloudrun.quota_project_id,
23
+ },
24
+ domain: {
25
+ hostname: dns.hostname,
26
+ baseDomain: dns.base_domain,
27
+ cloudflareApiBaseUrl: dns.cloudflare_api_base_url,
28
+ cloudflareVaultPath: dns.cloudflare_vault_path,
29
+ cloudflareVaultField: dns.cloudflare_vault_field,
30
+ },
31
+ auth: {
32
+ issuer: serviceConfig.auth.issuer,
33
+ audience: serviceConfig.auth.resource_server.audience,
34
+ jwksUrl: serviceConfig.auth.jwks_url,
35
+ },
36
+ temporal: {
37
+ enabled: serviceConfig.temporal.enabled,
38
+ address: serviceConfig.temporal.address,
39
+ namespace: serviceConfig.temporal.namespace,
40
+ taskQueue: serviceConfig.temporal.task_queue,
41
+ apiKeySecretName: serviceConfig.temporal.api_key_secret_name,
42
+ },
43
+ neon: {
44
+ projectId: neon.project_id,
45
+ baseBranchId: neon.base_branch_id,
46
+ baseBranchName: neon.base_branch_name,
47
+ databaseName: neon.database_name,
48
+ roleName: neon.role_name,
49
+ previewBranchPrefix: neon.preview_branch_prefix,
50
+ personalBranchPrefix: neon.personal_branch_prefix,
51
+ },
52
+ requiredApis: cloudrun.required_apis,
53
+ } as const;
54
+
55
+ export type DeployEnvironment = "main" | "preview" | "personal";
@@ -0,0 +1,8 @@
1
+ import { join } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ export const serviceRoot = process.env.CREATE_SVC_SERVICE_ROOT?.trim() || process.cwd();
5
+
6
+ export const serviceConfig = (
7
+ await import(pathToFileURL(join(serviceRoot, "service.config.ts")).href)
8
+ ).default;
@@ -4,17 +4,18 @@ import { confirm, intro, isCancel, log, outro } from "@clack/prompts";
4
4
  import { createApiClient } from "@neondatabase/api-client";
5
5
  import { Client } from "pg";
6
6
  import { ensureAuthClient, ensureAuthResourceServer, runAuthCommand, runAuthDoctor } from "../authctl";
7
+ import { serviceConfig } from "../runtime";
7
8
 
8
9
  const config = {
9
- serviceName: "{{SERVICE_NAME}}",
10
- hostname: "{{API_HOSTNAME}}",
11
- neonDatabaseName: "{{NEON_DATABASE_NAME}}",
12
- neonRoleName: "neondb_owner",
10
+ serviceName: serviceConfig.service_id,
11
+ hostname: serviceConfig.dns.hostname,
12
+ neonDatabaseName: serviceConfig.neon.database_name,
13
+ neonRoleName: serviceConfig.neon.role_name,
13
14
  };
14
15
 
15
16
  type DoctorStatus = "pass" | "warn" | "fail";
16
17
 
17
- async function main(argv = Bun.argv.slice(2)) {
18
+ export async function main(argv = Bun.argv.slice(2)) {
18
19
  const [command, ...rest] = argv;
19
20
 
20
21
  if (!command || command === "--help" || command === "-h" || command === "help") {
@@ -255,7 +256,7 @@ async function deleteNeonDatabase() {
255
256
 
256
257
  const payload = await neon.getProjectBranchDatabase(projectId, branchId, config.neonDatabaseName);
257
258
  const database = (payload.data as { database?: { name?: string; owner_name?: string } } | undefined)?.database;
258
- if (database?.name !== config.neonDatabaseName || (database.owner_name && database.owner_name !== config.neonRoleName)) {
259
+ if (!database || database.name !== config.neonDatabaseName || (database.owner_name && database.owner_name !== config.neonRoleName)) {
259
260
  throw new Error(`Refusing to delete Neon database ${database?.name ?? config.neonDatabaseName}; ownership metadata does not match`);
260
261
  }
261
262
 
@@ -20,10 +20,8 @@ test("findGeneratedServiceRoot detects generated service context from nested dir
20
20
  const root = await mkdtemp(join(tmpdir(), "create-svc-service-root-"));
21
21
  const serviceRoot = join(root, "generated-api");
22
22
  const nested = join(serviceRoot, "src", "waitlist");
23
- await mkdir(join(serviceRoot, "scripts", "cloudrun"), { recursive: true });
24
23
  await mkdir(nested, { recursive: true });
25
24
  await writeFile(join(serviceRoot, "service.config.ts"), "export default {}");
26
- await writeFile(join(serviceRoot, "scripts", "cloudrun", "cli.ts"), "");
27
25
 
28
26
  expect(findGeneratedServiceRoot(nested)).toBe(serviceRoot);
29
27
  expect(findGeneratedServiceRoot(root)).toBeUndefined();
package/src/service.ts CHANGED
@@ -7,7 +7,7 @@ const SCAFFOLD_COMMANDS = new Set(["create", "new", "init"]);
7
7
  export async function runServiceCommand(argv: string[], cwd = process.cwd()) {
8
8
  const serviceRoot = findGeneratedServiceRoot(cwd);
9
9
  if (serviceRoot) {
10
- delegateToGeneratedService(serviceRoot, argv);
10
+ await delegateToGeneratedService(serviceRoot, argv);
11
11
  return;
12
12
  }
13
13
 
@@ -41,29 +41,23 @@ export function findGeneratedServiceRoot(start: string): string | undefined {
41
41
  }
42
42
 
43
43
  function isGeneratedServiceRoot(path: string) {
44
- return (
45
- existsSync(join(path, "service.config.ts")) &&
46
- (existsSync(join(path, "scripts", "cloudrun", "cli.ts")) || existsSync(join(path, "scripts", "workers", "cli.ts")))
47
- );
44
+ return existsSync(join(path, "service.config.ts"));
48
45
  }
49
46
 
50
- function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
47
+ async function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
51
48
  ensureGeneratedDependencies(serviceRoot);
49
+ process.chdir(serviceRoot);
50
+ process.env.CREATE_SVC_SERVICE_ROOT = serviceRoot;
52
51
 
53
- const cliPath = existsSync(join(serviceRoot, "scripts", "cloudrun", "cli.ts"))
54
- ? "./scripts/cloudrun/cli.ts"
55
- : "./scripts/workers/cli.ts";
56
- const result = Bun.spawnSync(["bun", "run", cliPath, ...argv], {
57
- cwd: serviceRoot,
58
- env: process.env,
59
- stdin: "inherit",
60
- stdout: "inherit",
61
- stderr: "inherit",
62
- });
63
-
64
- if (!result.success) {
65
- process.exit(result.exitCode || 1);
52
+ const serviceConfig = (await import(`${serviceRoot}/service.config.ts`)).default;
53
+ if (serviceConfig.target === "workers") {
54
+ const { main } = await import("./service-runtime/workers/cli");
55
+ await main(argv);
56
+ return;
66
57
  }
58
+
59
+ const { main } = await import("./service-runtime/cloudrun/cli");
60
+ await main(argv);
67
61
  }
68
62
 
69
63
  export function generatedDependenciesInstalled(serviceRoot: string) {
@@ -59,7 +59,7 @@ No cloud credentials are required for local HTTP development after Docker and Po
59
59
 
60
60
  ## Remote provisioning
61
61
 
62
- The generated Cloud Run config lives in [scripts/cloudrun/config.ts](scripts/cloudrun/config.ts).
62
+ The generated service config lives in [service.config.ts](service.config.ts).
63
63
 
64
64
  Create, deploy, and destroy use:
65
65
 
@@ -3,7 +3,13 @@ export default {
3
3
  target: "{{TARGET}}",
4
4
  runtime: "{{RUNTIME}}",
5
5
  framework: "{{FRAMEWORK}}",
6
+ profile: "{{PROFILE}}",
6
7
  stage_default: "prod",
8
+ example: {
9
+ kind: "{{EXAMPLE_KIND}}",
10
+ domain: "{{EXAMPLE_DOMAIN}}",
11
+ label: "{{EXAMPLE_LABEL}}",
12
+ },
7
13
  dns: {
8
14
  hostname: "{{API_HOSTNAME}}",
9
15
  base_domain: "{{API_BASE_DOMAIN}}",
@@ -47,13 +53,38 @@ export default {
47
53
  temporal_path: "prod/providers/temporal",
48
54
  },
49
55
  },
56
+ neon: {
57
+ project_id: "{{NEON_PROJECT_ID}}",
58
+ base_branch_id: "{{NEON_BASE_BRANCH_ID}}",
59
+ base_branch_name: "{{NEON_BASE_BRANCH_NAME}}",
60
+ database_name: "{{NEON_DATABASE_NAME}}",
61
+ role_name: "{{NEON_ROLE_NAME}}",
62
+ preview_branch_prefix: "{{NEON_PREVIEW_BRANCH_PREFIX}}",
63
+ personal_branch_prefix: "{{NEON_PERSONAL_BRANCH_PREFIX}}",
64
+ },
50
65
  buf: {
51
66
  module: "buf.build/anmho/{{SERVICE_ID}}",
52
67
  },
53
68
  cloudrun: {
54
69
  project_id: "{{PROJECT_ID}}",
70
+ project_name: "{{PROJECT_NAME}}",
71
+ project_mode: "{{GCP_PROJECT_MODE}}",
72
+ create_if_missing: {{PROJECT_CREATE_IF_MISSING}},
73
+ billing_account: "{{BILLING_ACCOUNT}}",
74
+ quota_project_id: "{{QUOTA_PROJECT_ID}}",
55
75
  region: "{{REGION}}",
76
+ artifact_repository: "cloud-run",
56
77
  service_account: "{{RUNTIME_SERVICE_ACCOUNT}}",
78
+ required_apis: [
79
+ "run.googleapis.com",
80
+ "cloudbuild.googleapis.com",
81
+ "artifactregistry.googleapis.com",
82
+ "iam.googleapis.com",
83
+ "iamcredentials.googleapis.com",
84
+ "secretmanager.googleapis.com",
85
+ "serviceusage.googleapis.com",
86
+ "sts.googleapis.com",
87
+ ],
57
88
  },
58
89
  workers: {
59
90
  script_name: "{{SERVICE_ID}}",