@ankhorage/infra 3.1.1 → 3.3.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -10
  3. package/dist/adapters/minikube/auth/index.d.ts +2 -1
  4. package/dist/adapters/minikube/auth/index.d.ts.map +1 -1
  5. package/dist/adapters/minikube/auth/index.js +6 -3
  6. package/dist/adapters/minikube/auth/index.js.map +1 -1
  7. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.d.ts +7 -0
  8. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.d.ts.map +1 -0
  9. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.js +104 -0
  10. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.js.map +1 -0
  11. package/dist/adapters/minikube/index.d.ts.map +1 -1
  12. package/dist/adapters/minikube/index.js +5 -1
  13. package/dist/adapters/minikube/index.js.map +1 -1
  14. package/dist/authTargetRedirects.d.ts +12 -0
  15. package/dist/authTargetRedirects.d.ts.map +1 -0
  16. package/dist/authTargetRedirects.js +32 -0
  17. package/dist/authTargetRedirects.js.map +1 -0
  18. package/dist/project/index.d.ts +3 -1
  19. package/dist/project/index.d.ts.map +1 -1
  20. package/dist/project/index.js +3 -1
  21. package/dist/project/index.js.map +1 -1
  22. package/dist/projectDatabase.d.ts +5 -0
  23. package/dist/projectDatabase.d.ts.map +1 -0
  24. package/dist/projectDatabase.js +22 -0
  25. package/dist/projectDatabase.js.map +1 -0
  26. package/dist/projectEnvironment.d.ts +6 -0
  27. package/dist/projectEnvironment.d.ts.map +1 -0
  28. package/dist/projectEnvironment.js +45 -0
  29. package/dist/projectEnvironment.js.map +1 -0
  30. package/dist/projectInfrastructure.d.ts +16 -0
  31. package/dist/projectInfrastructure.d.ts.map +1 -1
  32. package/dist/projectInfrastructure.js +25 -0
  33. package/dist/projectInfrastructure.js.map +1 -1
  34. package/dist/runtime.d.ts.map +1 -1
  35. package/dist/runtime.js +8 -26
  36. package/dist/runtime.js.map +1 -1
  37. package/dist/types.d.ts +1 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/adapters/minikube/auth/index.ts +8 -4
  42. package/src/adapters/minikube/auth/targetAwareOAuthRuntime.test.ts +83 -0
  43. package/src/adapters/minikube/auth/targetAwareOAuthRuntime.ts +136 -0
  44. package/src/adapters/minikube/index.ts +5 -1
  45. package/src/authTargetRedirects.test.ts +62 -0
  46. package/src/authTargetRedirects.ts +46 -0
  47. package/src/project/index.test.ts +80 -1
  48. package/src/project/index.ts +4 -0
  49. package/src/projectDatabase.ts +27 -0
  50. package/src/projectEnvironment.ts +51 -0
  51. package/src/projectInfrastructure.ts +44 -0
  52. package/src/runtime.ts +8 -25
  53. package/src/types.ts +1 -1
@@ -3,7 +3,11 @@ import { promises as fs } from 'fs';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
5
 
6
+ import { createAppManifest } from '../testSupport.js';
6
7
  import {
8
+ inspectProjectInfrastructure,
9
+ readProjectInfrastructureEnvironment,
10
+ resolveProjectInfrastructureDatabaseUrl,
7
11
  resolveProjectInfrastructurePortForward,
8
12
  runProjectInfrastructureLifecycle,
9
13
  } from './index.js';
@@ -29,7 +33,13 @@ async function createProjectFixture(): Promise<string> {
29
33
  );
30
34
  await fs.writeFile(
31
35
  path.join(infraRoot, '.env.example'),
32
- 'APP_PORT_FORWARD_LOCAL_PORT=48123\n',
36
+ [
37
+ 'APP_PORT_FORWARD_LOCAL_PORT=48123',
38
+ 'SITE_URL=http://example.local',
39
+ 'DATABASE_URL=postgres://example',
40
+ 'PRIVATE_VALUE=not-selected',
41
+ '',
42
+ ].join('\n'),
33
43
  'utf8',
34
44
  );
