create-svc 0.1.10 → 0.1.11

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 (168) hide show
  1. package/README.md +46 -43
  2. package/bin/create-service.mjs +2 -0
  3. package/package.json +12 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +195 -30
  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 +231 -40
  14. package/src/scaffold.ts +84 -29
  15. package/src/vault.test.ts +61 -1
  16. package/src/vault.ts +77 -15
  17. package/templates/shared/.github/workflows/ci.yml +2 -1
  18. package/templates/shared/.github/workflows/deploy.yml +2 -0
  19. package/templates/shared/README.md +124 -47
  20. package/templates/shared/grafana/alerts.yaml +54 -0
  21. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  22. package/templates/shared/scripts/authctl.ts +231 -0
  23. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  24. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  25. package/templates/shared/scripts/cloudrun/cli.ts +324 -7
  26. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  27. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  28. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  29. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  30. package/templates/shared/scripts/dev.ts +22 -0
  31. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  32. package/templates/shared/scripts/local-docker.ts +63 -0
  33. package/templates/shared/scripts/local-env.ts +27 -0
  34. package/templates/shared/scripts/seed.ts +73 -0
  35. package/templates/shared/scripts/wait-for-db.ts +32 -0
  36. package/templates/shared/service.config.ts +59 -0
  37. package/templates/shared/service.yaml +24 -44
  38. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  39. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  40. package/templates/targets/workers/Makefile +33 -0
  41. package/templates/targets/workers/README.md +75 -0
  42. package/templates/targets/workers/package.json +35 -0
  43. package/templates/targets/workers/scripts/workers/cli.ts +397 -0
  44. package/templates/targets/workers/src/auth.ts +178 -0
  45. package/templates/targets/workers/src/index.ts +198 -0
  46. package/templates/targets/workers/src/storage.ts +370 -0
  47. package/templates/targets/workers/test/app.test.ts +108 -0
  48. package/templates/targets/workers/tsconfig.json +11 -0
  49. package/templates/targets/workers/wrangler.toml +24 -0
  50. package/templates/variants/bun-connectrpc/Makefile +14 -8
  51. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  52. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  53. package/templates/variants/bun-connectrpc/package.json +12 -5
  54. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  55. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  56. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  57. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  58. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  59. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  60. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  61. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  62. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  63. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  64. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  65. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  66. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  67. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  68. package/templates/variants/bun-hono/Makefile +14 -8
  69. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  70. package/templates/variants/bun-hono/package.json +12 -5
  71. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  72. package/templates/variants/bun-hono/src/auth.ts +181 -0
  73. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  74. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  75. package/templates/variants/bun-hono/src/index.ts +65 -180
  76. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  77. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  78. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  79. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  80. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  81. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  82. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  83. package/templates/variants/go-chi/Makefile +27 -11
  84. package/templates/variants/go-chi/atlas.hcl +8 -0
  85. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  86. package/templates/variants/go-chi/go.mod +1 -3
  87. package/templates/variants/go-chi/internal/app/service.go +202 -685
  88. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  89. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  90. package/templates/variants/go-chi/internal/config/config.go +27 -11
  91. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  92. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  93. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  94. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  95. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  96. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  97. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  98. package/templates/variants/go-chi/package.json +7 -1
  99. package/templates/variants/go-connectrpc/Makefile +26 -9
  100. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  101. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  102. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  103. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  104. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  105. package/templates/variants/go-connectrpc/go.mod +1 -1
  106. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  107. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  108. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  109. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  110. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  111. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  112. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  113. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  114. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  115. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  116. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  117. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  118. package/templates/variants/go-connectrpc/package.json +7 -1
  119. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  120. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  121. package/templates/root/.github/workflows/ci.yml +0 -26
  122. package/templates/root/.github/workflows/deploy.yml +0 -22
  123. package/templates/root/Dockerfile +0 -23
  124. package/templates/root/README.md +0 -69
  125. package/templates/root/buf.gen.yaml +0 -10
  126. package/templates/root/buf.yaml +0 -9
  127. package/templates/root/cmd/server/main.go +0 -44
  128. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  129. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  130. package/templates/root/go.mod +0 -10
  131. package/templates/root/internal/app/service.go +0 -152
  132. package/templates/root/internal/app/token_source.go +0 -50
  133. package/templates/root/internal/cloudflare/client.go +0 -160
  134. package/templates/root/internal/config/config.go +0 -55
  135. package/templates/root/internal/connectapi/handler.go +0 -79
  136. package/templates/root/internal/httpapi/routes.go +0 -93
  137. package/templates/root/internal/vault/client.go +0 -148
  138. package/templates/root/package.json +0 -12
  139. package/templates/root/protos/dns/v1/dns.proto +0 -58
  140. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  141. package/templates/root/scripts/cloudrun/config.ts +0 -50
  142. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  143. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  144. package/templates/root/service.yaml +0 -50
  145. package/templates/root/test/go.test.ts +0 -19
  146. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  147. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  148. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  149. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  150. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  151. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  152. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  153. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  154. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  155. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  156. package/templates/variants/bun-hono/src/storage.ts +0 -72
  157. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  158. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  159. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  160. package/templates/variants/go-chi/buf.yaml +0 -9
  161. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  162. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  163. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  164. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  165. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  166. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  167. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  168. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
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("create-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,19 @@ 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(isBun ? "bun run service -- create" : "make create")}`,
141
+ `Deploy: ${pc.cyan(isBun ? "bun run service -- deploy" : "make deploy")}`,
142
+ config.git.enabled ? `Repository: ${pc.cyan(`https://github.com/anmho/${config.git.repository}`)}` : undefined,
113
143
  `Personal env: ${pc.cyan(
114
144
  isBun
115
145
  ? `bun run deploy -- --environment personal --name ${config.serviceName}`
116
146
  : `make deploy ARGS="--environment personal --name ${config.serviceName}"`
117
147
  )}`,
118
148
  `Production API: ${pc.cyan(`https://${config.apiHostname}`)}`,
119
- ].join("\n")
149
+ ].filter(Boolean).join("\n")
120
150
  );
