create-svc 0.1.9 → 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 (163) hide show
  1. package/README.md +138 -16
  2. package/bin/create-service.mjs +2 -0
  3. package/package.json +19 -11
  4. package/src/cli.test.ts +46 -7
  5. package/src/cli.ts +282 -84
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +5 -2
  9. package/src/naming.ts +32 -1
  10. package/src/neon.ts +10 -8
  11. package/src/post-scaffold.test.ts +19 -0
  12. package/src/post-scaffold.ts +18 -26
  13. package/src/profiles.ts +25 -0
  14. package/src/scaffold.test.ts +320 -18
  15. package/src/scaffold.ts +154 -28
  16. package/src/vault.test.ts +94 -10
  17. package/src/vault.ts +81 -18
  18. package/templates/shared/.github/workflows/ci.yml +2 -1
  19. package/templates/shared/.github/workflows/deploy.yml +2 -0
  20. package/templates/shared/README.md +217 -29
  21. package/templates/shared/docker-compose.yml +19 -0
  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 +24 -42
  26. package/templates/shared/scripts/cloudrun/cleanup.ts +81 -35
  27. package/templates/shared/scripts/cloudrun/cli.ts +324 -7
  28. package/templates/shared/scripts/cloudrun/config.ts +21 -19
  29. package/templates/shared/scripts/cloudrun/deploy.ts +16 -11
  30. package/templates/shared/scripts/cloudrun/lib.ts +232 -123
  31. package/templates/shared/scripts/cloudrun/neon.ts +127 -13
  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 -1
  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 +397 -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/Dockerfile +1 -0
  53. package/templates/variants/bun-connectrpc/Makefile +17 -8
  54. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  55. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +20 -0
  56. package/templates/variants/bun-connectrpc/package.json +25 -1
  57. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  58. package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
  59. package/templates/variants/bun-connectrpc/scripts/migrate.ts +49 -0
  60. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  61. package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
  62. package/templates/variants/bun-connectrpc/src/db/repository.ts +126 -0
  63. package/templates/variants/bun-connectrpc/src/db/schema.ts +26 -0
  64. package/templates/variants/bun-connectrpc/src/index.ts +194 -22
  65. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  66. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  67. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  68. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  69. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  70. package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
  71. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  72. package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
  73. package/templates/variants/bun-hono/Makefile +17 -8
  74. package/templates/variants/bun-hono/migrations/0000_init.sql +20 -0
  75. package/templates/variants/bun-hono/package.json +21 -1
  76. package/templates/variants/bun-hono/scripts/migrate.ts +49 -0
  77. package/templates/variants/bun-hono/src/auth.ts +181 -0
  78. package/templates/variants/bun-hono/src/db/client.ts +15 -0
  79. package/templates/variants/bun-hono/src/db/repository.ts +126 -0
  80. package/templates/variants/bun-hono/src/db/schema.ts +26 -0
  81. package/templates/variants/bun-hono/src/index.ts +141 -10
  82. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  83. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  84. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  85. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  86. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  87. package/templates/variants/bun-hono/test/app.test.ts +90 -5
  88. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  89. package/templates/variants/bun-hono/tsconfig.json +1 -0
  90. package/templates/variants/go-chi/Makefile +30 -10
  91. package/templates/variants/go-chi/atlas.hcl +8 -0
  92. package/templates/variants/go-chi/cmd/server/main.go +25 -13
  93. package/templates/variants/go-chi/go.mod +3 -2
  94. package/templates/variants/go-chi/internal/app/service.go +279 -70
  95. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  96. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  97. package/templates/variants/go-chi/internal/config/config.go +38 -7
  98. package/templates/variants/go-chi/internal/httpapi/routes.go +170 -47
  99. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  100. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  101. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  102. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  103. package/templates/variants/go-chi/migrations/0000_init.sql +20 -0
  104. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  105. package/templates/variants/go-chi/package.json +7 -1
  106. package/templates/variants/go-chi/test/go.test.ts +4 -1
  107. package/templates/variants/go-connectrpc/Makefile +29 -8
  108. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  109. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
  110. package/templates/variants/go-connectrpc/cmd/server/main.go +44 -9
  111. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  112. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  113. package/templates/variants/go-connectrpc/go.mod +4 -0
  114. package/templates/variants/go-connectrpc/internal/app/service.go +279 -70
  115. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  116. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  117. package/templates/variants/go-connectrpc/internal/config/config.go +38 -7
  118. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +129 -40
  119. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  120. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +170 -47
  121. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  122. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  123. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  124. package/templates/variants/go-connectrpc/migrations/0000_init.sql +20 -0
  125. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  126. package/templates/variants/go-connectrpc/package.json +7 -1
  127. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  128. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  129. package/templates/root/.github/workflows/ci.yml +0 -26
  130. package/templates/root/.github/workflows/deploy.yml +0 -22
  131. package/templates/root/Dockerfile +0 -23
  132. package/templates/root/README.md +0 -69
  133. package/templates/root/buf.gen.yaml +0 -10
  134. package/templates/root/buf.yaml +0 -9
  135. package/templates/root/cmd/server/main.go +0 -44
  136. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  137. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  138. package/templates/root/go.mod +0 -10
  139. package/templates/root/internal/app/service.go +0 -152
  140. package/templates/root/internal/app/token_source.go +0 -50
  141. package/templates/root/internal/cloudflare/client.go +0 -160
  142. package/templates/root/internal/config/config.go +0 -55
  143. package/templates/root/internal/connectapi/handler.go +0 -79
  144. package/templates/root/internal/httpapi/routes.go +0 -93
  145. package/templates/root/internal/vault/client.go +0 -148
  146. package/templates/root/package.json +0 -12
  147. package/templates/root/protos/dns/v1/dns.proto +0 -58
  148. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  149. package/templates/root/scripts/cloudrun/config.ts +0 -50
  150. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  151. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  152. package/templates/root/service.yaml +0 -50
  153. package/templates/root/test/go.test.ts +0 -19
  154. package/templates/shared/.env.example +0 -10
  155. package/templates/variants/go-chi/buf.gen.yaml +0 -10
  156. package/templates/variants/go-chi/buf.yaml +0 -9
  157. package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
  158. package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  159. package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
  160. package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
  161. package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
  162. package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  163. package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
