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
@@ -2,30 +2,34 @@ import { expect, test } from "bun:test";
2
2
  import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
+ import { deriveLocalPostgresPort } from "./naming";
5
6
  import { DirectoryConflictError, assertTargetDirectoryIsEmpty, scaffoldProject, type ScaffoldConfig } from "./scaffold";
6
7
 
7
8
  function baseConfig(overrides: Partial<ScaffoldConfig> = {}): ScaffoldConfig {
8
9
  return {
9
10
  directory: "svc",
10
11
  serviceName: "dns-api",
11
- runtime: "go",
12
- framework: "chi",
12
+ modulePath: "example.com/dns-api",
13
+ target: "cloudrun",
14
+ runtime: "bun",
15
+ framework: "hono",
13
16
  region: "us-west1",
14
17
  gcpProjectMode: "create_new",
15
18
  gcpProject: "anmho-dns-api",
16
19
  gcpProjectName: "dns-api",
17
20
  billingAccount: "billingAccounts/01BD2E-3A6949-8F4C84",
18
21
  quotaProjectId: "anmho-infra-prod",
19
- githubRepo: "anmho/dns-api",
20
- githubVisibility: "public",
21
- createGithubRepo: true,
22
- autoDeploy: false,
23
- neonProjectId: "project-123",
24
- neonBaseBranchId: "br-main",
25
- neonBaseBranchName: "main",
22
+ profile: "microservice",
23
+ git: {
24
+ enabled: false,
25
+ owner: "anmho",
26
+ repository: "dns-api",
27
+ },
26
28
  neonDatabaseName: "dns_api",
29
+ apiHostname: "api.dns-api.anmho.com",
27
30
  generatorRoot: join(import.meta.dir, ".."),
28
31
  ...overrides,
32
+ autoDeploy: overrides.autoDeploy ?? false,
29
33
  };
30
34
  }
31
35
 
