create-svc 0.1.85 → 0.1.86

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.85",
3
+ "version": "0.1.86",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -113,6 +113,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
113
113
 
114
114
  const dockerCompose = await Bun.file(join(generatedRoot, "docker-compose.yml")).text();
115
115
  expect(dockerCompose).toContain('image: postgres:16-alpine');
116
+ expect(dockerCompose).toContain("image: temporalio/auto-setup:");
117
+ expect(dockerCompose).toContain("127.0.0.1:7233:7233");
116
118
  expect(dockerCompose).toContain(`127.0.0.1:${localPort}:5432`);
117
119
 
118
120
  const envExample = await Bun.file(join(generatedRoot, ".env.example")).text();
@@ -337,7 +339,7 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
337
339
  expect(readme).toContain("`microservice` profile");
338
340
  expect(readme).toContain("api.dns-api.anmho.com");
339
341
  expect(readme).toContain("open Docker Desktop if needed");
340
- expect(readme).toContain("local Postgres service in `docker-compose.yml`");
342
+ expect(readme).toContain("local Postgres and Temporal services in `docker-compose.yml`");
341
343
  expect(readme).toContain("gcloud auth login");
342
344
  expect(readme).toContain("known-good CLIs");
343
345
  expect(readme).toContain("service create");
@@ -0,0 +1,12 @@
1
+ export type AuthctlCommand = {
2
+ path: string;
3
+ runWithBun: boolean;
4
+ };
5
+
6
+ export function authctlSpawnArgs(command: AuthctlCommand, args: string[]) {
7
+ return command.runWithBun ? [bunExecutable(), command.path, ...args] : [command.path, ...args];
8
+ }
9
+
10
+ function bunExecutable() {
11
+ return Bun.which("bun") ?? process.execPath;
12
+ }
@@ -0,0 +1,16 @@
1
+ import { expect, test } from "bun:test";
2
+ import { authctlSpawnArgs } from "./authctl-command";
3
+
4
+ test("authctlSpawnArgs runs repo-local authctl through bun", () => {
5
+ const args = authctlSpawnArgs({ path: "./node_modules/.bin/authctl", runWithBun: true }, ["doctor", "--json"]);
6
+
7
+ expect(args[0]).toEndWith("bun");
8
+ expect(args.slice(1)).toEqual(["./node_modules/.bin/authctl", "doctor", "--json"]);
9
+ });
10
+
11
+ test("authctlSpawnArgs runs global authctl directly", () => {
12
+ expect(authctlSpawnArgs({ path: "/usr/local/bin/authctl", runWithBun: false }, ["version"])).toEqual([
13
+ "/usr/local/bin/authctl",
14
+ "version",
15
+ ]);
16
+ });
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { authctlSpawnArgs, type AuthctlCommand } from "./authctl-command";
2
3
  import { serviceConfig } from "./runtime";
3
4
 
