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