35
45
  return projectPath;
@@ -67,4 +77,73 @@ describe('@ankhorage/infra/project', () => {
67
77
  url: 'http://127.0.0.1:48123',
68
78
  });
69
79
  });
80
+
81
+ test('reads only requested runtime environment values from the generated fallback', async () => {
82
+ const projectPath = await createProjectFixture();
83
+ const result = await readProjectInfrastructureEnvironment({
84
+ keys: ['SITE_URL', 'MISSING_VALUE'],
85
+ projectPath,
86
+ target: 'minikube',
87
+ });
88
+
89
+ expect(result).toEqual({ SITE_URL: 'http://example.local' });
90
+ });
91
+
92
+ test('prefers generated runtime environment and resolves the trusted database URL', async () => {
93
+ const projectPath = await createProjectFixture();
94
+ const envPath = path.join(projectPath, 'infra', 'minikube', '.env');
95
+ await fs.writeFile(
96
+ envPath,
97
+ 'SITE_URL=http://runtime.local\nDATABASE_URL=postgres://runtime\n',
98
+ 'utf8',
99
+ );
100
+
101
+ const environment = await readProjectInfrastructureEnvironment({
102
+ keys: ['SITE_URL'],
103
+ projectPath,
104
+ target: 'minikube',
105
+ });
106
+ const databaseUrl = await resolveProjectInfrastructureDatabaseUrl({
107
+ projectPath,
108
+ target: 'minikube',
109
+ });
110
+
111
+ expect(environment).toEqual({ SITE_URL: 'http://runtime.local' });
112
+ expect(databaseUrl).toBe('postgres://runtime');
113
+ });
114
+
115
+ test('inspects deployment and generated state through the owner ledger', async () => {
116
+ const projectPath = await createProjectFixture();
117
+ await fs.mkdir(path.join(projectPath, '.ankh'), { recursive: true });
118
+ await fs.writeFile(
119
+ path.join(projectPath, '.ankh', 'infra-ledger.json'),
120
+ JSON.stringify({
121
+ schemaVersion: 1,
122
+ generatedAt: '2026-08-12T04:00:00.000Z',
123
+ target: 'minikube',
124
+ files: ['infra/minikube/.env.example', 'infra/minikube/scripts/status.sh'],
125
+ warnings: ['generated warning'],
126
+ }),
127
+ 'utf8',
128
+ );
129
+ const manifest = createAppManifest('example', {
130
+ deployment: { monitoring: false, target: 'minikube' },
131
+ modules: [],
132
+ });
133
+
134
+ const result = await inspectProjectInfrastructure({
135
+ manifest,
136
+ projectId: 'example',
137
+ projectPath,
138
+ });
139
+
140
+ expect(result).toEqual({
141
+ generated: true,
142
+ generatedAt: '2026-08-12T04:00:00.000Z',
143
+ hasDeployment: true,
144
+ target: 'minikube',
145
+ trackedFiles: 2,
146
+ warnings: ['generated warning'],
147
+ });
148
+ });
70
149
  });
@@ -2,8 +2,12 @@ export {
2
2
  type ResolvedInfraProject,
3
3
  resolveInfraProject as resolveInfraWorkspaceProject,
4
4
  } from '../project.js';
5
+ export { resolveProjectInfrastructureDatabaseUrl } from '../projectDatabase.js';
6
+ export { readProjectInfrastructureEnvironment } from '../projectEnvironment.js';
5
7
  export {
8
+ type InfraProjectInspection,
6
9
  type InfraSyncResult,
10
+ inspectProjectInfrastructure,
7
11
  resolveProjectInfrastructureTarget,
8
12
  syncProjectInfrastructure,
9
13
  } from '../projectInfrastructure.js';