121
151
  } catch (error) {
122
152
  handleCliError(error);
@@ -160,6 +190,21 @@ export function parseArgs(argv: string[]): ParsedArgs {
160
190
  continue;
161
191
  }
162
192
 
193
+ if (token === "--auto-update") {
194
+ parsed.autoUpdate = true;
195
+ continue;
196
+ }
197
+
198
+ if (token === "--no-update-check") {
199
+ parsed.noUpdateCheck = true;
200
+ continue;
201
+ }
202
+
203
+ if (token === "--no-git") {
204
+ parsed.noGit = true;
205
+ continue;
206
+ }
207
+
163
208
  if (token === "--runtime") {
164
209
  parsed.runtime = readValue() as Runtime;
165
210
  continue;
@@ -170,6 +215,16 @@ export function parseArgs(argv: string[]): ParsedArgs {
170
215
  continue;
171
216
  }
172
217
 
218
+ if (token === "--target") {
219
+ parsed.target = parseDeployTarget(readValue());
220
+ continue;
221
+ }
222
+
223
+ if (token.startsWith("--target=")) {
224
+ parsed.target = parseDeployTarget(token.slice("--target=".length));
225
+ continue;
226
+ }
227
+
173
228
  if (token === "--framework") {
174
229
  parsed.framework = readValue() as Framework;
175
230
  continue;
@@ -260,11 +315,6 @@ export function parseArgs(argv: string[]): ParsedArgs {
260
315
  continue;
261
316
  }
262
317
 
263
- if (token === "--bootstrap") {
264
- parsed.autoDeploy = true;
265
- continue;
266
- }
267
-
268
318
  if (token === "--no-auto-deploy") {
269
319
  parsed.autoDeploy = false;
270
320
  continue;
@@ -276,6 +326,69 @@ export function parseArgs(argv: string[]): ParsedArgs {
276
326
  return parsed;
277
327
  }
278
328
 
329
+ const CURRENT_VERSION = "0.1.9";
330
+ const PACKAGE_NAME = "create-svc";
331
+
332
+ async function maybeCheckForUpdate(args: ParsedArgs) {
333
+ if (args.noUpdateCheck || shouldSkipUpdateCheck()) {
334
+ return;
335
+ }
336
+
337
+ const latest = await resolveLatestVersion().catch(() => "");
338
+ if (!latest || !isVersionGreater(latest, CURRENT_VERSION)) {
339
+ return;
340
+ }
341
+
342
+ const command = `bunx ${PACKAGE_NAME}@latest ${Bun.argv.slice(2).filter((arg) => arg !== "--auto-update").join(" ")}`.trim();
343
+ if (!args.autoUpdate) {
344
+ log.info(`A newer ${PACKAGE_NAME} is available: ${CURRENT_VERSION} -> ${latest}. Run ${command}`);
345
+ return;
346
+ }
347
+
348
+ const result = Bun.spawnSync(["bunx", `${PACKAGE_NAME}@latest`, ...Bun.argv.slice(2).filter((arg) => arg !== "--auto-update")], {
349
+ stdin: "inherit",
350
+ stdout: "inherit",
351
+ stderr: "inherit",
352
+ env: {
353
+ ...process.env,
354
+ CREATE_SERVICE_NO_UPDATE_CHECK: "1",
355
+ },
356
+ });
357
+ process.exit(result.exitCode);
358
+ }
359
+
360
+ function shouldSkipUpdateCheck() {
361
+ return Boolean(
362
+ process.env.CI ||
363
+ process.env.CODEX_CI ||
364
+ process.env.CREATE_SERVICE_NO_UPDATE_CHECK ||
365
+ process.env.BUN_TEST ||
366
+ process.env.npm_lifecycle_event
367
+ );
368
+ }
369
+
370
+ async function resolveLatestVersion() {
371
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
372
+ signal: AbortSignal.timeout(1_500),
373
+ });
374
+ if (!response.ok) {
375
+ return "";
376
+ }
377
+ const payload = (await response.json()) as { version?: string };
378
+ return payload.version?.trim() ?? "";
379
+ }
380
+
381
+ function isVersionGreater(left: string, right: string) {
382
+ const parse = (value: string) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
383
+ const [leftMajor = 0, leftMinor = 0, leftPatch = 0] = parse(left);
384
+ const [rightMajor = 0, rightMinor = 0, rightPatch = 0] = parse(right);
385
+ return (
386
+ leftMajor > rightMajor ||
387
+ (leftMajor === rightMajor && leftMinor > rightMinor) ||
388
+ (leftMajor === rightMajor && leftMinor === rightMinor && leftPatch > rightPatch)
389
+ );
390
+ }
391
+
279
392
  export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
280
393
  const inferredName = slugify(basename(args.directory ?? "my-service"));
281
394
  const serviceName = args.yes
@@ -287,14 +400,17 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
287
400
 
288
401
  const discoveryPromise = discoverCloudInputs();
289
402
  const defaults = deriveDefaults(serviceName);
290
- const runtime = await resolveRuntime(args);
291
- const framework = await resolveFramework(args, runtime);
403
+ const target = await resolveTarget(args);
404
+ const runtime = await resolveRuntime(args, target);
405
+ const framework = await resolveFramework(args, target, runtime);
406
+ validateTargetRuntimeFramework(target, runtime, framework);
292
407
  const modulePath = await resolveModulePath(args, runtime, defaults.modulePath);
293
408
  const discovery = await waitForDiscovery(discoveryPromise);
294
409
  const gcpSelection = await resolveGcpSelection(args, defaults, discovery);
295
410
  const region = args.region ?? DEFAULT_REGION;
296
411
  const billingAccount = chooseBillingAccount(args.billingAccount, discovery.billingAccounts);
297
412
  const autoDeploy = resolveAutoDeploy(args.autoDeploy);
413
+ const git = buildGitBootstrapConfig(serviceName, args.noGit);
298
414
 
299
415
  if (!args.yes) {
300
416
  const okay = await confirm({
@@ -315,6 +431,7 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
315
431
  directory,
316
432
  serviceName,
317
433
  modulePath,
434
+ target,
318
435
  runtime,
319
436
  framework,
320
437
  profile: args.profile,
@@ -325,6 +442,7 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
325
442
  billingAccount,
326
443
  quotaProjectId: args.quotaProjectId ?? QUOTA_PROJECT_DEFAULT,
327
444
  autoDeploy,
445
+ git,
328
446
  neonDatabaseName: defaults.neonDatabaseName,
329
447
  apiHostname: defaults.apiHostname,
330
448
  generatorRoot: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
@@ -344,22 +462,55 @@ async function waitForDiscovery(discoveryPromise: Promise<DiscoveryState>) {
344
462
  }
345
463
  }
346
464
 
347
- async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
465
+ async function resolveTarget(args: ParsedArgs): Promise<DeployTarget> {
466
+ if (args.target) {
467
+ return args.target;
468
+ }
469
+
470
+ if (args.yes) {
471
+ return "cloudrun";
472
+ }
473
+
474
+ const value = await select({
475
+ message: "Deploy target",
476
+ initialValue: "cloudrun",
477
+ options: [
478
+ { value: "cloudrun", label: "Cloud Run", hint: "Default" },
479
+ { value: "workers", label: "Cloudflare Workers" },
480
+ ],
481
+ });
482
+
483
+ if (isCancel(value)) {
484
+ cancel("Aborted");
485
+ process.exit(1);
486
+ }
487
+
488
+ return value as DeployTarget;
489
+ }
490
+
491
+ async function resolveRuntime(args: ParsedArgs, target: DeployTarget): Promise<Runtime> {
348
492
  if (args.runtime) {
349
493
  return args.runtime;
350
494
  }
351
495
 
352
- if (args.yes) {
496
+ if (target === "workers") {
353
497
  return "bun";
354
498
  }
355
499
 
500
+ if (args.yes) {
501
+ return "go";
502
+ }
503
+
356
504
  const value = await select({
357
505
  message: "Runtime",
358
- initialValue: "bun",
359
- options: [
360
- { value: "bun", label: "Bun", hint: "Default" },
361
- { value: "go", label: "Go" },
362
- ],
506
+ initialValue: target === "cloudrun" ? "go" : "bun",
507
+ options:
508
+ target === "cloudrun"
509
+ ? [
510
+ { value: "go", label: "Go", hint: "Default" },
511
+ { value: "bun", label: "Bun" },
512
+ ]
513
+ : [{ value: "bun", label: "Bun/TypeScript", hint: "Workers runtime" }],
363
514
  });
364
515
 
365
516
  if (isCancel(value)) {
@@ -370,17 +521,17 @@ async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
370
521
  return value as Runtime;
371
522
  }
372
523
 
373
- async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Framework> {
374
- const allowed = FRAMEWORKS_BY_RUNTIME[runtime];
524
+ async function resolveFramework(args: ParsedArgs, target: DeployTarget, runtime: Runtime): Promise<Framework> {
525
+ const allowed = frameworksForTargetRuntime(target, runtime);
375
526
  if (args.framework) {
376
527
  if (allowed.some((framework) => framework === args.framework)) {
377
528
  return args.framework;
378
529
  }
379
- throw new Error(`Framework ${args.framework} is not valid for runtime ${runtime}`);
530
+ throw new Error(`Framework ${args.framework} is not valid for target ${target} and runtime ${runtime}`);
380
531
  }
381
532
 
382
533
  if (args.yes) {
383
- return allowed[0];
534
+ return target === "cloudrun" && runtime === "go" ? "connectrpc" : allowed[0]!;
384
535
  }
385
536
 
386
537
  const value = await select({
@@ -639,6 +790,17 @@ export function normalizeValidationResult(result: true | string): string | undef
639
790
  return result === true ? undefined : result;
640
791
  }
641
792
 
793
+ export function validateProfileRuntimeFramework(profile: Profile, runtime: Runtime, framework: Framework) {
794
+ validateTargetRuntimeFramework("cloudrun", runtime, framework);
795
+ }
796
+
797
+ export function validateTargetRuntimeFramework(target: DeployTarget, runtime: Runtime, framework: Framework) {
798
+ const allowed = frameworksForTargetRuntime(target, runtime);
799
+ if (!allowed.some((candidate) => candidate === framework)) {
800
+ throw new Error(`Framework ${framework} is not valid for target ${target} and runtime ${runtime}`);
801
+ }
802
+ }
803
+
642
804
  export function validateServiceNameInput(rawValue: string, directoryOverride?: string) {
643
805
  const serviceName = slugify(rawValue);
644
806
  if (!serviceName) {
@@ -665,10 +827,11 @@ export function validateServiceNameInput(rawValue: string, directoryOverride?: s
665
827
  function printHelp() {
666
828
  log.message(`
667
829
  Usage:
668
- bun run index.ts [directory] [options]
830
+ create-service [service_id] [options]
669
831
 
670
832
  Options:
671
- --profile <microservice> Compatibility no-op; create-svc only generates microservices
833
+ --target <cloudrun|workers> Deploy target for the generated service
834
+ --profile <microservice> Compatibility no-op; app workspaces moved out
672
835
  --runtime <go|bun> Runtime scaffold to generate
673
836
  --framework <name> Framework for the selected runtime
674
837
  --module-path <path> Go module path for generated Go scaffolds
@@ -677,9 +840,11 @@ Options:
677
840
  --billing-account <name> Billing account resource name
678
841
  --quota-project <id> Billing quota project for gcloud calls
679
842
  --region <region> Cloud Run region
680
- --auto-deploy Run bootstrap and first deploy after scaffold
681
- --bootstrap Alias for --auto-deploy
843
+ --auto-deploy Run service create and service deploy after scaffold
682
844
  --no-auto-deploy Scaffold only
845
+ --no-git Skip git init, initial commit, GitHub repo creation, and push
846
+ --auto-update Re-run through create-svc@latest when a newer version exists
847
+ --no-update-check Skip the best-effort npm update check
683
848
  --yes, -y Accept defaults without prompts
684
849
  --help, -h Show this message
685
850
  `);
@@ -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",