@ankhorage/infra 5.0.0 → 5.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ankhorage/infra
2
2
 
3
+ ## 5.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 51a60f2: Expose a typed project lifecycle that applies the canonical Infra use cases while preserving environment-scoped state and generated artifact ownership.
8
+
3
9
  ## 5.0.0
4
10
 
5
11
  ### Major Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # INFRA
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v4.1.12](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v5.0.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Executable infra provider and standalone CLI for Ankhorage project workflows.
9
9
 
@@ -0,0 +1,4 @@
1
+ import type { CreateProjectInfraLifecycleOptions, ProjectInfraLifecycle } from '../../../types/infraProject.js';
2
+ /*** Create the typed project-facing Infra lifecycle backed by canonical use cases and ownership state. */
3
+ export declare function createProjectInfraLifecycle(options?: CreateProjectInfraLifecycleOptions): ProjectInfraLifecycle;
4
+ //# sourceMappingURL=createProjectInfraLifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createProjectInfraLifecycle.d.ts","sourceRoot":"","sources":["../../../../src/features/environment-lifecycle/composition/createProjectInfraLifecycle.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kCAAkC,EAClC,qBAAqB,EAEtB,MAAM,gCAAgC,CAAC;AAGxC,0GAA0G;AAC1G,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,qBAAqB,CAiFvB"}
@@ -0,0 +1,123 @@
1
+ import { INFRA_PACKAGE_VERSION } from '../../../constants.js';
2
+ import { createInfraCommandServices } from './createInfraCommandServices.js';
3
+ /*** Create the typed project-facing Infra lifecycle backed by canonical use cases and ownership state. */
4
+ export function createProjectInfraLifecycle(options = {}) {
5
+ const services = createInfraCommandServices(options.services);
6
+ return {
7
+ async validateAsync(request) {
8
+ const prepared = await prepareProjectOperationAsync(request, services);
9
+ return services.operations.validate(prepared.operation, prepared.dependencies);
10
+ },
11
+ async planAsync(request) {
12
+ const prepared = await prepareProjectOperationAsync(request, services);
13
+ return services.operations.plan(prepared.operation, prepared.dependencies);
14
+ },
15
+ async generateAsync(request) {
16
+ const prepared = await prepareProjectOperationAsync(request, services);
17
+ const result = await services.operations.generate(prepared.operation, prepared.dependencies);
18
+ if (!result.ok)
19
+ return result;
20
+ await services.writeArtifacts(request.projectPath, result.value.artifacts, prepared.state?.ledger);
21
+ await services.writeState(request.projectPath, {
22
+ schemaVersion: 1,
23
+ desired: prepared.desired,
24
+ ledger: result.value.ledger,
25
+ });
26
+ return result;
27
+ },
28
+ async upAsync(request) {
29
+ const prepared = await prepareProjectOperationAsync(request, services);
30
+ const result = await services.operations.up(prepared.operation, prepared.dependencies);
31
+ if (!result.ok)
32
+ return result;
33
+ await services.writeState(request.projectPath, {
34
+ schemaVersion: 1,
35
+ desired: prepared.desired,
36
+ ledger: result.value.ledger,
37
+ });
38
+ return result;
39
+ },
40
+ async statusAsync(request) {
41
+ const prepared = await prepareProjectOperationAsync(request, services);
42
+ return services.operations.status(prepared.operation, prepared.dependencies);
43
+ },
44
+ async outputsAsync(request) {
45
+ const prepared = await prepareProjectOperationAsync(request, services);
46
+ return services.operations.outputs(prepared.operation);
47
+ },
48
+ async downAsync(request) {
49
+ const prepared = await prepareProjectOperationAsync(request, services);
50
+ const result = await services.operations.down(prepared.operation, prepared.dependencies);
51
+ if (!result.ok)
52
+ return result;
53
+ await services.writeState(request.projectPath, {
54
+ schemaVersion: 1,
55
+ desired: prepared.desired,
56
+ ledger: result.value.ledger,
57
+ });
58
+ return result;
59
+ },
60
+ async destroyAsync(request) {
61
+ const prepared = await prepareProjectOperationAsync(request, services, true);
62
+ const result = await services.operations.destroy({
63
+ ...prepared.operation,
64
+ confirmation: request.confirmation,
65
+ persistence: request.persistence,
66
+ }, prepared.dependencies);
67
+ if (!result.ok)
68
+ return result;
69
+ await services.writeArtifacts(request.projectPath, [], prepared.state?.ledger);
70
+ if (result.value.ledger === null)
71
+ await services.removeState(request.projectPath, request.environment);
72
+ else {
73
+ await services.writeState(request.projectPath, {
74
+ schemaVersion: 1,
75
+ desired: prepared.desired,
76
+ ledger: result.value.ledger,
77
+ });
78
+ }
79
+ return result;
80
+ },
81
+ };
82
+ }
83
+ /*** Resolve one explicit project/environment request with its prior ownership state and dependencies. */
84
+ async function prepareProjectOperationAsync(request, services, useStoredDesired = false) {
85
+ const state = await services.readState(request.projectPath, request.environment);
86
+ const currentDesired = readEnvironment(request.manifest, request.environment);
87
+ const desired = useStoredDesired && state !== null ? state.desired : currentDesired;
88
+ if (desired === undefined) {
89
+ throw new Error(`Infra environment "${request.environment}" is not configured.`);
90
+ }
91
+ const context = {
92
+ cwd: request.projectPath,
93
+ env: request.executionEnvironment ?? process.env,
94
+ version: INFRA_PACKAGE_VERSION,
95
+ writeStdout: () => undefined,
96
+ writeStderr: () => undefined,
97
+ };
98
+ return {
99
+ state,
100
+ desired,
101
+ operation: {
102
+ projectId: request.projectId,
103
+ manifest: replaceEnvironment(request.manifest, request.environment, desired),
104
+ environment: request.environment,
105
+ ...(state === null ? {} : { previous: state.ledger, previousDesired: state.desired }),
106
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
107
+ },
108
+ dependencies: services.createDependencies(context),
109
+ };
110
+ }
111
+ /*** Read one exact environment without inventing a fallback. */
112
+ function readEnvironment(manifest, environment) {
113
+ if (environment === 'local')
114
+ return manifest.environments.local;
115
+ if (environment === 'preview')
116
+ return manifest.environments.preview;
117
+ return manifest.environments.production;
118
+ }
119
+ /*** Replace one selected environment while preserving all other manifest state. */
120
+ function replaceEnvironment(manifest, environment, desired) {
121
+ return { ...manifest, environments: { ...manifest.environments, [environment]: desired } };
122
+ }
123
+ //# sourceMappingURL=createProjectInfraLifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createProjectInfraLifecycle.js","sourceRoot":"","sources":["../../../../src/features/environment-lifecycle/composition/createProjectInfraLifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["import type { InfraEnvironmentSpec, InfraManifest } from '@ankhorage/contracts/infra';\n\nimport { INFRA_PACKAGE_VERSION } from '../../../constants.js';\nimport type { InfraOperationRequest, InfraStoredState } from '../../../types/infraOrchestration.js';\nimport type {\n CreateProjectInfraLifecycleOptions,\n ProjectInfraLifecycle,\n ProjectInfraOperationRequest,\n} from '../../../types/infraProject.js';\nimport { createInfraCommandServices } from './createInfraCommandServices.js';\n\n/*** Create the typed project-facing Infra lifecycle backed by canonical use cases and ownership state. */\nexport function createProjectInfraLifecycle(\n options: CreateProjectInfraLifecycleOptions = {},\n): ProjectInfraLifecycle {\n const services = createInfraCommandServices(options.services);\n return {\n async validateAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n return services.operations.validate(prepared.operation, prepared.dependencies);\n },\n async planAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n return services.operations.plan(prepared.operation, prepared.dependencies);\n },\n async generateAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n const result = await services.operations.generate(prepared.operation, prepared.dependencies);\n if (!result.ok) return result;\n await services.writeArtifacts(\n request.projectPath,\n result.value.artifacts,\n prepared.state?.ledger,\n );\n await services.writeState(request.projectPath, {\n schemaVersion: 1,\n desired: prepared.desired,\n ledger: result.value.ledger,\n });\n return result;\n },\n async upAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n const result = await services.operations.up(prepared.operation, prepared.dependencies);\n if (!result.ok) return result;\n await services.writeState(request.projectPath, {\n schemaVersion: 1,\n desired: prepared.desired,\n ledger: result.value.ledger,\n });\n return result;\n },\n async statusAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n return services.operations.status(prepared.operation, prepared.dependencies);\n },\n async outputsAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n return services.operations.outputs(prepared.operation);\n },\n async downAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services);\n const result = await services.operations.down(prepared.operation, prepared.dependencies);\n if (!result.ok) return result;\n await services.writeState(request.projectPath, {\n schemaVersion: 1,\n desired: prepared.desired,\n ledger: result.value.ledger,\n });\n return result;\n },\n async destroyAsync(request) {\n const prepared = await prepareProjectOperationAsync(request, services, true);\n const result = await services.operations.destroy(\n {\n ...prepared.operation,\n confirmation: request.confirmation,\n persistence: request.persistence,\n },\n prepared.dependencies,\n );\n if (!result.ok) return result;\n await services.writeArtifacts(request.projectPath, [], prepared.state?.ledger);\n if (result.value.ledger === null)\n await services.removeState(request.projectPath, request.environment);\n else {\n await services.writeState(request.projectPath, {\n schemaVersion: 1,\n desired: prepared.desired,\n ledger: result.value.ledger,\n });\n }\n return result;\n },\n };\n}\n\ninterface PreparedProjectOperation {\n readonly operation: InfraOperationRequest;\n readonly desired: InfraEnvironmentSpec;\n readonly state: InfraStoredState | null;\n readonly dependencies: ReturnType<\n ReturnType<typeof createInfraCommandServices>['createDependencies']\n >;\n}\n\n/*** Resolve one explicit project/environment request with its prior ownership state and dependencies. */\nasync function prepareProjectOperationAsync(\n request: ProjectInfraOperationRequest,\n services: ReturnType<typeof createInfraCommandServices>,\n useStoredDesired = false,\n): Promise<PreparedProjectOperation> {\n const state = await services.readState(request.projectPath, request.environment);\n const currentDesired = readEnvironment(request.manifest, request.environment);\n const desired = useStoredDesired && state !== null ? state.desired : currentDesired;\n if (desired === undefined) {\n throw new Error(`Infra environment \"${request.environment}\" is not configured.`);\n }\n const context = {\n cwd: request.projectPath,\n env: request.executionEnvironment ?? process.env,\n version: INFRA_PACKAGE_VERSION,\n writeStdout: () => undefined,\n writeStderr: () => undefined,\n };\n return {\n state,\n desired,\n operation: {\n projectId: request.projectId,\n manifest: replaceEnvironment(request.manifest, request.environment, desired),\n environment: request.environment,\n ...(state === null ? {} : { previous: state.ledger, previousDesired: state.desired }),\n ...(request.signal === undefined ? {} : { signal: request.signal }),\n },\n dependencies: services.createDependencies(context),\n };\n}\n\n/*** Read one exact environment without inventing a fallback. */\nfunction readEnvironment(\n manifest: InfraManifest,\n environment: ProjectInfraOperationRequest['environment'],\n): InfraEnvironmentSpec | undefined {\n if (environment === 'local') return manifest.environments.local;\n if (environment === 'preview') return manifest.environments.preview;\n return manifest.environments.production;\n}\n\n/*** Replace one selected environment while preserving all other manifest state. */\nfunction replaceEnvironment(\n manifest: InfraManifest,\n environment: ProjectInfraOperationRequest['environment'],\n desired: InfraEnvironmentSpec,\n): InfraManifest {\n return { ...manifest, environments: { ...manifest.environments, [environment]: desired } };\n}\n"]}
@@ -3,6 +3,7 @@ export { removeStoredInfraStateAsync } from '../features/environment-lifecycle/a
3
3
  export { resolveInfraProjectAsync } from '../features/environment-lifecycle/adapters/outbound/resolveInfraProjectAsync.js';
4
4
  export { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';
5
5
  export { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';
6
+ export { createProjectInfraLifecycle } from '../features/environment-lifecycle/composition/createProjectInfraLifecycle.js';
6
7
  export { resolveProjectFile } from '../features/environment-lifecycle/utils/resolveProjectFile.js';
7
- export type { InfraArtifactWriteResult, ResolvedInfraProject } from '../types/infraProject.js';
8
+ export type { CreateProjectInfraLifecycleOptions, InfraArtifactWriteResult, ProjectInfraDestroyRequest, ProjectInfraLifecycle, ProjectInfraOperationRequest, ResolvedInfraProject, } from '../types/infraProject.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kFAAkF,CAAC;AAC7H,OAAO,EAAE,2BAA2B,EAAE,MAAM,oFAAoF,CAAC;AACjI,OAAO,EAAE,wBAAwB,EAAE,MAAM,iFAAiF,CAAC;AAC3H,OAAO,EAAE,iCAAiC,EAAE,MAAM,0FAA0F,CAAC;AAC7I,OAAO,EAAE,0BAA0B,EAAE,MAAM,mFAAmF,CAAC;AAC/H,OAAO,EAAE,kBAAkB,EAAE,MAAM,+DAA+D,CAAC;AACnG,YAAY,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kFAAkF,CAAC;AAC7H,OAAO,EAAE,2BAA2B,EAAE,MAAM,oFAAoF,CAAC;AACjI,OAAO,EAAE,wBAAwB,EAAE,MAAM,iFAAiF,CAAC;AAC3H,OAAO,EAAE,iCAAiC,EAAE,MAAM,0FAA0F,CAAC;AAC7I,OAAO,EAAE,0BAA0B,EAAE,MAAM,mFAAmF,CAAC;AAC/H,OAAO,EAAE,2BAA2B,EAAE,MAAM,8EAA8E,CAAC;AAC3H,OAAO,EAAE,kBAAkB,EAAE,MAAM,+DAA+D,CAAC;AACnG,YAAY,EACV,kCAAkC,EAClC,wBAAwB,EACxB,0BAA0B,EAC1B,qBAAqB,EACrB,4BAA4B,EAC5B,oBAAoB,GACrB,MAAM,0BAA0B,CAAC"}
@@ -3,5 +3,6 @@ export { removeStoredInfraStateAsync } from '../features/environment-lifecycle/a
3
3
  export { resolveInfraProjectAsync } from '../features/environment-lifecycle/adapters/outbound/resolveInfraProjectAsync.js';
4
4
  export { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';
5
5
  export { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';
6
+ export { createProjectInfraLifecycle } from '../features/environment-lifecycle/composition/createProjectInfraLifecycle.js';
6
7
  export { resolveProjectFile } from '../features/environment-lifecycle/utils/resolveProjectFile.js';
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kFAAkF,CAAC;AAC7H,OAAO,EAAE,2BAA2B,EAAE,MAAM,oFAAoF,CAAC;AACjI,OAAO,EAAE,wBAAwB,EAAE,MAAM,iFAAiF,CAAC;AAC3H,OAAO,EAAE,iCAAiC,EAAE,MAAM,0FAA0F,CAAC;AAC7I,OAAO,EAAE,0BAA0B,EAAE,MAAM,mFAAmF,CAAC;AAC/H,OAAO,EAAE,kBAAkB,EAAE,MAAM,+DAA+D,CAAC","sourcesContent":["export { readStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/readStoredInfraStateAsync.js';\nexport { removeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraStateAsync.js';\nexport { resolveInfraProjectAsync } from '../features/environment-lifecycle/adapters/outbound/resolveInfraProjectAsync.js';\nexport { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';\nexport { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';\nexport { resolveProjectFile } from '../features/environment-lifecycle/utils/resolveProjectFile.js';\nexport type { InfraArtifactWriteResult, ResolvedInfraProject } from '../types/infraProject.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kFAAkF,CAAC;AAC7H,OAAO,EAAE,2BAA2B,EAAE,MAAM,oFAAoF,CAAC;AACjI,OAAO,EAAE,wBAAwB,EAAE,MAAM,iFAAiF,CAAC;AAC3H,OAAO,EAAE,iCAAiC,EAAE,MAAM,0FAA0F,CAAC;AAC7I,OAAO,EAAE,0BAA0B,EAAE,MAAM,mFAAmF,CAAC;AAC/H,OAAO,EAAE,2BAA2B,EAAE,MAAM,8EAA8E,CAAC;AAC3H,OAAO,EAAE,kBAAkB,EAAE,MAAM,+DAA+D,CAAC","sourcesContent":["export { readStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/readStoredInfraStateAsync.js';\nexport { removeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraStateAsync.js';\nexport { resolveInfraProjectAsync } from '../features/environment-lifecycle/adapters/outbound/resolveInfraProjectAsync.js';\nexport { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';\nexport { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';\nexport { createProjectInfraLifecycle } from '../features/environment-lifecycle/composition/createProjectInfraLifecycle.js';\nexport { resolveProjectFile } from '../features/environment-lifecycle/utils/resolveProjectFile.js';\nexport type {\n CreateProjectInfraLifecycleOptions,\n InfraArtifactWriteResult,\n ProjectInfraDestroyRequest,\n ProjectInfraLifecycle,\n ProjectInfraOperationRequest,\n ResolvedInfraProject,\n} from '../types/infraProject.js';\n"]}
@@ -1,4 +1,8 @@
1
1
  import type { AppManifest } from '@ankhorage/contracts';
2
+ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
3
+ import type { InfraDestroyRequest, InfraManifest } from '@ankhorage/contracts/infra';
4
+ import type { InfraCommandServices } from './infraCli.js';
5
+ import type { InfraDestroyOperationResult, InfraDownOperationResult, InfraGenerateOperationResult, InfraOutputsOperationResult, InfraPlanOperationResult, InfraStatusOperationResult, InfraUpOperationResult, InfraValidateOperationResult } from './infraOrchestration.js';
2
6
  export interface InfraArtifactWriteResult {
3
7
  readonly written: number;
4
8
  readonly removed: number;
@@ -11,4 +15,29 @@ export interface ResolvedInfraProject {
11
15
  readonly projectPath: string;
12
16
  readonly workspaceRoot: string;
13
17
  }
18
+ export interface ProjectInfraOperationRequest {
19
+ readonly projectId: string;
20
+ readonly projectPath: string;
21
+ readonly manifest: InfraManifest;
22
+ readonly environment: AppEnvironmentId;
23
+ readonly executionEnvironment?: Readonly<Record<string, string | undefined>>;
24
+ readonly signal?: AbortSignal;
25
+ }
26
+ export interface ProjectInfraDestroyRequest extends ProjectInfraOperationRequest {
27
+ readonly confirmation: InfraDestroyRequest['confirmation'];
28
+ readonly persistence: InfraDestroyRequest['persistence'];
29
+ }
30
+ export interface ProjectInfraLifecycle {
31
+ validateAsync(request: ProjectInfraOperationRequest): Promise<InfraValidateOperationResult>;
32
+ planAsync(request: ProjectInfraOperationRequest): Promise<InfraPlanOperationResult>;
33
+ generateAsync(request: ProjectInfraOperationRequest): Promise<InfraGenerateOperationResult>;
34
+ upAsync(request: ProjectInfraOperationRequest): Promise<InfraUpOperationResult>;
35
+ statusAsync(request: ProjectInfraOperationRequest): Promise<InfraStatusOperationResult>;
36
+ outputsAsync(request: ProjectInfraOperationRequest): Promise<InfraOutputsOperationResult>;
37
+ downAsync(request: ProjectInfraOperationRequest): Promise<InfraDownOperationResult>;
38
+ destroyAsync(request: ProjectInfraDestroyRequest): Promise<InfraDestroyOperationResult>;
39
+ }
40
+ export interface CreateProjectInfraLifecycleOptions {
41
+ readonly services?: Partial<InfraCommandServices>;
42
+ }
14
43
  //# sourceMappingURL=infraProject.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"infraProject.d.ts","sourceRoot":"","sources":["../../src/types/infraProject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,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"}
1
+ {"version":3,"file":"infraProject.d.ts","sourceRoot":"","sources":["../../src/types/infraProject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAErF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EACV,2BAA2B,EAC3B,wBAAwB,EACxB,4BAA4B,EAC5B,2BAA2B,EAC3B,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,4BAA4B,EAC7B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,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,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA2B,SAAQ,4BAA4B;IAC9E,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAC;IAC3D,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IAC5F,SAAS,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACpF,aAAa,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IAC5F,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAChF,WAAW,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACxF,YAAY,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAC1F,SAAS,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACpF,YAAY,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;CACzF;AAED,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"infraProject.js","sourceRoot":"","sources":["../../src/types/infraProject.ts"],"names":[],"mappings":"","sourcesContent":["import type { AppManifest } from '@ankhorage/contracts';\n\nexport interface InfraArtifactWriteResult {\n readonly written: number;\n readonly removed: number;\n}\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"]}
1
+ {"version":3,"file":"infraProject.js","sourceRoot":"","sources":["../../src/types/infraProject.ts"],"names":[],"mappings":"","sourcesContent":["import type { AppManifest } from '@ankhorage/contracts';\nimport type { AppEnvironmentId } from '@ankhorage/contracts/environments';\nimport type { InfraDestroyRequest, InfraManifest } from '@ankhorage/contracts/infra';\n\nimport type { InfraCommandServices } from './infraCli.js';\nimport type {\n InfraDestroyOperationResult,\n InfraDownOperationResult,\n InfraGenerateOperationResult,\n InfraOutputsOperationResult,\n InfraPlanOperationResult,\n InfraStatusOperationResult,\n InfraUpOperationResult,\n InfraValidateOperationResult,\n} from './infraOrchestration.js';\n\nexport interface InfraArtifactWriteResult {\n readonly written: number;\n readonly removed: number;\n}\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 interface ProjectInfraOperationRequest {\n readonly projectId: string;\n readonly projectPath: string;\n readonly manifest: InfraManifest;\n readonly environment: AppEnvironmentId;\n readonly executionEnvironment?: Readonly<Record<string, string | undefined>>;\n readonly signal?: AbortSignal;\n}\n\nexport interface ProjectInfraDestroyRequest extends ProjectInfraOperationRequest {\n readonly confirmation: InfraDestroyRequest['confirmation'];\n readonly persistence: InfraDestroyRequest['persistence'];\n}\n\nexport interface ProjectInfraLifecycle {\n validateAsync(request: ProjectInfraOperationRequest): Promise<InfraValidateOperationResult>;\n planAsync(request: ProjectInfraOperationRequest): Promise<InfraPlanOperationResult>;\n generateAsync(request: ProjectInfraOperationRequest): Promise<InfraGenerateOperationResult>;\n upAsync(request: ProjectInfraOperationRequest): Promise<InfraUpOperationResult>;\n statusAsync(request: ProjectInfraOperationRequest): Promise<InfraStatusOperationResult>;\n outputsAsync(request: ProjectInfraOperationRequest): Promise<InfraOutputsOperationResult>;\n downAsync(request: ProjectInfraOperationRequest): Promise<InfraDownOperationResult>;\n destroyAsync(request: ProjectInfraDestroyRequest): Promise<InfraDestroyOperationResult>;\n}\n\nexport interface CreateProjectInfraLifecycleOptions {\n readonly services?: Partial<InfraCommandServices>;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "5.0.0",
4
+ "version": "5.1.0",
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": {
@@ -0,0 +1,157 @@
1
+ import type { InfraEnvironmentSpec, InfraManifest } from '@ankhorage/contracts/infra';
2
+
3
+ import { INFRA_PACKAGE_VERSION } from '../../../constants.js';
4
+ import type { InfraOperationRequest, InfraStoredState } from '../../../types/infraOrchestration.js';
5
+ import type {
6
+ CreateProjectInfraLifecycleOptions,
7
+ ProjectInfraLifecycle,
8
+ ProjectInfraOperationRequest,
9
+ } from '../../../types/infraProject.js';
10
+ import { createInfraCommandServices } from './createInfraCommandServices.js';
11
+
12
+ /*** Create the typed project-facing Infra lifecycle backed by canonical use cases and ownership state. */
13
+ export function createProjectInfraLifecycle(
14
+ options: CreateProjectInfraLifecycleOptions = {},
15
+ ): ProjectInfraLifecycle {
16
+ const services = createInfraCommandServices(options.services);
17
+ return {
18
+ async validateAsync(request) {
19
+ const prepared = await prepareProjectOperationAsync(request, services);
20
+ return services.operations.validate(prepared.operation, prepared.dependencies);
21
+ },
22
+ async planAsync(request) {
23
+ const prepared = await prepareProjectOperationAsync(request, services);
24
+ return services.operations.plan(prepared.operation, prepared.dependencies);
25
+ },
26
+ async generateAsync(request) {
27
+ const prepared = await prepareProjectOperationAsync(request, services);
28
+ const result = await services.operations.generate(prepared.operation, prepared.dependencies);
29
+ if (!result.ok) return result;
30
+ await services.writeArtifacts(
31
+ request.projectPath,
32
+ result.value.artifacts,
33
+ prepared.state?.ledger,
34
+ );
35
+ await services.writeState(request.projectPath, {
36
+ schemaVersion: 1,
37
+ desired: prepared.desired,
38
+ ledger: result.value.ledger,
39
+ });
40
+ return result;
41
+ },
42
+ async upAsync(request) {
43
+ const prepared = await prepareProjectOperationAsync(request, services);
44
+ const result = await services.operations.up(prepared.operation, prepared.dependencies);
45
+ if (!result.ok) return result;
46
+ await services.writeState(request.projectPath, {
47
+ schemaVersion: 1,
48
+ desired: prepared.desired,
49
+ ledger: result.value.ledger,
50
+ });
51
+ return result;
52
+ },
53
+ async statusAsync(request) {
54
+ const prepared = await prepareProjectOperationAsync(request, services);
55
+ return services.operations.status(prepared.operation, prepared.dependencies);
56
+ },
57
+ async outputsAsync(request) {
58
+ const prepared = await prepareProjectOperationAsync(request, services);
59
+ return services.operations.outputs(prepared.operation);
60
+ },
61
+ async downAsync(request) {
62
+ const prepared = await prepareProjectOperationAsync(request, services);
63
+ const result = await services.operations.down(prepared.operation, prepared.dependencies);
64
+ if (!result.ok) return result;
65
+ await services.writeState(request.projectPath, {
66
+ schemaVersion: 1,
67
+ desired: prepared.desired,
68
+ ledger: result.value.ledger,
69
+ });
70
+ return result;
71
+ },
72
+ async destroyAsync(request) {
73
+ const prepared = await prepareProjectOperationAsync(request, services, true);
74
+ const result = await services.operations.destroy(
75
+ {
76
+ ...prepared.operation,
77
+ confirmation: request.confirmation,
78
+ persistence: request.persistence,
79
+ },
80
+ prepared.dependencies,
81
+ );
82
+ if (!result.ok) return result;
83
+ await services.writeArtifacts(request.projectPath, [], prepared.state?.ledger);
84
+ if (result.value.ledger === null)
85
+ await services.removeState(request.projectPath, request.environment);
86
+ else {
87
+ await services.writeState(request.projectPath, {
88
+ schemaVersion: 1,
89
+ desired: prepared.desired,
90
+ ledger: result.value.ledger,
91
+ });
92
+ }
93
+ return result;
94
+ },
95
+ };
96
+ }
97
+
98
+ interface PreparedProjectOperation {
99
+ readonly operation: InfraOperationRequest;
100
+ readonly desired: InfraEnvironmentSpec;
101
+ readonly state: InfraStoredState | null;
102
+ readonly dependencies: ReturnType<
103
+ ReturnType<typeof createInfraCommandServices>['createDependencies']
104
+ >;
105
+ }
106
+
107
+ /*** Resolve one explicit project/environment request with its prior ownership state and dependencies. */
108
+ async function prepareProjectOperationAsync(
109
+ request: ProjectInfraOperationRequest,
110
+ services: ReturnType<typeof createInfraCommandServices>,
111
+ useStoredDesired = false,
112
+ ): Promise<PreparedProjectOperation> {
113
+ const state = await services.readState(request.projectPath, request.environment);
114
+ const currentDesired = readEnvironment(request.manifest, request.environment);
115
+ const desired = useStoredDesired && state !== null ? state.desired : currentDesired;
116
+ if (desired === undefined) {
117
+ throw new Error(`Infra environment "${request.environment}" is not configured.`);
118
+ }
119
+ const context = {
120
+ cwd: request.projectPath,
121
+ env: request.executionEnvironment ?? process.env,
122
+ version: INFRA_PACKAGE_VERSION,
123
+ writeStdout: () => undefined,
124
+ writeStderr: () => undefined,
125
+ };
126
+ return {
127
+ state,
128
+ desired,
129
+ operation: {
130
+ projectId: request.projectId,
131
+ manifest: replaceEnvironment(request.manifest, request.environment, desired),
132
+ environment: request.environment,
133
+ ...(state === null ? {} : { previous: state.ledger, previousDesired: state.desired }),
134
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
135
+ },
136
+ dependencies: services.createDependencies(context),
137
+ };
138
+ }
139
+
140
+ /*** Read one exact environment without inventing a fallback. */
141
+ function readEnvironment(
142
+ manifest: InfraManifest,
143
+ environment: ProjectInfraOperationRequest['environment'],
144
+ ): InfraEnvironmentSpec | undefined {
145
+ if (environment === 'local') return manifest.environments.local;
146
+ if (environment === 'preview') return manifest.environments.preview;
147
+ return manifest.environments.production;
148
+ }
149
+
150
+ /*** Replace one selected environment while preserving all other manifest state. */
151
+ function replaceEnvironment(
152
+ manifest: InfraManifest,
153
+ environment: ProjectInfraOperationRequest['environment'],
154
+ desired: InfraEnvironmentSpec,
155
+ ): InfraManifest {
156
+ return { ...manifest, environments: { ...manifest.environments, [environment]: desired } };
157
+ }
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import {
4
+ createProjectInfraLifecycle,
4
5
  readStoredInfraStateAsync,
5
6
  resolveInfraProjectAsync,
6
7
  writeInfraGeneratedArtifactsAsync,
@@ -8,6 +9,7 @@ import {
8
9
 
9
10
  describe('@ankhorage/infra/project', () => {
10
11
  test('exports project resolution, safe state and artifact owner APIs', () => {
12
+ expect(createProjectInfraLifecycle).toBeFunction();
11
13
  expect(resolveInfraProjectAsync).toBeFunction();
12
14
  expect(readStoredInfraStateAsync).toBeFunction();
13
15
  expect(writeInfraGeneratedArtifactsAsync).toBeFunction();
@@ -3,5 +3,13 @@ export { removeStoredInfraStateAsync } from '../features/environment-lifecycle/a
3
3
  export { resolveInfraProjectAsync } from '../features/environment-lifecycle/adapters/outbound/resolveInfraProjectAsync.js';
4
4
  export { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';
5
5
  export { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';
6
+ export { createProjectInfraLifecycle } from '../features/environment-lifecycle/composition/createProjectInfraLifecycle.js';
6
7
  export { resolveProjectFile } from '../features/environment-lifecycle/utils/resolveProjectFile.js';
7
- export type { InfraArtifactWriteResult, ResolvedInfraProject } from '../types/infraProject.js';
8
+ export type {
9
+ CreateProjectInfraLifecycleOptions,
10
+ InfraArtifactWriteResult,
11
+ ProjectInfraDestroyRequest,
12
+ ProjectInfraLifecycle,
13
+ ProjectInfraOperationRequest,
14
+ ResolvedInfraProject,
15
+ } from '../types/infraProject.js';
@@ -0,0 +1,136 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import type { InfraEnvironmentSpec, InfraLedger, InfraResult } from '@ankhorage/contracts/infra';
6
+ import { afterEach, expect, test } from 'bun:test';
7
+
8
+ import { createProjectInfraLifecycle, readStoredInfraStateAsync } from './project/index.js';
9
+ import type { InfraLifecycleOperations } from './types/infraCli.js';
10
+ import type { InfraDestroyOperationRequest } from './types/infraOrchestration.js';
11
+
12
+ const temporaryPaths = new Set<string>();
13
+
14
+ afterEach(async () => {
15
+ await Promise.all(
16
+ [...temporaryPaths].map((entry) => fs.rm(entry, { force: true, recursive: true })),
17
+ );
18
+ temporaryPaths.clear();
19
+ });
20
+
21
+ test('runs typed project lifecycle operations with canonical state and artifact ownership', async () => {
22
+ const projectPath = await fs.mkdtemp(path.join(os.tmpdir(), 'infra-project-lifecycle-'));
23
+ temporaryPaths.add(projectPath);
24
+ const destroyRequests: InfraDestroyOperationRequest[] = [];
25
+ const lifecycle = createProjectInfraLifecycle({
26
+ services: {
27
+ createDependencies: () => ({
28
+ adapterResolver: { loadAsync: () => Promise.resolve({}) },
29
+ credentials: { resolveAsync: () => Promise.resolve(success({})) },
30
+ secrets: { resolveAsync: () => Promise.resolve(success('secret')) },
31
+ }),
32
+ operations: createOperations(destroyRequests),
33
+ },
34
+ });
35
+ const request = {
36
+ projectId: 'sample',
37
+ projectPath,
38
+ manifest: { environments: { local: desired }, modules: [] },
39
+ environment: 'local' as const,
40
+ };
41
+
42
+ const generated = await lifecycle.generateAsync(request);
43
+ expect(generated.ok).toBe(true);
44
+ expect(await fs.readFile(path.join(projectPath, 'infra/example.txt'), 'utf8')).toBe('generated');
45
+ expect((await readStoredInfraStateAsync(projectPath, 'local'))?.ledger).toEqual(ledger);
46
+
47
+ const outputs = await lifecycle.outputsAsync(request);
48
+ expect(outputs.ok && outputs.value.outputs).toEqual(ledger.outputs);
49
+
50
+ const destroyed = await lifecycle.destroyAsync({
51
+ ...request,
52
+ confirmation: { projectId: 'sample', environment: 'local' },
53
+ persistence: { policy: 'retain' },
54
+ });
55
+ expect(destroyed.ok).toBe(true);
56
+ expect(destroyRequests[0]?.confirmation).toEqual({ projectId: 'sample', environment: 'local' });
57
+ expect(await readStoredInfraStateAsync(projectPath, 'local')).toBeNull();
58
+ expect(await fileExists(path.join(projectPath, 'infra/example.txt'))).toBe(false);
59
+ });
60
+
61
+ const desired = {
62
+ deployment: {
63
+ compute: { provider: 'local' },
64
+ runtime: { provider: 'docker-compose' },
65
+ },
66
+ } as const satisfies InfraEnvironmentSpec;
67
+
68
+ const owner = {
69
+ projectId: 'sample',
70
+ environment: 'local',
71
+ adapter: 'docker-compose',
72
+ resourceId: 'runtime',
73
+ } as const;
74
+
75
+ const ledger = {
76
+ schemaVersion: 1,
77
+ projectId: 'sample',
78
+ environment: 'local',
79
+ targets: [],
80
+ resources: [],
81
+ outputs: [{ owner, name: 'url', visibility: 'public', value: 'https://example.test' }],
82
+ artifacts: [{ owner, path: 'infra/example.txt' }],
83
+ } as const satisfies InfraLedger;
84
+
85
+ /*** Create deterministic operation fakes while recording destructive authorization. */
86
+ function createOperations(
87
+ destroyRequests: InfraDestroyOperationRequest[],
88
+ ): InfraLifecycleOperations {
89
+ return {
90
+ validate: () => Promise.resolve(success({ environment: 'local' })),
91
+ plan: (request) =>
92
+ Promise.resolve(success({ projectId: request.projectId, environment: 'local', actions: [] })),
93
+ generate: () =>
94
+ Promise.resolve(
95
+ success({
96
+ environment: 'local',
97
+ artifacts: [{ owner, path: 'infra/example.txt', content: 'generated' }],
98
+ ledger,
99
+ }),
100
+ ),
101
+ up: () =>
102
+ Promise.resolve(
103
+ success({ environment: 'local', targets: [], resources: [], outputs: [], ledger }),
104
+ ),
105
+ status: (request) =>
106
+ Promise.resolve(
107
+ success({
108
+ projectId: request.projectId,
109
+ environment: 'local',
110
+ state: 'ready',
111
+ resources: [],
112
+ }),
113
+ ),
114
+ outputs: () => success({ environment: 'local', outputs: ledger.outputs }),
115
+ down: () => Promise.resolve(success({ environment: 'local', ledger })),
116
+ destroy: (request) => {
117
+ destroyRequests.push(request);
118
+ return Promise.resolve(success({ environment: 'local', ledger: null }));
119
+ },
120
+ };
121
+ }
122
+
123
+ /*** Create one successful Infra result for operation fakes. */
124
+ function success<T>(value: T): InfraResult<T> {
125
+ return { ok: true, value, diagnostics: [] };
126
+ }
127
+
128
+ /*** Report whether a generated artifact still exists. */
129
+ async function fileExists(filePath: string): Promise<boolean> {
130
+ try {
131
+ await fs.access(filePath);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
@@ -1,4 +1,18 @@
1
1
  import type { AppManifest } from '@ankhorage/contracts';
2
+ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
3
+ import type { InfraDestroyRequest, InfraManifest } from '@ankhorage/contracts/infra';
4
+
5
+ import type { InfraCommandServices } from './infraCli.js';
6
+ import type {
7
+ InfraDestroyOperationResult,
8
+ InfraDownOperationResult,
9
+ InfraGenerateOperationResult,
10
+ InfraOutputsOperationResult,
11
+ InfraPlanOperationResult,
12
+ InfraStatusOperationResult,
13
+ InfraUpOperationResult,
14
+ InfraValidateOperationResult,
15
+ } from './infraOrchestration.js';
2
16
 
3
17
  export interface InfraArtifactWriteResult {
4
18
  readonly written: number;
@@ -13,3 +27,32 @@ export interface ResolvedInfraProject {
13
27
  readonly projectPath: string;
14
28
  readonly workspaceRoot: string;
15
29
  }
30
+
31
+ export interface ProjectInfraOperationRequest {
32
+ readonly projectId: string;
33
+ readonly projectPath: string;
34
+ readonly manifest: InfraManifest;
35
+ readonly environment: AppEnvironmentId;
36
+ readonly executionEnvironment?: Readonly<Record<string, string | undefined>>;
37
+ readonly signal?: AbortSignal;
38
+ }
39
+
40
+ export interface ProjectInfraDestroyRequest extends ProjectInfraOperationRequest {
41
+ readonly confirmation: InfraDestroyRequest['confirmation'];
42
+ readonly persistence: InfraDestroyRequest['persistence'];
43
+ }
44
+
45
+ export interface ProjectInfraLifecycle {
46
+ validateAsync(request: ProjectInfraOperationRequest): Promise<InfraValidateOperationResult>;
47
+ planAsync(request: ProjectInfraOperationRequest): Promise<InfraPlanOperationResult>;
48
+ generateAsync(request: ProjectInfraOperationRequest): Promise<InfraGenerateOperationResult>;
49
+ upAsync(request: ProjectInfraOperationRequest): Promise<InfraUpOperationResult>;
50
+ statusAsync(request: ProjectInfraOperationRequest): Promise<InfraStatusOperationResult>;
51
+ outputsAsync(request: ProjectInfraOperationRequest): Promise<InfraOutputsOperationResult>;
52
+ downAsync(request: ProjectInfraOperationRequest): Promise<InfraDownOperationResult>;
53
+ destroyAsync(request: ProjectInfraDestroyRequest): Promise<InfraDestroyOperationResult>;
54
+ }
55
+
56
+ export interface CreateProjectInfraLifecycleOptions {
57
+ readonly services?: Partial<InfraCommandServices>;
58
+ }