@ankhorage/infra 3.0.0 → 3.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ankhorage/infra
2
2
 
3
+ ## 3.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f38e50b: Delegate project `AppManifest` structural validation to the canonical parser published by `@ankhorage/contracts` and remove Infra's duplicate manifest guard implementation.
8
+
9
+ ## 3.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 2e90e4a: Expose the canonical project and lifecycle application-service boundary through `@ankhorage/infra/project`, including project reconciliation, lifecycle execution, and generated port-forward endpoint resolution for trusted consumers such as Studio.
14
+
3
15
  ## 3.0.0
4
16
 
5
17
  ### Major Changes
@@ -0,0 +1,4 @@
1
+ export { type ResolvedInfraProject, resolveInfraProject as resolveInfraWorkspaceProject, } from '../project.js';
2
+ export { type InfraSyncResult, resolveProjectInfrastructureTarget, syncProjectInfrastructure, } from '../projectInfrastructure.js';
3
+ export { type InfraLifecycleScript, type InfraPortForwardInfo, InfraScriptExecutionError, type InfraScriptOutput, resolveProjectInfrastructurePortForward, runProjectInfrastructureLifecycle, } from '../runtime.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,oBAAoB,EACzB,mBAAmB,IAAI,4BAA4B,GACpD,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,KAAK,eAAe,EACpB,kCAAkC,EAClC,yBAAyB,GAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,uCAAuC,EACvC,iCAAiC,GAClC,MAAM,eAAe,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { resolveInfraProject as resolveInfraWorkspaceProject, } from '../project.js';
2
+ export { resolveProjectInfrastructureTarget, syncProjectInfrastructure, } from '../projectInfrastructure.js';
3
+ export { InfraScriptExecutionError, resolveProjectInfrastructurePortForward, runProjectInfrastructureLifecycle, } from '../runtime.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,mBAAmB,IAAI,4BAA4B,GACpD,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,kCAAkC,EAClC,yBAAyB,GAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAGL,yBAAyB,EAEzB,uCAAuC,EACvC,iCAAiC,GAClC,MAAM,eAAe,CAAC","sourcesContent":["export {\n type ResolvedInfraProject,\n resolveInfraProject as resolveInfraWorkspaceProject,\n} from '../project.js';\nexport {\n type InfraSyncResult,\n resolveProjectInfrastructureTarget,\n syncProjectInfrastructure,\n} from '../projectInfrastructure.js';\nexport {\n type InfraLifecycleScript,\n type InfraPortForwardInfo,\n InfraScriptExecutionError,\n type InfraScriptOutput,\n resolveProjectInfrastructurePortForward,\n runProjectInfrastructureLifecycle,\n} from '../runtime.js';\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAAsB,MAAM,sBAAsB,CAAC;AAI5F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAqChC"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,sBAAsB,CAAC;AAIvE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAiChC"}
package/dist/project.js CHANGED
@@ -1,4 +1,4 @@
1
- import { APP_CATEGORIES } from '@ankhorage/contracts';
1
+ import { isAppManifest } from '@ankhorage/contracts';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  export async function resolveInfraProject(options) {
@@ -18,11 +18,7 @@ export async function resolveInfraProject(options) {
18
18
  throw new Error(`Project '${projectId}' not found.`);
19
19
  }
20
20
  const manifestPath = path.join(projectPath, 'ankh.config.json');
21
- const manifest = await readProjectManifest({
22
- manifestPath,
23
- projectId,
24
- projectPath,
25
- });
21
+ const manifest = await readProjectManifest({ manifestPath, projectId });
26
22
  return {
27
23
  appsRoot,
28
24
  manifest,
@@ -51,9 +47,7 @@ function createFallbackManifest(projectId) {
51
47
  },
52
48
  themes: [],
53
49
  activeThemeId: 'default',
54
- infra: {
55
- modules: [],
56
- },
50
+ infra: { modules: [] },
57
51
  navigator: {
58
52
  type: 'stack',
59
53
  routes: [{ name: 'index', screenId: 'index' }],
@@ -88,13 +82,11 @@ async function findWorkspaceRoot(startPath) {
88
82
  let currentPath = path.resolve(startPath);
89
83
  for (;;) {
90
84
  const appsRoot = path.join(currentPath, 'apps');
91
- if (await isDirectory(appsRoot)) {
85
+ if (await isDirectory(appsRoot))
92
86
  return currentPath;
93
- }
94
87
  const parentPath = path.dirname(currentPath);
95
- if (parentPath === currentPath) {
88
+ if (parentPath === currentPath)
96
89
  return null;
97
- }
98
90
  currentPath = parentPath;
99
91
  }
100
92
  }
@@ -119,51 +111,6 @@ function validateExplicitProjectId(projectId) {
119
111
  }
120
112
  return projectId;
121
113
  }
122
- function isAppManifest(value) {
123
- if (!isRecord(value)) {
124
- return false;
125
- }
126
- return (isAppManifestMetadata(value.metadata) &&
127
- Array.isArray(value.themes) &&
128
- typeof value.activeThemeId === 'string' &&
129
- isInfraManifestRecord(value.infra) &&
130
- isNavigatorSpec(value.navigator) &&
131
- isRecord(value.screens) &&
132
- isSettingsRecord(value.settings));
133
- }
134
- function isAppManifestMetadata(value) {
135
- return (isRecord(value) &&
136
- typeof value.name === 'string' &&
137
- typeof value.slug === 'string' &&
138
- typeof value.version === 'string' &&
139
- APP_CATEGORIES.some((category) => category === value.category) &&
140
- typeof value.themeId === 'string' &&
141
- (value.created === undefined || typeof value.created === 'string') &&
142
- (value.updated === undefined || typeof value.updated === 'string'));
143
- }
144
- function isInfraManifestRecord(value) {
145
- return isRecord(value) && Array.isArray(value.modules);
146
- }
147
- function isNavigatorSpec(value) {
148
- return (isRecord(value) &&
149
- typeof value.type === 'string' &&
150
- Array.isArray(value.routes) &&
151
- (value.initialRouteName === undefined || typeof value.initialRouteName === 'string'));
152
- }
153
- function isSettingsRecord(value) {
154
- return (isRecord(value) &&
155
- (value.apiBaseUrl === undefined || typeof value.apiBaseUrl === 'string') &&
156
- isLocalizationRecord(value.localization));
157
- }
158
- function isLocalizationRecord(value) {
159
- return (isRecord(value) &&
160
- typeof value.defaultLocale === 'string' &&
161
- Array.isArray(value.locales) &&
162
- value.locales.every((locale) => typeof locale === 'string'));
163
- }
164
- function isRecord(value) {
165
- return typeof value === 'object' && value !== null;
166
- }
167
114
  async function isDirectory(filePath) {
168
115
  try {
169
116
  const stat = await fs.stat(filePath);
@@ -1 +1 @@
1
- {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { APP_CATEGORIES, type AppManifest, type NavigatorSpec } from '@ankhorage/contracts';\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\nexport interface ResolvedInfraProject {\n readonly appsRoot: string;\n readonly manifest: AppManifest;\n readonly manifestPath: string;\n readonly projectId: string;\n readonly projectPath: string;\n readonly workspaceRoot: string;\n}\n\nexport async function resolveInfraProject(options: {\n readonly cwd: string;\n readonly projectId?: string;\n}): Promise<ResolvedInfraProject> {\n const workspaceRoot = await findWorkspaceRoot(options.cwd);\n if (workspaceRoot === null) {\n throw new Error(`Could not find an Ankh workspace root from cwd: ${options.cwd}`);\n }\n\n const appsRoot = path.join(workspaceRoot, 'apps');\n const projectId =\n options.projectId === undefined\n ? inferProjectId(options.cwd, appsRoot)\n : validateExplicitProjectId(options.projectId);\n if (projectId === null) {\n throw new Error(\n 'Could not infer a project from cwd. Pass a project id or run the command inside apps/<project>.',\n );\n }\n\n const projectPath = resolveProjectPath(appsRoot, projectId);\n if (!(await pathExists(projectPath))) {\n throw new Error(`Project '${projectId}' not found.`);\n }\n\n const manifestPath = path.join(projectPath, 'ankh.config.json');\n const manifest = await readProjectManifest({\n manifestPath,\n projectId,\n projectPath,\n });\n\n return {\n appsRoot,\n manifest,\n manifestPath,\n projectId,\n projectPath,\n workspaceRoot,\n };\n}\n\nfunction resolveProjectPath(appsRoot: string, projectId: string): string {\n const projectPath = path.resolve(appsRoot, projectId);\n const appsRootPath = `${path.resolve(appsRoot)}${path.sep}`;\n\n if (!projectPath.startsWith(appsRootPath)) {\n throw new Error('Security check failed while resolving the project path.');\n }\n\n return projectPath;\n}\n\nfunction createFallbackManifest(projectId: string): AppManifest {\n return {\n metadata: {\n name: projectId,\n slug: projectId,\n version: '0.0.0',\n category: 'developer_tools',\n themeId: 'default',\n },\n themes: [],\n activeThemeId: 'default',\n infra: {\n modules: [],\n },\n navigator: {\n type: 'stack',\n routes: [{ name: 'index', screenId: 'index' }],\n },\n screens: {},\n settings: {\n localization: {\n defaultLocale: 'en',\n locales: ['en'],\n },\n },\n };\n}\n\nasync function readProjectManifest(args: {\n readonly manifestPath: string;\n readonly projectId: string;\n readonly projectPath: string;\n}): Promise<AppManifest> {\n if (!(await pathExists(args.manifestPath))) {\n return createFallbackManifest(args.projectId);\n }\n\n const rawManifest = await fs.readFile(args.manifestPath, 'utf8');\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawManifest);\n } catch (error) {\n throw new Error(\n `Project manifest is not valid JSON: ${args.manifestPath}: ${getErrorMessage(error)}`,\n { cause: error },\n );\n }\n\n if (!isAppManifest(parsed)) {\n throw new Error(`Project manifest has an invalid shape: ${args.manifestPath}`);\n }\n\n return parsed;\n}\n\nasync function findWorkspaceRoot(startPath: string): Promise<string | null> {\n let currentPath = path.resolve(startPath);\n\n for (;;) {\n const appsRoot = path.join(currentPath, 'apps');\n if (await isDirectory(appsRoot)) {\n return currentPath;\n }\n\n const parentPath = path.dirname(currentPath);\n if (parentPath === currentPath) {\n return null;\n }\n\n currentPath = parentPath;\n }\n}\n\nfunction inferProjectId(cwd: string, appsRoot: string): string | null {\n const relativePath = path.relative(path.resolve(appsRoot), path.resolve(cwd));\n if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {\n return null;\n }\n\n const [projectId] = relativePath.split(path.sep);\n return projectId === undefined || projectId.length === 0 ? null : path.basename(projectId);\n}\n\nfunction validateExplicitProjectId(projectId: string): string {\n if (projectId.trim().length === 0) {\n throw new Error('Invalid project id: expected a single safe project segment.');\n }\n\n if (\n projectId === '.' ||\n projectId === '..' ||\n path.isAbsolute(projectId) ||\n projectId.includes('/') ||\n projectId.includes('\\\\')\n ) {\n throw new Error(`Invalid project id: ${projectId}. Expected a single safe project segment.`);\n }\n\n return projectId;\n}\n\nfunction isAppManifest(value: unknown): value is AppManifest {\n if (!isRecord(value)) {\n return false;\n }\n\n return (\n isAppManifestMetadata(value.metadata) &&\n Array.isArray(value.themes) &&\n typeof value.activeThemeId === 'string' &&\n isInfraManifestRecord(value.infra) &&\n isNavigatorSpec(value.navigator) &&\n isRecord(value.screens) &&\n isSettingsRecord(value.settings)\n );\n}\n\nfunction isAppManifestMetadata(value: unknown): value is AppManifest['metadata'] {\n return (\n isRecord(value) &&\n typeof value.name === 'string' &&\n typeof value.slug === 'string' &&\n typeof value.version === 'string' &&\n APP_CATEGORIES.some((category) => category === value.category) &&\n typeof value.themeId === 'string' &&\n (value.created === undefined || typeof value.created === 'string') &&\n (value.updated === undefined || typeof value.updated === 'string')\n );\n}\n\nfunction isInfraManifestRecord(value: unknown): value is AppManifest['infra'] {\n return isRecord(value) && Array.isArray(value.modules);\n}\n\nfunction isNavigatorSpec(value: unknown): value is NavigatorSpec {\n return (\n isRecord(value) &&\n typeof value.type === 'string' &&\n Array.isArray(value.routes) &&\n (value.initialRouteName === undefined || typeof value.initialRouteName === 'string')\n );\n}\n\nfunction isSettingsRecord(value: unknown): value is AppManifest['settings'] {\n return (\n isRecord(value) &&\n (value.apiBaseUrl === undefined || typeof value.apiBaseUrl === 'string') &&\n isLocalizationRecord(value.localization)\n );\n}\n\nfunction isLocalizationRecord(value: unknown): value is AppManifest['settings']['localization'] {\n return (\n isRecord(value) &&\n typeof value.defaultLocale === 'string' &&\n Array.isArray(value.locales) &&\n value.locales.every((locale) => typeof locale === 'string')\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nasync function isDirectory(filePath: string): Promise<boolean> {\n try {\n const stat = await fs.stat(filePath);\n return stat.isDirectory();\n } catch {\n return false;\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { type AppManifest, isAppManifest } from '@ankhorage/contracts';\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\nexport interface ResolvedInfraProject {\n readonly appsRoot: string;\n readonly manifest: AppManifest;\n readonly manifestPath: string;\n readonly projectId: string;\n readonly projectPath: string;\n readonly workspaceRoot: string;\n}\n\nexport async function resolveInfraProject(options: {\n readonly cwd: string;\n readonly projectId?: string;\n}): Promise<ResolvedInfraProject> {\n const workspaceRoot = await findWorkspaceRoot(options.cwd);\n if (workspaceRoot === null) {\n throw new Error(`Could not find an Ankh workspace root from cwd: ${options.cwd}`);\n }\n\n const appsRoot = path.join(workspaceRoot, 'apps');\n const projectId =\n options.projectId === undefined\n ? inferProjectId(options.cwd, appsRoot)\n : validateExplicitProjectId(options.projectId);\n if (projectId === null) {\n throw new Error(\n 'Could not infer a project from cwd. Pass a project id or run the command inside apps/<project>.',\n );\n }\n\n const projectPath = resolveProjectPath(appsRoot, projectId);\n if (!(await pathExists(projectPath))) {\n throw new Error(`Project '${projectId}' not found.`);\n }\n\n const manifestPath = path.join(projectPath, 'ankh.config.json');\n const manifest = await readProjectManifest({ manifestPath, projectId });\n\n return {\n appsRoot,\n manifest,\n manifestPath,\n projectId,\n projectPath,\n workspaceRoot,\n };\n}\n\nfunction resolveProjectPath(appsRoot: string, projectId: string): string {\n const projectPath = path.resolve(appsRoot, projectId);\n const appsRootPath = `${path.resolve(appsRoot)}${path.sep}`;\n\n if (!projectPath.startsWith(appsRootPath)) {\n throw new Error('Security check failed while resolving the project path.');\n }\n\n return projectPath;\n}\n\nfunction createFallbackManifest(projectId: string): AppManifest {\n return {\n metadata: {\n name: projectId,\n slug: projectId,\n version: '0.0.0',\n category: 'developer_tools',\n themeId: 'default',\n },\n themes: [],\n activeThemeId: 'default',\n infra: { modules: [] },\n navigator: {\n type: 'stack',\n routes: [{ name: 'index', screenId: 'index' }],\n },\n screens: {},\n settings: {\n localization: {\n defaultLocale: 'en',\n locales: ['en'],\n },\n },\n };\n}\n\nasync function readProjectManifest(args: {\n readonly manifestPath: string;\n readonly projectId: string;\n}): Promise<AppManifest> {\n if (!(await pathExists(args.manifestPath))) {\n return createFallbackManifest(args.projectId);\n }\n\n const rawManifest = await fs.readFile(args.manifestPath, 'utf8');\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(rawManifest);\n } catch (error) {\n throw new Error(\n `Project manifest is not valid JSON: ${args.manifestPath}: ${getErrorMessage(error)}`,\n { cause: error },\n );\n }\n\n if (!isAppManifest(parsed)) {\n throw new Error(`Project manifest has an invalid shape: ${args.manifestPath}`);\n }\n\n return parsed;\n}\n\nasync function findWorkspaceRoot(startPath: string): Promise<string | null> {\n let currentPath = path.resolve(startPath);\n\n for (;;) {\n const appsRoot = path.join(currentPath, 'apps');\n if (await isDirectory(appsRoot)) return currentPath;\n\n const parentPath = path.dirname(currentPath);\n if (parentPath === currentPath) return null;\n currentPath = parentPath;\n }\n}\n\nfunction inferProjectId(cwd: string, appsRoot: string): string | null {\n const relativePath = path.relative(path.resolve(appsRoot), path.resolve(cwd));\n if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {\n return null;\n }\n\n const [projectId] = relativePath.split(path.sep);\n return projectId === undefined || projectId.length === 0 ? null : path.basename(projectId);\n}\n\nfunction validateExplicitProjectId(projectId: string): string {\n if (projectId.trim().length === 0) {\n throw new Error('Invalid project id: expected a single safe project segment.');\n }\n\n if (\n projectId === '.' ||\n projectId === '..' ||\n path.isAbsolute(projectId) ||\n projectId.includes('/') ||\n projectId.includes('\\\\')\n ) {\n throw new Error(`Invalid project id: ${projectId}. Expected a single safe project segment.`);\n }\n\n return projectId;\n}\n\nasync function isDirectory(filePath: string): Promise<boolean> {\n try {\n const stat = await fs.stat(filePath);\n return stat.isDirectory();\n } catch {\n return false;\n }\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
@@ -1,10 +1,16 @@
1
1
  import { runCli } from './cli/bin.js';
2
2
  /***
3
- * Provider and CLI surface
3
+ * Provider, CLI, and project API surface
4
4
  *
5
- * `@ankhorage/infra` owns infra command behavior.
5
+ * `@ankhorage/infra` owns infrastructure generation, project reconciliation,
6
+ * and generated lifecycle execution.
6
7
  *
7
- * The same shared command implementation backs both:
8
+ * The same owner implementation backs CLI consumers and the public
9
+ * `@ankhorage/infra/project` application-service boundary. Trusted hosts such
10
+ * as Studio should call that subpath instead of reproducing ledger, generated
11
+ * file, lifecycle-script, or port-forward configuration semantics.
12
+ *
13
+ * CLI entrypoints:
8
14
  *
9
15
  * - `ankh infra ...`
10
16
  * - `bunx @ankhorage/infra ...`
@@ -17,12 +23,10 @@ import { runCli } from './cli/bin.js';
17
23
  * - `up`
18
24
  * - `down`
19
25
  *
20
- * `status` runs the generated live runtime status script for a project.
21
- *
22
- * Project resolution is project-aware:
23
- *
24
- * - pass `[project]`, or
25
- * - omit it when cwd is already inside `apps/<project>`
26
+ * Project resolution for the CLI is workspace-aware: pass `[project]`, or
27
+ * omit it when cwd is already inside `apps/<project>`. Programmatic project
28
+ * operations accept an explicit project path and do not require Studio or a
29
+ * particular workspace layout.
26
30
  *
27
31
  * @usage
28
32
  */
@@ -1 +1 @@
1
- {"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC","sourcesContent":["import { runCli } from './cli/bin.js';\n\n/***\n * Provider and CLI surface\n *\n * `@ankhorage/infra` owns infra command behavior.\n *\n * The same shared command implementation backs both:\n *\n * - `ankh infra ...`\n * - `bunx @ankhorage/infra ...`\n *\n * Current command surface:\n *\n * - `validate`\n * - `generate`\n * - `status`\n * - `up`\n * - `down`\n *\n * `status` runs the generated live runtime status script for a project.\n *\n * Project resolution is project-aware:\n *\n * - pass `[project]`, or\n * - omit it when cwd is already inside `apps/<project>`\n *\n * @usage\n */\nawait runCli(['--help']);\n"]}
1
+ {"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC","sourcesContent":["import { runCli } from './cli/bin.js';\n\n/***\n * Provider, CLI, and project API surface\n *\n * `@ankhorage/infra` owns infrastructure generation, project reconciliation,\n * and generated lifecycle execution.\n *\n * The same owner implementation backs CLI consumers and the public\n * `@ankhorage/infra/project` application-service boundary. Trusted hosts such\n * as Studio should call that subpath instead of reproducing ledger, generated\n * file, lifecycle-script, or port-forward configuration semantics.\n *\n * CLI entrypoints:\n *\n * - `ankh infra ...`\n * - `bunx @ankhorage/infra ...`\n *\n * Current command surface:\n *\n * - `validate`\n * - `generate`\n * - `status`\n * - `up`\n * - `down`\n *\n * Project resolution for the CLI is workspace-aware: pass `[project]`, or\n * omit it when cwd is already inside `apps/<project>`. Programmatic project\n * operations accept an explicit project path and do not require Studio or a\n * particular workspace layout.\n *\n * @usage\n */\nawait runCli(['--help']);\n"]}
package/dist/runtime.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import type { InfraCommandContext } from './commandContext.js';
2
- export type InfraLifecycleScript = 'down' | 'status' | 'up';
2
+ export type InfraLifecycleScript = 'destroy' | 'down' | 'port-forward' | 'reset' | 'status' | 'up';
3
+ export interface InfraScriptOutput {
4
+ readonly stderr: string;
5
+ readonly stdout: string;
6
+ }
7
+ export interface InfraPortForwardInfo {
8
+ readonly localPort: number;
9
+ readonly url: string;
10
+ }
3
11
  export declare class InfraScriptExecutionError extends Error {
4
12
  readonly exitCode: number | null;
5
13
  readonly stderr: string;
@@ -11,6 +19,20 @@ export declare class InfraScriptExecutionError extends Error {
11
19
  readonly stdout: string;
12
20
  });
13
21
  }
22
+ export declare function runProjectInfrastructureLifecycle(args: {
23
+ readonly args?: readonly string[];
24
+ readonly env?: Readonly<Record<string, string | undefined>>;
25
+ readonly onStderr?: (chunk: string) => void;
26
+ readonly onStdout?: (chunk: string) => void;
27
+ readonly projectId: string;
28
+ readonly projectPath: string;
29
+ readonly script: InfraLifecycleScript;
30
+ readonly target: string;
31
+ }): Promise<InfraScriptOutput>;
32
+ export declare function resolveProjectInfrastructurePortForward(args: {
33
+ readonly projectPath: string;
34
+ readonly target: string;
35
+ }): Promise<InfraPortForwardInfo>;
14
36
  export declare function runProjectInfraScript(args: {
15
37
  readonly context: InfraCommandContext;
16
38
  readonly projectId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AAE5D,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE;QAChB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB;CAOF;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,CAET"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEnG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE;QAChB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB;CAOF;AAED,wBAAsB,iCAAiC,CAAC,IAAI,EAAE;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAe7B;AAED,wBAAsB,uCAAuC,CAAC,IAAI,EAAE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkBhC;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,CAET"}
package/dist/runtime.js CHANGED
@@ -13,16 +13,45 @@ export class InfraScriptExecutionError extends Error {
13
13
  this.stdout = args.stdout;
14
14
  }
15
15
  }
16
- export async function runProjectInfraScript(args) {
16
+ export async function runProjectInfrastructureLifecycle(args) {
17
17
  const scriptPath = resolveProjectInfraScriptPath(args);
18
18
  if (!(await pathExists(scriptPath))) {
19
19
  throw new Error(`Infra script not found: ${scriptPath}. Run infra generate for project '${args.projectId}' first.`);
20
20
  }
21
- await runShellScript({
22
- context: args.context,
21
+ return runShellScript({
22
+ args: args.args ?? [],
23
+ env: args.env ?? process.env,
24
+ onStderr: args.onStderr,
25
+ onStdout: args.onStdout,
23
26
  scriptPath,
24
27
  });
25
28
  }
29
+ export async function resolveProjectInfrastructurePortForward(args) {
30
+ const infraRoot = path.join(args.projectPath, 'infra', args.target);
31
+ const envPath = path.join(infraRoot, '.env');
32
+ const fallbackEnvPath = path.join(infraRoot, '.env.example');
33
+ const sourcePath = (await pathExists(envPath)) ? envPath : fallbackEnvPath;
34
+ const env = await readSimpleEnvMap(sourcePath);
35
+ const localPort = parsePositivePort(env.get('APP_PORT_FORWARD_LOCAL_PORT'));
36
+ if (localPort === null) {
37
+ throw new Error(`Generated Infra did not provide a valid APP_PORT_FORWARD_LOCAL_PORT in ${sourcePath}. Regenerate infrastructure first.`);
38
+ }
39
+ return {
40
+ localPort,
41
+ url: `http://127.0.0.1:${localPort}`,
42
+ };
43
+ }
44
+ export async function runProjectInfraScript(args) {
45
+ await runProjectInfrastructureLifecycle({
46
+ env: args.context.env,
47
+ onStderr: (chunk) => args.context.writeStderr(chunk),
48
+ onStdout: (chunk) => args.context.writeStdout(chunk),
49
+ projectId: args.projectId,
50
+ projectPath: args.projectPath,
51
+ script: args.script,
52
+ target: args.target,
53
+ });
54
+ }
26
55
  export function resolveProjectInfraScriptPath(args) {
27
56
  return path.join(args.projectPath, getInfraScriptsDirectory(args.target), `${args.script}.sh`);
28
57
  }
@@ -35,46 +64,75 @@ function getInfraScriptsDirectory(target) {
35
64
  }
36
65
  }
37
66
  async function runShellScript(args) {
38
- await new Promise((resolve, reject) => {
39
- const child = spawn('bash', [args.scriptPath], {
67
+ return new Promise((resolve, reject) => {
68
+ const child = spawn('bash', [args.scriptPath, ...args.args], {
40
69
  cwd: path.dirname(args.scriptPath),
41
- env: { ...args.context.env },
70
+ env: { ...args.env },
42
71
  stdio: ['ignore', 'pipe', 'pipe'],
43
72
  });
44
73
  let stdout = '';
45
74
  let stderr = '';
46
75
  child.stdout.setEncoding('utf8');
76
+ child.stderr.setEncoding('utf8');
47
77
  child.stdout.on('data', (chunk) => {
48
78
  stdout += chunk;
49
- args.context.writeStdout(chunk);
79
+ args.onStdout?.(chunk);
50
80
  });
51
- child.stderr.setEncoding('utf8');
52
81
  child.stderr.on('data', (chunk) => {
53
82
  stderr += chunk;
54
- args.context.writeStderr(chunk);
55
- });
56
- child.once('error', (error) => {
57
- reject(new InfraScriptExecutionError({
58
- exitCode: null,
59
- message: `Failed to start infra script '${args.scriptPath}': ${error.message}`,
60
- stderr,
61
- stdout,
62
- }));
83
+ args.onStderr?.(chunk);
63
84
  });
85
+ child.once('error', (error) => reject(createStartError(args.scriptPath, error, stdout, stderr)));
64
86
  child.once('close', (exitCode) => {
65
87
  if (exitCode === 0) {
66
- resolve();
88
+ resolve({ stderr, stdout });
67
89
  return;
68
90
  }
69
- reject(new InfraScriptExecutionError({
70
- exitCode,
71
- message: `Infra script '${args.scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,
72
- stderr,
73
- stdout,
74
- }));
91
+ reject(createExitError(args.scriptPath, exitCode, stdout, stderr));
75
92
  });
76
93
  });
77
94
  }
95
+ function createStartError(scriptPath, error, stdout, stderr) {
96
+ return new InfraScriptExecutionError({
97
+ exitCode: null,
98
+ message: `Failed to start infra script '${scriptPath}': ${error.message}`,
99
+ stderr,
100
+ stdout,
101
+ });
102
+ }
103
+ function createExitError(scriptPath, exitCode, stdout, stderr) {
104
+ return new InfraScriptExecutionError({
105
+ exitCode,
106
+ message: `Infra script '${scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,
107
+ stderr,
108
+ stdout,
109
+ });
110
+ }
111
+ async function readSimpleEnvMap(filePath) {
112
+ const out = new Map();
113
+ const content = await fs.readFile(filePath, 'utf8');
114
+ for (const line of content.split(/\r?\n/u)) {
115
+ const trimmed = line.trim();
116
+ if (!trimmed || trimmed.startsWith('#'))
117
+ continue;
118
+ const separator = trimmed.indexOf('=');
119
+ if (separator <= 0)
120
+ continue;
121
+ const key = trimmed.slice(0, separator).trim();
122
+ const value = trimmed
123
+ .slice(separator + 1)
124
+ .trim()
125
+ .replace(/^['"]|['"]$/g, '');
126
+ out.set(key, value);
127
+ }
128
+ return out;
129
+ }
130
+ function parsePositivePort(value) {
131
+ if (value === undefined)
132
+ return null;
133
+ const parsed = Number.parseInt(value.trim(), 10);
134
+ return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535 ? parsed : null;
135
+ }
78
136
  async function pathExists(filePath) {
79
137
  try {
80
138
  await fs.access(filePath);
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'child_process';\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\nimport type { InfraCommandContext } from './commandContext.js';\n\nexport type InfraLifecycleScript = 'down' | 'status' | 'up';\n\nexport class InfraScriptExecutionError extends Error {\n readonly exitCode: number | null;\n readonly stderr: string;\n readonly stdout: string;\n\n constructor(args: {\n readonly exitCode: number | null;\n readonly message: string;\n readonly stderr: string;\n readonly stdout: string;\n }) {\n super(args.message);\n this.name = 'InfraScriptExecutionError';\n this.exitCode = args.exitCode;\n this.stderr = args.stderr;\n this.stdout = args.stdout;\n }\n}\n\nexport async function runProjectInfraScript(args: {\n readonly context: InfraCommandContext;\n readonly projectId: string;\n readonly projectPath: string;\n readonly script: InfraLifecycleScript;\n readonly target: string;\n}): Promise<void> {\n const scriptPath = resolveProjectInfraScriptPath(args);\n if (!(await pathExists(scriptPath))) {\n throw new Error(\n `Infra script not found: ${scriptPath}. Run infra generate for project '${args.projectId}' first.`,\n );\n }\n\n await runShellScript({\n context: args.context,\n scriptPath,\n });\n}\n\nexport function resolveProjectInfraScriptPath(args: {\n readonly projectPath: string;\n readonly script: InfraLifecycleScript;\n readonly target: string;\n}): string {\n return path.join(args.projectPath, getInfraScriptsDirectory(args.target), `${args.script}.sh`);\n}\n\nfunction getInfraScriptsDirectory(target: string): string {\n switch (target) {\n case 'minikube':\n return 'infra/minikube/scripts';\n default:\n throw new Error(`Unsupported deployment target for infra scripts: ${target}`);\n }\n}\n\nasync function runShellScript(args: {\n readonly context: InfraCommandContext;\n readonly scriptPath: string;\n}): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const child = spawn('bash', [args.scriptPath], {\n cwd: path.dirname(args.scriptPath),\n env: { ...args.context.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n\n child.stdout.setEncoding('utf8');\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n args.context.writeStdout(chunk);\n });\n\n child.stderr.setEncoding('utf8');\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n args.context.writeStderr(chunk);\n });\n\n child.once('error', (error) => {\n reject(\n new InfraScriptExecutionError({\n exitCode: null,\n message: `Failed to start infra script '${args.scriptPath}': ${error.message}`,\n stderr,\n stdout,\n }),\n );\n });\n\n child.once('close', (exitCode) => {\n if (exitCode === 0) {\n resolve();\n return;\n }\n\n reject(\n new InfraScriptExecutionError({\n exitCode,\n message: `Infra script '${args.scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,\n stderr,\n stdout,\n }),\n );\n });\n });\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'child_process';\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\nimport type { InfraCommandContext } from './commandContext.js';\n\nexport type InfraLifecycleScript = 'destroy' | 'down' | 'port-forward' | 'reset' | 'status' | 'up';\n\nexport interface InfraScriptOutput {\n readonly stderr: string;\n readonly stdout: string;\n}\n\nexport interface InfraPortForwardInfo {\n readonly localPort: number;\n readonly url: string;\n}\n\nexport class InfraScriptExecutionError extends Error {\n readonly exitCode: number | null;\n readonly stderr: string;\n readonly stdout: string;\n\n constructor(args: {\n readonly exitCode: number | null;\n readonly message: string;\n readonly stderr: string;\n readonly stdout: string;\n }) {\n super(args.message);\n this.name = 'InfraScriptExecutionError';\n this.exitCode = args.exitCode;\n this.stderr = args.stderr;\n this.stdout = args.stdout;\n }\n}\n\nexport async function runProjectInfrastructureLifecycle(args: {\n readonly args?: readonly string[];\n readonly env?: Readonly<Record<string, string | undefined>>;\n readonly onStderr?: (chunk: string) => void;\n readonly onStdout?: (chunk: string) => void;\n readonly projectId: string;\n readonly projectPath: string;\n readonly script: InfraLifecycleScript;\n readonly target: string;\n}): Promise<InfraScriptOutput> {\n const scriptPath = resolveProjectInfraScriptPath(args);\n if (!(await pathExists(scriptPath))) {\n throw new Error(\n `Infra script not found: ${scriptPath}. Run infra generate for project '${args.projectId}' first.`,\n );\n }\n\n return runShellScript({\n args: args.args ?? [],\n env: args.env ?? process.env,\n onStderr: args.onStderr,\n onStdout: args.onStdout,\n scriptPath,\n });\n}\n\nexport async function resolveProjectInfrastructurePortForward(args: {\n readonly projectPath: string;\n readonly target: string;\n}): Promise<InfraPortForwardInfo> {\n const infraRoot = path.join(args.projectPath, 'infra', args.target);\n const envPath = path.join(infraRoot, '.env');\n const fallbackEnvPath = path.join(infraRoot, '.env.example');\n const sourcePath = (await pathExists(envPath)) ? envPath : fallbackEnvPath;\n const env = await readSimpleEnvMap(sourcePath);\n const localPort = parsePositivePort(env.get('APP_PORT_FORWARD_LOCAL_PORT'));\n\n if (localPort === null) {\n throw new Error(\n `Generated Infra did not provide a valid APP_PORT_FORWARD_LOCAL_PORT in ${sourcePath}. Regenerate infrastructure first.`,\n );\n }\n\n return {\n localPort,\n url: `http://127.0.0.1:${localPort}`,\n };\n}\n\nexport async function runProjectInfraScript(args: {\n readonly context: InfraCommandContext;\n readonly projectId: string;\n readonly projectPath: string;\n readonly script: InfraLifecycleScript;\n readonly target: string;\n}): Promise<void> {\n await runProjectInfrastructureLifecycle({\n env: args.context.env,\n onStderr: (chunk) => args.context.writeStderr(chunk),\n onStdout: (chunk) => args.context.writeStdout(chunk),\n projectId: args.projectId,\n projectPath: args.projectPath,\n script: args.script,\n target: args.target,\n });\n}\n\nexport function resolveProjectInfraScriptPath(args: {\n readonly projectPath: string;\n readonly script: InfraLifecycleScript;\n readonly target: string;\n}): string {\n return path.join(args.projectPath, getInfraScriptsDirectory(args.target), `${args.script}.sh`);\n}\n\nfunction getInfraScriptsDirectory(target: string): string {\n switch (target) {\n case 'minikube':\n return 'infra/minikube/scripts';\n default:\n throw new Error(`Unsupported deployment target for infra scripts: ${target}`);\n }\n}\n\nasync function runShellScript(args: {\n readonly args: readonly string[];\n readonly env: Readonly<Record<string, string | undefined>>;\n readonly onStderr?: (chunk: string) => void;\n readonly onStdout?: (chunk: string) => void;\n readonly scriptPath: string;\n}): Promise<InfraScriptOutput> {\n return new Promise<InfraScriptOutput>((resolve, reject) => {\n const child = spawn('bash', [args.scriptPath, ...args.args], {\n cwd: path.dirname(args.scriptPath),\n env: { ...args.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n child.stdout.setEncoding('utf8');\n child.stderr.setEncoding('utf8');\n child.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n args.onStdout?.(chunk);\n });\n child.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n args.onStderr?.(chunk);\n });\n child.once('error', (error) =>\n reject(createStartError(args.scriptPath, error, stdout, stderr)),\n );\n child.once('close', (exitCode) => {\n if (exitCode === 0) {\n resolve({ stderr, stdout });\n return;\n }\n reject(createExitError(args.scriptPath, exitCode, stdout, stderr));\n });\n });\n}\n\nfunction createStartError(\n scriptPath: string,\n error: Error,\n stdout: string,\n stderr: string,\n): InfraScriptExecutionError {\n return new InfraScriptExecutionError({\n exitCode: null,\n message: `Failed to start infra script '${scriptPath}': ${error.message}`,\n stderr,\n stdout,\n });\n}\n\nfunction createExitError(\n scriptPath: string,\n exitCode: number | null,\n stdout: string,\n stderr: string,\n): InfraScriptExecutionError {\n return new InfraScriptExecutionError({\n exitCode,\n message: `Infra script '${scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,\n stderr,\n stdout,\n });\n}\n\nasync function readSimpleEnvMap(filePath: string): Promise<Map<string, string>> {\n const out = new Map<string, string>();\n const content = await fs.readFile(filePath, 'utf8');\n for (const line of content.split(/\\r?\\n/u)) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const separator = trimmed.indexOf('=');\n if (separator <= 0) continue;\n const key = trimmed.slice(0, separator).trim();\n const value = trimmed\n .slice(separator + 1)\n .trim()\n .replace(/^['\"]|['\"]$/g, '');\n out.set(key, value);\n }\n return out;\n}\n\nfunction parsePositivePort(value: string | undefined): number | null {\n if (value === undefined) return null;\n const parsed = Number.parseInt(value.trim(), 10);\n return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535 ? parsed : null;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "3.0.0",
4
+ "version": "3.1.1",
5
5
  "description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
6
6
  "homepage": "https://github.com/ankhorage/infra#readme",
7
7
  "bugs": {
@@ -35,7 +35,7 @@
35
35
  "openapi"
36
36
  ],
37
37
  "dependencies": {
38
- "@ankhorage/contracts": "^6.0.0",
38
+ "@ankhorage/contracts": "^7.2.0",
39
39
  "@ankhorage/supabase-vault": "^0.2.4"
40
40
  },
41
41
  "files": [
@@ -62,6 +62,11 @@
62
62
  "import": "./dist/metadata/index.js",
63
63
  "default": "./dist/metadata/index.js"
64
64
  },
65
+ "./project": {
66
+ "types": "./dist/project/index.d.ts",
67
+ "import": "./dist/project/index.js",
68
+ "default": "./dist/project/index.js"
69
+ },
65
70
  "./package.json": "./package.json"
66
71
  },
67
72
  "bin": {
@@ -24,6 +24,11 @@ describe('package metadata', () => {
24
24
  'ankhorage-infra': './dist/cli/bin.js',
25
25
  });
26
26
  expect(packageJson.ankh).toEqual(expectedAnkhMetadata);
27
+ expect(packageJson.exports['./project']).toEqual({
28
+ types: './dist/project/index.d.ts',
29
+ import: './dist/project/index.js',
30
+ default: './dist/project/index.js',
31
+ });
27
32
  expect(JSON.parse(JSON.stringify(expectedAnkhMetadata))).toEqual(expectedAnkhMetadata);
28
33
 
29
34
  const capabilityText = JSON.stringify(packageJson.ankh.capabilities);
@@ -0,0 +1,70 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { promises as fs } from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import {
7
+ resolveProjectInfrastructurePortForward,
8
+ runProjectInfrastructureLifecycle,
9
+ } from './index.js';
10
+
11
+ const temporaryPaths: string[] = [];
12
+
13
+ afterEach(async () => {
14
+ await Promise.all(
15
+ temporaryPaths.splice(0).map((entry) => fs.rm(entry, { force: true, recursive: true })),
16
+ );
17
+ });
18
+
19
+ async function createProjectFixture(): Promise<string> {
20
+ const projectPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ankh-infra-project-'));
21
+ temporaryPaths.push(projectPath);
22
+ const infraRoot = path.join(projectPath, 'infra', 'minikube');
23
+ const scriptsRoot = path.join(infraRoot, 'scripts');
24
+ await fs.mkdir(scriptsRoot, { recursive: true });
25
+ await fs.writeFile(
26
+ path.join(scriptsRoot, 'status.sh'),
27
+ "#!/usr/bin/env bash\nprintf 'status-ok'\nprintf 'status-warning' >&2\n",
28
+ 'utf8',
29
+ );
30
+ await fs.writeFile(
31
+ path.join(infraRoot, '.env.example'),
32
+ 'APP_PORT_FORWARD_LOCAL_PORT=48123\n',
33
+ 'utf8',
34
+ );
35
+ return projectPath;
36
+ }
37
+
38
+ describe('@ankhorage/infra/project', () => {
39
+ test('executes generated lifecycle scripts without exposing script paths to consumers', async () => {
40
+ const projectPath = await createProjectFixture();
41
+ const stdout: string[] = [];
42
+ const stderr: string[] = [];
43
+
44
+ const result = await runProjectInfrastructureLifecycle({
45
+ projectId: 'example',
46
+ projectPath,
47
+ script: 'status',
48
+ target: 'minikube',
49
+ onStdout: (chunk) => stdout.push(chunk),
50
+ onStderr: (chunk) => stderr.push(chunk),
51
+ });
52
+
53
+ expect(result).toEqual({ stdout: 'status-ok', stderr: 'status-warning' });
54
+ expect(stdout.join('')).toBe('status-ok');
55
+ expect(stderr.join('')).toBe('status-warning');
56
+ });
57
+
58
+ test('resolves the generated app port-forward endpoint through the owner API', async () => {
59
+ const projectPath = await createProjectFixture();
60
+ const result = await resolveProjectInfrastructurePortForward({
61
+ projectPath,
62
+ target: 'minikube',
63
+ });
64
+
65
+ expect(result).toEqual({
66
+ localPort: 48123,
67
+ url: 'http://127.0.0.1:48123',
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,17 @@
1
+ export {
2
+ type ResolvedInfraProject,
3
+ resolveInfraProject as resolveInfraWorkspaceProject,
4
+ } from '../project.js';
5
+ export {
6
+ type InfraSyncResult,
7
+ resolveProjectInfrastructureTarget,
8
+ syncProjectInfrastructure,
9
+ } from '../projectInfrastructure.js';
10
+ export {
11
+ type InfraLifecycleScript,
12
+ type InfraPortForwardInfo,
13
+ InfraScriptExecutionError,
14
+ type InfraScriptOutput,
15
+ resolveProjectInfrastructurePortForward,
16
+ runProjectInfrastructureLifecycle,
17
+ } from '../runtime.js';
package/src/project.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { APP_CATEGORIES, type AppManifest, type NavigatorSpec } from '@ankhorage/contracts';
1
+ import { type AppManifest, isAppManifest } from '@ankhorage/contracts';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
 
@@ -37,11 +37,7 @@ export async function resolveInfraProject(options: {
37
37
  }
38
38
 
39
39
  const manifestPath = path.join(projectPath, 'ankh.config.json');
40
- const manifest = await readProjectManifest({
41
- manifestPath,
42
- projectId,
43
- projectPath,
44
- });
40
+ const manifest = await readProjectManifest({ manifestPath, projectId });
45
41
 
46
42
  return {
47
43
  appsRoot,
@@ -75,9 +71,7 @@ function createFallbackManifest(projectId: string): AppManifest {
75
71
  },
76
72
  themes: [],
77
73
  activeThemeId: 'default',
78
- infra: {
79
- modules: [],
80
- },
74
+ infra: { modules: [] },
81
75
  navigator: {
82
76
  type: 'stack',
83
77
  routes: [{ name: 'index', screenId: 'index' }],
@@ -95,15 +89,14 @@ function createFallbackManifest(projectId: string): AppManifest {
95
89
  async function readProjectManifest(args: {
96
90
  readonly manifestPath: string;
97
91
  readonly projectId: string;
98
- readonly projectPath: string;
99
92
  }): Promise<AppManifest> {
100
93
  if (!(await pathExists(args.manifestPath))) {
101
94
  return createFallbackManifest(args.projectId);
102
95
  }
103
96
 
104
97
  const rawManifest = await fs.readFile(args.manifestPath, 'utf8');
105
-
106
98
  let parsed: unknown;
99
+
107
100
  try {
108
101
  parsed = JSON.parse(rawManifest);
109
102
  } catch (error) {
@@ -125,15 +118,10 @@ async function findWorkspaceRoot(startPath: string): Promise<string | null> {
125
118
 
126
119
  for (;;) {
127
120
  const appsRoot = path.join(currentPath, 'apps');
128
- if (await isDirectory(appsRoot)) {
129
- return currentPath;
130
- }
121
+ if (await isDirectory(appsRoot)) return currentPath;
131
122
 
132
123
  const parentPath = path.dirname(currentPath);
133
- if (parentPath === currentPath) {
134
- return null;
135
- }
136
-
124
+ if (parentPath === currentPath) return null;
137
125
  currentPath = parentPath;
138
126
  }
139
127
  }
@@ -166,69 +154,6 @@ function validateExplicitProjectId(projectId: string): string {
166
154
  return projectId;
167
155
  }
168
156
 
169
- function isAppManifest(value: unknown): value is AppManifest {
170
- if (!isRecord(value)) {
171
- return false;
172
- }
173
-
174
- return (
175
- isAppManifestMetadata(value.metadata) &&
176
- Array.isArray(value.themes) &&
177
- typeof value.activeThemeId === 'string' &&
178
- isInfraManifestRecord(value.infra) &&
179
- isNavigatorSpec(value.navigator) &&
180
- isRecord(value.screens) &&
181
- isSettingsRecord(value.settings)
182
- );
183
- }
184
-
185
- function isAppManifestMetadata(value: unknown): value is AppManifest['metadata'] {
186
- return (
187
- isRecord(value) &&
188
- typeof value.name === 'string' &&
189
- typeof value.slug === 'string' &&
190
- typeof value.version === 'string' &&
191
- APP_CATEGORIES.some((category) => category === value.category) &&
192
- typeof value.themeId === 'string' &&
193
- (value.created === undefined || typeof value.created === 'string') &&
194
- (value.updated === undefined || typeof value.updated === 'string')
195
- );
196
- }
197
-
198
- function isInfraManifestRecord(value: unknown): value is AppManifest['infra'] {
199
- return isRecord(value) && Array.isArray(value.modules);
200
- }
201
-
202
- function isNavigatorSpec(value: unknown): value is NavigatorSpec {
203
- return (
204
- isRecord(value) &&
205
- typeof value.type === 'string' &&
206
- Array.isArray(value.routes) &&
207
- (value.initialRouteName === undefined || typeof value.initialRouteName === 'string')
208
- );
209
- }
210
-
211
- function isSettingsRecord(value: unknown): value is AppManifest['settings'] {
212
- return (
213
- isRecord(value) &&
214
- (value.apiBaseUrl === undefined || typeof value.apiBaseUrl === 'string') &&
215
- isLocalizationRecord(value.localization)
216
- );
217
- }
218
-
219
- function isLocalizationRecord(value: unknown): value is AppManifest['settings']['localization'] {
220
- return (
221
- isRecord(value) &&
222
- typeof value.defaultLocale === 'string' &&
223
- Array.isArray(value.locales) &&
224
- value.locales.every((locale) => typeof locale === 'string')
225
- );
226
- }
227
-
228
- function isRecord(value: unknown): value is Record<string, unknown> {
229
- return typeof value === 'object' && value !== null;
230
- }
231
-
232
157
  async function isDirectory(filePath: string): Promise<boolean> {
233
158
  try {
234
159
  const stat = await fs.stat(filePath);
@@ -0,0 +1,49 @@
1
+ import { afterEach, expect, test } from 'bun:test';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+
5
+ import { resolveInfraProject } from './project.js';
6
+ import { createAppManifest, createWorkspaceFixture } from './testSupport.js';
7
+
8
+ const tempRoots = new Set<string>();
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(
12
+ [...tempRoots].map((rootPath) => fs.rm(rootPath, { force: true, recursive: true })),
13
+ );
14
+ tempRoots.clear();
15
+ });
16
+
17
+ test('Infra rejects nested manifest shapes rejected by Contracts', async () => {
18
+ const manifest = createAppManifest('cards', { modules: [] });
19
+ const fixture = await createWorkspaceFixture({ manifest, projectId: 'cards' });
20
+ tempRoots.add(fixture.rootPath);
21
+
22
+ const malformedManifest = {
23
+ ...manifest,
24
+ navigator: {
25
+ ...manifest.navigator,
26
+ type: 'not-a-canonical-navigator',
27
+ },
28
+ };
29
+ await fs.writeFile(
30
+ path.join(fixture.projectPath, 'ankh.config.json'),
31
+ JSON.stringify(malformedManifest),
32
+ 'utf8',
33
+ );
34
+
35
+ const error = await captureError(
36
+ resolveInfraProject({ cwd: fixture.rootPath, projectId: fixture.projectId }),
37
+ );
38
+ expect(error.message).toContain('Project manifest has an invalid shape');
39
+ });
40
+
41
+ async function captureError(promise: Promise<unknown>): Promise<Error> {
42
+ try {
43
+ await promise;
44
+ } catch (error) {
45
+ if (error instanceof Error) return error;
46
+ throw error;
47
+ }
48
+ throw new Error('Expected project resolution to reject.');
49
+ }
@@ -1,11 +1,17 @@
1
1
  import { runCli } from './cli/bin.js';
2
2
 
3
3
  /***
4
- * Provider and CLI surface
4
+ * Provider, CLI, and project API surface
5
5
  *
6
- * `@ankhorage/infra` owns infra command behavior.
6
+ * `@ankhorage/infra` owns infrastructure generation, project reconciliation,
7
+ * and generated lifecycle execution.
7
8
  *
8
- * The same shared command implementation backs both:
9
+ * The same owner implementation backs CLI consumers and the public
10
+ * `@ankhorage/infra/project` application-service boundary. Trusted hosts such
11
+ * as Studio should call that subpath instead of reproducing ledger, generated
12
+ * file, lifecycle-script, or port-forward configuration semantics.
13
+ *
14
+ * CLI entrypoints:
9
15
  *
10
16
  * - `ankh infra ...`
11
17
  * - `bunx @ankhorage/infra ...`
@@ -18,12 +24,10 @@ import { runCli } from './cli/bin.js';
18
24
  * - `up`
19
25
  * - `down`
20
26
  *
21
- * `status` runs the generated live runtime status script for a project.
22
- *
23
- * Project resolution is project-aware:
24
- *
25
- * - pass `[project]`, or
26
- * - omit it when cwd is already inside `apps/<project>`
27
+ * Project resolution for the CLI is workspace-aware: pass `[project]`, or
28
+ * omit it when cwd is already inside `apps/<project>`. Programmatic project
29
+ * operations accept an explicit project path and do not require Studio or a
30
+ * particular workspace layout.
27
31
  *
28
32
  * @usage
29
33
  */
package/src/runtime.ts CHANGED
@@ -4,7 +4,17 @@ import path from 'path';
4
4
 
5
5
  import type { InfraCommandContext } from './commandContext.js';
6
6
 
7
- export type InfraLifecycleScript = 'down' | 'status' | 'up';
7
+ export type InfraLifecycleScript = 'destroy' | 'down' | 'port-forward' | 'reset' | 'status' | 'up';
8
+
9
+ export interface InfraScriptOutput {
10
+ readonly stderr: string;
11
+ readonly stdout: string;
12
+ }
13
+
14
+ export interface InfraPortForwardInfo {
15
+ readonly localPort: number;
16
+ readonly url: string;
17
+ }
8
18
 
9
19
  export class InfraScriptExecutionError extends Error {
10
20
  readonly exitCode: number | null;
@@ -25,13 +35,16 @@ export class InfraScriptExecutionError extends Error {
25
35
  }
26
36
  }
27
37
 
28
- export async function runProjectInfraScript(args: {
29
- readonly context: InfraCommandContext;
38
+ export async function runProjectInfrastructureLifecycle(args: {
39
+ readonly args?: readonly string[];
40
+ readonly env?: Readonly<Record<string, string | undefined>>;
41
+ readonly onStderr?: (chunk: string) => void;
42
+ readonly onStdout?: (chunk: string) => void;
30
43
  readonly projectId: string;
31
44
  readonly projectPath: string;
32
45
  readonly script: InfraLifecycleScript;
33
46
  readonly target: string;
34
- }): Promise<void> {
47
+ }): Promise<InfraScriptOutput> {
35
48
  const scriptPath = resolveProjectInfraScriptPath(args);
36
49
  if (!(await pathExists(scriptPath))) {
37
50
  throw new Error(
@@ -39,12 +52,56 @@ export async function runProjectInfraScript(args: {
39
52
  );
40
53
  }
41
54
 
42
- await runShellScript({
43
- context: args.context,
55
+ return runShellScript({
56
+ args: args.args ?? [],
57
+ env: args.env ?? process.env,
58
+ onStderr: args.onStderr,
59
+ onStdout: args.onStdout,
44
60
  scriptPath,
45
61
  });
46
62
  }
47
63
 
64
+ export async function resolveProjectInfrastructurePortForward(args: {
65
+ readonly projectPath: string;
66
+ readonly target: string;
67
+ }): 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'));
74
+
75
+ if (localPort === null) {
76
+ throw new Error(
77
+ `Generated Infra did not provide a valid APP_PORT_FORWARD_LOCAL_PORT in ${sourcePath}. Regenerate infrastructure first.`,
78
+ );
79
+ }
80
+
81
+ return {
82
+ localPort,
83
+ url: `http://127.0.0.1:${localPort}`,
84
+ };
85
+ }
86
+
87
+ export async function runProjectInfraScript(args: {
88
+ readonly context: InfraCommandContext;
89
+ readonly projectId: string;
90
+ readonly projectPath: string;
91
+ readonly script: InfraLifecycleScript;
92
+ readonly target: string;
93
+ }): Promise<void> {
94
+ await runProjectInfrastructureLifecycle({
95
+ env: args.context.env,
96
+ onStderr: (chunk) => args.context.writeStderr(chunk),
97
+ onStdout: (chunk) => args.context.writeStdout(chunk),
98
+ projectId: args.projectId,
99
+ projectPath: args.projectPath,
100
+ script: args.script,
101
+ target: args.target,
102
+ });
103
+ }
104
+
48
105
  export function resolveProjectInfraScriptPath(args: {
49
106
  readonly projectPath: string;
50
107
  readonly script: InfraLifecycleScript;
@@ -63,60 +120,96 @@ function getInfraScriptsDirectory(target: string): string {
63
120
  }
64
121
 
65
122
  async function runShellScript(args: {
66
- readonly context: InfraCommandContext;
123
+ readonly args: readonly string[];
124
+ readonly env: Readonly<Record<string, string | undefined>>;
125
+ readonly onStderr?: (chunk: string) => void;
126
+ readonly onStdout?: (chunk: string) => void;
67
127
  readonly scriptPath: string;
68
- }): Promise<void> {
69
- await new Promise<void>((resolve, reject) => {
70
- const child = spawn('bash', [args.scriptPath], {
128
+ }): Promise<InfraScriptOutput> {
129
+ return new Promise<InfraScriptOutput>((resolve, reject) => {
130
+ const child = spawn('bash', [args.scriptPath, ...args.args], {
71
131
  cwd: path.dirname(args.scriptPath),
72
- env: { ...args.context.env },
132
+ env: { ...args.env },
73
133
  stdio: ['ignore', 'pipe', 'pipe'],
74
134
  });
75
135
 
76
136
  let stdout = '';
77
137
  let stderr = '';
78
-
79
138
  child.stdout.setEncoding('utf8');
139
+ child.stderr.setEncoding('utf8');
80
140
  child.stdout.on('data', (chunk: string) => {
81
141
  stdout += chunk;
82
- args.context.writeStdout(chunk);
142
+ args.onStdout?.(chunk);
83
143
  });
84
-
85
- child.stderr.setEncoding('utf8');
86
144
  child.stderr.on('data', (chunk: string) => {
87
145
  stderr += chunk;
88
- args.context.writeStderr(chunk);
89
- });
90
-
91
- child.once('error', (error) => {
92
- reject(
93
- new InfraScriptExecutionError({
94
- exitCode: null,
95
- message: `Failed to start infra script '${args.scriptPath}': ${error.message}`,
96
- stderr,
97
- stdout,
98
- }),
99
- );
146
+ args.onStderr?.(chunk);
100
147
  });
101
-
148
+ child.once('error', (error) =>
149
+ reject(createStartError(args.scriptPath, error, stdout, stderr)),
150
+ );
102
151
  child.once('close', (exitCode) => {
103
152
  if (exitCode === 0) {
104
- resolve();
153
+ resolve({ stderr, stdout });
105
154
  return;
106
155
  }
107
-
108
- reject(
109
- new InfraScriptExecutionError({
110
- exitCode,
111
- message: `Infra script '${args.scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,
112
- stderr,
113
- stdout,
114
- }),
115
- );
156
+ reject(createExitError(args.scriptPath, exitCode, stdout, stderr));
116
157
  });
117
158
  });
118
159
  }
119
160
 
161
+ function createStartError(
162
+ scriptPath: string,
163
+ error: Error,
164
+ stdout: string,
165
+ stderr: string,
166
+ ): InfraScriptExecutionError {
167
+ return new InfraScriptExecutionError({
168
+ exitCode: null,
169
+ message: `Failed to start infra script '${scriptPath}': ${error.message}`,
170
+ stderr,
171
+ stdout,
172
+ });
173
+ }
174
+
175
+ function createExitError(
176
+ scriptPath: string,
177
+ exitCode: number | null,
178
+ stdout: string,
179
+ stderr: string,
180
+ ): InfraScriptExecutionError {
181
+ return new InfraScriptExecutionError({
182
+ exitCode,
183
+ message: `Infra script '${scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,
184
+ stderr,
185
+ stdout,
186
+ });
187
+ }
188
+
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
+ function parsePositivePort(value: string | undefined): number | null {
208
+ if (value === undefined) return null;
209
+ const parsed = Number.parseInt(value.trim(), 10);
210
+ return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535 ? parsed : null;
211
+ }
212
+
120
213
  async function pathExists(filePath: string): Promise<boolean> {
121
214
  try {
122
215
  await fs.access(filePath);