@ankhorage/infra 0.2.1 → 0.3.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +43 -2
  3. package/dist/cli/index.d.ts +10 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +27 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli.d.ts +10 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +39 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commandContext.d.ts +12 -0
  12. package/dist/commandContext.d.ts.map +1 -0
  13. package/dist/commandContext.js +15 -0
  14. package/dist/commandContext.js.map +1 -0
  15. package/dist/commands.d.ts +95 -0
  16. package/dist/commands.d.ts.map +1 -0
  17. package/dist/commands.js +256 -0
  18. package/dist/commands.js.map +1 -0
  19. package/dist/infraValidation.d.ts +3 -0
  20. package/dist/infraValidation.d.ts.map +1 -0
  21. package/dist/infraValidation.js +58 -0
  22. package/dist/infraValidation.js.map +1 -0
  23. package/dist/metadata/index.d.ts +2 -0
  24. package/dist/metadata/index.d.ts.map +1 -0
  25. package/dist/metadata/index.js +2 -0
  26. package/dist/metadata/index.js.map +1 -0
  27. package/dist/packageMetadata.d.ts +10 -0
  28. package/dist/packageMetadata.d.ts.map +1 -0
  29. package/dist/packageMetadata.js +17 -0
  30. package/dist/packageMetadata.js.map +1 -0
  31. package/dist/project.d.ts +14 -0
  32. package/dist/project.d.ts.map +1 -0
  33. package/dist/project.js +202 -0
  34. package/dist/project.js.map +1 -0
  35. package/dist/projectInfrastructure.d.ts +22 -0
  36. package/dist/projectInfrastructure.d.ts.map +1 -0
  37. package/dist/projectInfrastructure.js +120 -0
  38. package/dist/projectInfrastructure.js.map +1 -0
  39. package/dist/readme-usage.d.ts +2 -0
  40. package/dist/readme-usage.d.ts.map +1 -0
  41. package/dist/readme-usage.js +30 -0
  42. package/dist/readme-usage.js.map +1 -0
  43. package/dist/runtime.d.ts +26 -0
  44. package/dist/runtime.d.ts.map +1 -0
  45. package/dist/runtime.js +87 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/testSupport.d.ts +24 -0
  48. package/dist/testSupport.d.ts.map +1 -0
  49. package/dist/testSupport.js +73 -0
  50. package/dist/testSupport.js.map +1 -0
  51. package/package.json +23 -3
  52. package/src/cli/index.test.ts +32 -0
  53. package/src/cli/index.ts +53 -0
  54. package/src/cli.test.ts +137 -0
  55. package/src/cli.ts +67 -0
  56. package/src/commandContext.ts +27 -0
  57. package/src/commands.test.ts +219 -0
  58. package/src/commands.ts +404 -0
  59. package/src/infraValidation.ts +90 -0
  60. package/src/metadata/index.ts +7 -0
  61. package/src/package.test.ts +27 -0
  62. package/src/packageMetadata.ts +24 -0
  63. package/src/project.test.ts +218 -0
  64. package/src/project.ts +270 -0
  65. package/src/projectInfrastructure.test.ts +207 -0
  66. package/src/projectInfrastructure.ts +177 -0
  67. package/src/readme-usage.ts +30 -0
  68. package/src/runtime.test.ts +124 -0
  69. package/src/runtime.ts +127 -0
  70. package/src/testSupport.ts +112 -0