4
5
  type CommandResult = {
@@ -9,6 +10,7 @@ type CommandResult = {
9
10
  };
10
11
 
11
12
  const decoder = new TextDecoder();
13
+ const localAuthctlPath = "./node_modules/.bin/authctl";
12
14
 
13
15
  export type AuthDoctorResult = {
14
16
  hasAuthctl: boolean;
@@ -418,12 +420,12 @@ function resolveResourceServerCommand(): ResourceServerCommand | undefined {
418
420
  }
419
421
 
420
422
  function authctl(args: string[], options: { allowFailure?: boolean; quiet?: boolean } = {}): CommandResult {
421
- const command = authctlPath();
423
+ const command = authctlCommand();
422
424
  if (!command) {
423
425
  throw new Error("authctl is not installed; run bun install in this generated service or link @anmho/authctl");
424
426
  }
425
427
 
426
- const result = Bun.spawnSync([command, ...args], {
428
+ const result = Bun.spawnSync(authctlSpawnArgs(command, args), {
427
429
  cwd: process.cwd(),
428
430
  env: authctlEnvironment(),
429
431
  stdin: "inherit",
@@ -464,8 +466,24 @@ function formatAuthctlFailure(args: string[], output: CommandResult) {
464
466
  return `authctl ${args.join(" ")} failed with exit code ${output.exitCode}\n${detail}`;
465
467
  }
466
468
 
469
+ function authctlCommand(): AuthctlCommand | undefined {
470
+ if (existsSync(localAuthctlPath)) {
471
+ return {
472
+ path: localAuthctlPath,
473
+ runWithBun: true,
474
+ };
475
+ }
476
+ const global = Bun.which("authctl");
477
+ return global
478
+ ? {
479
+ path: global,
480
+ runWithBun: false,
481
+ }
482
+ : undefined;
483
+ }
484
+
467
485
  function authctlPath() {
468
- return existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl");
486
+ return authctlCommand()?.path;
469
487
  }
470
488
 
471
489
  function authctlEnvironment() {
@@ -43,7 +43,7 @@ console to create and deploy.
43
43
 
44
44
  ## Local development
45
45
 
46
- The scaffold writes a ready-to-use `.env.local` and includes a local Postgres service in `docker-compose.yml`.
46
+ The scaffold writes a ready-to-use `.env.local` and includes local Postgres and Temporal services in `docker-compose.yml`.
47
47
 
48
48
  First local run:
49
49
 
@@ -55,8 +55,8 @@ First local run:
55
55
  Local runtime uses:
56
56
 
57
57
  - `DATABASE_URL` from `.env.local`, pointed at Docker Compose Postgres
58
- - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres
59
- - `TEMPORAL_ENABLED=true` by default with `localhost:7233` and `default`; set `TEMPORAL_ENABLED=false` when you are not running a local Temporal server
58
+ - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres and Temporal
59
+ - `TEMPORAL_ENABLED=true` by default with `localhost:7233` and `default`; `{{COMMAND_DEV}}` waits for the local Temporal container before starting the API and worker
60
60
 
61
61
  No cloud credentials are required for local HTTP development after Docker and Postgres are running.
62
62
 
@@ -78,7 +78,7 @@ creates a trigger, the API service starts `waitlistFollowUpWorkflow` /
78
78
  `WaitlistFollowUpWorkflow` asynchronously on the service task queue. The API
79
79
  request only waits for the trigger record; workflow completion happens through
80
80
  Temporal and is polled by the worker service.
81
- Local `{{COMMAND_DEV}}` starts the API process and the worker process together.
81
+ Local `{{COMMAND_DEV}}` starts the API process and the worker process together after Docker Compose starts the local Temporal server.
82
82
 
83
83
  Production and preview deploys render `TEMPORAL_ENABLED=true` into the Cloud Run
84
84
  manifest unless you override it. For Temporal Cloud, replace the local defaults
@@ -15,5 +15,21 @@ services:
15
15
  timeout: 5s
16
16
  retries: 10
17
17
 
18
+ temporal:
19
+ image: temporalio/auto-setup:1.28.1
20
+ depends_on:
21
+ postgres:
22
+ condition: service_healthy
23
+ environment:
24
+ DB: postgres12
25
+ DB_PORT: 5432
26
+ POSTGRES_USER: {{LOCAL_DATABASE_USER}}
27
+ POSTGRES_PWD: {{LOCAL_DATABASE_PASSWORD}}
28
+ POSTGRES_SEEDS: postgres
29
+ DBNAME: temporal
30
+ VISIBILITY_DBNAME: temporal_visibility
31
+ ports:
32
+ - "127.0.0.1:7233:7233"
33
+
18
34
  volumes:
19
35
  postgres-data:
@@ -16,6 +16,9 @@ const env = {
16
16
  if (env.DATABASE_URL && !env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE) {
17
17
  env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE = env.DATABASE_URL;
18
18
  }
19
+ if (temporalEnabled(env)) {
20
+ await waitForTemporal(env.TEMPORAL_ADDRESS || "localhost:7233");
21
+ }
19
22
 
20
23
  const api = Bun.spawn(apiCommand, {
21
24
  stdin: "inherit",
@@ -57,3 +60,36 @@ function parseCommands(argv: string[]) {
57
60
  const workerCommand = argv.slice(separator + 1);
58
61
  return { apiCommand, workerCommand: workerCommand.length > 0 ? workerCommand : undefined };
59
62
  }
63
+
64
+ function temporalEnabled(env: Record<string, string | undefined>) {
65
+ return (env.TEMPORAL_ENABLED ?? "true").trim().toLowerCase() !== "false";
66
+ }
67
+
68
+ async function waitForTemporal(address: string) {
69
+ const { host, port } = parseTemporalAddress(address);
70
+ const deadline = Date.now() + 120_000;
71
+
72
+ while (Date.now() < deadline) {
73
+ const exitCode = await Bun.spawn(["nc", "-z", host, String(port)], {
74
+ stdin: "ignore",
75
+ stdout: "ignore",
76
+ stderr: "ignore",
77
+ }).exited;
78
+ if (exitCode === 0) {
79
+ return;
80
+ }
81
+ await Bun.sleep(2_000);
82
+ }
83
+
84
+ throw new Error(`Temporal did not become ready at ${host}:${port} within 120 seconds`);
85
+ }
86
+
87
+ function parseTemporalAddress(address: string) {
88
+ const trimmed = address.trim();
89
+ const withoutScheme = trimmed.includes("://") ? new URL(trimmed).host : trimmed;
90
+ const [host = "localhost", port = "7233"] = withoutScheme.split(":");
91
+ return {
92
+ host: host || "localhost",
93
+ port: Number(port || "7233"),
94
+ };
95
+ }
@@ -1,6 +1,6 @@
1
1
  export async function ensureLocalPostgres() {
2
2
  await ensureDockerRunning();
3
- await run(["docker", "compose", "up", "-d"], { label: "start local postgres" });
3
+ await run(["docker", "compose", "up", "-d"], { label: "start local services" });
4
4
  }
5
5
 
6
6
  async function ensureDockerRunning() {