create-svc 0.1.8 → 0.1.9

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 CHANGED
@@ -1,12 +1,14 @@
1
1
  # create-svc
2
2
 
3
- `create-svc` is a Bun-authored scaffold CLI for generating a Go Cloud Run service with:
3
+ `create-svc` is a Bun-authored scaffold CLI for generating Cloud Run services with:
4
4
 
5
- - Chi HTTP routes
6
- - ConnectRPC handlers
7
- - a real Cloud Run service manifest
8
- - Bun-based deployment helpers
9
- - Vault-backed Cloudflare DNS CRUD as the default example
5
+ - `go + chi`
6
+ - `go + connectrpc`
7
+ - `bun + hono`
8
+ - `bun + connectrpc`
9
+ - a real `service.yaml` manifest
10
+ - shared Cloud Run bootstrap, deploy, and cleanup automation
11
+ - Neon-backed main, preview, and personal environments
10
12
 
11
13
  ## Usage
12
14
 
@@ -15,14 +17,22 @@ bun install
15
17
  bun run index.ts my-service
16
18
  ```
17
19
 
18
- The generated service supports:
20
+ The generator discovers:
21
+
22
+ - accessible GCP projects
23
+ - open billing accounts
24
+ - Neon defaults from `NEON_API_KEY`, or Vault via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
25
+
26
+ Generated repos are `Makefile`-first. The shared Cloud Run control plane is exposed as a local CLI bin and invoked by `make`.
19
27
 
20
28
  ```bash