@@ -0,0 +1,87 @@
1
+ import { spawn } from 'child_process';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ export class InfraScriptExecutionError extends Error {
5
+ exitCode;
6
+ stderr;
7
+ stdout;
8
+ constructor(args) {
9
+ super(args.message);
10
+ this.name = 'InfraScriptExecutionError';
11
+ this.exitCode = args.exitCode;
12
+ this.stderr = args.stderr;
13
+ this.stdout = args.stdout;
14
+ }
15
+ }
16
+ export async function runProjectInfraScript(args) {
17
+ const scriptPath = resolveProjectInfraScriptPath(args);
18
+ if (!(await pathExists(scriptPath))) {
19
+ throw new Error(`Infra script not found: ${scriptPath}. Run infra generate for project '${args.projectId}' first.`);
20
+ }
21
+ await runShellScript({
22
+ context: args.context,
23
+ scriptPath,
24
+ });
25
+ }
26
+ export function resolveProjectInfraScriptPath(args) {
27
+ return path.join(args.projectPath, getInfraScriptsDirectory(args.target), `${args.script}.sh`);
28
+ }
29
+ function getInfraScriptsDirectory(target) {
30
+ switch (target) {
31
+ case 'minikube':
32
+ return 'infra/minikube/scripts';
33
+ default:
34
+ throw new Error(`Unsupported deployment target for infra scripts: ${target}`);
35
+ }
36
+ }
37
+ async function runShellScript(args) {
38
+ await new Promise((resolve, reject) => {
39
+ const child = spawn('bash', [args.scriptPath], {
40
+ cwd: path.dirname(args.scriptPath),
41
+ env: { ...args.context.env },
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ });
44
+ let stdout = '';
45
+ let stderr = '';
46
+ child.stdout.setEncoding('utf8');
47
+ child.stdout.on('data', (chunk) => {
48
+ stdout += chunk;
49
+ args.context.writeStdout(chunk);
50
+ });
51
+ child.stderr.setEncoding('utf8');
52
+ child.stderr.on('data', (chunk) => {
53
+ 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
+ }));
63
+ });
64
+ child.once('close', (exitCode) => {
65
+ if (exitCode === 0) {
66
+ resolve();
67
+ return;
68
+ }
69
+ reject(new InfraScriptExecutionError({
70
+ exitCode,
71
+ message: `Infra script '${args.scriptPath}' exited with code ${exitCode ?? 'unknown'}.`,
72
+ stderr,
73
+ stdout,
74
+ }));
75
+ });
76
+ });
77
+ }
78
+ async function pathExists(filePath) {
79
+ try {
80
+ await fs.access(filePath);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,24 @@
1
+ import type { AppManifest } from '@ankhorage/contracts';
2
+ import type { InfraCommandContext } from './commandContext.js';
3
+ export interface CapturedCommandContext {
4
+ readonly context: InfraCommandContext;
5
+ readonly stderr: {
6
+ value: string;
7
+ };
8
+ readonly stdout: {
9
+ value: string;
10
+ };
11
+ }
12
+ export declare function createCapturedCommandContext(cwd: string, version?: string): CapturedCommandContext;
13
+ export declare function createWorkspaceFixture(args?: {
14
+ readonly manifest?: AppManifest;
15
+ readonly manifestFile?: 'invalid-json' | 'missing';
16
+ readonly projectId?: string;
17
+ }): Promise<{
18
+ readonly appsRoot: string;
19
+ readonly projectId: string;
20
+ readonly projectPath: string;
21
+ readonly rootPath: string;
22
+ }>;
23
+ export declare function createAppManifest(projectId: string, infra?: AppManifest['infra']): AppManifest;
24
+ //# sourceMappingURL=testSupport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testSupport.d.ts","sourceRoot":"","sources":["../src/testSupport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACpC;AAED,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,SAAU,GAChB,sBAAsB,CAmBxB;AAED,wBAAsB,sBAAsB,CAC1C,IAAI,GAAE;IACJ,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CACxB,GACL,OAAO,CAAC;IACT,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC,CA6BD;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,WAAW,CAAC,OAAO,CAAmB,GAC5C,WAAW,CA6Bb"}
@@ -0,0 +1,73 @@
1
+ import { promises as fs } from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ export function createCapturedCommandContext(cwd, version = '0.2.1') {
5
+ const stdout = { value: '' };
6
+ const stderr = { value: '' };
7
+ return {
8
+ stdout,
9
+ stderr,
10
+ context: {
11
+ cwd,
12
+ env: process.env,
13
+ version,
14
+ writeStdout(text) {
15
+ stdout.value += text;
16
+ },
17
+ writeStderr(text) {
18
+ stderr.value += text;
19
+ },
20
+ },
21
+ };
22
+ }
23
+ export async function createWorkspaceFixture(args = {}) {
24
+ const rootPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ankhorage-infra-'));
25
+ const appsRoot = path.join(rootPath, 'apps');
26
+ const projectId = args.projectId ?? 'shop';
27
+ const projectPath = path.join(appsRoot, projectId);
28
+ await fs.mkdir(projectPath, { recursive: true });
29
+ await fs.writeFile(path.join(projectPath, 'package.json'), JSON.stringify({ name: projectId, version: '1.0.0' }, null, 2), 'utf8');
30
+ if (args.manifestFile === 'invalid-json') {
31
+ await fs.writeFile(path.join(projectPath, 'ankh.config.json'), '{invalid', 'utf8');
32
+ }
33
+ else if (args.manifestFile !== 'missing') {
34
+ await fs.writeFile(path.join(projectPath, 'ankh.config.json'), `${JSON.stringify(args.manifest ?? createAppManifest(projectId), null, 2)}\n`, 'utf8');
35
+ }
36
+ return {
37
+ appsRoot,
38
+ projectId,
39
+ projectPath,
40
+ rootPath,
41
+ };
42
+ }
43
+ export function createAppManifest(projectId, infra = { plugins: [] }) {
44
+ return {
45
+ metadata: {
46
+ name: projectId,
47
+ slug: projectId,
48
+ version: '1.0.0',
49
+ themeId: 'default',
50
+ },
51
+ themes: [],
52
+ activeThemeId: 'default',
53
+ infra,
54
+ navigator: {
55
+ type: 'stack',
56
+ routes: [{ name: 'index', screenId: 'index' }],
57
+ },
58
+ screens: {},
59
+ settings: {
60
+ localization: {
61
+ defaultLocale: 'en',
62
+ locales: ['en'],
63
+ },
64
+ authFlow: {
65
+ signInRoute: '/sign-in',
66
+ signOutRoute: '/sign-out',
67
+ postSignInRoute: '/',
68
+ unauthorizedRoute: '/sign-in',
69
+ },
70
+ },
71
+ };
72
+ }
73
+ //# sourceMappingURL=testSupport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testSupport.js","sourceRoot":"","sources":["../src/testSupport.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AppManifest } from '@ankhorage/contracts';\nimport { promises as fs } from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport type { InfraCommandContext } from './commandContext.js';\n\nexport interface CapturedCommandContext {\n readonly context: InfraCommandContext;\n readonly stderr: { value: string };\n readonly stdout: { value: string };\n}\n\nexport function createCapturedCommandContext(\n cwd: string,\n version = '0.2.1',\n): CapturedCommandContext {\n const stdout = { value: '' };\n const stderr = { value: '' };\n\n return {\n stdout,\n stderr,\n context: {\n cwd,\n env: process.env,\n version,\n writeStdout(text: string) {\n stdout.value += text;\n },\n writeStderr(text: string) {\n stderr.value += text;\n },\n },\n };\n}\n\nexport async function createWorkspaceFixture(\n args: {\n readonly manifest?: AppManifest;\n readonly manifestFile?: 'invalid-json' | 'missing';\n readonly projectId?: string;\n } = {},\n): Promise<{\n readonly appsRoot: string;\n readonly projectId: string;\n readonly projectPath: string;\n readonly rootPath: string;\n}> {\n const rootPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ankhorage-infra-'));\n const appsRoot = path.join(rootPath, 'apps');\n const projectId = args.projectId ?? 'shop';\n const projectPath = path.join(appsRoot, projectId);\n\n await fs.mkdir(projectPath, { recursive: true });\n await fs.writeFile(\n path.join(projectPath, 'package.json'),\n JSON.stringify({ name: projectId, version: '1.0.0' }, null, 2),\n 'utf8',\n );\n\n if (args.manifestFile === 'invalid-json') {\n await fs.writeFile(path.join(projectPath, 'ankh.config.json'), '{invalid', 'utf8');\n } else if (args.manifestFile !== 'missing') {\n await fs.writeFile(\n path.join(projectPath, 'ankh.config.json'),\n `${JSON.stringify(args.manifest ?? createAppManifest(projectId), null, 2)}\\n`,\n 'utf8',\n );\n }\n\n return {\n appsRoot,\n projectId,\n projectPath,\n rootPath,\n };\n}\n\nexport function createAppManifest(\n projectId: string,\n infra: AppManifest['infra'] = { plugins: [] },\n): AppManifest {\n return {\n metadata: {\n name: projectId,\n slug: projectId,\n version: '1.0.0',\n themeId: 'default',\n },\n themes: [],\n activeThemeId: 'default',\n infra,\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 authFlow: {\n signInRoute: '/sign-in',\n signOutRoute: '/sign-out',\n postSignInRoute: '/',\n unauthorizedRoute: '/sign-in',\n },\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "0.2.1",
5
- "description": "Provider-neutral infrastructure generation: turns app manifests, datasets, auth, state, storage, and deployment config into generated infra artifacts, migrations, seeds, APIs, and runtime dependencies.",
4
+ "version": "0.3.1",
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": {
8
8
  "url": "https://github.com/ankhorage/infra/issues"
@@ -33,7 +33,7 @@
33
33
  "openapi"
34
34
  ],
35
35
  "dependencies": {
36
- "@ankhorage/contracts": "^1.18.0"
36
+ "@ankhorage/contracts": "^1.19.0"
37
37
  },
38
38
  "files": [
39
39
  "dist",
@@ -49,6 +49,11 @@
49
49
  "import": "./dist/index.js",
50
50
  "default": "./dist/index.js"
51
51
  },
52
+ "./cli": {
53
+ "types": "./dist/cli/index.d.ts",
54
+ "import": "./dist/cli/index.js",
55
+ "default": "./dist/cli/index.js"
56
+ },
52
57
  "./metadata": {
53
58
  "types": "./dist/metadata/index.d.ts",
54
59
  "import": "./dist/metadata/index.js",
@@ -56,6 +61,20 @@
56
61
  },
57
62
  "./package.json": "./package.json"
58
63
  },