@@ -40,6 +44,7 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
40
44
  for (const variant of cases) {
41
45
  const root = await mkdtemp(join(tmpdir(), "create-svc-"));
42
46
  const generatedRoot = join(root, `${variant.runtime}-${variant.framework}`);
47
+ const localPort = deriveLocalPostgresPort("dns-api");
43
48
 
44
49
  await scaffoldProject(
45
50
  baseConfig({
@@ -50,37 +55,334 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
50
55
  );
51
56
 
52
57
  const configScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "config.ts")).text();
58
+ const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
59
+ expect(serviceConfig).toContain('service_id: "dns-api"');
60
+ expect(serviceConfig).toContain('target: "cloudrun"');
61
+ expect(serviceConfig).toContain('module: "buf.build/anmho/dns-api"');
62
+ expect(serviceConfig).toContain('issuer: "https://auth.anmho.com/api/auth"');
63
+ expect(serviceConfig).toContain('audience: "api://dns-api"');
64
+ expect(serviceConfig).toContain('vault_path_prefix: "prod/apps/dns-api/server/oauth-clients"');
65
+ expect(serviceConfig).toContain('api_key_secret_name: "dns-api-temporal-api-key"');
66
+ expect(configScript).toContain('profile: "microservice"');
67
+ expect(configScript).toContain('domain: "waitlist"');
68
+ expect(configScript).toContain('kind: "microservice"');
53
69
  expect(configScript).toContain(`runtime: "${variant.runtime}"`);
54
70
  expect(configScript).toContain(`framework: "${variant.framework}"`);
55
71
  expect(configScript).toContain('mode: "create_new"');
56
72
  expect(configScript).toContain('quotaProjectId: "anmho-infra-prod"');
57
- expect(configScript).toContain('projectId: "project-123"');
73
+ expect(configScript).toContain('issuer: "https://auth.anmho.com/api/auth"');
74
+ expect(configScript).toContain('audience: "api://dns-api"');
75
+ expect(configScript).toContain('jwksUrl: "https://auth.anmho.com/api/auth/jwks"');
76
+ expect(configScript).toContain('apiKeySecretName: "dns-api-temporal-api-key"');
77
+ expect(configScript).toContain('projectId: ""');
78
+ expect(configScript).toContain('baseBranchId: ""');
79
+ expect(configScript).toContain('baseBranchName: "main"');
58
80
  expect(configScript).toContain('previewBranchPrefix: "dns-api-pr"');
81
+ expect(configScript).toContain('hostname: "api.dns-api.anmho.com"');
82
+ expect(configScript).not.toContain("github:");
83
+ expect(configScript).not.toContain("attachmentBucket");
59
84
 
60
85
  const deployScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
61
86
  expect(deployScript).toContain('--billing-project", config.project.quotaProjectId');
87
+ expect(deployScript).toContain('config.project.mode === "use_existing"');
88
+ expect(deployScript).toContain("serviceDomain");
89
+ expect(deployScript).toContain("ensureProductionDomainMapping");
90
+ expect(deployScript).toContain('"domain-mappings",');
91
+ expect(deployScript).toContain('"--region",');
92
+ expect(deployScript).toContain("assertProductionDomainAvailable");
93
+ expect(deployScript).toContain("assertServiceNameAvailable");
94
+ expect(deployScript).not.toContain("ensureStorageBucket");
62
95
 
63
- const workflow = await Bun.file(join(generatedRoot, ".github", "workflows", "personal.yml")).text();
64
- expect(workflow).toContain("workflow_dispatch");
65
- expect(workflow).toContain("--environment personal");
96
+ expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "integrations.ts")).exists()).toBeFalse();
97
+ const destroyScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cleanup.ts")).text();
98
+ expect(destroyScript).toContain("assertOwnedResource");
99
+ expect(destroyScript).toContain("assertProductionDomainMappingOwned");
100
+ expect(destroyScript).toContain("deleteGrafanaResources");
101
+ expect(destroyScript).toContain('gcx", ["resources", "delete"');
102
+ expect(destroyScript).toContain("config.temporal.apiKeySecretName");
103
+ const neonScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "neon.ts")).text();
104
+ expect(neonScript).toContain("assertDatabaseOwned");
105
+ expect(neonScript).toContain("assertDisposableBranchName");
106
+ const seedScript = await Bun.file(join(generatedRoot, "scripts", "seed.ts")).text();
107
+ expect(seedScript).toContain("SEED_PROD=true");
108
+ expect(seedScript).toContain("waitlist_entries");
109
+
110
+ const manifest = await Bun.file(join(generatedRoot, "service.yaml")).text();
111
+ expect(manifest).toContain("DATABASE_URL");
112
+ expect(manifest).toContain("TEMPORAL_ENABLED");
113
+ expect(manifest).toContain("TEMPORAL_TASK_QUEUE");
114
+ expect(manifest).toContain("TEMPORAL_API_KEY_ENV");
115
+ expect(manifest).toContain("AUTH_ENABLED");
116
+ expect(manifest).toContain("${AUTH_AUDIENCE}");
117
+ expect(manifest).toContain("managed_by: create-service");
118
+ expect(manifest).toContain("service_id: ${SERVICE_ID}");
119
+ expect(manifest).not.toContain("CLERK_SECRET_KEY");
120
+ expect(manifest).not.toContain("STRIPE_SECRET_KEY");
121
+ expect(manifest).not.toContain("REVENUECAT_API_KEY");
122
+ expect(manifest).not.toContain("RESEND_API_KEY");
123
+ expect(manifest).not.toContain("POSTHOG_API_KEY");
124
+
125
+ const gitignore = await Bun.file(join(generatedRoot, ".gitignore")).text();
126
+ expect(gitignore).toContain("node_modules");
127
+ expect(await Bun.file(join(generatedRoot, "website", "package.json")).exists()).toBeFalse();
128
+
129
+ const dockerCompose = await Bun.file(join(generatedRoot, "docker-compose.yml")).text();
130
+ expect(dockerCompose).toContain('image: postgres:16-alpine');
131
+ expect(dockerCompose).toContain(`127.0.0.1:${localPort}:5432`);
132
+
133
+ const envExample = await Bun.file(join(generatedRoot, ".env.example")).text();
134
+ expect(envExample).toContain(`DATABASE_URL=postgres://postgres:postgres@127.0.0.1:${localPort}/dns_api?sslmode=disable`);
135
+ expect(envExample).toContain("AUTH_ENABLED=false");
136
+ expect(envExample).toContain("AUTH_AUDIENCE=api://dns-api");
137
+ expect(envExample).toContain("CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID=");
138
+ expect(envExample).toContain("TEMPORAL_API_KEY=");
139
+ expect(envExample).toContain("The base waitlist service does not require");
140
+ expect(envExample).not.toContain("ATTACHMENT_BUCKET=");
141
+ expect(envExample).not.toContain("CLERK_SECRET_KEY=");
142
+ expect(envExample).not.toContain("STRIPE_SECRET_KEY=");
143
+ expect(envExample).not.toContain("REVENUECAT_API_KEY=");
144
+ expect(envExample).not.toContain("RESEND_API_KEY=");
145
+ expect(envExample).not.toContain("POSTHOG_API_KEY=");
146
+
147
+ const localEnv = await Bun.file(join(generatedRoot, ".env.local")).text();
148
+ expect(localEnv).toContain(`DATABASE_URL=postgres://postgres:postgres@127.0.0.1:${localPort}/dns_api?sslmode=disable`);
149
+ expect(localEnv).not.toContain("ATTACHMENT_PUBLIC_BASE_URL=");
150
+
151
+ const ciWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "ci.yml")).text();
152
+ expect(ciWorkflow).toContain("bun run dashboards");
153
+ expect(ciWorkflow).toContain("GCX_ENABLED");
154
+ expect(await Bun.file(join(generatedRoot, "grafana", "waitlist-dashboard.json")).exists()).toBeTrue();
155
+ expect(await Bun.file(join(generatedRoot, "grafana", "alerts.yaml")).exists()).toBeTrue();
66
156
 
