create-svc 0.1.10 → 0.1.12

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 (171) hide show
  1. package/README.md +51 -47
  2. package/index.ts +2 -2
  3. package/package.json +10 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +196 -33
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +1 -0
  9. package/src/naming.ts +23 -0
  10. package/src/post-scaffold.test.ts +19 -0
  11. package/src/post-scaffold.ts +17 -4
  12. package/src/profiles.ts +2 -5
  13. package/src/scaffold.test.ts +232 -41
  14. package/src/scaffold.ts +81 -36
  15. package/src/service.test.ts +30 -0
  16. package/src/service.ts +65 -0
  17. package/src/vault.test.ts +61 -1
  18. package/src/vault.ts +77 -15
  19. package/templates/shared/.github/workflows/ci.yml +2 -1
  20. package/templates/shared/.github/workflows/deploy.yml +2 -0
  21. package/templates/shared/README.md +124 -47
  22. package/templates/shared/grafana/alerts.yaml +54 -0
  23. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  24. package/templates/shared/scripts/authctl.ts +231 -0
  25. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  26. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  27. package/templates/shared/scripts/cloudrun/cli.ts +329 -7
  28. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  29. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  30. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  31. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  32. package/templates/shared/scripts/dev.ts +22 -0
  33. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  34. package/templates/shared/scripts/local-docker.ts +63 -0
  35. package/templates/shared/scripts/local-env.ts +27 -0
  36. package/templates/shared/scripts/seed.ts +73 -0
  37. package/templates/shared/scripts/wait-for-db.ts +32 -0
  38. package/templates/shared/service.config.ts +59 -0
  39. package/templates/shared/service.yaml +24 -44
  40. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  41. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  42. package/templates/targets/workers/Makefile +33 -0
  43. package/templates/targets/workers/README.md +75 -0
  44. package/templates/targets/workers/package.json +35 -0
  45. package/templates/targets/workers/scripts/workers/cli.ts +402 -0
  46. package/templates/targets/workers/src/auth.ts +178 -0
  47. package/templates/targets/workers/src/index.ts +198 -0
  48. package/templates/targets/workers/src/storage.ts +370 -0
  49. package/templates/targets/workers/test/app.test.ts +108 -0
  50. package/templates/targets/workers/tsconfig.json +11 -0
  51. package/templates/targets/workers/wrangler.toml +24 -0
  52. package/templates/variants/bun-connectrpc/Makefile +14 -8
  53. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  54. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  55. package/templates/variants/bun-connectrpc/package.json +12 -5
  56. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  57. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  58. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  59. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  60. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  61. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  62. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  63. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  64. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  65. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  66. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  67. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  68. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  69. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  70. package/templates/variants/bun-hono/Makefile +14 -8
  71. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  72. package/templates/variants/bun-hono/package.json +12 -5
  73. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  74. package/templates/variants/bun-hono/src/auth.ts +181 -0
  75. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  76. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  77. package/templates/variants/bun-hono/src/index.ts +65 -180
  78. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  79. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  80. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  81. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  82. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  83. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  84. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  85. package/templates/variants/go-chi/Makefile +27 -11
  86. package/templates/variants/go-chi/atlas.hcl +8 -0
  87. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  88. package/templates/variants/go-chi/go.mod +1 -3
  89. package/templates/variants/go-chi/internal/app/service.go +202 -685
  90. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  91. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  92. package/templates/variants/go-chi/internal/config/config.go +27 -11
  93. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  94. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  95. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  96. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  97. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  98. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  99. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  100. package/templates/variants/go-chi/package.json +7 -1
  101. package/templates/variants/go-connectrpc/Makefile +26 -9
  102. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  103. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  104. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  105. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  106. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  107. package/templates/variants/go-connectrpc/go.mod +1 -1
  108. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  109. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  110. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  111. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  112. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  113. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  114. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  115. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  116. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  117. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  118. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  119. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  120. package/templates/variants/go-connectrpc/package.json +7 -1
  121. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  122. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  123. package/templates/root/.github/workflows/ci.yml +0 -26
  124. package/templates/root/.github/workflows/deploy.yml +0 -22
  125. package/templates/root/Dockerfile +0 -23
  126. package/templates/root/README.md +0 -69
  127. package/templates/root/buf.gen.yaml +0 -10
  128. package/templates/root/buf.yaml +0 -9
  129. package/templates/root/cmd/server/main.go +0 -44
  130. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  131. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  132. package/templates/root/go.mod +0 -10
  133. package/templates/root/internal/app/service.go +0 -152
  134. package/templates/root/internal/app/token_source.go +0 -50
  135. package/templates/root/internal/cloudflare/client.go +0 -160
  136. package/templates/root/internal/config/config.go +0 -55
  137. package/templates/root/internal/connectapi/handler.go +0 -79
  138. package/templates/root/internal/httpapi/routes.go +0 -93
  139. package/templates/root/internal/vault/client.go +0 -148
  140. package/templates/root/package.json +0 -12
  141. package/templates/root/protos/dns/v1/dns.proto +0 -58
  142. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  143. package/templates/root/scripts/cloudrun/config.ts +0 -50
  144. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  145. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  146. package/templates/root/service.yaml +0 -50
  147. package/templates/root/test/go.test.ts +0 -19
  148. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  149. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  150. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  151. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  152. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  153. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  154. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  155. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  156. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  157. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  158. package/templates/variants/bun-hono/src/storage.ts +0 -72
  159. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  160. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  161. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  162. package/templates/variants/go-chi/buf.yaml +0 -9
  163. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  164. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  165. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  166. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  167. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  168. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  169. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  170. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
  171. /package/bin/{create-svc.mjs → service.mjs} +0 -0
