create-svc 0.1.9 → 0.1.10

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 (91) hide show
  1. package/README.md +130 -11
  2. package/package.json +9 -4
  3. package/src/cli.test.ts +29 -8
  4. package/src/cli.ts +103 -70
  5. package/src/naming.test.ts +4 -2
  6. package/src/naming.ts +9 -1
  7. package/src/neon.ts +10 -8
  8. package/src/post-scaffold.ts +7 -28
  9. package/src/profiles.ts +28 -0
  10. package/src/scaffold.test.ts +126 -15
  11. package/src/scaffold.ts +94 -23
  12. package/src/vault.test.ts +33 -9
  13. package/src/vault.ts +4 -3
  14. package/templates/shared/README.md +135 -24
  15. package/templates/shared/docker-compose.yml +19 -0
  16. package/templates/shared/scripts/cloudrun/bootstrap.ts +15 -42
  17. package/templates/shared/scripts/cloudrun/cleanup.ts +17 -31
  18. package/templates/shared/scripts/cloudrun/config.ts +14 -19
  19. package/templates/shared/scripts/cloudrun/deploy.ts +19 -10
  20. package/templates/shared/scripts/cloudrun/integrations.ts +111 -0
  21. package/templates/shared/scripts/cloudrun/lib.ts +88 -112
  22. package/templates/shared/scripts/cloudrun/neon.ts +82 -13
  23. package/templates/shared/service.yaml +44 -1
  24. package/templates/variants/bun-connectrpc/Dockerfile +1 -0
  25. package/templates/variants/bun-connectrpc/Makefile +4 -1
  26. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +1078 -0
  27. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +63 -0
  28. package/templates/variants/bun-connectrpc/package.json +17 -0
  29. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +228 -0
  30. package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
  31. package/templates/variants/bun-connectrpc/scripts/migrate.ts +46 -0
  32. package/templates/variants/bun-connectrpc/src/chat/service.ts +384 -0
  33. package/templates/variants/bun-connectrpc/src/chat/types.ts +142 -0
  34. package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
  35. package/templates/variants/bun-connectrpc/src/db/repository.ts +479 -0
  36. package/templates/variants/bun-connectrpc/src/db/schema.ts +75 -0
  37. package/templates/variants/bun-connectrpc/src/index.ts +294 -22
  38. package/templates/variants/bun-connectrpc/src/storage.ts +72 -0
  39. package/templates/variants/bun-connectrpc/src/webhooks.ts +35 -0
  40. package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
  41. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +182 -0
  42. package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
  43. package/templates/variants/bun-hono/Makefile +4 -1
  44. package/templates/variants/bun-hono/migrations/0000_init.sql +63 -0
  45. package/templates/variants/bun-hono/package.json +13 -0
  46. package/templates/variants/bun-hono/scripts/migrate.ts +46 -0
  47. package/templates/variants/bun-hono/src/chat/service.ts +384 -0
  48. package/templates/variants/bun-hono/src/chat/types.ts +142 -0
  49. package/templates/variants/bun-hono/src/db/client.ts +15 -0
  50. package/templates/variants/bun-hono/src/db/repository.ts +479 -0
  51. package/templates/variants/bun-hono/src/db/schema.ts +75 -0
  52. package/templates/variants/bun-hono/src/index.ts +254 -8
  53. package/templates/variants/bun-hono/src/storage.ts +72 -0
  54. package/templates/variants/bun-hono/src/webhooks.ts +35 -0
  55. package/templates/variants/bun-hono/test/app.test.ts +60 -6
  56. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +256 -0
  57. package/templates/variants/bun-hono/tsconfig.json +1 -0
  58. package/templates/variants/go-chi/Makefile +6 -2
  59. package/templates/variants/go-chi/buf.gen.yaml +2 -0
  60. package/templates/variants/go-chi/cmd/migrate/main.go +101 -0
  61. package/templates/variants/go-chi/cmd/server/main.go +16 -15
  62. package/templates/variants/go-chi/go.mod +3 -0
  63. package/templates/variants/go-chi/internal/app/service.go +763 -71
  64. package/templates/variants/go-chi/internal/config/config.go +22 -7
  65. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +298 -0
  66. package/templates/variants/go-chi/internal/httpapi/routes.go +245 -43
  67. package/templates/variants/go-chi/migrations/0000_init.sql +63 -0
  68. package/templates/variants/go-chi/protos/chat/v1/chat.proto +219 -0
  69. package/templates/variants/go-chi/test/go.test.ts +4 -1
  70. package/templates/variants/go-connectrpc/Makefile +6 -2
  71. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
  72. package/templates/variants/go-connectrpc/cmd/migrate/main.go +101 -0
  73. package/templates/variants/go-connectrpc/cmd/server/main.go +35 -11
  74. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +2512 -0
  75. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +571 -0
  76. package/templates/variants/go-connectrpc/go.mod +4 -0
  77. package/templates/variants/go-connectrpc/internal/app/service.go +763 -71
  78. package/templates/variants/go-connectrpc/internal/config/config.go +22 -7
  79. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +254 -42
  80. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +216 -0
  81. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +41 -56
  82. package/templates/variants/go-connectrpc/migrations/0000_init.sql +63 -0
  83. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +232 -0
  84. package/templates/shared/.env.example +0 -10
  85. package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
  86. package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  87. package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
  88. package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
  89. package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
  90. package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  91. package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