67
157
  if (variant.runtime === "go") {
68
158
  const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
69
- expect(goMod).toContain("connectrpc.com/connect");
159
+ expect(goMod).toContain("module example.com/dns-api");
160
+ expect(goMod).not.toContain("module github.com/anmho/dns-api");
70
161
 
71
162
  const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
72
- expect(mainGo).toContain("NewDNSService");
163
+ expect(mainGo).toContain("example.com/dns-api");
164
+ if (variant.framework === "connectrpc") {
165
+ expect(goMod).toContain("connectrpc.com/connect");
166
+ expect(mainGo).toContain("NewWaitlistService");
167
+ expect(mainGo).toContain("WaitlistServiceName");
168
+ } else {
169
+ expect(goMod).not.toContain("connectrpc.com/connect");
170
+ expect(mainGo).toContain("NewWaitlistService");
171
+ const routes = await Bun.file(join(generatedRoot, "internal", "httpapi", "routes.go")).text();
172
+ expect(routes).toContain("/v1/waitlist");
173
+ expect(routes).toContain("/v1/admin/waitlist");
174
+ expect(routes).toContain("/v1/triggers/waitlist");
175
+ expect(await Bun.file(join(generatedRoot, "buf.yaml")).exists()).toBeFalse();
176
+ }
177
+ expect(mainGo).toContain("internal/auth");
178
+ expect(mainGo).toContain("cfg.AuthAudience");
179
+ expect(mainGo).toContain("cfg.TemporalAPIKey");
180
+ expect(await Bun.file(join(generatedRoot, "internal", "auth", "middleware.go")).exists()).toBeTrue();
181
+ const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
182
+ expect(makefile).toContain("$(ATLAS) migrate apply --env local");
183
+ expect(makefile).toContain("$(ATLAS) migrate lint --env local --latest 1");
184
+ expect(makefile).toContain("bun run ./scripts/ensure-local-db.ts");
185
+ expect(makefile).toContain("bun run ./scripts/wait-for-db.ts");
186
+ expect(makefile).toContain("bun run ./scripts/dev.ts go run ./cmd/server");
187
+ expect(await Bun.file(join(generatedRoot, "atlas.hcl")).exists()).toBeTrue();
188
+ expect(await Bun.file(join(generatedRoot, "migrations", "atlas.sum")).exists()).toBeTrue();
189
+ expect(await Bun.file(join(generatedRoot, "cmd", "migrate", "main.go")).exists()).toBeFalse();
190
+ expect(await Bun.file(join(generatedRoot, "internal", "temporal", "worker.go")).exists()).toBeTrue();
191
+ expect(goMod).toContain("go.temporal.io/sdk");
73
192
  } else {
74
193
  const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
75
- expect(packageJson).toContain('"svc-cloudrun": "./scripts/cloudrun/cli.ts"');
194
+ expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
195
+ expect(packageJson).toContain("@temporalio/worker");
196
+ expect(packageJson).toContain('"service": "./scripts/cloudrun/cli.ts"');
197
+ expect(packageJson).toContain('"dev": "bun run ./scripts/dev.ts bun run ./src/index.ts"');
198
+ expect(packageJson).toContain('"gen": "bun run ./scripts/codegen.ts"');
199
+ expect(packageJson).toContain('"create": "bun run ./scripts/cloudrun/cli.ts create"');
200
+ expect(packageJson).toContain('"deploy": "bun run ./scripts/cloudrun/cli.ts deploy"');
201
+ expect(packageJson).toContain('"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"');
202
+ expect(packageJson).toContain('"auth": "bun run ./scripts/cloudrun/cli.ts auth"');
203
+ expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
204
+ const serviceCli = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).text();
205
+ expect(serviceCli).toContain("service <create|deploy|migrate|seed|dashboards|dns|doctor|destroy|auth|sdk>");
206
+ expect(serviceCli).toContain("assertServiceNameAvailable(config.serviceName)");
207
+ expect(serviceCli).toContain("ensureAuthResourceServer");
208
+ expect(serviceCli).toContain('["resources", "push", "--path", "./grafana"]');
209
+ const cloudrunLib = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
210
+ expect(cloudrunLib).toContain("resolveTemporalRuntimeConfig");
211
+ expect(cloudrunLib).toContain("TEMPORAL_API_KEY_ENV");
212
+ expect(cloudrunLib).toContain("value === undefined");
213
+
214
+ const authctlScript = await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).text();
215
+ expect(authctlScript).toContain("authctl");
216
+ expect(authctlScript).toContain("resource-servers");
217
+ expect(authctlScript).toContain("clients");
218
+ expect(authctlScript).toContain("defaultClientTargetArgs");
219
+ expect(authctlScript).toContain('existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl")');
220
+ expect(authctlScript).not.toContain('defaultAuthResourceServerArgs(), "--yes", "--json"');
221
+ const authScript = await Bun.file(join(generatedRoot, "src", "auth.ts")).text();
222
+ expect(authScript).toContain('"Ed25519"');
76
223
 