@@ -0,0 +1,27 @@
1
+ import { readProjectInfrastructureEnvironment } from './projectEnvironment.js';
2
+
3
+ const DATABASE_URL_KEYS = [
4
+ 'ANKH_SECRET_STORE_DATABASE_URL',
5
+ 'SUPABASE_DB_URL',
6
+ 'POSTGRES_URL',
7
+ 'DB_URL',
8
+ 'DATABASE_URL',
9
+ ] as const;
10
+
11
+ export async function resolveProjectInfrastructureDatabaseUrl(args: {
12
+ readonly projectPath: string;
13
+ readonly target: string;
14
+ }): Promise<string | null> {
15
+ const environment = await readProjectInfrastructureEnvironment({
16
+ keys: DATABASE_URL_KEYS,
17
+ projectPath: args.projectPath,
18
+ target: args.target,
19
+ });
20
+
21
+ for (const key of DATABASE_URL_KEYS) {
22
+ const value = environment[key]?.trim();
23
+ if (value) return value;
24
+ }
25
+
26
+ return null;
27
+ }
@@ -0,0 +1,51 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+
4
+ export async function readProjectInfrastructureEnvironment(args: {
5
+ readonly keys: readonly string[];
6
+ readonly projectPath: string;
7
+ readonly target: string;
8
+ }): Promise<Readonly<Record<string, string>>> {
9
+ const infraRoot = path.join(args.projectPath, 'infra', args.target);
10
+ const envPath = path.join(infraRoot, '.env');
11
+ const fallbackPath = path.join(infraRoot, '.env.example');
12
+ const sourcePath = (await pathExists(envPath)) ? envPath : fallbackPath;
13
+ const environment = await readEnvironmentFile(sourcePath);
14
+ const selected: Record<string, string> = {};
15
+
16
+ for (const key of new Set(args.keys)) {
17
+ const value = environment.get(key);
18
+ if (value !== undefined) selected[key] = value;
19
+ }
20
+
21
+ return selected;
22
+ }
23
+
24
+ async function readEnvironmentFile(filePath: string): Promise<Map<string, string>> {
25
+ const out = new Map<string, string>();
26
+ const content = await fs.readFile(filePath, 'utf8');
27
+
28
+ for (const line of content.split(/\r?\n/u)) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed || trimmed.startsWith('#')) continue;
31
+ const separator = trimmed.indexOf('=');
32
+ if (separator <= 0) continue;
33
+ const key = trimmed.slice(0, separator).trim();
34
+ const value = trimmed
35
+ .slice(separator + 1)
36
+ .trim()
37
+ .replace(/^['"]|['"]$/g, '');
38
+ out.set(key, value);
39
+ }
40
+
41
+ return out;
42
+ }
43
+
44
+ async function pathExists(filePath: string): Promise<boolean> {
45
+ try {
46
+ await fs.access(filePath);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
@@ -25,6 +25,18 @@ export interface InfraSyncResult {
25
25
  };
26
26
  }
27
27
 
28
+ export interface InfraProjectInspection {
29
+ readonly generated: boolean;
30
+ readonly generatedAt: string | null;
31
+ readonly hasDeployment: boolean;
32
+ readonly target: string | null;
33
+ readonly trackedFiles: number;
34
+ readonly warnings: readonly string[];
35
+ readonly skipped?: {
36
+ readonly reason: string;
37
+ };
38
+ }
39
+
28
40
  export async function syncProjectInfrastructure(args: {
29
41
  readonly generateInfrastructureImpl?: typeof generateInfrastructure;
30
42
  readonly manifest: AppManifest;
@@ -97,6 +109,38 @@ export async function syncProjectInfrastructure(args: {
97
109
  };
98
110
  }
99
111
 
112
+ export async function inspectProjectInfrastructure(args: {
113
+ readonly manifest: AppManifest;
114
+ readonly projectId: string;
115
+ readonly projectPath: string;
116
+ }): Promise<InfraProjectInspection> {
117
+ const supportWarnings = validateInfraSupport(args.manifest.infra);
118
+
119
+ if (args.projectId === STUDIO_PROJECT_ID) {
120
+ return {
121
+ generated: false,
122
+ generatedAt: null,
123
+ hasDeployment: false,
124
+ target: null,
125
+ trackedFiles: 0,
126
+ warnings: supportWarnings,
127
+ skipped: {
128
+ reason: 'apps/studio is the dashboard and is not a generated app target.',
129
+ },
130
+ };
131
+ }
132
+
133
+ const ledger = await readInfraLedger(args.projectPath);
134
+ return {
135
+ generated: ledger !== null,
136
+ generatedAt: ledger?.generatedAt ?? null,
137
+ hasDeployment: args.manifest.infra.deployment !== undefined,
138
+ target: args.manifest.infra.deployment?.target ?? ledger?.target ?? null,
139
+ trackedFiles: ledger?.files.length ?? 0,
140
+ warnings: uniqueStrings([...supportWarnings, ...(ledger?.warnings ?? [])]),
141
+ };
142
+ }
143
+
100
144
  export async function resolveProjectInfrastructureTarget(args: {
101
145
  readonly manifest: AppManifest;
102
146
  readonly projectPath: string;
package/src/runtime.ts CHANGED
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
 
5
5
  import type { InfraCommandContext } from './commandContext.js';
6
+ import { readProjectInfrastructureEnvironment } from './projectEnvironment.js';
6
7
 
7
8
  export type InfraLifecycleScript = 'destroy' | 'down' | 'port-forward' | 'reset' | 'status' | 'up';
8
9
 
@@ -65,16 +66,16 @@ export async function resolveProjectInfrastructurePortForward(args: {
65
66
  readonly projectPath: string;
66
67
  readonly target: string;
67
68
  }): Promise<InfraPortForwardInfo> {
68
- const infraRoot = path.join(args.projectPath, 'infra', args.target);
69
- const envPath = path.join(infraRoot, '.env');
70
- const fallbackEnvPath = path.join(infraRoot, '.env.example');
71
- const sourcePath = (await pathExists(envPath)) ? envPath : fallbackEnvPath;
72
- const env = await readSimpleEnvMap(sourcePath);
73
- const localPort = parsePositivePort(env.get('APP_PORT_FORWARD_LOCAL_PORT'));
69
+ const environment = await readProjectInfrastructureEnvironment({
70
+ keys: ['APP_PORT_FORWARD_LOCAL_PORT'],
71
+ projectPath: args.projectPath,
72
+ target: args.target,
73
+ });
74
+ const localPort = parsePositivePort(environment.APP_PORT_FORWARD_LOCAL_PORT);
74
75
 
75
76
  if (localPort === null) {
76
77
  throw new Error(
77
- `Generated Infra did not provide a valid APP_PORT_FORWARD_LOCAL_PORT in ${sourcePath}. Regenerate infrastructure first.`,
78
+ 'Generated Infra did not provide a valid APP_PORT_FORWARD_LOCAL_PORT. Regenerate infrastructure first.',
78
79
  );
79
80
  }
80
81
 
@@ -186,24 +187,6 @@ function createExitError(
186
187
  });
187
188
  }
188
189
 
189
- async function readSimpleEnvMap(filePath: string): Promise<Map<string, string>> {
190
- const out = new Map<string, string>();
191
- const content = await fs.readFile(filePath, 'utf8');
192
- for (const line of content.split(/\r?\n/u)) {
193
- const trimmed = line.trim();
194
- if (!trimmed || trimmed.startsWith('#')) continue;
195
- const separator = trimmed.indexOf('=');
196
- if (separator <= 0) continue;
197
- const key = trimmed.slice(0, separator).trim();
198
- const value = trimmed
199
- .slice(separator + 1)
200
- .trim()
201
- .replace(/^['"]|['"]$/g, '');
202
- out.set(key, value);
203
- }
204
- return out;
205
- }
206
-
207
190
  function parsePositivePort(value: string | undefined): number | null {
208
191
  if (value === undefined) return null;
209
192
  const parsed = Number.parseInt(value.trim(), 10);
package/src/types.ts CHANGED
@@ -37,7 +37,7 @@ export interface InfrastructureGenerationOptions {
37
37
  */
38
38
  readonly appManifest?: Pick<
39
39
  AppManifest,
40
- 'generatedApis' | 'infra' | 'metadata' | 'navigator' | 'screens' | 'settings'
40
+ 'deploy' | 'generatedApis' | 'infra' | 'metadata' | 'navigator' | 'screens' | 'settings'
41
41
  >;
42
42
  }
43
43