package/README.md CHANGED
@@ -1,31 +1,101 @@
1
1
  # create-svc
2
2
 
3
- `create-svc` is a Bun-authored scaffold CLI for generating Cloud Run services with:
3
+ `create-svc` is a local backend bootstrap CLI for generating standalone Cloud Run API services with:
4
4
 
5
- - `go + chi`
6
- - `go + connectrpc`
7
- - `bun + hono`
8
- - `bun + connectrpc`
5
+ - a single `microservice` generation path
6
+ - a Bun-first backend path built around `hono` and ConnectRPC
7
+ - standalone package output that does not assume repo bootstrap
8
+ - compatibility with future monorepo use in layouts like `apps/<service>`
9
9
  - a real `service.yaml` manifest
10
10
  - shared Cloud Run bootstrap, deploy, and cleanup automation
11
- - Neon-backed main, preview, and personal environments
11
+ - local Docker Compose Postgres for first-run development
12
+ - Neon-backed remote main, preview, and personal environments
13
+ - GCS-backed image attachments
14
+ - typed HTTP webhook ingress
15
+ - a production API origin at `https://api.<appname>.anmho.com`
16
+
17
+ Local provisioning intentionally prefers known-good CLIs, especially `gcloud`, over SDK-heavy orchestration for Google Cloud operations.
18
+ Terraform, control planes, and platform consoles are optional advanced paths, not default prerequisites.
19
+
20
+ npm: <https://www.npmjs.com/package/create-svc>
12
21
 
13
22
  ## Usage
14
23
 
24
+ ```bash
25
+ bun create svc my-service
26
+ ```
27
+
28
+ or:
29
+
30
+ ```bash
31
+ bunx create-svc my-service
32
+ ```
33
+
34
+ For the strict one-command production path:
35
+
36
+ ```bash
37
+ bun create svc my-service --profile microservice --bootstrap --yes
38
+ ```
39
+
40
+ `--profile microservice` is accepted as a compatibility no-op. Full app workspaces live in the private GitHub template repos `anmho/create-app-consumer` and `anmho/create-app-saas`.
41
+
42
+ ## Local Testing
43
+
44
+ Without publishing to npm:
45
+
15
46
  ```bash
16
47
  bun install
17
- bun run index.ts my-service
48
+ npm pack
49
+ bunx ./create-svc-*.tgz my-service
50
+ ```
51
+
52
+ For faster iteration against your working tree:
53
+
54
+ ```bash
55
+ bun link
56
+ bun link create-svc
57
+ create-svc my-service
18
58
  ```
19
59
 
20
- The generator discovers:
60
+ During scaffold, the generator can discover:
21
61
 
22
62
  - accessible GCP projects