77
224
  const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
78
- expect(makefile).toContain("npx --no-install svc-cloudrun");
225
+ expect(makefile).toContain("npx --no-install service");
226
+ expect(makefile).toContain("dashboards:");
227
+ expect(makefile).toContain("auth:");
228
+ expect(makefile).toContain("bun run dev");
229
+ const devScript = await Bun.file(join(generatedRoot, "scripts", "dev.ts")).text();
230
+ expect(devScript).toContain("ensureLocalPostgres");
231
+ const localDocker = await Bun.file(join(generatedRoot, "scripts", "local-docker.ts")).text();
232
+ expect(localDocker).toContain('["docker", "info"]');
233
+ expect(localDocker).toContain('["open", "-a", "Docker"]');
234
+ expect(localDocker).toContain('["docker", "compose", "up", "-d"]');
79
235
 
80
236
  const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
81
- expect(entrypoint).toContain(variant.framework === "hono" ? "Hono" : "rpc.example.v1.Service/Ping");
237
+ expect(await Bun.file(join(generatedRoot, "src", "auth.ts")).exists()).toBeTrue();
238
+ expect(entrypoint).toContain(variant.framework === "hono" ? 'app.use("/v1/*", authMiddleware())' : "withServiceAuth");
239
+ expect(await Bun.file(join(generatedRoot, "src", "temporal", "worker.ts")).exists()).toBeTrue();
240
+ const readme = await Bun.file(join(generatedRoot, "README.md")).text();
241
+ if (variant.framework === "connectrpc") {
242
+ expect(entrypoint).toContain("WaitlistService");
243
+ expect(gitignore).not.toContain("gen/");
244
+ expect(readme).toContain("Local introspection");
245
+ } else {
246
+ expect(entrypoint).toContain("/v1/waitlist");
247
+ expect(entrypoint).toContain("/v1/admin/waitlist");
248
+ expect(entrypoint).toContain("/v1/triggers/waitlist");
249
+ expect(gitignore).not.toContain("gen/");
250
+ expect(readme).not.toContain("Local introspection");
251
+ }
252
+ expect(entrypoint).toContain(variant.framework === "hono" ? "Hono" : "connectNodeAdapter");
253
+ expect(readme).toContain("/webhooks/:provider");
254
+ expect(readme).toContain("microservice profile");
255
+ expect(readme).toContain("waitlist/launch service");
256
+ expect(readme).toContain("resource=api://<resource_server_id>");
257
+ expect(readme).toContain("Terraform is optional");
258
+ expect(readme).toContain("AUTH_ENABLED=true");
259
+ expect(readme).toContain("verifies JWT bearer tokens");
260
+ expect(readme).toContain("prod/apps/auth/authctl/cloudflare-access");
261
+ expect(readme).toContain(variant.runtime === "bun" ? "bun run auth -- resource-server" : 'make auth ARGS="resource-server"');
82
262
  }
