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