23
63
  - open billing accounts
24
- - Neon defaults from `NEON_API_KEY`, or Vault via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
25
64
 
26
- Generated repos are `Makefile`-first. The shared Cloud Run control plane is exposed as a local CLI bin and invoked by `make`.
65
+ Remote `bootstrap` and `deploy` use Neon credentials from `NEON_API_KEY`, or Vault via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`.
66
+ Provider runtime credentials can be supplied through environment variables or Vault paths under `secret/prod/providers/*`; generated Cloud Run services receive runtime values through app-project Secret Manager.
67
+
68
+ Before running generated provisioning commands locally, authenticate `gcloud` on the machine:
27
69
 
28
70
  ```bash
71
+ gcloud auth login
72
+ ```
73
+
74
+ ## Generated Backend Package
75
+
76
+ First local run:
77
+
78
+ ```bash
79
+ docker compose up -d
80
+ ```
81
+
82
+ For Bun variants:
83
+
84
+ ```bash
85
+ bun run migrate
86
+ bun run dev
87
+ bun run gen
88
+ bun run lint
89
+ bun run test
90
+ bun run bootstrap
91
+ bun run deploy
92
+ bun run cleanup
93
+ ```
94
+
95
+ For Go variants:
96
+
97
+ ```bash
98
+ make migrate
29
99
  make dev
30
100
  make gen
31
101
  make lint
@@ -35,8 +105,57 @@ make deploy
35
105
  make cleanup
36
106
  ```
37
107
 
108
+ The generated package is intended to be consumed by a Next.js web app or a mobile client over HTTPS. In v1, production is expected to live at `https://api.<appname>.anmho.com`, while preview and personal environments keep using deterministic Cloud Run URLs.
109
+
110
+ The microservice profile is moving toward a small waitlist/launch service example. The current generated plumbing still includes:
111
+
112
+ - Postgres-backed `users`, `conversations`, `conversation_participants`, and `messages`
113
+ - image attachment upload/finalize plumbing via GCS
114
+ - generic typed webhook ingestion on plain HTTP
115
+
38
116
  ## Development
39
117
 
40
118
  ```bash
41
- bun test src
119
+ bun install
120
+ bun test src scripts
121
+ bun run index.ts my-service
42
122
  ```
123
+
124
+ Validate the generated app matrix against local Docker Compose Postgres:
125
+
126
+ ```bash
127
+ bun run validate:generated
128
+ bun run validate:generated -- --variant bun-hono
129
+ bun run validate:generated -- --variant go-connectrpc --keep
130
+ ```
131
+
132
+ The validation harness scaffolds generated apps into ignored `bin/generated/run-*` workspaces, runs the generated public commands, starts the local server, and smoke-tests health or typed ConnectRPC clients where applicable.
133
+
134
+ ## npm Trusted Publishing
135
+
136
+ `create-svc` is set up for npm trusted publishing from GitHub Actions, so there is no long-lived npm publish token to store in Vault.
137
+
138
+ Repository workflow:
139
+ - [publish.yml](.github/workflows/publish.yml)
140
+ - Trigger: Git tags matching `v*`
141
+ - CI runtime: Bun for install/test/typecheck, npm for the final publish step
142
+
143
+ npm package setup still has to be configured once in the npm UI to trust this repository and workflow:
144
+
145
+ 1. Open the `create-svc` package settings on npm.
146
+ 2. Go to `Settings` -> `Trusted Publisher`.
147
+ 3. Select `GitHub Actions`.
148
+ 4. Enter:
149
+ - Organization or user: `anmho`
150
+ - Repository: `create-svc`
151
+ - Workflow filename: `publish.yml`
152
+ 5. Save the trusted publisher.
153
+
154
+ After that, publishing is:
155
+
156
+ ```bash
157
+ git tag v0.1.10
158
+ git push origin v0.1.10
159
+ ```
160
+
161
+ The GitHub Actions workflow will authenticate with npm via OIDC and run `npm publish` without an npm token.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.9",
4
- "description": "Bun-authored CLI to scaffold Go Cloud Run services with Chi, ConnectRPC, Vault, and Cloudflare examples.",
3
+ "version": "0.1.10",
4
+ "description": "Local backend bootstrap CLI for Bun-first Cloud Run API services with Neon and Vault provisioning.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -18,7 +18,9 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "dev": "bun run index.ts",
21
- "test": "bun test src"
21
+ "test": "bun test src scripts",
22
+ "validate:generated": "bun run ./scripts/validate-generated.ts",
23
+ "typecheck": "bunx tsc --noEmit"
22
24
  },
23
25
  "repository": {
24
26
  "type": "git",
@@ -30,12 +32,15 @@
30
32
  },
31
33
  "keywords": [
32
34
  "bun",
35
+ "backend",
33
36
  "cloud-run",
34
37
  "connectrpc",
35
- "go",
36
38
  "grpc",
39
+ "hono",
40
+ "monorepo",
37
41
  "scaffold"
38
42
  ],
43
+ "packageManager": "bun@1.3.2",
39
44
  "devDependencies": {
40
45
  "@types/bun": "latest"
41
46
  },
package/src/cli.test.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from "bun:test";
2
2
  import { mkdir } from "node:fs/promises";
3
- import { assertDiscoveryReady, normalizeValidationResult, validateServiceNameInput } from "./cli";
3
+ import { assertDiscoveryReady, normalizeValidationResult, parseArgs, validateServiceNameInput } from "./cli";
4
4
 
5
5
  test("normalizeValidationResult converts success to undefined", () => {
6
6
  expect(normalizeValidationResult(true)).toBeUndefined();
@@ -10,17 +10,38 @@ test("normalizeValidationResult preserves validation errors", () => {
10
10
  expect(normalizeValidationResult("Service name is required")).toBe("Service name is required");
11
11
  });
12
12
 
13
- test("assertDiscoveryReady requires Neon discovery to succeed", () => {
14
- expect(() =>
13
+ test("assertDiscoveryReady no longer blocks scaffold when remote discovery is unavailable", () => {
14
+ expect(
15
15
  assertDiscoveryReady({
16
16
  projects: [],
17
17
  billingAccounts: [],
18
- warnings: [],
19
- neonError: "Vault secret resolution requires VAULT_ADDR, VAULT_TOKEN, and a secret path",
18
+ warnings: ["Skipping GCP project discovery: gcloud not installed"],
20
19
  })
21
- ).toThrow(
22
- "Neon discovery is required before scaffolding. Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and either VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token. Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD."
23
- );
20
+ ).toEqual({
21
+ projects: [],
22
+ billingAccounts: [],
23
+ warnings: ["Skipping GCP project discovery: gcloud not installed"],
24
+ });
25
+ });
26
+
27
+ test("parseArgs defaults to the microservice profile and treats bootstrap as strict deploy", () => {
28
+ expect(parseArgs(["launch-api", "--yes"])).toMatchObject({
29
+ directory: "launch-api",
30
+ profile: "microservice",
31
+ yes: true,
32
+ });
33
+ expect(parseArgs(["launch-api", "--yes"]).autoDeploy).toBeUndefined();
34
+
35
+ expect(parseArgs(["launch-api", "--profile", "microservice", "--bootstrap"])).toMatchObject({
36
+ directory: "launch-api",
37
+ profile: "microservice",
38
+ autoDeploy: true,
39
+ });
40
+ });
41
+
42
+ test("parseArgs rejects the moved app profile with private template guidance", () => {
43
+ expect(() => parseArgs(["tracker", "--profile=app", "--yes"])).toThrow("anmho/create-app-consumer");
44
+ expect(() => parseArgs(["tracker", "--profile", "app", "--yes"])).toThrow("anmho/create-app-saas");
24
45
  });
25
46
 
26
47
  test("validateServiceNameInput rejects a taken target directory", async () => {
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,
@@ -16,7 +17,6 @@ import { basename, dirname, resolve } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
17
18
  import { runPostScaffoldFlow } from "./post-scaffold";
18
19
  import { listOpenBillingAccounts, listAccessibleProjects, type BillingAccount, type GcpProject } from "./gcp";
19
- import { discoverNeonDefaults } from "./neon";
20
20
  import {
21
21
  BILLING_ACCOUNT_DEFAULT,
22
22
  FRAMEWORKS_BY_RUNTIME,
@@ -27,6 +27,7 @@ import {
27
27
  type GcpProjectMode,
28
28
  type Runtime,
29
29
  } from "./naming";
30
+ import { parseProfile, type Profile } from "./profiles";
30
31
  import {
31
32
  DirectoryConflictError,
32
33
  assertTargetDirectoryIsEmpty,
@@ -38,13 +39,14 @@ type ParsedArgs = {
38
39
  directory?: string;
39
40
  runtime?: Runtime;
40
41
  framework?: Framework;
42
+ modulePath?: string;
41
43
  gcpProjectMode?: GcpProjectMode;
42
44
  gcpProject?: string;
43
- githubRepo?: string;
44
45
  region?: string;
45
46
  billingAccount?: string;
46
47
  quotaProjectId?: string;
47
48
  autoDeploy?: boolean;
49
+ profile: Profile;
48
50
  yes: boolean;
49
51
  help: boolean;
50
52
  };
@@ -52,10 +54,6 @@ type ParsedArgs = {
52
54
  type DiscoveryState = {
53
55
  projects: GcpProject[];
54
56
  billingAccounts: BillingAccount[];
55
- neonProjectId?: string;
56
- neonBaseBranchId?: string;
57
- neonBaseBranchName?: string;
58
- neonError?: string;
59
57
  warnings: string[];
60
58
  };
61
59
 
@@ -69,7 +67,7 @@ export async function run(argv: string[]) {
69
67
  return;
70
68
  }
71
69
 
72
- intro(`${pc.bold("create-svc")} ${pc.dim("Cloud Run scaffold")}`);
70
+ intro(`${pc.bold("create-svc")} ${pc.dim("backend bootstrap")}`);
73
71
 
74
72
  const config = await resolveConfig(args);
75
73
  const targetDir = resolve(process.cwd(), config.directory);
@@ -79,8 +77,8 @@ export async function run(argv: string[]) {
79
77
  `${pc.bold("Output")}: ${targetDir}`,
80
78
  `${pc.bold("Runtime")}: ${config.runtime} + ${config.framework}`,
81
79
  `${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)"}`,
80
+ `${pc.bold("API")}: https://${config.apiHostname}`,
81
+ `${pc.bold("Local DB")}: docker compose postgres`,
84
82
  ].join("\n"),
85
83
  "Scaffold"
86
84
  );
@@ -90,7 +88,7 @@ export async function run(argv: string[]) {
90
88
  await scaffoldProject(config);
91
89
  buildSpinner.stop("Project files generated");
92
90
 
93
- const shouldRunPostScaffoldFlow = Boolean(process.stdout.isTTY && process.stdin.isTTY && (config.createGithubRepo || config.autoDeploy));
91
+ const shouldRunPostScaffoldFlow = config.autoDeploy;
94
92
  if (shouldRunPostScaffoldFlow) {
95
93
  const automationSpinner = spinner();
96
94
  automationSpinner.start("Running post-scaffold automation");
@@ -103,13 +101,21 @@ export async function run(argv: string[]) {
103
101
  }
104
102
  }
105
103
 
104
+ const isBun = config.runtime === "bun";
106
105
  outro(
107
106
  [
108
107
  `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}`)}`,
108
+ `Local DB: ${pc.cyan("docker compose up -d")}`,
109
+ `Migrate: ${pc.cyan(isBun ? "bun run migrate" : "make migrate")}`,
110
+ `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")}`,
113
+ `Personal env: ${pc.cyan(
114
+ isBun
115
+ ? `bun run deploy -- --environment personal --name ${config.serviceName}`
116
+ : `make deploy ARGS="--environment personal --name ${config.serviceName}"`
117
+ )}`,
118
+ `Production API: ${pc.cyan(`https://${config.apiHostname}`)}`,
113
119
  ].join("\n")
114
120
  );
115
121
  } catch (error) {
@@ -117,8 +123,9 @@ export async function run(argv: string[]) {
117
123
  }
118
124
  }
119
125
 
120
- function parseArgs(argv: string[]): ParsedArgs {
126
+ export function parseArgs(argv: string[]): ParsedArgs {
121
127
  const parsed: ParsedArgs = {
128
+ profile: "microservice",
122
129
  yes: false,
123
130
  help: false,
124
131
  };
@@ -173,6 +180,26 @@ function parseArgs(argv: string[]): ParsedArgs {
173
180
  continue;
174
181
  }
175
182
 
183
+ if (token === "--profile") {
184
+ parsed.profile = parseProfile(readValue());
185
+ continue;
186
+ }
187
+
188
+ if (token.startsWith("--profile=")) {
189
+ parsed.profile = parseProfile(token.slice("--profile=".length));
190
+ continue;
191
+ }
192
+
193
+ if (token === "--module-path") {
194
+ parsed.modulePath = readValue();
195
+ continue;
196
+ }
197
+
198
+ if (token.startsWith("--module-path=")) {
199
+ parsed.modulePath = token.slice("--module-path=".length);
200
+ continue;
201
+ }
202
+
176
203
  if (token === "--project-mode") {
177
204
  parsed.gcpProjectMode = readValue() as GcpProjectMode;
178
205
  continue;
@@ -198,16 +225,6 @@ function parseArgs(argv: string[]): ParsedArgs {
198
225
  continue;
199
226
  }
200
227
 
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
228
  if (token === "--region") {
212
229
  parsed.region = readValue();
213
230
  continue;
@@ -243,6 +260,11 @@ function parseArgs(argv: string[]): ParsedArgs {
243
260
  continue;
244
261
  }
245
262
 
263
+ if (token === "--bootstrap") {
264
+ parsed.autoDeploy = true;
265
+ continue;
266
+ }
267
+
246
268
  if (token === "--no-auto-deploy") {
247
269
  parsed.autoDeploy = false;
248
270
  continue;
@@ -267,10 +289,9 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
267
289
  const defaults = deriveDefaults(serviceName);
268
290
  const runtime = await resolveRuntime(args);
269
291
  const framework = await resolveFramework(args, runtime);
270
- const discovery = await discoveryPromise;
271
- assertDiscoveryReady(discovery);
292
+ const modulePath = await resolveModulePath(args, runtime, defaults.modulePath);
293
+ const discovery = await waitForDiscovery(discoveryPromise);
272
294
  const gcpSelection = await resolveGcpSelection(args, defaults, discovery);
273
- const githubRepo = args.githubRepo ?? defaults.githubRepo;
274
295
  const region = args.region ?? DEFAULT_REGION;
275
296
  const billingAccount = chooseBillingAccount(args.billingAccount, discovery.billingAccounts);
276
297
  const autoDeploy = resolveAutoDeploy(args.autoDeploy);
@@ -293,41 +314,51 @@ export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
293
314
  return {
294
315
  directory,
295
316
  serviceName,
317
+ modulePath,
296
318
  runtime,
297
319
  framework,
320
+ profile: args.profile,
298
321
  region,
299
322
  gcpProjectMode: gcpSelection.mode,
300
323
  gcpProject: gcpSelection.projectId,
301
324
  gcpProjectName: gcpSelection.projectName,
302
325
  billingAccount,
303
326
  quotaProjectId: args.quotaProjectId ?? QUOTA_PROJECT_DEFAULT,
304
- githubRepo,
305
- githubVisibility: "public",
306
- createGithubRepo: true,
307
327
  autoDeploy,
308
- neonProjectId: discovery.neonProjectId ?? "",
309
- neonBaseBranchId: discovery.neonBaseBranchId ?? "",
310
- neonBaseBranchName: discovery.neonBaseBranchName ?? "main",
311
328
  neonDatabaseName: defaults.neonDatabaseName,
329
+ apiHostname: defaults.apiHostname,
312
330
  generatorRoot: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
313
331
  };
314
332
  }
315
333
 
334
+ async function waitForDiscovery(discoveryPromise: Promise<DiscoveryState>) {
335
+ const indicator = spinner();
336
+ indicator.start("Discovering GCP defaults");
337
+ try {
338
+ const discovery = await discoveryPromise;
339
+ indicator.stop("GCP defaults discovered");
340
+ return discovery;
341
+ } catch (error) {
342
+ indicator.stop("GCP defaults discovery failed");
343
+ throw error;
344
+ }
345
+ }
346
+
316
347
  async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
317
348
  if (args.runtime) {
318
349
  return args.runtime;
319
350
  }
320
351
 
321
352
  if (args.yes) {
322
- return "go";
353
+ return "bun";
323
354
  }
324
355
 
325
356
  const value = await select({
326
357
  message: "Runtime",
327
- initialValue: "go",
358
+ initialValue: "bun",
328
359
  options: [
329
- { value: "go", label: "Go", hint: "Default" },
330
- { value: "bun", label: "Bun" },
360
+ { value: "bun", label: "Bun", hint: "Default" },
361
+ { value: "go", label: "Go" },
331
362
  ],
332
363
  });
333
364
 
@@ -336,13 +367,13 @@ async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
336
367
  process.exit(1);
337
368
  }
338
369
 
339
- return value;
370
+ return value as Runtime;
340
371
  }
341
372
 
342
373
  async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Framework> {
343
374
  const allowed = FRAMEWORKS_BY_RUNTIME[runtime];
344
375
  if (args.framework) {
345
- if (allowed.includes(args.framework)) {
376
+ if (allowed.some((framework) => framework === args.framework)) {
346
377
  return args.framework;
347
378
  }
348
379
  throw new Error(`Framework ${args.framework} is not valid for runtime ${runtime}`);
@@ -367,7 +398,28 @@ async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Fra
367
398
  process.exit(1);
368
399
  }
369
400
 
370
- return value;
401
+ return value as Framework;
402
+ }
403
+
404
+ async function resolveModulePath(args: ParsedArgs, runtime: Runtime, initialValue: string) {
405
+ if (runtime !== "go") {
406
+ return args.modulePath ?? initialValue;
407
+ }
408
+
409
+ if (args.modulePath) {
410
+ return args.modulePath.trim();
411
+ }
412
+
413
+ if (args.yes) {
414
+ return initialValue;
415
+ }
416
+
417
+ return promptText("Go module path", initialValue, (value) => {
418
+ if (!value.trim()) {
419
+ return "Go module path is required";
420
+ }
421
+ return true;
422
+ });
371
423
  }
372
424
 
373
425
  async function resolveGcpSelection(
@@ -483,24 +535,11 @@ async function discoverCloudInputs(): Promise<DiscoveryState> {
483
535
  result.warnings.push(`Skipping billing account discovery: ${formatError(error)}`);
484
536
  }
485
537
 
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
538
  return result;
496
539
  }
497
540
 
498
541
  export function assertDiscoveryReady(discovery: DiscoveryState) {
499
- if (!discovery.neonError) {
500
- return;
501
- }
502
-
503
- throw new Error(formatNeonDiscoveryRequirement(discovery.neonError));
542
+ return discovery;
504
543
  }
505
544
 
506
545
  function chooseBillingAccount(input: string | undefined, accounts: BillingAccount[]) {
@@ -520,7 +559,7 @@ function resolveAutoDeploy(value: boolean | undefined) {
520
559
  if (value !== undefined) {
521
560
  return value;
522
561
  }
523
- return Boolean(process.stdout.isTTY && process.stdin.isTTY);
562
+ return false;
524
563
  }
525
564
 
526
565
  async function promptText(
@@ -531,7 +570,7 @@ async function promptText(
531
570
  const value = await text({
532
571
  message,
533
572
  initialValue,
534
- validate: (input) => normalizeValidationResult(validate(input.trim())),
573
+ validate: (input) => normalizeValidationResult(validate((input ?? "").trim())),
535
574
  });
536
575
 
537
576
  if (isCancel(value)) {
@@ -546,18 +585,6 @@ function formatError(error: unknown) {
546
585
  return error instanceof Error ? error.message : String(error);
547
586
  }
548
587
 
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
588
  function handleCliError(error: unknown) {
562
589
  if (error instanceof DirectoryConflictError) {
563
590
  log.error(`Target directory already exists and is not empty: ${error.targetDir}`);
@@ -641,17 +668,23 @@ Usage:
641
668
  bun run index.ts [directory] [options]
642
669
 
643
670
  Options:
671
+ --profile <microservice> Compatibility no-op; create-svc only generates microservices
644
672
  --runtime <go|bun> Runtime scaffold to generate
645
673
  --framework <name> Framework for the selected runtime
674
+ --module-path <path> Go module path for generated Go scaffolds
646
675
  --project-mode <mode> create_new or use_existing
647
676
  --project-id <id> GCP project id
648
- --github-repo <owner/repo> GitHub repository
649
677
  --billing-account <name> Billing account resource name
650
678
  --quota-project <id> Billing quota project for gcloud calls
651
679
  --region <region> Cloud Run region
652
680
  --auto-deploy Run bootstrap and first deploy after scaffold
681
+ --bootstrap Alias for --auto-deploy
653
682
  --no-auto-deploy Scaffold only
654
683
  --yes, -y Accept defaults without prompts
655
684
  --help, -h Show this message
656
685
  `);
657
686
  }
687
+
688
+ function matchesProject(project: GcpProject, query: string) {
689
+ return project.projectId === query || project.name === query;
690
+ }
@@ -1,14 +1,16 @@
1
1
  import { expect, test } from "bun:test";
2
- import { buildGcpProjectOptions, compactDatabaseName, compactIdentifier, deriveDefaults } from "./naming";
2
+ import { buildGcpProjectOptions, compactDatabaseName, compactIdentifier, deriveDefaults, deriveLocalPostgresPort } from "./naming";
3
3
 
4
4
  test("deriveDefaults uses the service name for project, repo, and database naming", () => {
5
5
  expect(deriveDefaults("edge-api")).toEqual({
6
6
  serviceName: "edge-api",
7
7
  projectName: "edge-api",
8
8
  projectId: "anmho-edge-api",
9
- githubRepo: "anmho/edge-api",
10
9
  cloudRunService: "edge-api",
11
10
  neonDatabaseName: "edge_api",
11
+ localDatabasePort: deriveLocalPostgresPort("edge-api"),
12
+ apiHostname: "api.edge-api.anmho.com",
13
+ modulePath: "example.com/edge-api",
12
14
  });
13
15
  });
14
16
 
package/src/naming.ts CHANGED
@@ -54,6 +54,12 @@ export function compactDatabaseName(serviceName: string) {
54
54
  });
55
55
  }
56
56
 
57
+ export function deriveLocalPostgresPort(serviceName: string) {
58
+ const normalized = slugify(serviceName) || "my-service";
59
+ const hash = Number.parseInt(shortHash(normalized).slice(0, 4), 16);
60
+ return String(55000 + (hash % 1000));
61
+ }
62
+
57
63
  export function deriveDefaults(serviceName: string) {
58
64
  const normalizedServiceName = slugify(serviceName) || "my-service";
59
65
 
@@ -61,9 +67,11 @@ export function deriveDefaults(serviceName: string) {
61
67
  serviceName: normalizedServiceName,
62
68
  projectName: normalizedServiceName,
63
69
  projectId: compactIdentifier(`anmho-${normalizedServiceName}`, 30),
64
- githubRepo: `anmho/${normalizedServiceName}`,
65
70
  cloudRunService: normalizedServiceName,
66
71
  neonDatabaseName: compactDatabaseName(normalizedServiceName),
72
+ localDatabasePort: deriveLocalPostgresPort(normalizedServiceName),
73
+ apiHostname: `api.${normalizedServiceName}.anmho.com`,
74
+ modulePath: `example.com/${normalizedServiceName}`,
67
75
  };
68
76
  }
69
77