263
+
264
+ const deployWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "deploy.yml")).text();
265
+ expect(deployWorkflow).toContain("bun run dashboards");
83
266
  }
267
+ }, 30000);
268
+
269
+ test("scaffolds a backend package cleanly into a nested monorepo-style directory", async () => {
270
+ const root = await mkdtemp(join(tmpdir(), "create-svc-monorepo-"));
271
+ const generatedRoot = join(root, "apps", "dns-api");
272
+
273
+ await scaffoldProject(
274
+ baseConfig({
275
+ directory: generatedRoot,
276
+ runtime: "bun",
277
+ framework: "hono",
278
+ })
279
+ );
280
+
281
+ const readme = await Bun.file(join(generatedRoot, "README.md")).text();
282
+ expect(readme).toContain("`microservice` profile");
283
+ expect(readme).toContain("api.dns-api.anmho.com");
284
+ expect(readme).toContain("open Docker Desktop if needed");
285
+ expect(readme).toContain("local Postgres service in `docker-compose.yml`");
286
+ expect(readme).toContain("gcloud auth login");
287
+ expect(readme).toContain("known-good CLIs");
288
+ expect(readme).toContain("bun run create");
289
+ expect(readme).toContain("bun run deploy");
290
+ expect(readme).toContain("one-command production create");
291
+ expect(readme).toContain("waitlist/launch service");
292
+ expect(readme).toContain("Terraform is optional");
293
+ expect(readme).toContain("waitlist/launch service");
294
+ expect(readme).not.toContain("Neon main, preview, and personal branch provisioning");
295
+ const ciWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "ci.yml")).text();
296
+ expect(ciWorkflow).toContain("bun run dashboards");
297
+ expect(readme).not.toContain("repository");
298
+
299
+ const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
300
+ expect(packageJson).toContain('"hono"');
301
+
302
+ const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
303
+ expect(entrypoint).toContain("/v1/waitlist");
304
+ expect(entrypoint).toContain("/v1/admin/waitlist");
305
+ }, 15000);
306
+
307
+ test("scaffolds the workers target with wrangler lifecycle commands", async () => {
308
+ const root = await mkdtemp(join(tmpdir(), "create-svc-workers-"));
309
+ const generatedRoot = join(root, "edge-api");
310
+
311
+ await scaffoldProject(
312
+ baseConfig({
313
+ directory: generatedRoot,
314
+ target: "workers",
315
+ runtime: "bun",
316
+ framework: "hono",
317
+ })
318
+ );
319
+
320
+ const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
321
+ expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
322
+ expect(packageJson).toContain('"service": "./scripts/workers/cli.ts"');
323
+ expect(packageJson).toContain('"dev": "wrangler dev"');
324
+ expect(packageJson).toContain('"auth": "bun run ./scripts/workers/cli.ts auth"');
325
+ expect(packageJson).toContain('"wrangler"');
326
+ expect(packageJson).toContain('"pg"');
327
+
328
+ const wranglerConfig = await Bun.file(join(generatedRoot, "wrangler.toml")).text();
329
+ expect(wranglerConfig).toContain('name = "dns-api"');
330
+ expect(wranglerConfig).toContain('compatibility_flags = ["nodejs_compat"]');
331
+ expect(wranglerConfig).toContain('pattern = "api.dns-api.anmho.com/*"');
332
+ expect(wranglerConfig).toContain('binding = "HYPERDRIVE"');
333
+ expect(wranglerConfig).toContain('AUTH_ENABLED = "true"');
334
+ expect(wranglerConfig).toContain('AUTH_AUDIENCE = "api://dns-api"');
335
+
336
+ const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
337
+ expect(entrypoint).toContain("/v1/waitlist");
338
+ expect(entrypoint).toContain("/v1/admin/waitlist");
339
+ expect(entrypoint).toContain('app.use("/v1/*", authMiddleware())');
340
+ expect(entrypoint).toContain("createStorage(context.env)");
341
+ expect(entrypoint).toContain("scheduled");
342
+ const readme = await Bun.file(join(generatedRoot, "README.md")).text();
343
+ expect(readme).toContain("Cloudflare Workers");
344
+ expect(readme).toContain("Hyperdrive binding for Neon-backed Postgres persistence");
345
+ expect(readme).not.toContain("Cloud Run");
346
+ const workerCli = await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).text();
347
+ expect(workerCli).toContain("hyperdrive");
348
+ expect(workerCli).toContain('["resources", "push", "--path", "./grafana"]');
349
+ expect(workerCli).toContain("ensureAuthResourceServer");
350
+ expect(workerCli).toContain("Workers database schema applied");
351
+ expect(workerCli).toContain("create table if not exists waitlist_entries");
352
+ expect(workerCli).toContain("DATABASE_URL or NEON_API_KEY is required to provision the Hyperdrive binding");
353
+ expect(workerCli).toContain("createProjectBranchDatabase");
354
+ expect(workerCli).toContain("deleteNeonDatabase");
355
+ expect(workerCli).toContain("deleteGrafanaResources");
356
+ expect(workerCli).toContain("hyperdrive\", \"delete");
357
+ const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
358
+ expect(makefile).toContain('no generated code for workers');
359
+ expect(makefile).toContain("auth:");
360
+ expect(makefile).not.toContain("scripts/codegen.ts");
361
+
362
+ expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeTrue();
363
+ expect(await Bun.file(join(generatedRoot, "src", "auth.ts")).exists()).toBeTrue();
364
+ expect(await Bun.file(join(generatedRoot, "src", "storage.ts")).exists()).toBeTrue();
365
+ expect(await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).exists()).toBeTrue();
366
+ expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
367
+ expect(await Bun.file(join(generatedRoot, "scripts", "dev.ts")).exists()).toBeFalse();
368
+ expect(await Bun.file(join(generatedRoot, "scripts", "ensure-local-db.ts")).exists()).toBeFalse();
369
+ expect(await Bun.file(join(generatedRoot, "scripts", "local-docker.ts")).exists()).toBeFalse();
370
+ expect(await Bun.file(join(generatedRoot, "scripts", "wait-for-db.ts")).exists()).toBeFalse();
371
+ expect(await Bun.file(join(generatedRoot, "service.yaml")).exists()).toBeFalse();
372
+ expect(await Bun.file(join(generatedRoot, "Dockerfile")).exists()).toBeFalse();
373
+ expect(await Bun.file(join(generatedRoot, "docker-compose.yml")).exists()).toBeFalse();
374
+ expect(await Bun.file(join(generatedRoot, "src", "db", "repository.ts")).exists()).toBeFalse();
375
+ expect(await Bun.file(join(generatedRoot, "src", "temporal", "worker.ts")).exists()).toBeFalse();
376
+ expect(await Bun.file(join(generatedRoot, "scripts", "codegen.ts")).exists()).toBeFalse();
377
+ });
378
+
379
+ test("microservice profile does not generate a website package", async () => {
380
+ const root = await mkdtemp(join(tmpdir(), "create-svc-microservice-profile-"));
381
+ const generatedRoot = join(root, "service");
382
+
383
+ await scaffoldProject(baseConfig({ directory: generatedRoot, profile: "microservice" }));
384
+
385
+ expect(await Bun.file(join(generatedRoot, "website", "package.json")).exists()).toBeFalse();
84
386
  });
85
387
 
86
388
  test("detects conflicting files before scaffold generation", async () => {