Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 69 additions & 8 deletions packages/cli/src/kernel/domain/deploy/deploy-target-port.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
/**
* Canonical deploy lifecycle operations (Archetype 7 — the uniform 7-op
* contract every deploy target adapter conforms to).
*
* A target adapter implements only the subset it supports. The legacy CLI verbs
* map onto the canonical ops as: `build` → `plan`/`emit`, `install` → `up`,
* `uninstall` → `down`. `status`, `logs`, `rollback`, and `secrets` are net-new
* lifecycle ops the legacy 3-op seed did not have. `rollback`/`secrets` bodies
* land with the deployment hardening slice (#341); until then adapters may
* declare them unsupported (omit the method) rather than provide a silent no-op.
*/
export type DeployOperation =
| 'plan'
| 'emit'
| 'up'
| 'down'
| 'status'
| 'logs'
| 'rollback'
| 'secrets';

/**
* Legacy CLI deploy verbs retained as thin-router aliases of the canonical
* {@link DeployOperation} set (`build` → `plan`/`emit`, `install` → `up`,
* `uninstall` → `down`). Kept alongside the canonical names so existing adapters
* and the CLI verb surface stay stable; new adapters should prefer the canonical
* operation names.
*/
export type LegacyDeployOperation = 'build' | 'install' | 'uninstall';

/** Public deploy operation exposed by a deploy target adapter. */
export type DeployTargetOperation = 'build' | 'install' | 'uninstall';
export type DeployTargetOperation = DeployOperation | LegacyDeployOperation;

/** Request passed to deploy target operations. */
export interface DeployTargetRequest {
Expand All @@ -19,18 +49,49 @@ export interface DeployTargetResult {
readonly message: string;
}

/** Deploy target adapter surface used by CLI extension registries. */
/** Signature shared by every deploy target lifecycle operation. */
export type DeployTargetOperationHandler = (
request: DeployTargetRequest,
) => Promise<DeployTargetResult>;

/**
* Deploy target adapter surface used by CLI extension registries.
*
* Every operation method is optional: an adapter declares the subset of the
* canonical {@link DeployOperation} contract it supports (mirrored by
* {@link DeployTargetPort.operations}). The legacy `build`/`install`/`uninstall`
* aliases are retained so the shipped seed and CLI verbs keep working while the
* bare-metal and cloud adapters adopt the canonical op names.
*/
export interface DeployTargetPort {
/** Stable target identifier. */
readonly key: string;
/** Human-readable target label. */
readonly label: string;
/** Supported public deploy operations for the target. */
readonly operations: readonly DeployTargetOperation[];
/** Build deployment assets for this target. */
readonly build?: (request: DeployTargetRequest) => Promise<DeployTargetResult>;
/** Install deployment assets for this target. */
readonly install?: (request: DeployTargetRequest) => Promise<DeployTargetResult>;
/** Uninstall deployment assets for this target. */
readonly uninstall?: (request: DeployTargetRequest) => Promise<DeployTargetResult>;

/** Compute the deployment plan/artifact spec for this target. */
readonly plan?: DeployTargetOperationHandler;
/** Emit deployment artifacts for this target. */
readonly emit?: DeployTargetOperationHandler;
/** Bring the deployment up (install + enable + start). */
readonly up?: DeployTargetOperationHandler;
/** Bring the deployment down (stop + disable + uninstall). */
readonly down?: DeployTargetOperationHandler;
/** Report the current deployment status for this target. */
readonly status?: DeployTargetOperationHandler;
/** Stream or tail deployment logs for this target. */
readonly logs?: DeployTargetOperationHandler;
/** Roll the deployment back to a previous revision (bodies → #341). */
readonly rollback?: DeployTargetOperationHandler;
/** Reconcile deployment secrets for this target (bodies → #341). */
readonly secrets?: DeployTargetOperationHandler;

/** Legacy alias of `plan`/`emit`: build deployment assets for this target. */
readonly build?: DeployTargetOperationHandler;
/** Legacy alias of `up`: install deployment assets for this target. */
readonly install?: DeployTargetOperationHandler;
/** Legacy alias of `down`: uninstall deployment assets for this target. */
readonly uninstall?: DeployTargetOperationHandler;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { assertEquals } from 'jsr:@std/assert@^1';

import type { DeployTargetOperation, DeployTargetPort } from './deploy-target-port.ts';
import type { KnownDeployTargetKey } from './deploy-target-registry-port.ts';
import { DeployTargetRegistry } from '../../application/registries/deploy-target-registry.ts';

Deno.test('deploy target contract exposes the canonical 7-op names', () => {
const canonical: readonly DeployTargetOperation[] = [
'plan',
'emit',
'up',
'down',
'status',
'logs',
'rollback',
'secrets',
];

assertEquals(canonical, ['plan', 'emit', 'up', 'down', 'status', 'logs', 'rollback', 'secrets']);
});

Deno.test('deploy target contract retains the legacy build/install/uninstall verb aliases', () => {
const legacy: readonly DeployTargetOperation[] = ['build', 'install', 'uninstall'];

assertEquals(legacy, ['build', 'install', 'uninstall']);
});

Deno.test('deploy target port accepts an adapter that implements only the canonical subset', () => {
const target: DeployTargetPort = {
key: 'linux-service',
label: 'Linux service',
operations: ['plan', 'up', 'down', 'status', 'logs'],
up: (request) =>
Promise.resolve({
target: 'linux-service',
operation: 'up',
message: `up for ${request.projectRoot}`,
}),
};

assertEquals(target.operations.includes('up'), true);
assertEquals(typeof target.up, 'function');
assertEquals(target.rollback, undefined);
});

Deno.test('deploy target registry reserves the linux-service key at the type level', () => {
const reserved: KnownDeployTargetKey = 'linux-service';
const registry = new DeployTargetRegistry([]);
const linuxTarget: DeployTargetPort = {
key: reserved,
label: 'Linux service',
operations: ['up', 'down'],
};

registry.register(reserved, linuxTarget);

assertEquals(registry.get(reserved)?.key, 'linux-service');
assertEquals(registry.entries().map(([key]) => key), ['linux-service']);
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import type { DeployTargetPort } from './deploy-target-port.ts';

/**
* Well-known deploy target keys reserved by the framework registry shape.
*
* `windows-service` ships today; `linux-service` is reserved for the bare-metal
* Linux (systemd) adapter that is registered at the bare-metal realization slice
* (#339). Adapters may still register under any string key — this union
* documents the first-party reservations so sibling deployment slices agree on
* the canonical keys without registering an adapter here.
*/
export type KnownDeployTargetKey = 'windows-service' | 'linux-service';

/** Registry surface for deploy target adapters. */
export interface DeployTargetRegistryPort {
/** Register or replace a deploy target adapter. */
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/kernel/extension-points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ export { PresetRegistry } from './application/registries/preset-registry.ts';
/** Deployment target descriptors supported by deploy commands. */
export { DeployTargetRegistry } from './application/registries/deploy-target-registry.ts';
export type {
DeployOperation,
DeployTargetOperation,
DeployTargetOperationHandler,
DeployTargetPort,
DeployTargetRequest,
DeployTargetResult,
LegacyDeployOperation,
} from './domain/deploy/deploy-target-port.ts';
export type { DeployTargetRegistryPort } from './domain/deploy/deploy-target-registry-port.ts';
export type {
DeployTargetRegistryPort,
KnownDeployTargetKey,
} from './domain/deploy/deploy-target-registry-port.ts';
Loading