21
- bun dev
22
- bun gen
23
- bun lint
24
- bun test
25
- bun run deploy
29
+ make dev
30
+ make gen
31
+ make lint
32
+ make test
33
+ make bootstrap
34
+ make deploy
35
+ make cleanup
26
36
  ```
27
37
 
28
38
  ## Development
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",
package/src/cli.test.ts CHANGED
@@ -19,7 +19,7 @@ test("assertDiscoveryReady requires Neon discovery to succeed", () => {
19
19
  neonError: "Vault secret resolution requires VAULT_ADDR, VAULT_TOKEN, and a secret path",
20
20
  })
21
21
  ).toThrow(
22
- "Neon discovery is required before scaffolding. Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and VAULT_TOKEN. Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD."
22
+ "Neon discovery is required before scaffolding. Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and either VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token. Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD."
23
23
  );
24
24
  });
25
25
 
package/src/cli.ts CHANGED
@@ -550,7 +550,7 @@ function formatNeonDiscoveryRequirement(reason: string) {
550
550
  if (reason.includes("Vault secret resolution requires")) {
551
551
  return [
552
552
  "Neon discovery is required before scaffolding.",
553
- "Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and VAULT_TOKEN.",
553
+ "Set NEON_API_KEY, or use Vault by providing VAULT_ADDR and either VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token.",
554
554
  "Optional overrides: VAULT_SECRET_MOUNT, VAULT_NEON_API_KEY_PATH, VAULT_NEON_API_KEY_FIELD.",
555
555
  ].join(" ");
556
556
  }
package/src/vault.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { afterEach, expect, mock, test } from "bun:test";
2
+ import { mkdir } from "node:fs/promises";
2
3
  import { readVaultSecret, resolveNeonApiKey } from "./vault";
3
4
 
4
5
  const originalEnv = { ...process.env };
@@ -40,3 +41,35 @@ test("readVaultSecret reads KV v2 secret data using existing vault login env", a
40
41
  })
41
42
  ).resolves.toBe("vault-token");
42
43
  });
44
+
45
+ test("readVaultSecret falls back to ~/.vault-token", async () => {
46
+ const home = "/tmp/create-svc-vault-home";
47
+ process.env.HOME = home;
48
+ process.env.VAULT_ADDR = "https://vault.example.com";
49
+ delete process.env.VAULT_TOKEN;
50
+
51
+ await mkdir(home, { recursive: true });
52
+ await Bun.write(`${home}/.vault-token`, "token-from-file\n");
53
+
54
+ const fetchMock = mock(async () => {
55
+ return new Response(
56
+ JSON.stringify({
57
+ data: {
58
+ data: {
59
+ value: "vault-token",
60
+ },
61
+ },
62
+ }),
63
+ { status: 200 }
64
+ );
65
+ });
66
+
67
+ globalThis.fetch = fetchMock as typeof fetch;
68
+
69
+ await expect(
70
+ readVaultSecret({
71
+ path: "provider/neon-api-key",
72
+ field: "value",
73
+ })
74
+ ).resolves.toBe("vault-token");
75
+ });
package/src/vault.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+
1
4
  const DEFAULT_VAULT_SECRET_MOUNT = "secret";
2
5
  const DEFAULT_NEON_API_KEY_PATH = "provider/neon-api-key";
3
6
  const DEFAULT_NEON_API_KEY_FIELD = "value";
@@ -24,13 +27,13 @@ export async function resolveNeonApiKey() {
24
27
 
25
28
  export async function readVaultSecret(options: VaultSecretOptions = {}) {
26
29
  const addr = options.addr ?? process.env.VAULT_ADDR?.trim() ?? "";
27
- const token = options.token ?? process.env.VAULT_TOKEN?.trim() ?? "";
30
+ const token = options.token ?? (await resolveVaultToken());
28
31
  const mount = options.mount ?? process.env.VAULT_SECRET_MOUNT?.trim() ?? DEFAULT_VAULT_SECRET_MOUNT;
29
32
  const path = options.path?.trim() ?? "";
30
33
  const field = options.field?.trim() ?? "value";
31
34
 
32
35
  if (!addr || !token || !path) {
33
- throw new Error("Vault secret resolution requires VAULT_ADDR, VAULT_TOKEN, and a secret path");
36
+ throw new Error("Vault secret resolution requires VAULT_ADDR, a Vault token, and a secret path");
34
37
  }
35
38
 
36
39
  const normalizedAddr = addr.replace(/\/+$/g, "");
@@ -61,3 +64,19 @@ export async function readVaultSecret(options: VaultSecretOptions = {}) {
61
64
 
62
65
  return value;
63
66
  }
67
+
68
+ async function resolveVaultToken() {
69
+ const direct = process.env.VAULT_TOKEN?.trim();
70
+ if (direct) {
71
+ return direct;
72
+ }
73
+
74
+ const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
75
+
76
+ try {
77
+ const value = (await Bun.file(tokenFile).text()).trim();
78
+ return value;
79
+ } catch {
80
+ return "";
81
+ }
82
+ }
@@ -6,5 +6,5 @@ VAULT_SECRET_MOUNT=secret
6
6
  VAULT_NEON_API_KEY_PATH=provider/neon-api-key
7
7
  VAULT_NEON_API_KEY_FIELD=value
8
8
 
9
- # Do not commit VAULT_TOKEN. Prefer `vault login` in your shell session.
10
-
9
+ # Do not commit VAULT_TOKEN. Prefer `vault login`; the CLI will also use
10
+ # VAULT_TOKEN_FILE or ~/.vault-token when available.
@@ -5,7 +5,7 @@ Generated by `create-svc`.
5
5
  This scaffold targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
6
6
 
7
7
  - one generated `service.yaml` manifest
8
- - Bun-based `bootstrap` and `deploy` helpers
8
+ - a local `svc-cloudrun` CLI for bootstrap, deploy, and cleanup
9
9
  - GitHub Actions for CI, `main` deploys, PR previews, and personal environments
10
10
  - GCP project bootstrap with billing and quota-project-aware `gcloud` calls
11
11
  - Neon main, preview, and personal branch provisioning
@@ -13,25 +13,28 @@ This scaffold targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
13
13
  ## Commands
14
14
 
15
15
  ```bash
