@crossdelta/infrastructure 0.3.2 → 0.4.1

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
@@ -89,15 +89,27 @@ The architecture is **provider-agnostic** - runtime components (NATS, Ingress, C
89
89
  npm install @crossdelta/infrastructure
90
90
  ```
91
91
 
92
- ## Quick Start
92
+ ### Peer Dependencies
93
93
 
94
- ### 1. Install dependencies
94
+ This package requires Pulumi packages as peer dependencies. Install them based on your target cloud provider:
95
95
 
96
96
  ```bash
97
- npm install @crossdelta/infrastructure @pulumi/pulumi @pulumi/kubernetes @pulumi/digitalocean
97
+ # Required - Pulumi core and Kubernetes
98
+ npm install @pulumi/pulumi @pulumi/kubernetes
99
+
100
+ # Optional - DigitalOcean provider (for DOKS)
101
+ npm install @pulumi/digitalocean
98
102
  ```
99
103
 
100
- ### 2. Create service configs
104
+ | Package | Required | Description |
105
+ |---------|----------|-------------|
106
+ | `@pulumi/pulumi` | ✅ Yes | Pulumi core SDK |
107
+ | `@pulumi/kubernetes` | ✅ Yes | Kubernetes resources |
108
+ | `@pulumi/digitalocean` | Optional | DigitalOcean provider (DOKS, VPC, etc.) |
109
+
110
+ ## Quick Start
111
+
112
+ ### 1. Create service configs
101
113
 
102
114
  ```typescript
103
115
  // infra/services/orders.ts
@@ -135,7 +147,7 @@ const config: K8sServiceConfig = {
135
147
  export default config
136
148
  ```
137
149
 
138
- ### 3. Create your Pulumi infrastructure
150
+ ### 2. Create your Pulumi infrastructure
139
151
 
140
152
  ```typescript
141
153
  // infra/index.ts
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/generate-env.ts
4
+ import { execSync } from "node:child_process";
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
8
+ var resolveNumber = (value, fileContent) => {
9
+ const literal = Number.parseInt(value, 10);
10
+ if (!Number.isNaN(literal))
11
+ return literal;
12
+ const varMatch = fileContent.match(new RegExp(`(?:const|let|var)\\s+${value}\\s*=\\s*(\\d+)`));
13
+ return varMatch?.[1] ? Number.parseInt(varMatch[1], 10) : undefined;
14
+ };
15
+ var extract = {
16
+ string: (content, pattern) => content.match(pattern)?.[1],
17
+ number: (content, pattern) => {
18
+ const match = content.match(pattern)?.[1];
19
+ return match ? resolveNumber(match, content) : undefined;
20
+ },
21
+ numberArray: (content, pattern) => {
22
+ const match = content.match(pattern)?.[1];
23
+ if (!match)
24
+ return;
25
+ const numbers = match.split(",").map((v) => resolveNumber(v.trim(), content)).filter((n) => n !== undefined);
26
+ return numbers.length > 0 ? numbers : undefined;
27
+ }
28
+ };
29
+ var LOCK_FILES = ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
30
+ var hasLockFile = (dir) => LOCK_FILES.some((file) => existsSync(join(dir, file)));
31
+ var findWorkspaceRoot = () => {
32
+ let dir = process.cwd();
33
+ while (!hasLockFile(dir)) {
34
+ const parent = join(dir, "..");
35
+ if (parent === dir)
36
+ throw new Error("Could not locate workspace root");
37
+ dir = parent;
38
+ }
39
+ return dir;
40
+ };
41
+ var loadPulumiConfig = async (infraDir, stack) => {
42
+ try {
43
+ const stdout = execSync(`pulumi config --show-secrets --json --stack ${stack} --cwd ${infraDir}`, {
44
+ encoding: "utf-8",
45
+ stdio: ["pipe", "pipe", "pipe"]
46
+ });
47
+ const config = JSON.parse(stdout);
48
+ return Object.entries(config).filter(([key]) => key.includes(":")).filter(([, entry]) => entry.value !== undefined && entry.value !== null && entry.value !== "undefined").map(([fullKey, entry]) => {
49
+ const [, rawKey] = fullKey.split(":");
50
+ return `${rawKey}=${entry.value}`;
51
+ });
52
+ } catch {
53
+ return [];
54
+ }
55
+ };
56
+ var discoverServices = (servicesDir) => {
57
+ if (!existsSync(servicesDir))
58
+ return [];
59
+ const files = readdirSync(servicesDir).filter((file) => file.endsWith(".ts") && file !== "index.ts");
60
+ return files.map((file) => {
61
+ const content = readFileSync(join(servicesDir, file), "utf-8");
62
+ const portsApiMatch = content.match(/ports\(\)\.(?:http|https|grpc|primary)\((\d+)\)/) || content.match(/ports\(\)\.add\((\d+)/);
63
+ return {
64
+ name: extract.string(content, /name:\s*['"]([^'"]+)['"]/) ?? file.replace(".ts", ""),
65
+ primaryPort: portsApiMatch?.[1] ? Number.parseInt(portsApiMatch[1], 10) : undefined,
66
+ containerPort: extract.number(content, /containerPort:\s*(\w+)/),
67
+ httpPort: extract.number(content, /httpPort:\s*(\w+)/),
68
+ internalPorts: extract.numberArray(content, /internalPorts:\s*\[([^\]]+)\]/),
69
+ internalUrl: extract.string(content, /internalUrl:\s*[`'"]([^`'"]+)[`'"]/)
70
+ };
71
+ });
72
+ };
73
+ var getServicePort = (config) => {
74
+ if (config.primaryPort)
75
+ return config.primaryPort;
76
+ if (config.containerPort)
77
+ return config.containerPort;
78
+ if (config.httpPort)
79
+ return config.httpPort;
80
+ if (config.internalPorts?.[0])
81
+ return config.internalPorts[0];
82
+ return;
83
+ };
84
+ var getLocalUrl = (config) => {
85
+ const port = getServicePort(config);
86
+ if (!port)
87
+ return;
88
+ if (config.internalUrl) {
89
+ const protocolMatch = config.internalUrl.match(/^(\w+):\/\//);
90
+ if (protocolMatch) {
91
+ return `${protocolMatch[1]}://localhost:${port}`;
92
+ }
93
+ }
94
+ return `http://localhost:${port}`;
95
+ };
96
+ var main = async () => {
97
+ const noPulumi = process.argv.includes("--no-pulumi");
98
+ const workspaceRootDir = findWorkspaceRoot();
99
+ const infraDir = join(workspaceRootDir, "infra");
100
+ const servicesDir = join(infraDir, "services");
101
+ const envLines = ["# Generated .env.local"];
102
+ if (!noPulumi && existsSync(join(infraDir, "Pulumi.yaml"))) {
103
+ const pulumiEnvs = await loadPulumiConfig(infraDir, "dev");
104
+ if (pulumiEnvs.length > 0) {
105
+ envLines.push("", "# Pulumi Secrets");
106
+ envLines.push(...pulumiEnvs);
107
+ }
108
+ }
109
+ const serviceConfigs = discoverServices(servicesDir);
110
+ if (serviceConfigs.length > 0) {
111
+ envLines.push("", "# Service URLs");
112
+ for (const config of serviceConfigs) {
113
+ const url = getLocalUrl(config);
114
+ if (url) {
115
+ envLines.push(`${toEnvKey(config.name)}_URL=${url}`);
116
+ }
117
+ }
118
+ envLines.push("", "# Service Ports");
119
+ for (const config of serviceConfigs) {
120
+ const port = getServicePort(config);
121
+ if (port) {
122
+ envLines.push(`${toEnvKey(config.name)}_PORT=${port}`);
123
+ }
124
+ }
125
+ console.log(`✅ Discovered ${serviceConfigs.length} services`);
126
+ }
127
+ writeFileSync(join(workspaceRootDir, ".env.local"), `${envLines.join(`
128
+ `)}
129
+ `);
130
+ console.log(`✅ .env.local generated at ${workspaceRootDir}`);
131
+ };
132
+ main().catch((err) => {
133
+ console.error("❌", err.message);
134
+ process.exit(1);
135
+ });
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Generate environment variables for local development.
4
4
  *
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Usage: generate-env [--no-pulumi]
12
12
  */
13
+ import { execSync } from 'node:child_process'
13
14
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
14
15
  import { join } from 'node:path'
15
16
 
@@ -58,9 +59,13 @@ const extract = {
58
59
  },
59
60
  }
60
61
 
62
+ const LOCK_FILES = ['bun.lock', 'bun.lockb', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']
63
+
64
+ const hasLockFile = (dir: string): boolean => LOCK_FILES.some((file) => existsSync(join(dir, file)))
65
+
61
66
  const findWorkspaceRoot = (): string => {
62
67
  let dir = process.cwd()
63
- while (!existsSync(join(dir, 'bun.lock')) && !existsSync(join(dir, 'package-lock.json'))) {
68
+ while (!hasLockFile(dir)) {
64
69
  const parent = join(dir, '..')
65
70
  if (parent === dir) throw new Error('Could not locate workspace root')
66
71
  dir = parent
@@ -70,10 +75,12 @@ const findWorkspaceRoot = (): string => {
70
75
 
71
76
  const loadPulumiConfig = async (infraDir: string, stack: string): Promise<string[]> => {
72
77
  try {
73
- const result = Bun.spawnSync(['pulumi', 'config', '--show-secrets', '--json', '--stack', stack, '--cwd', infraDir])
74
- if (result.exitCode !== 0) return []
78
+ const stdout = execSync(`pulumi config --show-secrets --json --stack ${stack} --cwd ${infraDir}`, {
79
+ encoding: 'utf-8',
80
+ stdio: ['pipe', 'pipe', 'pipe'],
81
+ })
75
82
 
76
- const config = JSON.parse(result.stdout.toString()) as Record<string, PulumiConfigEntry>
83
+ const config = JSON.parse(stdout) as Record<string, PulumiConfigEntry>
77
84
 
78
85
  return Object.entries(config)
79
86
  .filter(([key]) => key.includes(':'))