create-svc 0.1.1 → 0.1.3
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.
- package/README.md +1 -1
- package/package.json +4 -1
- package/src/cli.test.ts +10 -0
- package/src/cli.ts +331 -107
- package/src/gcp.test.ts +71 -0
- package/src/gcp.ts +97 -0
- package/src/naming.test.ts +37 -0
- package/src/naming.ts +103 -0
- package/src/neon.test.ts +48 -0
- package/src/neon.ts +76 -0
- package/src/post-scaffold.ts +77 -0
- package/src/scaffold.test.ts +66 -31
- package/src/scaffold.ts +60 -55
- package/templates/root/.github/workflows/deploy.yml +1 -1
- package/templates/root/README.md +3 -3
- package/templates/shared/.github/workflows/ci.yml +22 -0
- package/templates/shared/.github/workflows/deploy.yml +30 -0
- package/templates/shared/.github/workflows/personal.yml +41 -0
- package/templates/shared/.github/workflows/preview-cleanup.yml +25 -0
- package/templates/shared/.github/workflows/preview.yml +29 -0
- package/templates/shared/README.md +37 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +76 -0
- package/templates/shared/scripts/cloudrun/config.ts +57 -0
- package/templates/shared/scripts/cloudrun/deploy.ts +82 -0
- package/templates/shared/scripts/cloudrun/lib.ts +380 -0
- package/templates/shared/scripts/cloudrun/neon.ts +104 -0
- package/templates/shared/service.yaml +28 -0
- package/templates/variants/bun-connectrpc/Dockerfile +13 -0
- package/templates/variants/bun-connectrpc/package.json +20 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -0
- package/templates/variants/bun-connectrpc/src/index.ts +32 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +17 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +10 -0
- package/templates/variants/bun-hono/Dockerfile +13 -0
- package/templates/variants/bun-hono/package.json +21 -0
- package/templates/variants/bun-hono/scripts/codegen.ts +1 -0
- package/templates/variants/bun-hono/src/index.ts +24 -0
- package/templates/variants/bun-hono/test/app.test.ts +12 -0
- package/templates/variants/bun-hono/tsconfig.json +10 -0
- package/templates/variants/go-chi/Dockerfile +23 -0
- package/templates/variants/go-chi/buf.gen.yaml +10 -0
- package/templates/variants/go-chi/buf.yaml +9 -0
- package/templates/variants/go-chi/cmd/server/main.go +52 -0
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +623 -0
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
- package/templates/variants/go-chi/go.mod +10 -0
- package/templates/variants/go-chi/internal/app/service.go +109 -0
- package/templates/variants/go-chi/internal/app/token_source.go +50 -0
- package/templates/variants/go-chi/internal/cloudflare/client.go +160 -0
- package/templates/variants/go-chi/internal/config/config.go +23 -0
- package/templates/variants/go-chi/internal/connectapi/handler.go +79 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +93 -0
- package/templates/variants/go-chi/internal/vault/client.go +148 -0
- package/templates/variants/go-chi/package.json +16 -0
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +58 -0
- package/templates/variants/go-chi/test/go.test.ts +19 -0
- package/templates/variants/go-connectrpc/Dockerfile +23 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +10 -0
- package/templates/variants/go-connectrpc/buf.yaml +9 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +51 -0
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +623 -0
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
- package/templates/variants/go-connectrpc/go.mod +10 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +109 -0
- package/templates/variants/go-connectrpc/internal/app/token_source.go +50 -0
- package/templates/variants/go-connectrpc/internal/cloudflare/client.go +160 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +23 -0
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +79 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +93 -0
- package/templates/variants/go-connectrpc/internal/vault/client.go +148 -0
- package/templates/variants/go-connectrpc/package.json +16 -0
- package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +58 -0
- package/templates/variants/go-connectrpc/test/go.test.ts +19 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-svc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Bun-authored CLI to scaffold Go Cloud Run services with Chi, ConnectRPC, Vault, and Cloudflare examples.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@clack/prompts": "^1.2.0",
|
|
47
|
+
"@google-cloud/billing": "^5.1.1",
|
|
48
|
+
"@google-cloud/resource-manager": "^6.2.1",
|
|
49
|
+
"@neondatabase/api-client": "^2.7.1",
|
|
47
50
|
"picocolors": "^1.1.1"
|
|
48
51
|
}
|
|
49
52
|
}
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { normalizeValidationResult } from "./cli";
|
|
3
|
+
|
|
4
|
+
test("normalizeValidationResult converts success to undefined", () => {
|
|
5
|
+
expect(normalizeValidationResult(true)).toBeUndefined();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test("normalizeValidationResult preserves validation errors", () => {
|
|
9
|
+
expect(normalizeValidationResult("Service name is required")).toBe("Service name is required");
|
|
10
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -1,29 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
cancel,
|
|
3
|
+
confirm,
|
|
4
|
+
intro,
|
|
5
|
+
isCancel,
|
|
6
|
+
log,
|
|
7
|
+
note,
|
|
8
|
+
outro,
|
|
9
|
+
select,
|
|
10
|
+
spinner,
|
|
11
|
+
text,
|
|
12
|
+
} from "@clack/prompts";
|
|
2
13
|
import pc from "picocolors";
|
|
3
14
|
import { basename, dirname, resolve } from "node:path";
|
|
4
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { runPostScaffoldFlow } from "./post-scaffold";
|
|
17
|
+
import { listOpenBillingAccounts, listAccessibleProjects, type BillingAccount, type GcpProject } from "./gcp";
|
|
18
|
+
import { discoverNeonDefaults } from "./neon";
|
|
19
|
+
import {
|
|
20
|
+
BILLING_ACCOUNT_DEFAULT,
|
|
21
|
+
FRAMEWORKS_BY_RUNTIME,
|
|
22
|
+
QUOTA_PROJECT_DEFAULT,
|
|
23
|
+
buildCreateProjectLabel,
|
|
24
|
+
buildGcpProjectOptions,
|
|
25
|
+
deriveDefaults,
|
|
26
|
+
slugify,
|
|
27
|
+
type Framework,
|
|
28
|
+
type GcpProjectMode,
|
|
29
|
+
type Runtime,
|
|
30
|
+
} from "./naming";
|
|
5
31
|
import { scaffoldProject, type ScaffoldConfig } from "./scaffold";
|
|
6
32
|
|
|
7
33
|
type ParsedArgs = {
|
|
8
34
|
directory?: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
35
|
+
runtime?: Runtime;
|
|
36
|
+
framework?: Framework;
|
|
37
|
+
gcpProjectMode?: GcpProjectMode;
|
|
38
|
+
gcpProject?: string;
|
|
12
39
|
githubRepo?: string;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
bufModule?: string;
|
|
40
|
+
region?: string;
|
|
41
|
+
billingAccount?: string;
|
|
42
|
+
quotaProjectId?: string;
|
|
43
|
+
autoDeploy?: boolean;
|
|
18
44
|
yes: boolean;
|
|
19
45
|
help: boolean;
|
|
20
46
|
};
|
|
21
47
|
|
|
48
|
+
type DiscoveryState = {
|
|
49
|
+
projects: GcpProject[];
|
|
50
|
+
billingAccounts: BillingAccount[];
|
|
51
|
+
neonProjectId?: string;
|
|
52
|
+
neonBaseBranchId?: string;
|
|
53
|
+
neonBaseBranchName?: string;
|
|
54
|
+
warnings: string[];
|
|
55
|
+
};
|
|
56
|
+
|
|
22
57
|
const DEFAULT_REGION = "us-west1";
|
|
23
|
-
const DEFAULT_VAULT_ADDR = "https://vault.anmho.com";
|
|
24
|
-
const DEFAULT_VAULT_SECRET_PATH = "provider/cloudflare-api-token";
|
|
25
|
-
const DEFAULT_VAULT_SECRET_KEY = "value";
|
|
26
|
-
const DEFAULT_CLOUDFLARE_ZONE_ID = "893c2371cc222826de6e00583f4902ea";
|
|
27
58
|
|
|
28
59
|
export async function run(argv: string[]) {
|
|
29
60
|
const args = parseArgs(argv);
|
|
@@ -32,7 +63,7 @@ export async function run(argv: string[]) {
|
|
|
32
63
|
return;
|
|
33
64
|
}
|
|
34
65
|
|
|
35
|
-
intro(`${pc.bold("create-
|
|
66
|
+
intro(`${pc.bold("create-svc")} ${pc.dim("Cloud Run scaffold")}`);
|
|
36
67
|
|
|
37
68
|
const config = await resolveConfig(args);
|
|
38
69
|
const targetDir = resolve(process.cwd(), config.directory);
|
|
@@ -40,8 +71,10 @@ export async function run(argv: string[]) {
|
|
|
40
71
|
note(
|
|
41
72
|
[
|
|
42
73
|
`${pc.bold("Output")}: ${targetDir}`,
|
|
43
|
-
`${pc.bold("
|
|
44
|
-
`${pc.bold("
|
|
74
|
+
`${pc.bold("Runtime")}: ${config.runtime} + ${config.framework}`,
|
|
75
|
+
`${pc.bold("Project")}: ${config.gcpProjectMode === "create_new" ? "create" : "use"} ${config.gcpProjectName} (${config.gcpProject})`,
|
|
76
|
+
`${pc.bold("GitHub")}: ${config.githubRepo}`,
|
|
77
|
+
`${pc.bold("Neon")}: ${config.neonProjectId || "(set later)"} / ${config.neonBaseBranchName || "(set later)"}`,
|
|
45
78
|
].join("\n"),
|
|
46
79
|
"Scaffold"
|
|
47
80
|
);
|
|
@@ -51,12 +84,26 @@ export async function run(argv: string[]) {
|
|
|
51
84
|
await scaffoldProject(config);
|
|
52
85
|
buildSpinner.stop("Project files generated");
|
|
53
86
|
|
|
87
|
+
const shouldRunPostScaffoldFlow = Boolean(process.stdout.isTTY && process.stdin.isTTY && (config.createGithubRepo || config.autoDeploy));
|
|
88
|
+
if (shouldRunPostScaffoldFlow) {
|
|
89
|
+
const automationSpinner = spinner();
|
|
90
|
+
automationSpinner.start("Running post-scaffold automation");
|
|
91
|
+
try {
|
|
92
|
+
const result = await runPostScaffoldFlow(config, targetDir);
|
|
93
|
+
automationSpinner.stop(result.message);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
automationSpinner.stop("Post-scaffold automation skipped");
|
|
96
|
+
log.warn(error instanceof Error ? error.message : String(error));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
54
100
|
outro(
|
|
55
101
|
[
|
|
56
102
|
`Next: ${pc.cyan(`cd ${config.directory}`)}`,
|
|
57
103
|
`Local dev: ${pc.cyan("bun dev")}`,
|
|
58
|
-
`
|
|
59
|
-
`
|
|
104
|
+
`Bootstrap: ${pc.cyan("bun run bootstrap")}`,
|
|
105
|
+
`Deploy: ${pc.cyan("bun run deploy")}`,
|
|
106
|
+
`Personal env: ${pc.cyan(`bun run deploy -- --environment personal --name ${config.serviceName}`)}`,
|
|
60
107
|
].join("\n")
|
|
61
108
|
);
|
|
62
109
|
}
|
|
@@ -97,93 +144,98 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
97
144
|
continue;
|
|
98
145
|
}
|
|
99
146
|
|
|
100
|
-
if (token === "--
|
|
101
|
-
parsed.
|
|
147
|
+
if (token === "--runtime") {
|
|
148
|
+
parsed.runtime = readValue() as Runtime;
|
|
102
149
|
continue;
|
|
103
150
|
}
|
|
104
151
|
|
|
105
|
-
if (token.startsWith("--
|
|
106
|
-
parsed.
|
|
152
|
+
if (token.startsWith("--runtime=")) {
|
|
153
|
+
parsed.runtime = token.slice("--runtime=".length) as Runtime;
|
|
107
154
|
continue;
|
|
108
155
|
}
|
|
109
156
|
|
|
110
|
-
if (token === "--
|
|
111
|
-
parsed.
|
|
157
|
+
if (token === "--framework") {
|
|
158
|
+
parsed.framework = readValue() as Framework;
|
|
112
159
|
continue;
|
|
113
160
|
}
|
|
114
161
|
|
|
115
|
-
if (token.startsWith("--
|
|
116
|
-
parsed.
|
|
162
|
+
if (token.startsWith("--framework=")) {
|
|
163
|
+
parsed.framework = token.slice("--framework=".length) as Framework;
|
|
117
164
|
continue;
|
|
118
165
|
}
|
|
119
166
|
|
|
120
|
-
if (token === "--
|
|
121
|
-
parsed.
|
|
167
|
+
if (token === "--project-mode") {
|
|
168
|
+
parsed.gcpProjectMode = readValue() as GcpProjectMode;
|
|
122
169
|
continue;
|
|
123
170
|
}
|
|
124
171
|
|
|
125
|
-
if (token.startsWith("--
|
|
126
|
-
parsed.
|
|
172
|
+
if (token.startsWith("--project-mode=")) {
|
|
173
|
+
parsed.gcpProjectMode = token.slice("--project-mode=".length) as GcpProjectMode;
|
|
127
174
|
continue;
|
|
128
175
|
}
|
|
129
176
|
|
|
130
|
-
if (token === "--
|
|
131
|
-
parsed.
|
|
177
|
+
if (token === "--project-id" || token === "--gcp-project") {
|
|
178
|
+
parsed.gcpProject = readValue();
|
|
132
179
|
continue;
|
|
133
180
|
}
|
|
134
181
|
|
|
135
|
-
if (token.startsWith("--
|
|
136
|
-
parsed.
|
|
182
|
+
if (token.startsWith("--project-id=")) {
|
|
183
|
+
parsed.gcpProject = token.slice("--project-id=".length);
|
|
137
184
|
continue;
|
|
138
185
|
}
|
|
139
186
|
|
|
140
|
-
if (token
|
|
141
|
-
parsed.
|
|
187
|
+
if (token.startsWith("--gcp-project=")) {
|
|
188
|
+
parsed.gcpProject = token.slice("--gcp-project=".length);
|
|
142
189
|
continue;
|
|
143
190
|
}
|
|
144
191
|
|
|
145
|
-
if (token
|
|
146
|
-
parsed.
|
|
192
|
+
if (token === "--github-repo") {
|
|
193
|
+
parsed.githubRepo = readValue();
|
|
147
194
|
continue;
|
|
148
195
|
}
|
|
149
196
|
|
|
150
|
-
if (token
|
|
151
|
-
parsed.
|
|
197
|
+
if (token.startsWith("--github-repo=")) {
|
|
198
|
+
parsed.githubRepo = token.slice("--github-repo=".length);
|
|
152
199
|
continue;
|
|
153
200
|
}
|
|
154
201
|
|
|
155
|
-
if (token
|
|
156
|
-
parsed.
|
|
202
|
+
if (token === "--region") {
|
|
203
|
+
parsed.region = readValue();
|
|
157
204
|
continue;
|
|
158
205
|
}
|
|
159
206
|
|
|
160
|
-
if (token
|
|
161
|
-
parsed.
|
|
207
|
+
if (token.startsWith("--region=")) {
|
|
208
|
+
parsed.region = token.slice("--region=".length);
|
|
162
209
|
continue;
|
|
163
210
|
}
|
|
164
211
|
|
|
165
|
-
if (token
|
|
166
|
-
parsed.
|
|
212
|
+
if (token === "--billing-account") {
|
|
213
|
+
parsed.billingAccount = readValue();
|
|
167
214
|
continue;
|
|
168
215
|
}
|
|
169
216
|
|
|
170
|
-
if (token
|
|
171
|
-
parsed.
|
|
217
|
+
if (token.startsWith("--billing-account=")) {
|
|
218
|
+
parsed.billingAccount = token.slice("--billing-account=".length);
|
|
172
219
|
continue;
|
|
173
220
|
}
|
|
174
221
|
|
|
175
|
-
if (token
|
|
176
|
-
parsed.
|
|
222
|
+
if (token === "--quota-project") {
|
|
223
|
+
parsed.quotaProjectId = readValue();
|
|
177
224
|
continue;
|
|
178
225
|
}
|
|
179
226
|
|
|
180
|
-
if (token
|
|
181
|
-
parsed.
|
|
227
|
+
if (token.startsWith("--quota-project=")) {
|
|
228
|
+
parsed.quotaProjectId = token.slice("--quota-project=".length);
|
|
182
229
|
continue;
|
|
183
230
|
}
|
|
184
231
|
|
|
185
|
-
if (token
|
|
186
|
-
parsed.
|
|
232
|
+
if (token === "--auto-deploy") {
|
|
233
|
+
parsed.autoDeploy = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (token === "--no-auto-deploy") {
|
|
238
|
+
parsed.autoDeploy = false;
|
|
187
239
|
continue;
|
|
188
240
|
}
|
|
189
241
|
|
|
@@ -193,36 +245,22 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
193
245
|
return parsed;
|
|
194
246
|
}
|
|
195
247
|
|
|
196
|
-
async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
|
|
197
|
-
const inferredName = slugify(basename(args.directory ?? "
|
|
248
|
+
export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
|
|
249
|
+
const inferredName = slugify(basename(args.directory ?? "my-service"));
|
|
198
250
|
const serviceName = args.yes
|
|
199
251
|
? inferredName
|
|
200
252
|
: await promptText("Service name", inferredName, (value) => slugify(value).length > 0 || "Service name is required");
|
|
201
253
|
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
254
|
+
const defaults = deriveDefaults(serviceName);
|
|
255
|
+
const discovery = await discoverCloudInputs(serviceName);
|
|
256
|
+
const runtime = await resolveRuntime(args);
|
|
257
|
+
const framework = await resolveFramework(args, runtime);
|
|
258
|
+
const gcpSelection = await resolveGcpSelection(args, defaults, discovery);
|
|
259
|
+
const githubRepo = args.githubRepo ?? defaults.githubRepo;
|
|
206
260
|
const region = args.region ?? DEFAULT_REGION;
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const cloudflareZoneId = args.cloudflareZoneId ?? DEFAULT_CLOUDFLARE_ZONE_ID;
|
|
211
|
-
const bufModule = args.bufModule ?? `buf.build/${githubRepo}`;
|
|
212
|
-
|
|
213
|
-
const confirmedProjectId = projectId || (await promptText("GCP project ID", "my-gcp-project", (value) => value.trim().length > 0 || "Project ID is required"));
|
|
214
|
-
const confirmedModulePath = args.yes
|
|
215
|
-
? modulePath
|
|
216
|
-
: await promptText("Go module path", modulePath, (value) => value.trim().length > 0 || "Module path is required");
|
|
217
|
-
const confirmedGithubRepo = args.yes
|
|
218
|
-
? githubRepo
|
|
219
|
-
: await promptText("GitHub repo", githubRepo, (value) => value.includes("/") || "Use owner/repo format");
|
|
220
|
-
const confirmedRegion = args.yes
|
|
221
|
-
? region
|
|
222
|
-
: await promptText("Cloud Run region", region, (value) => value.trim().length > 0 || "Region is required");
|
|
223
|
-
const confirmedBufModule = args.yes
|
|
224
|
-
? bufModule
|
|
225
|
-
: await promptText("Buf module", bufModule, (value) => value.trim().length > 0 || "Buf module is required");
|
|
261
|
+
const billingAccount = chooseBillingAccount(args.billingAccount, discovery.billingAccounts);
|
|
262
|
+
const autoDeploy = resolveAutoDeploy(args.autoDeploy);
|
|
263
|
+
const directory = args.directory ?? serviceName;
|
|
226
264
|
|
|
227
265
|
if (!args.yes) {
|
|
228
266
|
const okay = await confirm({
|
|
@@ -235,22 +273,208 @@ async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
|
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
|
|
276
|
+
for (const warning of discovery.warnings) {
|
|
277
|
+
log.warn(warning);
|
|
278
|
+
}
|
|
279
|
+
|
|
238
280
|
return {
|
|
239
281
|
directory,
|
|
240
282
|
serviceName,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
region
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
283
|
+
runtime,
|
|
284
|
+
framework,
|
|
285
|
+
region,
|
|
286
|
+
gcpProjectMode: gcpSelection.mode,
|
|
287
|
+
gcpProject: gcpSelection.projectId,
|
|
288
|
+
gcpProjectName: gcpSelection.projectName,
|
|
289
|
+
billingAccount,
|
|
290
|
+
quotaProjectId: args.quotaProjectId ?? QUOTA_PROJECT_DEFAULT,
|
|
291
|
+
githubRepo,
|
|
292
|
+
githubVisibility: "public",
|
|
293
|
+
createGithubRepo: true,
|
|
294
|
+
autoDeploy,
|
|
295
|
+
neonProjectId: discovery.neonProjectId ?? "",
|
|
296
|
+
neonBaseBranchId: discovery.neonBaseBranchId ?? "",
|
|
297
|
+
neonBaseBranchName: discovery.neonBaseBranchName ?? "main",
|
|
298
|
+
neonDatabaseName: defaults.neonDatabaseName,
|
|
250
299
|
generatorRoot: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
|
251
300
|
};
|
|
252
301
|
}
|
|
253
302
|
|
|
303
|
+
async function resolveRuntime(args: ParsedArgs): Promise<Runtime> {
|
|
304
|
+
if (args.runtime) {
|
|
305
|
+
return args.runtime;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (args.yes) {
|
|
309
|
+
return "go";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const value = await select({
|
|
313
|
+
message: "Runtime",
|
|
314
|
+
initialValue: "go",
|
|
315
|
+
options: [
|
|
316
|
+
{ value: "go", label: "Go", hint: "Default" },
|
|
317
|
+
{ value: "bun", label: "Bun" },
|
|
318
|
+
],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (isCancel(value)) {
|
|
322
|
+
cancel("Aborted");
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function resolveFramework(args: ParsedArgs, runtime: Runtime): Promise<Framework> {
|
|
330
|
+
const allowed = FRAMEWORKS_BY_RUNTIME[runtime];
|
|
331
|
+
if (args.framework) {
|
|
332
|
+
if (allowed.includes(args.framework)) {
|
|
333
|
+
return args.framework;
|
|
334
|
+
}
|
|
335
|
+
throw new Error(`Framework ${args.framework} is not valid for runtime ${runtime}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (args.yes) {
|
|
339
|
+
return allowed[0];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const value = await select({
|
|
343
|
+
message: "Framework",
|
|
344
|
+
initialValue: allowed[0],
|
|
345
|
+
options: allowed.map((framework, index) => ({
|
|
346
|
+
value: framework,
|
|
347
|
+
label: framework,
|
|
348
|
+
hint: index === 0 ? "Default" : undefined,
|
|
349
|
+
})),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (isCancel(value)) {
|
|
353
|
+
cancel("Aborted");
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function resolveGcpSelection(
|
|
361
|
+
args: ParsedArgs,
|
|
362
|
+
defaults: ReturnType<typeof deriveDefaults>,
|
|
363
|
+
discovery: DiscoveryState
|
|
364
|
+
) {
|
|
365
|
+
const options = buildGcpProjectOptions(defaults.serviceName, defaults.projectId, defaults.projectName, discovery.projects);
|
|
366
|
+
|
|
367
|
+
if (args.gcpProjectMode && args.gcpProject) {
|
|
368
|
+
return {
|
|
369
|
+
mode: args.gcpProjectMode,
|
|
370
|
+
projectId: args.gcpProject,
|
|
371
|
+
projectName: args.gcpProjectMode === "create_new" ? defaults.projectName : args.gcpProject,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (args.gcpProjectMode === "create_new") {
|
|
376
|
+
return {
|
|
377
|
+
mode: "create_new" as const,
|
|
378
|
+
projectId: args.gcpProject ?? defaults.projectId,
|
|
379
|
+
projectName: defaults.projectName,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (args.gcpProjectMode === "use_existing") {
|
|
384
|
+
const existing = discovery.projects.find((project) => project.projectId === args.gcpProject);
|
|
385
|
+
return {
|
|
386
|
+
mode: "use_existing" as const,
|
|
387
|
+
projectId: args.gcpProject ?? discovery.projects[0]?.projectId ?? defaults.projectId,
|
|
388
|
+
projectName: existing?.name ?? args.gcpProject ?? defaults.projectName,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (args.yes) {
|
|
393
|
+
return {
|
|
394
|
+
mode: "create_new" as const,
|
|
395
|
+
projectId: defaults.projectId,
|
|
396
|
+
projectName: defaults.projectName,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const value = await select({
|
|
401
|
+
message: "GCP project",
|
|
402
|
+
initialValue: buildCreateProjectLabel(defaults.serviceName, defaults.projectId),
|
|
403
|
+
options: options.map((option) => ({
|
|
404
|
+
value: option.label,
|
|
405
|
+
label: option.label,
|
|
406
|
+
hint: option.mode === "create_new" ? "Default" : undefined,
|
|
407
|
+
})),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (isCancel(value)) {
|
|
411
|
+
cancel("Aborted");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const selected = options.find((option) => option.label === value);
|
|
416
|
+
if (!selected) {
|
|
417
|
+
throw new Error(`Unknown GCP project selection: ${value}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
mode: selected.mode,
|
|
422
|
+
projectId: selected.projectId,
|
|
423
|
+
projectName: selected.projectName,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function discoverCloudInputs(serviceName: string): Promise<DiscoveryState> {
|
|
428
|
+
const result: DiscoveryState = {
|
|
429
|
+
projects: [],
|
|
430
|
+
billingAccounts: [],
|
|
431
|
+
warnings: [],
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
result.projects = await listAccessibleProjects();
|
|
436
|
+
} catch (error) {
|
|
437
|
+
result.warnings.push(`Skipping GCP project discovery: ${formatError(error)}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
result.billingAccounts = await listOpenBillingAccounts();
|
|
442
|
+
} catch (error) {
|
|
443
|
+
result.warnings.push(`Skipping billing account discovery: ${formatError(error)}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const neonDefaults = await discoverNeonDefaults(serviceName);
|
|
448
|
+
result.neonProjectId = neonDefaults.projectId;
|
|
449
|
+
result.neonBaseBranchId = neonDefaults.baseBranchId;
|
|
450
|
+
result.neonBaseBranchName = neonDefaults.baseBranchName;
|
|
451
|
+
} catch (error) {
|
|
452
|
+
result.warnings.push(`Skipping Neon discovery: ${formatError(error)}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function chooseBillingAccount(input: string | undefined, accounts: BillingAccount[]) {
|
|
459
|
+
if (input) {
|
|
460
|
+
return input;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const preferred = accounts.find((account) => account.name === BILLING_ACCOUNT_DEFAULT);
|
|
464
|
+
if (preferred) {
|
|
465
|
+
return preferred.name;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return accounts[0]?.name ?? BILLING_ACCOUNT_DEFAULT;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function resolveAutoDeploy(value: boolean | undefined) {
|
|
472
|
+
if (value !== undefined) {
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
476
|
+
}
|
|
477
|
+
|
|
254
478
|
async function promptText(
|
|
255
479
|
message: string,
|
|
256
480
|
initialValue: string,
|
|
@@ -259,7 +483,7 @@ async function promptText(
|
|
|
259
483
|
const value = await text({
|
|
260
484
|
message,
|
|
261
485
|
initialValue,
|
|
262
|
-
validate: (input) => validate(input.trim()),
|
|
486
|
+
validate: (input) => normalizeValidationResult(validate(input.trim())),
|
|
263
487
|
});
|
|
264
488
|
|
|
265
489
|
if (isCancel(value)) {
|
|
@@ -270,13 +494,12 @@ async function promptText(
|
|
|
270
494
|
return value.trim();
|
|
271
495
|
}
|
|
272
496
|
|
|
273
|
-
function
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
.slice(0, 63);
|
|
497
|
+
function formatError(error: unknown) {
|
|
498
|
+
return error instanceof Error ? error.message : String(error);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function normalizeValidationResult(result: true | string): string | undefined {
|
|
502
|
+
return result === true ? undefined : result;
|
|
280
503
|
}
|
|
281
504
|
|
|
282
505
|
function printHelp() {
|
|
@@ -285,16 +508,17 @@ Usage:
|
|
|
285
508
|
bun run index.ts [directory] [options]
|
|
286
509
|
|
|
287
510
|
Options:
|
|
288
|
-
--
|
|
289
|
-
--
|
|
290
|
-
--
|
|
291
|
-
--
|
|
292
|
-
--
|
|
293
|
-
--
|
|
294
|
-
--
|
|
295
|
-
--
|
|
296
|
-
--
|
|
297
|
-
--
|
|
298
|
-
--
|
|
511
|
+
--runtime <go|bun> Runtime scaffold to generate
|
|
512
|
+
--framework <name> Framework for the selected runtime
|
|
513
|
+
--project-mode <mode> create_new or use_existing
|
|
514
|
+
--project-id <id> GCP project id
|
|
515
|
+
--github-repo <owner/repo> GitHub repository
|
|
516
|
+
--billing-account <name> Billing account resource name
|
|
517
|
+
--quota-project <id> Billing quota project for gcloud calls
|
|
518
|
+
--region <region> Cloud Run region
|
|
519
|
+
--auto-deploy Run bootstrap and first deploy after scaffold
|
|
520
|
+
--no-auto-deploy Scaffold only
|
|
521
|
+
--yes, -y Accept defaults without prompts
|
|
522
|
+
--help, -h Show this message
|
|
299
523
|
`);
|
|
300
524
|
}
|