16
- bun dev
17
- bun gen
18
- bun lint
19
- bun test
20
- bun run bootstrap
21
- bun run deploy
22
- bun run deploy -- --environment personal --name <slug>
23
- bun run deploy -- --destroy --environment personal --name <slug>
16
+ make dev
17
+ make gen
18
+ make lint
19
+ make test
20
+ make bootstrap
21
+ make deploy
22
+ make deploy ARGS="--environment personal --name <slug>"
23
+ make deploy ARGS="--destroy --environment personal --name <slug>"
24
+ make cleanup
25
+ make cleanup ARGS="--repo --project"
24
26
  ```
25
27
 
26
28
  ## Configuration
27
29
 
28
30
  The generated Cloud Run config lives in [scripts/cloudrun/config.ts](scripts/cloudrun/config.ts).
29
31
 
30
- Bootstrap and deploy use:
32
+ Bootstrap, deploy, and cleanup use:
31
33
 
32
34
  - `gcloud`
33
35
  - `gh`
34
- - `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR` + `VAULT_TOKEN`
36
+ - `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
37
+ - the local repo CLI via `npx --no-install svc-cloudrun ...`
35
38
 
36
39
  ## Environment setup
37
40
 
@@ -43,14 +46,17 @@ cp .env.example .env.local
43
46
 
44
47
  Then edit `.env.local` with your Vault address and secret path overrides.
45
48
 
46
- For the token itself, prefer a live shell session:
49
+ For the token itself, prefer a normal Vault login flow:
47
50
 
48
51
  ```bash
49
52
  vault login
50
- export VAULT_TOKEN="$(vault print token)"
51
53
  ```
52
54
 
53
- or however your existing Vault login flow exposes `VAULT_TOKEN`.
55
+ The scaffold will use, in order:
56
+
57
+ 1. `VAULT_TOKEN`
58
+ 2. `VAULT_TOKEN_FILE`
59
+ 3. `~/.vault-token`
54
60
 
55
61
  That keeps stable settings in the repo and keeps the token out of `~/.zshrc`.
56
62
 
@@ -1,4 +1,6 @@
1
1
  import { createApiClient } from "@neondatabase/api-client";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
2
4
  import { config } from "./config";
3
5
 
4
6
  type NeonBranch = {
@@ -13,13 +15,13 @@ async function resolveNeonApiKey() {
13
15
  }
14
16
 
15
17
  const addr = process.env.VAULT_ADDR?.trim() ?? "";
16
- const token = process.env.VAULT_TOKEN?.trim() ?? "";
18
+ const token = await resolveVaultToken();
17
19
  const mount = process.env.VAULT_SECRET_MOUNT?.trim() ?? "secret";
18
20
  const path = process.env.VAULT_NEON_API_KEY_PATH?.trim() ?? "provider/neon-api-key";
19
21
  const field = process.env.VAULT_NEON_API_KEY_FIELD?.trim() ?? "value";
20
22
 
21
23
  if (!addr || !token) {
22
- throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR and VAULT_TOKEN");
24
+ throw new Error("NEON_API_KEY is required for Neon provisioning, or set VAULT_ADDR with VAULT_TOKEN, VAULT_TOKEN_FILE, or ~/.vault-token");
23
25
  }
24
26
 
25
27
  const normalizedAddr = addr.replace(/\/+$/g, "");
@@ -49,6 +51,21 @@ async function resolveNeonApiKey() {
49
51
  return apiKey;
50
52
  }
51
53
 
54
+ async function resolveVaultToken() {
55
+ const direct = process.env.VAULT_TOKEN?.trim();
56
+ if (direct) {
57
+ return direct;
58
+ }
59
+
60
+ const tokenFile = process.env.VAULT_TOKEN_FILE?.trim() || join(homedir(), ".vault-token");
61
+
62
+ try {
63
+ return (await Bun.file(tokenFile).text()).trim();
64
+ } catch {
65
+ return "";
66
+ }
67
+ }
68
+
52
69
  async function neonClient() {
53
70
  const apiKey = await resolveNeonApiKey();
54
71
  return createApiClient({ apiKey });