64
+ "bin": {
65
+ "ankhorage-infra": "./dist/cli.js"
66
+ },
67
+ "ankh": {
68
+ "category": "infra",
69
+ "provider": "./dist/cli/index.js",
70
+ "capabilities": [
71
+ "infra.validate",
72
+ "infra.generate",
73
+ "infra.status",
74
+ "infra.up",
75
+ "infra.down"
76
+ ]
77
+ },
59
78
  "publishConfig": {
60
79
  "access": "public"
61
80
  },
@@ -75,6 +94,7 @@
75
94
  "version-packages": "changeset version"
76
95
  },
77
96
  "devDependencies": {
97
+ "@ankhorage/ankh": "^0.4.0",
78
98
  "@ankhorage/devtools": "^1.0.6",
79
99
  "@ankhorage/paradox": "^0.1.10",
80
100
  "@changesets/cli": "^2.31.0",
@@ -0,0 +1,32 @@
1
+ import type { AnkhRuntimeCommandProvider } from '@ankhorage/ankh';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { createProviderCommandDescriptors, INFRA_COMMANDS } from '../commands.js';
5
+ import provider, { createInfraRuntimeProvider } from './index.js';
6
+
7
+ describe('infra package cli provider', () => {
8
+ test('publishes the expected runtime provider shape', () => {
9
+ const expectedProvider = createInfraRuntimeProvider() satisfies AnkhRuntimeCommandProvider;
10
+
11
+ expect(expectedProvider.id).toBe('@ankhorage/infra');
12
+ expect(expectedProvider.category).toBe('infra');
13
+ expect(expectedProvider.capabilities).toEqual([
14
+ 'infra.validate',
15
+ 'infra.generate',
16
+ 'infra.status',
17
+ 'infra.up',
18
+ 'infra.down',
19
+ ]);
20
+ expect(expectedProvider.commands).toEqual(createProviderCommandDescriptors());
21
+ expect(expectedProvider.handlers?.map((handler) => handler.path.join(' '))).toEqual(
22
+ INFRA_COMMANDS.map((command) => command.path.join(' ')),
23
+ );
24
+ expect(JSON.stringify(expectedProvider)).not.toContain('port-forward');
25
+ });
26
+
27
+ test('default export is the runtime provider', () => {
28
+ expect(provider.category).toBe('infra');
29
+ expect(provider.commands).toHaveLength(5);
30
+ expect(provider.handlers).toHaveLength(5);
31
+ });
32
+ });
@@ -0,0 +1,53 @@
1
+ import type { AnkhRuntimeCommandProvider } from '@ankhorage/ankh';
2
+
3
+ import {
4
+ createProviderCommandDescriptors,
5
+ INFRA_COMMANDS,
6
+ type InfraCommandServices,
7
+ runInfraCommand,
8
+ type RunInfraCommandImpl,
9
+ } from '../commands.js';
10
+ import {
11
+ INFRA_CAPABILITIES,
12
+ INFRA_COMMAND_CATEGORY,
13
+ INFRA_PACKAGE_NAME,
14
+ INFRA_PACKAGE_VERSION,
15
+ } from '../packageMetadata.js';
16
+
17
+ export interface CreateInfraRuntimeProviderOptions {
18
+ readonly runCommandImpl?: RunInfraCommandImpl;
19
+ readonly services?: Partial<InfraCommandServices>;
20
+ }
21
+
22
+ export function createInfraRuntimeProvider(
23
+ options: CreateInfraRuntimeProviderOptions = {},
24
+ ): AnkhRuntimeCommandProvider {
25
+ const runCommandImpl = options.runCommandImpl ?? runInfraCommand;
26
+
27
+ return {
28
+ id: INFRA_PACKAGE_NAME,
29
+ category: INFRA_COMMAND_CATEGORY,
30
+ version: INFRA_PACKAGE_VERSION,
31
+ capabilities: [...INFRA_CAPABILITIES],
32
+ commands: createProviderCommandDescriptors(),
33
+ handlers: INFRA_COMMANDS.map((command) => ({
34
+ path: command.path,
35
+ handler(request) {
36
+ return runCommandImpl(
37
+ {
38
+ argv: request.argv,
39
+ command,
40
+ context: request.context,
41
+ },
42
+ {
43
+ services: options.services,
44
+ },
45
+ );
46
+ },
47
+ })),
48
+ } satisfies AnkhRuntimeCommandProvider;
49
+ }
50
+
51
+ const provider = createInfraRuntimeProvider();
52
+
53
+ export default provider;
@@ -0,0 +1,137 @@
1
+ import type { AnkhDiscoveredPackage, AnkhLoadedProvider } from '@ankhorage/ankh';
2
+ import {
3
+ createPackageRegistry,
4
+ createProviderRegistry,
5
+ runCli as runAnkhCli,
6
+ } from '@ankhorage/ankh';
7
+ import { describe, expect, test } from 'bun:test';
8
+
9
+ import { runCli } from './cli.js';
10
+ import { createInfraRuntimeProvider } from './cli/index.js';
11
+ import { createCapturedCommandContext } from './testSupport.js';
12
+
13
+ describe('standalone cli and provider-backed dispatch', () => {
14
+ test('prints help and version for the standalone CLI', async () => {
15
+ const help = createCapturedCommandContext('/workspace', '9.9.9');
16
+ const version = createCapturedCommandContext('/workspace', '9.9.9');
17
+
18
+ expect((await runCli([], { context: help.context })).exitCode).toBe(0);
19
+ expect(help.stdout.value).toContain('ankhorage-infra <command> [project]');
20
+ expect(help.stdout.value).not.toContain('port-forward');
21
+
22
+ expect((await runCli(['--version'], { context: version.context })).exitCode).toBe(0);
23
+ expect(version.stdout.value).toBe('9.9.9\n');
24
+ });
25
+
26
+ test('uses the shared command runner for standalone CLI dispatch', async () => {
27
+ const captured = createCapturedCommandContext('/workspace');
28
+ const calls: string[] = [];
29
+
30
+ const result = await runCli(['validate', 'shop'], {
31
+ context: captured.context,
32
+ runCommandImpl(request) {
33
+ calls.push(request.command.standaloneName);
34
+ request.context.writeStdout(
35
+ `ran:${request.command.standaloneName}:${request.argv.join(',')}\n`,
36
+ );
37
+ return Promise.resolve({ exitCode: 0 });
38
+ },
39
+ });
40
+
41
+ expect(result.exitCode).toBe(0);
42
+ expect(calls).toEqual(['validate']);
43
+ expect(captured.stdout.value).toContain('ran:validate:shop');
44
+ });
45
+
46
+ test('routes provider-backed dispatch through @ankhorage/ankh', async () => {
47
+ const captured = createCapturedCommandContext('/workspace');
48
+ const calls: string[] = [];
49
+ const provider = createInfraRuntimeProvider({
50
+ runCommandImpl(request) {
51
+ calls.push(request.command.standaloneName);
52
+ request.context.writeStdout(`provider:${request.command.standaloneName}\n`);
53
+ return Promise.resolve({ exitCode: 0 });
54
+ },
55
+ });
56
+
57
+ const packageRegistry = createPackageRegistry([createDiscoveredPackage()]);
58
+ const providerRegistry = createProviderRegistry([createLoadedProvider(provider)]);
59
+
60
+ const result = await runAnkhCli(['infra', 'status', 'shop'], {
61
+ context: captured.context,
62
+ registry: packageRegistry,
63
+ providerRegistry,
64
+ });
65
+
66
+ expect(result.exitCode).toBe(0);
67
+ expect(calls).toEqual(['status']);
68
+ expect(captured.stdout.value).toContain('provider:status');
69
+ });
70
+
71
+ test('uses the same injected shared runner from both surfaces', async () => {
72
+ const standalone = createCapturedCommandContext('/workspace');
73
+ const providerContext = createCapturedCommandContext('/workspace');
74
+ const calls: string[] = [];
75
+
76
+ const runCommandImpl = (request: {
77
+ readonly argv: readonly string[];
78
+ readonly command: { readonly standaloneName: string };
79
+ readonly context: { writeStdout(text: string): void };
80
+ }) => {
81
+ calls.push(request.command.standaloneName);
82
+ request.context.writeStdout(`shared:${request.command.standaloneName}\n`);
83
+ return Promise.resolve({ exitCode: 0 });
84
+ };
85
+
86
+ await runCli(['generate', 'shop'], {
87
+ context: standalone.context,
88
+ runCommandImpl,
89
+ });
90
+
91
+ const provider = createInfraRuntimeProvider({ runCommandImpl });
92
+ const packageRegistry = createPackageRegistry([createDiscoveredPackage()]);
93
+ const providerRegistry = createProviderRegistry([createLoadedProvider(provider)]);
94
+
95
+ await runAnkhCli(['infra', 'generate', 'shop'], {
96
+ context: providerContext.context,
97
+ registry: packageRegistry,
98
+ providerRegistry,
99
+ });
100
+
101
+ expect(calls).toEqual(['generate', 'generate']);
102
+ expect(standalone.stdout.value).toContain('shared:generate');
103
+ expect(providerContext.stdout.value).toContain('shared:generate');
104
+ });
105
+ });
106
+
107
+ function createDiscoveredPackage(): AnkhDiscoveredPackage {
108
+ return {
109
+ metadata: {
110
+ category: 'infra',
111
+ provider: './dist/cli/index.js',
112
+ capabilities: ['infra.validate', 'infra.generate', 'infra.status', 'infra.up', 'infra.down'],
113
+ },
114
+ packageJsonPath: '/workspace/package.json',
115
+ packageName: '@ankhorage/infra',
116
+ packageRoot: '/workspace',
117
+ source: 'workspace',
118
+ };
119
+ }
120
+
121
+ function createLoadedProvider(
122
+ provider: ReturnType<typeof createInfraRuntimeProvider>,
123
+ ): AnkhLoadedProvider {
124
+ return {
125
+ discoveredPackage: createDiscoveredPackage(),
126
+ manifest: {
127
+ id: provider.id,
128
+ category: provider.category,
129
+ version: provider.version,
130
+ capabilities: provider.capabilities,
131
+ commands: provider.commands,
132
+ },
133
+ providerModuleDefaultExport: provider,
134
+ providerModulePath: '/workspace/dist/cli/index.js',
135
+ providerModuleUrl: 'file:///workspace/dist/cli/index.js',
136
+ };
137
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import type { InfraCommandContext, InfraCommandRunResult } from './commandContext.js';
4
+ import { createDefaultCommandContext } from './commandContext.js';
5
+ import {
6
+ findInfraCommandByStandaloneName,
7
+ type InfraCommandServices,
8
+ renderRootHelp,
9
+ renderUnknownCommand,
10
+ runInfraCommand,
11
+ type RunInfraCommandImpl,
12
+ } from './commands.js';
13
+
14
+ export interface InfraCliOptions {
15
+ readonly context?: InfraCommandContext;
16
+ readonly runCommandImpl?: RunInfraCommandImpl;
17
+ readonly services?: Partial<InfraCommandServices>;
18
+ }
19
+
20
+ export async function runCli(
21
+ argv: readonly string[],
22
+ options: InfraCliOptions = {},
23
+ ): Promise<InfraCommandRunResult> {
24
+ const context = options.context ?? createDefaultCommandContext();
25
+ const runCommand = options.runCommandImpl ?? runInfraCommand;
26
+ const [firstToken, ...restTokens] = argv;
27
+
28
+ if (firstToken === undefined || isHelpToken(firstToken)) {
29
+ context.writeStdout(renderRootHelp(context.version));
30
+ return { exitCode: 0 };
31
+ }
32
+
33
+ if (isVersionToken(firstToken)) {
34
+ context.writeStdout(`${context.version}\n`);
35
+ return { exitCode: 0 };
36
+ }
37
+
38
+ const command = findInfraCommandByStandaloneName(firstToken);
39
+ if (command === null) {
40
+ context.writeStderr(renderUnknownCommand(firstToken));
41
+ return { exitCode: 1 };
42
+ }
43
+
44
+ return runCommand(
45
+ {
46
+ argv: restTokens,
47
+ command,
48
+ context,
49
+ },
50
+ {
51
+ services: options.services,
52
+ },
53
+ );
54
+ }
55
+
56
+ function isHelpToken(value: string): boolean {
57
+ return value === '--help' || value === '-h' || value === 'help';
58
+ }
59
+
60
+ function isVersionToken(value: string): boolean {
61
+ return value === '--version' || value === '-v' || value === 'version';
62
+ }
63
+
64
+ if (import.meta.main) {
65
+ const result = await runCli(process.argv.slice(2));
66
+ process.exit(result.exitCode);
67
+ }
@@ -0,0 +1,27 @@
1
+ import { INFRA_PACKAGE_VERSION } from './packageMetadata.js';
2
+
3
+ export interface InfraCommandContext {
4
+ readonly cwd: string;
5
+ readonly env: Readonly<Record<string, string | undefined>>;
6
+ readonly version: string;
7
+ writeStdout(text: string): void;
8
+ writeStderr(text: string): void;
9
+ }
10
+
11
+ export interface InfraCommandRunResult {
12
+ readonly exitCode: number;
13
+ }
14
+
15
+ export function createDefaultCommandContext(): InfraCommandContext {
16
+ return {
17
+ cwd: process.cwd(),
18
+ env: process.env,
19
+ version: INFRA_PACKAGE_VERSION,
20
+ writeStdout(text: string) {
21
+ process.stdout.write(text);
22
+ },
23
+ writeStderr(text: string) {
24
+ process.stderr.write(text);
25
+ },
26
+ };
27
+ }