package/src/cli.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  intro,
6
6
  isCancel,
7
7
  log,
8
+ note,
8
9
  outro,
9
10
  select,
10
11
  spinner,
@@ -15,18 +16,21 @@ import { readdirSync } from "node:fs";
15
16
  import { basename, dirname, resolve } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
17
18
  import { runPostScaffoldFlow } from "./post-scaffold";
19
+ import { bootstrapGitHubRepository, buildGitBootstrapConfig, commitAndPushGeneratedArtifacts } from "./git-bootstrap";
18
20
  import { listOpenBillingAccounts, listAccessibleProjects, type BillingAccount, type GcpProject } from "./gcp";
19
- import { discoverNeonDefaults } from "./neon";
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,
29
32
  } from "./naming";
33
+ import { parseProfile, type Profile } from "./profiles";
30
34
  import {
31
35
  DirectoryConflictError,
32
36
  assertTargetDirectoryIsEmpty,
@@ -36,15 +40,20 @@ import {
36
40
 
37
41
  type ParsedArgs = {
38
42
  directory?: string;
43
+ target?: DeployTarget;
39
44
  runtime?: Runtime;
40
45
  framework?: Framework;
46
+ modulePath?: string;
41
47
  gcpProjectMode?: GcpProjectMode;
42
48
  gcpProject?: string;
43
- githubRepo?: string;
44
49
  region?: string;
45
50
  billingAccount?: string;
46
51
  quotaProjectId?: string;
47
52
  autoDeploy?: boolean;
53
+ autoUpdate?: boolean;
54
+ noUpdateCheck?: boolean;
55
+ noGit?: boolean;
56
+ profile: Profile;
48
57
  yes: boolean;
49
58
  help: boolean;
50
59
  };
@@ -52,10 +61,6 @@ type ParsedArgs = {
52
61
  type DiscoveryState = {
53
62
  projects: GcpProject[];
54
63
  billingAccounts: BillingAccount[];
55
- neonProjectId?: string;
56
- neonBaseBranchId?: string;
57
- neonBaseBranchName?: string;
58
- neonError?: string;
59
64
  warnings: string[];
60
65
  };
61
66
 
@@ -69,7 +74,9 @@ export async function run(argv: string[]) {
69
74
  return;
70
75
  }
71
76
 
72
- intro(`${pc.bold("create-svc")} ${pc.dim("Cloud Run scaffold")}`);
77
+ await maybeCheckForUpdate(args);
78
+
79
+ intro(`${pc.bold("create-service")} ${pc.dim("microservice bootstrap")}`);
73
80
 
74
81
  const config = await resolveConfig(args);
75
82
  const targetDir = resolve(process.cwd(), config.directory);
@@ -77,10 +84,12 @@ export async function run(argv: string[]) {
77
84
  note(
78
85
  [
79
86
  `${pc.bold("Output")}: ${targetDir}`,
87
+ `${pc.bold("Target")}: ${config.target}`,
80
88
  `${pc.bold("Runtime")}: ${config.runtime} + ${config.framework}`,
81
89
  `${pc.bold("Project")}: ${config.gcpProjectMode === "create_new" ? "create" : "use"} ${config.gcpProjectName} (${config.gcpProject})`,
82
- `${pc.bold("GitHub")}: ${config.githubRepo}`,
83
- `${pc.bold("Neon")}: ${config.neonProjectId || "(set later)"} / ${config.neonBaseBranchName || "(set later)"}`,
90
+ `${pc.bold("API")}: https://${config.apiHostname}`,
91
+ `${pc.bold("Local DB")}: docker compose postgres`,
92
+ `${pc.bold("GitHub")}: ${config.git.enabled ? `anmho/${config.git.repository}` : "disabled"}`,
84
93
  ].join("\n"),
85
94
  "Scaffold"
86
95
  );
@@ -90,7 +99,18 @@ export async function run(argv: string[]) {
90
99
  await scaffoldProject(config);
91
100
  buildSpinner.stop("Project files generated");
92
101
 
93
- const shouldRunPostScaffoldFlow = Boolean(process.stdout.isTTY && process.stdin.isTTY && (config.createGithubRepo || config.autoDeploy));
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
+
113
+ const shouldRunPostScaffoldFlow = config.autoDeploy;
94
114
  if (shouldRunPostScaffoldFlow) {
95
115
  const automationSpinner = spinner();
96
116
  automationSpinner.start("Running post-scaffold automation");
@@ -98,27 +118,44 @@ export async function run(argv: string[]) {
98
118
  const result = await runPostScaffoldFlow(config, targetDir);
99
119
  automationSpinner.stop(result.message);
100
120
  } catch (error) {
101
- automationSpinner.stop("Post-scaffold automation skipped");
102
- 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");
103
130
  }
104
131
  }
105
132
 
133
+ const isBun = config.runtime === "bun";
106
134
  outro(
107
135
  [
108
136
  `Next: ${pc.cyan(`cd ${config.directory}`)}`,
109
- `Local dev: ${pc.cyan("bun dev")}`,
110
- `Bootstrap: ${pc.cyan("bun run bootstrap")}`,
111
- `Deploy: ${pc.cyan("bun run deploy")}`,
112
- `Personal env: ${pc.cyan(`bun run deploy -- --environment personal --name ${config.serviceName}`)}`,
113
- ].join("\n")
137
+ `Local DB: ${pc.cyan("started by local dev command")}`,
138
+ `Migrate: ${pc.cyan(isBun ? "bun run migrate" : "make migrate")}`,
139
+ `Local dev: ${pc.cyan(isBun ? "bun run dev" : "make dev")}`,
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,
143
+ `Personal env: ${pc.cyan(
144
+ isBun
145
+ ? `bun run deploy -- --environment personal --name ${config.serviceName}`
146
+ : `make deploy ARGS="--environment personal --name ${config.serviceName}"`
147
+ )}`,
148
+ `Production API: ${pc.cyan(`https://${config.apiHostname}`)}`,
149
+ ].filter(Boolean).join("\n")
114
150
  );
115
151
  } catch (error) {
116
152
  handleCliError(error);
117
153
  }
118
154
  }
119
155
 
120
- function parseArgs(argv: string[]): ParsedArgs {
156
+ export function parseArgs(argv: string[]): ParsedArgs {
121
157
  const parsed: ParsedArgs = {
158
+ profile: "microservice",
122
159
  yes: false,
123
160
  help: false,
124
161
  };
@@ -153,6 +190,21 @@ function parseArgs(argv: string[]): ParsedArgs {
153
190
  continue;
154
191
  }
155
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
+
156
208
  if (token === "--runtime") {
157
209
  parsed.runtime = readValue() as Runtime;
158
210
  continue;
@@ -163,6 +215,16 @@ function parseArgs(argv: string[]): ParsedArgs {
163
215
  continue;
164
216
  }
165
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
+
166
228
  if (token === "--framework") {
167
229
  parsed.framework = readValue() as Framework;
168
230
  continue;
@@ -173,6 +235,26 @@ function parseArgs(argv: string[]): ParsedArgs {
173
235
  continue;
174
236
  }
175
237
 
238
+ if (token === "--profile") {
239
+ parsed.profile = parseProfile(readValue());
240
+ continue;
241
+ }
242
+
243
+ if (token.startsWith("--profile=")) {
244
+ parsed.profile = parseProfile(token.slice("--profile=".length));
245
+ continue;
246
+ }
247
+
248
+ if (token === "--module-path") {
249
+ parsed.modulePath = readValue();
250
+ continue;
251
+ }
252
+
253
+ if (token.startsWith("--module-path=")) {
254
+ parsed.modulePath = token.slice("--module-path=".length);
255
+ continue;
256
+ }
257
+
176
258
  if (token === "--project-mode") {
177
259
  parsed.gcpProjectMode = readValue() as GcpProjectMode;
178
260
  continue;
@@ -198,16 +280,6 @@ function parseArgs(argv: string[]): ParsedArgs {
198
280
  continue;
199
281
  }
200
282
 
201
- if (token === "--github-repo") {
202
- parsed.githubRepo = readValue();
203
- continue;
204
- }
205
-
206
- if (token.startsWith("--github-repo=")) {
207
- parsed.githubRepo = token.slice("--github-repo=".length);
208
- continue;
209
- }
210
-
211
283
  if (token === "--region") {
212
284
  parsed.region = readValue();
213
285
  continue;
@@ -254,6 +326,69 @@ function parseArgs(argv: string[]): ParsedArgs {
254
326
  return parsed;
255
327
  }
256
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
+
257
392
  export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
258
393
  const inferredName = slugify(basename(args.directory ?? "my-service"));
259
394
  const serviceName = args.yes
@@ -265,15 +400,17 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
265
400
 
266
401
  const discoveryPromise = discoverCloudInputs();
267
402
  const defaults = deriveDefaults(serviceName);
268
- const runtime = await resolveRuntime(args);
269
- const framework = await resolveFramework(args, runtime);
270
- const discovery = await discoveryPromise;
271
- assertDiscoveryReady(discovery);
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);
407
+ const modulePath = await resolveModulePath(args, runtime, defaults.modulePath);
408
+ const discovery = await waitForDiscovery(discoveryPromise);
272
409
  const gcpSelection = await resolveGcpSelection(args, defaults, discovery);
273
- const githubRepo = args.githubRepo ?? defaults.githubRepo;
274
410
  const region = args.region ?? DEFAULT_REGION;
275
411
  const billingAccount = chooseBillingAccount(args.billingAccount, discovery.billingAccounts);
276
412
  const autoDeploy = resolveAutoDeploy(args.autoDeploy);
413
+ const git = buildGitBootstrapConfig(serviceName, args.noGit);
277
414
 
278
415
  if (!args.yes) {
279
416
  const okay = await confirm({
@@ -293,42 +430,87 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
293
430
  return {
294
431
  directory,
295
432
  serviceName,
433
+ modulePath,
434
+ target,
296
435
  runtime,
297
436
  framework,
437
+ profile: args.profile,
298
438
  region,
299
439
  gcpProjectMode: gcpSelection.mode,
300
440
  gcpProject: gcpSelection.projectId,
301
441
  gcpProjectName: gcpSelection.projectName,
302
442
  billingAccount,
303
443
  quotaProjectId: args.quotaProjectId ?? QUOTA_PROJECT_DEFAULT,
304
- githubRepo,
305
- githubVisibility: "public",
306
- createGithubRepo: true,
307
444
  autoDeploy,
308
- neonProjectId: discovery.neonProjectId ?? "",
309
- neonBaseBranchId: discovery.neonBaseBranchId ?? "",
310
- neonBaseBranchName: discovery.neonBaseBranchName ?? "main",
445
+ git,
311
446
  neonDatabaseName: defaults.neonDatabaseName,
447
+ apiHostname: defaults.apiHostname,
312
448
  generatorRoot: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
313
449
  };
314
450
  }
315
451
 
316
- async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
452
+ async function waitForDiscovery(discoveryPromise: Promise<DiscoveryState>) {
453
+ const indicator = spinner();
454
+ indicator.start("Discovering GCP defaults");
455
+ try {
456
+ const discovery = await discoveryPromise;
457
+ indicator.stop("GCP defaults discovered");
458
+ return discovery;
459
+ } catch (error) {
460
+ indicator.stop("GCP defaults discovery failed");
461
+ throw error;
462
+ }
463
+ }
464
+
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> {
317
492
  if (args.runtime) {
318
493
  return args.runtime;
319
494
  }
320
495
 
496
+ if (target === "workers") {
497
+ return "bun";
498
+ }
499
+
321
500
  if (args.yes) {
322
501
  return "go";
323
502
  }
324
503
 
325
504
  const value = await select({
326
505
  message: "Runtime",
327
- initialValue: "go",
328
- options: [
329
- { value: "go", label: "Go", hint: "Default" },
330
- { value: "bun", label: "Bun" },
331
- ],
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" }],
332
514
  });
333
515
 
334
516
  if (isCancel(value)) {
@@ -336,20 +518,20 @@ async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
336
518
  process.exit(1);
337
519
  }
338
520
 
339
- return value;
521
+ return value as Runtime;
340
522
  }
341
523
 
342
- async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Framework> {
343
- const allowed = FRAMEWORKS_BY_RUNTIME[runtime];
524
+ async function resolveFramework(args: ParsedArgs, target: DeployTarget, runtime: Runtime): Promise<Framework> {
525
+ const allowed = frameworksForTargetRuntime(target, runtime);
344
526
  if (args.framework) {
345
- if (allowed.includes(args.framework)) {
527
+ if (allowed.some((framework) => framework === args.framework)) {
346
528
  return args.framework;
347
529
  }
348
- 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}`);
349
531
  }
350
532
 
351
533
  if (args.yes) {
352
- return allowed[0];
534
+ return target === "cloudrun" && runtime === "go" ? "connectrpc" : allowed[0]!;
353
535
  }
354
536
 
355
537
  const value = await select({
@@ -367,7 +549,28 @@ async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Fra
367
549
  process.exit(1);
368
550
  }
369
551
 
370
- return value;
552
+ return value as Framework;
553
+ }
554
+
555
+ async function resolveModulePath(args: ParsedArgs, runtime: Runtime, initialValue: string) {
556
+ if (runtime !== "go") {
557
+ return args.modulePath ?? initialValue;
558
+ }
559
+
560
+ if (args.modulePath) {
561
+ return args.modulePath.trim();
562
+ }
563
+
564
+ if (args.yes) {
565
+ return initialValue;
566
+ }
567
+
568
+ return promptText("Go module path", initialValue, (value) => {
569
+ if (!value.trim()) {
570
+ return "Go module path is required";
571
+ }
572
+ return true;
573
+ });
371
574
  }
372
575
 
373
576
  async function resolveGcpSelection(
@@ -483,24 +686,11 @@ async function discoverCloudInputs(): Promise<DiscoveryState> {
483
686
  result.warnings.push(`Skipping billing account discovery: ${formatError(error)}`);
484
687
  }
485
688
 
486
- try {
487
- const neonDefaults = await discoverNeonDefaults();
488
- result.neonProjectId = neonDefaults.projectId;
489
- result.neonBaseBranchId = neonDefaults.baseBranchId;
490
- result.neonBaseBranchName = neonDefaults.baseBranchName;
491
- } catch (error) {
492
- result.neonError = formatError(error);
493
- }
494
-
495
689
  return result;
496
690
  }
497
691
 
498
692
  export function assertDiscoveryReady(discovery: DiscoveryState) {
499
- if (!discovery.neonError) {
500
- return;
501
- }
502
-
503
- throw new Error(formatNeonDiscoveryRequirement(discovery.neonError));
693
+ return discovery;
504
694
  }
505
695
 
506
696
  function chooseBillingAccount(input: string | undefined, accounts: BillingAccount[]) {
@@ -520,7 +710,7 @@ function resolveAutoDeploy(value: boolean | undefined) {
520
710
  if (value !== undefined) {
521
711
  return value;
522
712
  }
523
- return Boolean(process.stdout.isTTY && process.stdin.isTTY);
713
+ return false;
524
714
  }
525
715
 
526
716
  async function promptText(
@@ -531,7 +721,7 @@ async function promptText(
531
721
  const value = await text({
532
722
  message,
533
723
  initialValue,
534
- validate: (input) => normalizeValidationResult(validate(input.trim())),
724
+ validate: (input) => normalizeValidationResult(validate((input ?? "").trim())),
535
725
  });
536
726
 
537
727
  if (isCancel(value)) {
@@ -546,18 +736,6 @@ function formatError(error: unknown) {
546
736
  return error instanceof Error ? error.message : String(error);
547
737
  }
548
738
 
549
- function formatNeonDiscoveryRequirement(reason: string) {
550
- if (reason.includes("Vault secret resolution requires")) {
551
- return [
552
- "Neon discovery is required before scaffolding.",
553
- "Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and either VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token.",
554
- "Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD.",
555
- ].join(" ");
556
- }
557
-
558
- return `Neon discovery is required before scaffolding: ${reason}`;
559
- }
560
-
561
739
  function handleCliError(error: unknown) {
562
740
  if (error instanceof DirectoryConflictError) {
563
741
  log.error(`Target directory already exists and is not empty: ${error.targetDir}`);
@@ -612,6 +790,17 @@ export function normalizeValidationResult(result: true | string): string | undef
612
790
  return result === true ? undefined : result;
613
791
  }
614
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
+
615
804
  export function validateServiceNameInput(rawValue: string, directoryOverride?: string) {
616
805
  const serviceName = slugify(rawValue);
617
806
  if (!serviceName) {
@@ -638,20 +827,29 @@ export function validateServiceNameInput(rawValue: string, directoryOverride?: s
638
827
  function printHelp() {
639
828
  log.message(`
640
829
  Usage:
641
- bun run index.ts [directory] [options]
830
+ create-service [service_id] [options]
642
831
 
643
832
  Options:
833
+ --target <cloudrun|workers> Deploy target for the generated service
834
+ --profile <microservice> Compatibility no-op; app workspaces moved out
644
835
  --runtime <go|bun> Runtime scaffold to generate
645
836
  --framework <name> Framework for the selected runtime
837
+ --module-path <path> Go module path for generated Go scaffolds
646
838
  --project-mode <mode> create_new or use_existing
647
839
  --project-id <id> GCP project id
648
- --github-repo <owner/repo> GitHub repository
649
840
  --billing-account <name> Billing account resource name
650
841
  --quota-project <id> Billing quota project for gcloud calls
651
842
  --region <region> Cloud Run region
652
- --auto-deploy Run bootstrap and first deploy after scaffold
843
+ --auto-deploy Run service create and service deploy after scaffold
653
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
654
848
  --yes, -y Accept defaults without prompts
655
849
  --help, -h Show this message
656
850
  `);
657
851
  }
852
+
853
+ function matchesProject(project: GcpProject, query: string) {
854
+ return project.projectId === query || project.name === query;
855
+ }
@@ -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
+ }