package/src/cli.ts CHANGED
@@ -16,13 +16,16 @@ import { readdirSync } from "node:fs";
16
16
  import { basename, dirname, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { runPostScaffoldFlow } from "./post-scaffold";
19
+ import { bootstrapGitHubRepository, buildGitBootstrapConfig, commitAndPushGeneratedArtifacts } from "./git-bootstrap";
19
20
  import { listOpenBillingAccounts, listAccessibleProjects, type BillingAccount, type GcpProject } from "./gcp";
20
21
  import {
21
22
  BILLING_ACCOUNT_DEFAULT,
22
- FRAMEWORKS_BY_RUNTIME,
23
23
  QUOTA_PROJECT_DEFAULT,
24
24
  deriveDefaults,
25
+ frameworksForTargetRuntime,
26
+ parseDeployTarget,
25
27
  slugify,
28
+ type DeployTarget,
26
29
  type Framework,
27
30
  type GcpProjectMode,
28
31
  type Runtime,
@@ -37,6 +40,7 @@ import {
37
40
 
38
41
  type ParsedArgs = {
39
42
  directory?: string;
43
+ target?: DeployTarget;
40
44
  runtime?: Runtime;
41
45
  framework?: Framework;
42
46
  modulePath?: string;
@@ -46,6 +50,9 @@ type ParsedArgs = {
46
50
  billingAccount?: string;
47
51
  quotaProjectId?: string;
48
52
  autoDeploy?: boolean;
53
+ autoUpdate?: boolean;
54
+ noUpdateCheck?: boolean;
55
+ noGit?: boolean;
49
56
  profile: Profile;
50
57
  yes: boolean;
51
58
  help: boolean;
@@ -67,7 +74,9 @@ export async function run(argv: string[]) {
67
74
  return;
68
75
  }
69
76
 
70
- intro(`${pc.bold("create-svc")} ${pc.dim("backend bootstrap")}`);
77
+ await maybeCheckForUpdate(args);
78
+
79
+ intro(`${pc.bold("service")} ${pc.dim("microservice bootstrap")}`);
71
80
 
72
81
  const config = await resolveConfig(args);
73
82
  const targetDir = resolve(process.cwd(), config.directory);
@@ -75,10 +84,12 @@ export async function run(argv: string[]) {
75
84
  note(
76
85
  [
77
86
  `${pc.bold("Output")}: ${targetDir}`,
87
+ `${pc.bold("Target")}: ${config.target}`,
78
88
  `${pc.bold("Runtime")}: ${config.runtime} + ${config.framework}`,
79
89
  `${pc.bold("Project")}: ${config.gcpProjectMode === "create_new" ? "create" : "use"} ${config.gcpProjectName} (${config.gcpProject})`,
80
90
  `${pc.bold("API")}: https://${config.apiHostname}`,
81
91
  `${pc.bold("Local DB")}: docker compose postgres`,
92
+ `${pc.bold("GitHub")}: ${config.git.enabled ? `anmho/${config.git.repository}` : "disabled"}`,
82
93
  ].join("\n"),
83
94
  "Scaffold"
84
95
  );
@@ -88,6 +99,17 @@ export async function run(argv: string[]) {
88
99
  await scaffoldProject(config);
89
100
  buildSpinner.stop("Project files generated");
90
101
 
102
+ const gitSpinner = spinner();
103
+ gitSpinner.start("Preparing git repository");
104
+ const gitResult = await bootstrapGitHubRepository(targetDir, config.git);
105
+ if (gitResult.status === "created") {
106
+ gitSpinner.stop(`GitHub repository created: ${gitResult.url}`);
107
+ } else if (gitResult.status === "skipped-existing-worktree") {
108
+ gitSpinner.stop(`Existing git worktree detected: ${gitResult.root}`);
109
+ } else {
110
+ gitSpinner.stop("Git bootstrap disabled");
111
+ }
112
+
91
113
  const shouldRunPostScaffoldFlow = config.autoDeploy;
92
114
  if (shouldRunPostScaffoldFlow) {
93
115
  const automationSpinner = spinner();
@@ -96,8 +118,15 @@ export async function run(argv: string[]) {
96
118
  const result = await runPostScaffoldFlow(config, targetDir);
97
119
  automationSpinner.stop(result.message);
98
120
  } catch (error) {
99
- automationSpinner.stop("Post-scaffold automation skipped");
100
- log.warn(error instanceof Error ? error.message : String(error));
121
+ automationSpinner.stop("Post-scaffold automation failed");
122
+ throw error;
123
+ }
124
+
125
+ if (gitResult.status === "created") {
126
+ const publishSpinner = spinner();
127
+ publishSpinner.start("Publishing generated artifacts");
128
+ const result = commitAndPushGeneratedArtifacts(targetDir, "Record generated deployment artifacts");
129
+ publishSpinner.stop(result.committed ? "Generated artifacts committed and pushed" : "Generated artifacts already committed");
101
130
  }
102
131
  }
103
132
 
@@ -105,18 +134,17 @@ export async function run(argv: string[]) {
105
134
  outro(
106
135
  [
107
136
  `Next: ${pc.cyan(`cd ${config.directory}`)}`,
108
- `Local DB: ${pc.cyan("docker compose up -d")}`,
137
+ `Local DB: ${pc.cyan("started by local dev command")}`,
109
138
  `Migrate: ${pc.cyan(isBun ? "bun run migrate" : "make migrate")}`,
110
139
  `Local dev: ${pc.cyan(isBun ? "bun run dev" : "make dev")}`,
111
- `Bootstrap: ${pc.cyan(isBun ? "bun run bootstrap" : "make bootstrap")}`,
112
- `Deploy: ${pc.cyan(isBun ? "bun run deploy" : "make deploy")}`,
140
+ `Create: ${pc.cyan("service create")}`,
141
+ `Deploy: ${pc.cyan("service deploy")}`,
142
+ config.git.enabled ? `Repository: ${pc.cyan(`https://github.com/anmho/${config.git.repository}`)}` : undefined,
113
143
  `Personal env: ${pc.cyan(
114
- isBun
115
- ? `bun run deploy -- --environment personal --name ${config.serviceName}`
116
- : `make deploy ARGS="--environment personal --name ${config.serviceName}"`
144
+ `service deploy --environment personal --name ${config.serviceName}`
117
145
  )}`,
118
146
  `Production API: ${pc.cyan(`https://${config.apiHostname}`)}`,
119
- ].join("\n")
147
+ ].filter(Boolean).join("\n")
120
148
  );
121
149
  } catch (error) {
122
150
  handleCliError(error);
@@ -160,6 +188,21 @@ export function parseArgs(argv: string[]): ParsedArgs {
160
188
  continue;
161
189
  }
162
190
 
191
+ if (token === "--auto-update") {
192
+ parsed.autoUpdate = true;
193
+ continue;
194
+ }
195
+
196
+ if (token === "--no-update-check") {
197
+ parsed.noUpdateCheck = true;
198
+ continue;
199
+ }
200
+
201
+ if (token === "--no-git") {
202
+ parsed.noGit = true;
203
+ continue;
204
+ }
205
+
163
206
  if (token === "--runtime") {
164
207
  parsed.runtime = readValue() as Runtime;
165
208
  continue;
@@ -170,6 +213,16 @@ export function parseArgs(argv: string[]): ParsedArgs {
170
213
  continue;
171
214
  }
172
215
 
216
+ if (token === "--target") {
217
+ parsed.target = parseDeployTarget(readValue());
218
+ continue;
219
+ }
220
+
221
+ if (token.startsWith("--target=")) {
222
+ parsed.target = parseDeployTarget(token.slice("--target=".length));
223
+ continue;
224
+ }
225
+
173
226
  if (token === "--framework") {
174
227
  parsed.framework = readValue() as Framework;
175
228
  continue;
@@ -260,11 +313,6 @@ export function parseArgs(argv: string[]): ParsedArgs {
260
313
  continue;
261
314
  }
262
315
 
263
- if (token === "--bootstrap") {
264
- parsed.autoDeploy = true;
265
- continue;
266
- }
267
-
268
316
  if (token === "--no-auto-deploy") {
269
317
  parsed.autoDeploy = false;
270
318
  continue;
@@ -276,6 +324,69 @@ export function parseArgs(argv: string[]): ParsedArgs {
276
324
  return parsed;
277
325
  }
278
326
 
327
+ const CURRENT_VERSION = "0.1.9";
328
+ const PACKAGE_NAME = "create-svc";
329
+
330
+ async function maybeCheckForUpdate(args: ParsedArgs) {
331
+ if (args.noUpdateCheck || shouldSkipUpdateCheck()) {
332
+ return;
333
+ }
334
+
335
+ const latest = await resolveLatestVersion().catch(() => "");
336
+ if (!latest || !isVersionGreater(latest, CURRENT_VERSION)) {
337
+ return;
338
+ }
339
+
340
+ const command = `bunx ${PACKAGE_NAME}@latest ${Bun.argv.slice(2).filter((arg) => arg !== "--auto-update").join(" ")}`.trim();
341
+ if (!args.autoUpdate) {
342
+ log.info(`A newer ${PACKAGE_NAME} is available: ${CURRENT_VERSION} -> ${latest}. Run ${command}`);
343
+ return;
344
+ }
345
+
346
+ const result = Bun.spawnSync(["bunx", `${PACKAGE_NAME}@latest`, ...Bun.argv.slice(2).filter((arg) => arg !== "--auto-update")], {
347
+ stdin: "inherit",
348
+ stdout: "inherit",
349
+ stderr: "inherit",
350
+ env: {
351
+ ...process.env,
352
+ CREATE_SERVICE_NO_UPDATE_CHECK: "1",
353
+ },
354
+ });
355
+ process.exit(result.exitCode);
356
+ }
357
+
358
+ function shouldSkipUpdateCheck() {
359
+ return Boolean(
360
+ process.env.CI ||
361
+ process.env.CODEX_CI ||
362
+ process.env.CREATE_SERVICE_NO_UPDATE_CHECK ||
363
+ process.env.BUN_TEST ||
364
+ process.env.npm_lifecycle_event
365
+ );
366
+ }
367
+
368
+ async function resolveLatestVersion() {
369
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
370
+ signal: AbortSignal.timeout(1_500),
371
+ });
372
+ if (!response.ok) {
373
+ return "";
374
+ }
375
+ const payload = (await response.json()) as { version?: string };
376
+ return payload.version?.trim() ?? "";
377
+ }
378
+
379
+ function isVersionGreater(left: string, right: string) {
380
+ const parse = (value: string) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
381
+ const [leftMajor = 0, leftMinor = 0, leftPatch = 0] = parse(left);
382
+ const [rightMajor = 0, rightMinor = 0, rightPatch = 0] = parse(right);
383
+ return (
384
+ leftMajor > rightMajor ||
385
+ (leftMajor === rightMajor && leftMinor > rightMinor) ||
386
+ (leftMajor === rightMajor && leftMinor === rightMinor && leftPatch > rightPatch)
387
+ );
388
+ }
389
+
279
390
  export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
280
391
  const inferredName = slugify(basename(args.directory ?? "my-service"));
281
392
  const serviceName = args.yes
@@ -287,14 +398,17 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
287
398
 
288
399
  const discoveryPromise = discoverCloudInputs();
289
400
  const defaults = deriveDefaults(serviceName);
290
- const runtime = await resolveRuntime(args);
291
- const framework = await resolveFramework(args, runtime);
401
+ const target = await resolveTarget(args);
402
+ const runtime = await resolveRuntime(args, target);
403
+ const framework = await resolveFramework(args, target, runtime);
404
+ validateTargetRuntimeFramework(target, runtime, framework);
292
405
  const modulePath = await resolveModulePath(args, runtime, defaults.modulePath);
293
406
  const discovery = await waitForDiscovery(discoveryPromise);
294
407
  const gcpSelection = await resolveGcpSelection(args, defaults, discovery);
295
408
  const region = args.region ?? DEFAULT_REGION;
296
409
  const billingAccount = chooseBillingAccount(args.billingAccount, discovery.billingAccounts);
297
410
  const autoDeploy = resolveAutoDeploy(args.autoDeploy);
411
+ const git = buildGitBootstrapConfig(serviceName, args.noGit);
298
412
 
299
413
  if (!args.yes) {
300
414
  const okay = await confirm({
@@ -315,6 +429,7 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
315
429
  directory,
316
430
  serviceName,
317
431
  modulePath,
432
+ target,
318
433
  runtime,
319
434
  framework,
320
435
  profile: args.profile,
@@ -325,6 +440,7 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
325
440
  billingAccount,
326
441
  quotaProjectId: args.quotaProjectId ?? QUOTA_PROJECT_DEFAULT,
327
442
  autoDeploy,
443
+ git,
328
444
  neonDatabaseName: defaults.neonDatabaseName,
329
445
  apiHostname: defaults.apiHostname,
330
446
  generatorRoot: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
@@ -344,22 +460,55 @@ async function waitForDiscovery(discoveryPromise: Promise<DiscoveryState>) {
344
460
  }
345
461
  }
346
462
 
347
- async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
463
+ async function resolveTarget(args: ParsedArgs): Promise<DeployTarget> {
464
+ if (args.target) {
465
+ return args.target;
466
+ }
467
+
468
+ if (args.yes) {
469
+ return "cloudrun";
470
+ }
471
+
472
+ const value = await select({
473
+ message: "Deploy target",
474
+ initialValue: "cloudrun",
475
+ options: [
476
+ { value: "cloudrun", label: "Cloud Run", hint: "Default" },
477
+ { value: "workers", label: "Cloudflare Workers" },
478
+ ],
479
+ });
480
+
481
+ if (isCancel(value)) {
482
+ cancel("Aborted");
483
+ process.exit(1);
484
+ }
485
+
486
+ return value as DeployTarget;
487
+ }
488
+
489
+ async function resolveRuntime(args: ParsedArgs, target: DeployTarget): Promise<Runtime> {
348
490
  if (args.runtime) {
349
491
  return args.runtime;
350
492
  }
351
493
 
352
- if (args.yes) {
494
+ if (target === "workers") {
353
495
  return "bun";
354
496
  }
355
497
 
498
+ if (args.yes) {
499
+ return "go";
500
+ }
501
+
356
502
  const value = await select({
357
503
  message: "Runtime",
358
- initialValue: "bun",
359
- options: [
360
- { value: "bun", label: "Bun", hint: "Default" },
361
- { value: "go", label: "Go" },
362
- ],
504
+ initialValue: target === "cloudrun" ? "go" : "bun",
505
+ options:
506
+ target === "cloudrun"
507
+ ? [
508
+ { value: "go", label: "Go", hint: "Default" },
509
+ { value: "bun", label: "Bun" },
510
+ ]
511
+ : [{ value: "bun", label: "Bun/TypeScript", hint: "Workers runtime" }],
363
512
  });
364
513
 
365
514
  if (isCancel(value)) {
@@ -370,17 +519,17 @@ async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
370
519
  return value as Runtime;
371
520
  }
372
521
 
373
- async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Framework> {
374
- const allowed = FRAMEWORKS_BY_RUNTIME[runtime];
522
+ async function resolveFramework(args: ParsedArgs, target: DeployTarget, runtime: Runtime): Promise<Framework> {
523
+ const allowed = frameworksForTargetRuntime(target, runtime);
375
524
  if (args.framework) {
376
525
  if (allowed.some((framework) => framework === args.framework)) {
377
526
  return args.framework;
378
527
  }
379
- throw new Error(`Framework ${args.framework} is not valid for runtime ${runtime}`);
528
+ throw new Error(`Framework ${args.framework} is not valid for target ${target} and runtime ${runtime}`);
380
529
  }
381
530
 
382
531
  if (args.yes) {
383
- return allowed[0];
532
+ return target === "cloudrun" && runtime === "go" ? "connectrpc" : allowed[0]!;
384
533
  }
385
534
 
386
535
  const value = await select({
@@ -639,6 +788,17 @@ export function normalizeValidationResult(result: true | string): string | undef
639
788
  return result === true ? undefined : result;
640
789
  }
641
790
 
791
+ export function validateProfileRuntimeFramework(profile: Profile, runtime: Runtime, framework: Framework) {
792
+ validateTargetRuntimeFramework("cloudrun", runtime, framework);
793
+ }
794
+
795
+ export function validateTargetRuntimeFramework(target: DeployTarget, runtime: Runtime, framework: Framework) {
796
+ const allowed = frameworksForTargetRuntime(target, runtime);
797
+ if (!allowed.some((candidate) => candidate === framework)) {
798
+ throw new Error(`Framework ${framework} is not valid for target ${target} and runtime ${runtime}`);
799
+ }
800
+ }
801
+
642
802
  export function validateServiceNameInput(rawValue: string, directoryOverride?: string) {
643
803
  const serviceName = slugify(rawValue);
644
804
  if (!serviceName) {
@@ -665,10 +825,11 @@ export function validateServiceNameInput(rawValue: string, directoryOverride?: s
665
825
  function printHelp() {
666
826
  log.message(`
667
827
  Usage:
668
- bun run index.ts [directory] [options]
828
+ service create [service_id] [options]
669
829
 
670
830
  Options:
671
- --profile <microservice> Compatibility no-op; create-svc only generates microservices
831
+ --target <cloudrun|workers> Deploy target for the generated service
832
+ --profile <microservice> Compatibility no-op; app workspaces moved out
672
833
  --runtime <go|bun> Runtime scaffold to generate
673
834
  --framework <name> Framework for the selected runtime
674
835
  --module-path <path> Go module path for generated Go scaffolds
@@ -677,9 +838,11 @@ Options:
677
838
  --billing-account <name> Billing account resource name
678
839
  --quota-project <id> Billing quota project for gcloud calls
679
840
  --region <region> Cloud Run region
680
- --auto-deploy Run bootstrap and first deploy after scaffold
681
- --bootstrap Alias for --auto-deploy
841
+ --auto-deploy Run service create and service deploy after scaffold
682
842
  --no-auto-deploy Scaffold only
843
+ --no-git Skip git init, initial commit, GitHub repo creation, and push
844
+ --auto-update Re-run through create-svc@latest when a newer version exists
845
+ --no-update-check Skip the best-effort npm update check
683
846
  --yes, -y Accept defaults without prompts
684
847
  --help, -h Show this message
685
848
  `);
@@ -0,0 +1,40 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, realpath } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { buildGitBootstrapConfig, findExistingGitWorktree } from "./git-bootstrap";
6
+
7
+ test("buildGitBootstrapConfig defaults to anmho private repo creation", () => {
8
+ expect(buildGitBootstrapConfig("launch-api", undefined)).toEqual({
9
+ enabled: true,
10
+ owner: "anmho",
11
+ repository: "launch-api",
12
+ });
13
+ });
14
+
15
+ test("buildGitBootstrapConfig honors --no-git", () => {
16
+ expect(buildGitBootstrapConfig("launch-api", true)).toEqual({
17
+ enabled: false,
18
+ owner: "anmho",
19
+ repository: "launch-api",
20
+ });
21
+ });
22
+
23
+ test("findExistingGitWorktree detects parent repositories", async () => {
24
+ const root = await mkdtemp(join(tmpdir(), "create-svc-git-"));
25
+ run(["git", "init", "-b", "main"], root);
26
+ await mkdir(join(root, "apps", "launch-api"), { recursive: true });
27
+
28
+ expect(findExistingGitWorktree(join(root, "apps", "launch-api"))).toBe(await realpath(root));
29
+ });
30
+
31
+ function run(command: string[], cwd: string) {
32
+ const result = Bun.spawnSync(command, {
33
+ cwd,
34
+ stdout: "pipe",
35
+ stderr: "pipe",
36
+ });
37
+ if (result.exitCode !== 0) {
38
+ throw new Error(result.stderr.toString());
39
+ }
40
+ }
@@ -0,0 +1,110 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ export type GitBootstrapConfig = {
5
+ enabled: boolean;
6
+ owner: string;
7
+ repository: string;
8
+ };
9
+
10
+ export type GitBootstrapResult =
11
+ | { status: "disabled" }
12
+ | { status: "skipped-existing-worktree"; root: string }
13
+ | { status: "created"; url: string };
14
+
15
+ export function buildGitBootstrapConfig(serviceName: string, noGit: boolean | undefined): GitBootstrapConfig {
16
+ return {
17
+ enabled: !noGit,
18
+ owner: "anmho",
19
+ repository: serviceName,
20
+ };
21
+ }
22
+
23
+ export async function bootstrapGitHubRepository(targetDir: string, config: GitBootstrapConfig): Promise<GitBootstrapResult> {
24
+ if (!config.enabled) {
25
+ return { status: "disabled" };
26
+ }
27
+
28
+ const existingRoot = findExistingGitWorktree(targetDir);
29
+ if (existingRoot) {
30
+ return { status: "skipped-existing-worktree", root: existingRoot };
31
+ }
32
+
33
+ run(["git", "--version"], targetDir, "git is required to initialize the generated repository");
34
+ run(["gh", "--version"], targetDir, "GitHub CLI `gh` is required to create the generated repository");
35
+ run(["gh", "auth", "status"], targetDir, "Authenticate GitHub CLI with `gh auth login` before creating the repository");
36
+
37
+ run(["git", "init", "-b", "main"], targetDir);
38
+ run(["git", "add", "."], targetDir);
39
+
40
+ if (hasStagedChanges(targetDir)) {
41
+ run(["git", "commit", "-m", "Initial commit"], targetDir);
42
+ }
43
+
44
+ const repository = `${config.owner}/${config.repository}`;
45
+ run(["gh", "repo", "create", repository, "--private", "--source", ".", "--remote", "origin", "--push"], targetDir);
46
+
47
+ return {
48
+ status: "created",
49
+ url: `https://github.com/${repository}`,
50
+ };
51
+ }
52
+
53
+ export function commitAndPushGeneratedArtifacts(targetDir: string, message: string) {
54
+ run(["git", "add", "."], targetDir);
55
+ if (!hasStagedChanges(targetDir)) {
56
+ return { committed: false };
57
+ }
58
+ run(["git", "commit", "-m", message], targetDir);
59
+ run(["git", "push"], targetDir);
60
+ return { committed: true };
61
+ }
62
+
63
+ export function findExistingGitWorktree(targetDir: string) {
64
+ const cwd = existingPath(targetDir);
65
+ const result = Bun.spawnSync(["git", "-C", cwd, "rev-parse", "--show-toplevel"], {
66
+ stdout: "pipe",
67
+ stderr: "pipe",
68
+ });
69
+ if (result.exitCode !== 0) {
70
+ return undefined;
71
+ }
72
+ return result.stdout.toString().trim() || undefined;
73
+ }
74
+
75
+ function existingPath(path: string): string {
76
+ if (existsSync(path)) {
77
+ return path;
78
+ }
79
+
80
+ const parent = dirname(path);
81
+ if (parent === path) {
82
+ return path;
83
+ }
84
+
85
+ return existingPath(parent);
86
+ }
87
+
88
+ function hasStagedChanges(cwd: string) {
89
+ const result = Bun.spawnSync(["git", "diff", "--cached", "--quiet"], {
90
+ cwd,
91
+ stdout: "pipe",
92
+ stderr: "pipe",
93
+ });
94
+ return result.exitCode === 1;
95
+ }
96
+
97
+ function run(command: string[], cwd: string, message?: string) {
98
+ const result = Bun.spawnSync(command, {
99
+ cwd,
100
+ stdin: "inherit",
101
+ stdout: "inherit",
102
+ stderr: "pipe",
103
+ });
104
+ if (result.exitCode === 0) {
105
+ return;
106
+ }
107
+
108
+ const detail = result.stderr.toString().trim();
109
+ throw new Error([message, `Command failed: ${command.join(" ")}`, detail].filter(Boolean).join("\n"));
110
+ }
@@ -3,6 +3,7 @@ import { buildGcpProjectOptions, compactDatabaseName, compactIdentifier, deriveD
3
3
 
4
4
  test("deriveDefaults uses the service name for project, repo, and database naming", () => {
5
5
  expect(deriveDefaults("edge-api")).toEqual({
6
+ serviceId: "edge-api",
6
7
  serviceName: "edge-api",
7
8
  projectName: "edge-api",
8
9
  projectId: "anmho-edge-api",
package/src/naming.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export const BILLING_ACCOUNT_DEFAULT = "billingAccounts/01BD2E-3A6949-8F4C84";
2
2
  export const QUOTA_PROJECT_DEFAULT = "anmho-infra-prod";
3
3
 
4
+ export const DEPLOY_TARGETS = ["cloudrun", "workers"] as const;
5
+
6
+ export type DeployTarget = (typeof DEPLOY_TARGETS)[number];
7
+
4
8
  export const FRAMEWORKS_BY_RUNTIME = {
5
9
  go: ["chi", "connectrpc"],
6
10
  bun: ["hono", "connectrpc"],
@@ -10,6 +14,24 @@ export type Runtime = keyof typeof FRAMEWORKS_BY_RUNTIME;
10
14
  export type Framework = (typeof FRAMEWORKS_BY_RUNTIME)[Runtime][number];
11
15
  export type GcpProjectMode = "create_new" | "use_existing";
12
16
 
17
+ export function parseDeployTarget(value: string): DeployTarget {
18
+ if (DEPLOY_TARGETS.includes(value as DeployTarget)) {
19
+ return value as DeployTarget;
20
+ }
21
+
22
+ throw new Error(`Unknown target: ${value}`);
23
+ }
24
+
25
+ export function frameworksForTargetRuntime(target: DeployTarget, runtime: Runtime): readonly Framework[] {
26
+ if (target === "workers") {
27
+ if (runtime === "bun") {
28
+ return ["hono"];
29
+ }
30
+ return [];
31
+ }
32
+ return FRAMEWORKS_BY_RUNTIME[runtime];
33
+ }
34
+
13
35
  export function slugify(value: string, maxLength = 63) {
14
36
  return value
15
37
  .trim()
@@ -64,6 +86,7 @@ export function deriveDefaults(serviceName: string) {
64
86
  const normalizedServiceName = slugify(serviceName) || "my-service";
65
87
 
66
88
  return {
89
+ serviceId: normalizedServiceName,
67
90
  serviceName: normalizedServiceName,
68
91
  projectName: normalizedServiceName,
69
92
  projectId: compactIdentifier(`anmho-${normalizedServiceName}`, 30),
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildPostScaffoldCommands } from "./post-scaffold";
3
+
4
+ describe("buildPostScaffoldCommands", () => {
5
+ test("runs create and deploy for HTTP services", () => {
6
+ expect(buildPostScaffoldCommands({ framework: "hono" })).toEqual([
7
+ { command: "bun", args: ["run", "service", "--", "create"] },
8
+ { command: "bun", args: ["run", "service", "--", "deploy"] },
9
+ ]);
10
+ });
11
+
12
+ test("builds SDK artifacts before create and deploy for ConnectRPC services", () => {
13
+ expect(buildPostScaffoldCommands({ framework: "connectrpc" })).toEqual([
14
+ { command: "bun", args: ["run", "service", "--", "sdk", "build"] },
15
+ { command: "bun", args: ["run", "service", "--", "create"] },
16
+ { command: "bun", args: ["run", "service", "--", "deploy"] },
17
+ ]);
18
+ });
19
+ });
@@ -12,21 +12,34 @@ type CommandResult = {
12
12
  stderr: string;
13
13
  };
14
14
 
15
+ type PostScaffoldCommand = {
16
+ command: string;
17
+ args: string[];
18
+ };
19
+
15
20
  const decoder = new TextDecoder();
16
21
  const encoder = new TextEncoder();
17
22
 
18
23
  export async function runPostScaffoldFlow(config: ScaffoldConfig, cwd: string) {
19
24
  if (config.autoDeploy) {
20
25
  installProjectDependencies(cwd);
21
- const command = config.runtime === "bun" ? "bun" : "make";
22
- run(command, config.runtime === "bun" ? ["run", "bootstrap"] : ["bootstrap"], { cwd });
23
- run(command, config.runtime === "bun" ? ["run", "deploy"] : ["deploy"], { cwd });
24
- return { message: "Dependencies installed and first deploy started" };
26
+ for (const command of buildPostScaffoldCommands(config)) {
27
+ run(command.command, command.args, { cwd });
28
+ }
29
+ return { message: "Dependencies installed, service created, and service deployed" };
25
30
  }
26
31
 
27
32
  return { message: "Backend package generated" };
28
33
  }
29
34
 
35
+ export function buildPostScaffoldCommands(config: Pick<ScaffoldConfig, "framework">): PostScaffoldCommand[] {
36
+ return [
37
+ ...(config.framework === "connectrpc" ? [{ command: "bun", args: ["run", "service", "--", "sdk", "build"] }] : []),
38
+ { command: "bun", args: ["run", "service", "--", "create"] },
39
+ { command: "bun", args: ["run", "service", "--", "deploy"] },
40
+ ];
41
+ }
42
+
30
43
  function installProjectDependencies(cwd: string) {
31
44
  requireCommand("bun");
32
45
  run("bun", ["install"], { cwd });
package/src/profiles.ts CHANGED
@@ -9,17 +9,14 @@ export function parseProfile(value: string): Profile {
9
9
 
10
10
  if (value === "app") {
11
11
  throw new Error(
12
- [
13
- "The app profile moved out of create-svc.",
14
- "Use the private GitHub template repos anmho/create-app-consumer or anmho/create-app-saas instead.",
15
- ].join(" ")
12
+ "The app profile has moved out of create-service. Use the private create-app template repositories instead."
16
13
  );
17
14
  }
18
15
 
19
16
  throw new Error(`Unknown profile: ${value}`);
20
17
  }
21
18
 
22
- export function exampleForProfile(profile: Profile) {
19
+ export function exampleForProfile(_profile: Profile) {
23
20
  return {
24
21
  kind: "microservice",
25
22
  domain: "waitlist",