@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.46

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 (100) hide show
  1. package/package.json +3 -3
  2. package/src/blueprints/external-sandbox.d.ts +1 -1
  3. package/src/cli/main.d.ts +11 -5
  4. package/src/cli/main.js +88 -29
  5. package/src/container-images.d.ts +1 -1
  6. package/src/container-images.js +1 -1
  7. package/src/developer-agent/generator-planning.js +10 -3
  8. package/src/developer-agent/operation-registry.js +1 -1
  9. package/src/executors/deployment-plan/deployment-plan.js +5 -1
  10. package/src/executors/deployment-plan/schema.json +1 -1
  11. package/src/executors/fleet/fleet.js +4 -0
  12. package/src/executors/fleet/schema.json +1 -1
  13. package/src/executors/github-bootstrap/github-bootstrap.d.ts +3 -0
  14. package/src/executors/github-bootstrap/github-bootstrap.js +28 -0
  15. package/src/executors/github-bootstrap/schema.json +14 -0
  16. package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
  17. package/src/executors/local-deploy/federated-credential-transport.js +15 -5
  18. package/src/foundation/cost-estimate.d.ts +8 -3
  19. package/src/foundation/cost-estimate.js +42 -19
  20. package/src/foundation/index.d.ts +1 -0
  21. package/src/foundation/index.js +1 -0
  22. package/src/foundation/local-private-smoke-access.js +6 -0
  23. package/src/foundation/logging.d.ts +3 -1
  24. package/src/foundation/logging.js +8 -1
  25. package/src/foundation/observability.d.ts +42 -0
  26. package/src/foundation/observability.js +181 -0
  27. package/src/foundation/preflight.js +1 -1
  28. package/src/generators/function/function.js +5 -2
  29. package/src/generators/observability-infrastructure.d.ts +2 -0
  30. package/src/generators/observability-infrastructure.js +31 -0
  31. package/src/generators/observability-policy.d.ts +4 -0
  32. package/src/generators/observability-policy.js +23 -0
  33. package/src/generators/preset/dependency-overrides.d.ts +17 -6
  34. package/src/generators/preset/dependency-overrides.js +17 -6
  35. package/src/generators/preset/tool-versions.d.ts +2 -2
  36. package/src/generators/preset/tool-versions.js +2 -2
  37. package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
  38. package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
  39. package/src/generators/preset/workspace-files-agent-development.js +42 -0
  40. package/src/generators/preset/workspace-files-thin-deploy.js +17 -4
  41. package/src/generators/preset/workspace-files-thin-docs.js +10 -22
  42. package/src/generators/preset/workspace-files.d.ts +1 -0
  43. package/src/generators/preset/workspace-files.js +120 -35
  44. package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
  45. package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
  46. package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
  47. package/src/generators/slice/built-in-plan-data-files.js +10 -3
  48. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
  49. package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
  50. package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
  51. package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
  52. package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
  53. package/src/generators/slice/built-in-plan-metadata-files.js +37 -2
  54. package/src/generators/slice/built-in-plan-operations-files.js +106 -6
  55. package/src/generators/slice/built-in-plan-project-files.js +1 -0
  56. package/src/generators/slice/built-in-plan-release-files.js +2 -44
  57. package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
  58. package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
  59. package/src/generators/slice/built-in-plan-shared.js +29 -2
  60. package/src/generators/slice/built-in-plan-verification-files.js +0 -1
  61. package/src/generators/slice/built-in-plan-web-files.js +22 -6
  62. package/src/generators/slice/built-in-plan.js +2 -2
  63. package/src/generators/slice/slice.js +2 -0
  64. package/src/generators/upgrade/upgrade.d.ts +4 -3
  65. package/src/generators/upgrade/upgrade.js +139 -29
  66. package/src/generators/workload/frozen-container.d.ts +1 -1
  67. package/src/generators/workload/reliability-registration.d.ts +1 -1
  68. package/src/generators/workload/reliability-registration.js +9 -9
  69. package/src/generators/workload/workload.d.ts +1 -1
  70. package/src/generators/workload/workload.js +32 -10
  71. package/src/git-policy/github-bootstrap.d.ts +18 -0
  72. package/src/git-policy/github-bootstrap.js +99 -6
  73. package/src/local-dev/dev-command-core.d.ts +24 -3
  74. package/src/local-dev/dev-command-core.js +124 -12
  75. package/src/local-dev/errors.d.ts +19 -1
  76. package/src/local-dev/errors.js +18 -1
  77. package/src/local-dev/fleet-client.d.ts +64 -6
  78. package/src/local-dev/fleet-client.js +189 -15
  79. package/src/local-dev/fleet-process.d.ts +47 -2
  80. package/src/local-dev/fleet-process.js +248 -31
  81. package/src/local-dev/fleet-runtime.d.ts +37 -0
  82. package/src/local-dev/fleet-runtime.js +127 -69
  83. package/src/local-dev/fleet-session.d.ts +1 -0
  84. package/src/local-dev/fleet-session.js +83 -7
  85. package/src/local-dev/fleet-state.d.ts +5 -0
  86. package/src/local-dev/fleet-state.js +11 -2
  87. package/src/local-dev/fleet-supervisor-daemon.js +1 -5
  88. package/src/local-dev/fleet-supervisor.d.ts +7 -0
  89. package/src/local-dev/fleet-supervisor.js +83 -12
  90. package/src/local-dev/slice-runner.d.ts +19 -1
  91. package/src/local-dev/slice-runner.js +290 -91
  92. package/src/repository-policy/deployment-plan.d.ts +1 -1
  93. package/src/repository-policy/deployment-plan.js +16 -1
  94. package/src/secrets/generated-aws-control-plane.d.ts +11 -1
  95. package/src/secrets/generated-aws-control-plane.js +92 -29
  96. package/src/secrets/operator.d.ts +3 -2
  97. package/src/secrets/operator.js +27 -5
  98. package/src/supply-chain/blueprint-verification-authority.js +24 -3
  99. package/src/supply-chain/generated-files.js +9 -0
  100. package/src/supply-chain/sbom.js +74 -12
@@ -203,11 +203,7 @@ function requiresImmutableOidcSubjectCutover(state) {
203
203
  function immutableOidcTrustAcknowledgement(identity) {
204
204
  return `${GITHUB_IMMUTABLE_OIDC_TRUST_ACKNOWLEDGEMENT_PREFIX}${identity.subjectPrefix}`;
205
205
  }
206
- const PRIVATE_ENVIRONMENT_REVIEWER_PLANS = new Set([
207
- 'business_plus',
208
- 'enterprise',
209
- 'enterprise_cloud',
210
- ]);
206
+ const PRIVATE_ENVIRONMENT_REVIEWER_PLANS = new Set(['enterprise']);
211
207
  class GitHubApiError extends Error {
212
208
  status;
213
209
  constructor(status, message) {
@@ -575,6 +571,14 @@ async function inspectEnvironment(api, repositoryPath, name) {
575
571
  branchPolicies: branchPolicyValues,
576
572
  customBranchPolicies,
577
573
  exists: true,
574
+ canAdminsBypass: environment.can_admins_bypass === false ? false : true,
575
+ protectedBranches: typeof environment.deployment_branch_policy === 'object' &&
576
+ environment.deployment_branch_policy !== null &&
577
+ 'protected_branches' in environment.deployment_branch_policy &&
578
+ typeof environment.deployment_branch_policy.protected_branches ===
579
+ 'boolean'
580
+ ? environment.deployment_branch_policy.protected_branches
581
+ : undefined,
578
582
  preventSelfReview: reviewers.preventSelfReview,
579
583
  reviewers: reviewers.reviewers,
580
584
  secretNames: namedValues(secrets),
@@ -786,7 +790,7 @@ function evaluate(repository, state, inspectionBlockers, productionApprovalMode,
786
790
  privateRepository &&
787
791
  state.ownerPlanName !== undefined &&
788
792
  !PRIVATE_ENVIRONMENT_REVIEWER_PLANS.has(state.ownerPlanName)) {
789
- blockers.push(`Committed environment-reviewer mode requires GitHub Enterprise support for a private repository; the inspected owner plan is ${state.ownerPlanName}.`);
793
+ blockers.push(`Committed environment-reviewer mode does not support inspected owner plan ${state.ownerPlanName} under EBK's verified private environment-reviewer contract; supported REST plan.name: enterprise.`);
790
794
  }
791
795
  if (state.repo.allow_merge_commit !== true) {
792
796
  changes.push('Enable repository merge commits.');
@@ -1099,7 +1103,96 @@ async function applyChanges(api, repositoryPath, state, productionApprovalMode,
1099
1103
  }
1100
1104
  }
1101
1105
  }
1106
+ async function runFrameworkNpmStableBootstrap(options) {
1107
+ if (options.repository !== 'empire-builder-kit/empire-builder-kit')
1108
+ throw new Error('npm-stable is restricted to the exact framework repository.');
1109
+ if (!options.token.trim())
1110
+ throw new Error('GH_TOKEN or GITHUB_TOKEN is required for GitHub bootstrap.');
1111
+ if (!Array.isArray(options.npmStableReviewerIds) ||
1112
+ options.npmStableReviewerIds.length < 1 ||
1113
+ options.npmStableReviewerIds.length > 6 ||
1114
+ options.npmStableReviewerIds.some((id) => !Number.isSafeInteger(id) || id <= 0) ||
1115
+ new Set(options.npmStableReviewerIds).size !==
1116
+ options.npmStableReviewerIds.length ||
1117
+ typeof options.npmStablePreventSelfReview !== 'boolean') {
1118
+ throw new Error('npm-stable requires exact reviewer IDs and explicit prevent-self-review choice.');
1119
+ }
1120
+ const reviewers = existingReviewerPayload([...options.npmStableReviewerIds].sort((left, right) => left - right));
1121
+ const api = new GitHubApi(options.apiBaseUrl?.trim() || 'https://api.github.com', options.token);
1122
+ const repositoryPath = '/repos/empire-builder-kit/empire-builder-kit';
1123
+ const repo = await api.request('GET', repositoryPath);
1124
+ if (repo.full_name !== options.repository)
1125
+ throw new Error('npm-stable repository readback differs from its exact target.');
1126
+ const environmentPath = `${repositoryPath}/environments/npm-stable`;
1127
+ const inspectStable = () => inspectEnvironment(api, repositoryPath, 'npm-stable');
1128
+ const evaluateStable = (state) => {
1129
+ const changes = [];
1130
+ const blockers = [];
1131
+ if (!state.exists ||
1132
+ !state.customBranchPolicies ||
1133
+ state.protectedBranches !== false ||
1134
+ state.canAdminsBypass !== false ||
1135
+ state.preventSelfReview !== options.npmStablePreventSelfReview ||
1136
+ state.waitTimer !== 0 ||
1137
+ JSON.stringify(state.reviewers) !== JSON.stringify(reviewers)) {
1138
+ changes.push('Set exact npm-stable reviewers, explicit self-review choice, administrator bypass false, and custom main-only policy.');
1139
+ }
1140
+ if (state.branchPolicies.length !== 1 ||
1141
+ state.branchPolicies[0]?.name !== 'main' ||
1142
+ state.branchPolicies[0]?.type !== 'branch')
1143
+ changes.push('Restrict npm-stable to the main branch only.');
1144
+ if ([...state.secretNames, ...state.variableNames].some((name) => FORBIDDEN_NPM_TOKEN_NAMES.some((tokenName) => name.toUpperCase() === tokenName)))
1145
+ blockers.push('npm-stable may not contain npm token credentials.');
1146
+ return {
1147
+ kind: 'framework-npm-stable',
1148
+ repository: options.repository,
1149
+ ready: changes.length === 0 && blockers.length === 0,
1150
+ blockers,
1151
+ changes,
1152
+ warnings: [],
1153
+ };
1154
+ };
1155
+ const initial = await inspectStable();
1156
+ const report = evaluateStable(initial);
1157
+ if (!options.apply)
1158
+ return report;
1159
+ if (report.blockers.length)
1160
+ throw new Error(`npm-stable apply is blocked: ${report.blockers.join(' ')}`);
1161
+ if (report.ready)
1162
+ return report;
1163
+ await api.request('PUT', environmentPath, {
1164
+ can_admins_bypass: false,
1165
+ deployment_branch_policy: {
1166
+ protected_branches: false,
1167
+ custom_branch_policies: true,
1168
+ },
1169
+ prevent_self_review: options.npmStablePreventSelfReview,
1170
+ reviewers,
1171
+ wait_timer: 0,
1172
+ });
1173
+ const policies = await readPagedObjects(api, `${environmentPath}/deployment-branch-policies`, 'branch_policies');
1174
+ let mainRetained = false;
1175
+ for (const policy of policies) {
1176
+ if (policy.name === 'main' && policy.type === 'branch' && !mainRetained) {
1177
+ mainRetained = true;
1178
+ continue;
1179
+ }
1180
+ if (!Number.isSafeInteger(policy.id) ||
1181
+ typeof policy.id !== 'number' ||
1182
+ policy.id <= 0)
1183
+ throw new Error('npm-stable branch policy has no exact deletion identity.');
1184
+ await api.request('DELETE', `${environmentPath}/deployment-branch-policies/${policy.id}`);
1185
+ }
1186
+ if (!mainRetained)
1187
+ await api.request('POST', `${environmentPath}/deployment-branch-policies`, {
1188
+ name: 'main',
1189
+ type: 'branch',
1190
+ });
1191
+ return evaluateStable(await inspectStable());
1192
+ }
1102
1193
  async function runGitHubBootstrap(options) {
1194
+ if ('npmStable' in options)
1195
+ return runFrameworkNpmStableBootstrap(options);
1103
1196
  const repository = options.repository.trim();
1104
1197
  const deploymentProfile = options.deploymentProfile ?? 'fully-isolated';
1105
1198
  const { owner, repo } = parseRepository(repository);
@@ -1,4 +1,5 @@
1
1
  import { inspectDevelopmentEnvironment } from './environment-inspector.js';
2
+ import type { FleetOperationProgress, FleetOperationWaitOptions } from './fleet-client.js';
2
3
  import type { FleetOperationStateRecord, FleetStateRecord } from './fleet-state.js';
3
4
  import { type FleetLaunchInput } from './fleet-launcher.js';
4
5
  import type { LocalDevContext } from './local-context.js';
@@ -22,9 +23,8 @@ export interface FleetUpCoreClient {
22
23
  operationId: string;
23
24
  state: 'accepted';
24
25
  }>;
25
- waitForOperation(operationId: string, options?: {
26
- timeoutMs?: number;
27
- }): Promise<FleetOperationStateRecord>;
26
+ /** Resolves on the terminal record, `succeeded` or `failed`. */
27
+ waitForOperationOutcome(operationId: string, options?: FleetOperationWaitOptions): Promise<FleetOperationStateRecord>;
28
28
  }
29
29
  export interface FleetUpCoreLaunchResult {
30
30
  attached: boolean;
@@ -48,7 +48,28 @@ export interface FleetUpCoreEffects {
48
48
  inspectEnvironment?: typeof inspectDevelopmentEnvironment;
49
49
  launchFleet?: (input: FleetLaunchInput) => Promise<FleetUpCoreLaunchResult>;
50
50
  reportCleanupFailure?: (message: string) => void;
51
+ /** One human progress line while an operation is observed (stderr). */
52
+ reportProgress?: (line: string) => void;
51
53
  }
54
+ /** Aspirational duration targets per Fleet command, taken from the measured
55
+ * two-Slice figures in docs/local-development.md (fresh up 334-442 s, down
56
+ * 242-345 s, restart about 70-85 s). Exceeding one prints a "slower than
57
+ * target" line; it never fails or stops observing an operation. */
58
+ export declare const FLEET_OPERATION_TARGET_MS: Readonly<{
59
+ down: 350000;
60
+ restart: 90000;
61
+ up: 450000;
62
+ }>;
63
+ /** Progress lines for a watched Fleet operation; the first report past the
64
+ * command's target says so once, and observation continues. */
65
+ export declare function fleetOperationProgressReporter(command: keyof typeof FLEET_OPERATION_TARGET_MS, report: (line: string) => void): (progress: FleetOperationProgress) => void;
66
+ /** Human cause for a terminal failed operation record. */
67
+ export declare function fleetOperationFailureDetail(operation: FleetOperationStateRecord): string;
68
+ /** Owned-cleanup settlement attached to an error from a foreground `up` whose
69
+ * in-process supervisor can outlive the failure. Callers report the error
70
+ * first, then keep interrupt handling installed until this settles, exactly as
71
+ * they do with `afterWrite` after a successful foreground `up`. */
72
+ export declare function foregroundUpSettlement(error: unknown): Promise<void> | undefined;
52
73
  export declare function executeFleetUpCore(command: FleetUpCoreCommand, input: {
53
74
  appData: string;
54
75
  context: LocalDevContext;
@@ -1,12 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLEET_OPERATION_TARGET_MS = void 0;
4
+ exports.fleetOperationProgressReporter = fleetOperationProgressReporter;
5
+ exports.fleetOperationFailureDetail = fleetOperationFailureDetail;
6
+ exports.foregroundUpSettlement = foregroundUpSettlement;
3
7
  exports.executeFleetUpCore = executeFleetUpCore;
4
8
  const node_crypto_1 = require("node:crypto");
5
9
  const environment_inspector_js_1 = require("./environment-inspector.js");
6
10
  const errors_js_1 = require("./errors.js");
7
11
  const fleet_launcher_js_1 = require("./fleet-launcher.js");
8
12
  const nx_workspace_js_1 = require("./nx-workspace.js");
9
- function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFailure) {
13
+ /** Aspirational duration targets per Fleet command, taken from the measured
14
+ * two-Slice figures in docs/local-development.md (fresh up 334-442 s, down
15
+ * 242-345 s, restart about 70-85 s). Exceeding one prints a "slower than
16
+ * target" line; it never fails or stops observing an operation. */
17
+ exports.FLEET_OPERATION_TARGET_MS = Object.freeze({
18
+ down: 350_000,
19
+ restart: 90_000,
20
+ up: 450_000,
21
+ });
22
+ function progressClock(milliseconds) {
23
+ const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
24
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
25
+ }
26
+ function describeSliceProgress(slice) {
27
+ const details = [
28
+ ...(slice.cleanup === 'failed' ? ['cleanup failed'] : []),
29
+ ...(slice.restartAttempts > 0
30
+ ? [`restart attempt ${slice.restartAttempts}`]
31
+ : []),
32
+ ];
33
+ return `${slice.name} ${slice.routeState}${details.length > 0 ? ` (${details.join(', ')})` : ''}`;
34
+ }
35
+ /** Progress lines for a watched Fleet operation; the first report past the
36
+ * command's target says so once, and observation continues. */
37
+ function fleetOperationProgressReporter(command, report) {
38
+ const targetMs = exports.FLEET_OPERATION_TARGET_MS[command];
39
+ let exceeded = false;
40
+ return ({ elapsedMs, operation, slices }) => {
41
+ const summary = slices.length > 0
42
+ ? slices.map(describeSliceProgress).join(', ')
43
+ : `operation ${operation.state}`;
44
+ if (!exceeded && elapsedMs > targetMs) {
45
+ exceeded = true;
46
+ report(`[ebk dev ${command}] ${progressClock(elapsedMs)} slower than the ${progressClock(targetMs)} target for ${command}; still running: ${summary}`);
47
+ return;
48
+ }
49
+ report(`[ebk dev ${command}] ${progressClock(elapsedMs)} ${summary}`);
50
+ };
51
+ }
52
+ /** Human cause for a terminal failed operation record. */
53
+ function fleetOperationFailureDetail(operation) {
54
+ return operation.failureMessage ? ` Cause: ${operation.failureMessage}` : '';
55
+ }
56
+ function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFailure, reportProgress) {
10
57
  if (!command.foreground || !launch.runtime)
11
58
  return undefined;
12
59
  const stop = async () => {
@@ -24,11 +71,15 @@ function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFa
24
71
  }
25
72
  }
26
73
  if (down) {
27
- const operation = await launch.client.waitForOperation(down.operationId, {
28
- timeoutMs: command.timeoutMs ?? 120_000,
29
- });
74
+ // The supervisor lives in this process: a capped wait could only
75
+ // abandon its shutdown, so observe the down until it settles.
76
+ const operation = await launch.client.waitForOperationOutcome(down.operationId, reportProgress
77
+ ? {
78
+ onProgress: fleetOperationProgressReporter('down', reportProgress),
79
+ }
80
+ : {});
30
81
  if (operation.state !== 'succeeded') {
31
- throw new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'operation-conflict', 'Foreground Fleet down did not complete successfully; ownership was retained.');
82
+ throw new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'operation-conflict', `Foreground Fleet down did not complete successfully; ownership was retained.${fleetOperationFailureDetail(operation)}`);
32
83
  }
33
84
  }
34
85
  await launch.client.shutdown(idempotencyKey());
@@ -46,6 +97,14 @@ function foregroundWait(launch, command, signal, idempotencyKey, reportCleanupFa
46
97
  signal.removeEventListener('abort', onAbort);
47
98
  });
48
99
  }
100
+ const foregroundSettlements = new WeakMap();
101
+ /** Owned-cleanup settlement attached to an error from a foreground `up` whose
102
+ * in-process supervisor can outlive the failure. Callers report the error
103
+ * first, then keep interrupt handling installed until this settles, exactly as
104
+ * they do with `afterWrite` after a successful foreground `up`. */
105
+ function foregroundUpSettlement(error) {
106
+ return error instanceof Error ? foregroundSettlements.get(error) : undefined;
107
+ }
49
108
  async function executeFleetUpCore(command, input, effects = {}) {
50
109
  const slices = (0, nx_workspace_js_1.selectWorkspaceSlices)(input.plan, command.selector);
51
110
  const inspect = effects.inspectEnvironment ?? environment_inspector_js_1.inspectDevelopmentEnvironment;
@@ -78,7 +137,8 @@ async function executeFleetUpCore(command, input, effects = {}) {
78
137
  }
79
138
  throw error;
80
139
  }
81
- const afterWrite = foregroundWait(launch, command, input.signal, idempotencyKey, effects.reportCleanupFailure ?? (() => undefined));
140
+ const reportCleanupFailure = effects.reportCleanupFailure ?? (() => undefined);
141
+ const afterWrite = foregroundWait(launch, command, input.signal, idempotencyKey, reportCleanupFailure, effects.reportProgress);
82
142
  if (command.noWait) {
83
143
  return {
84
144
  ...(afterWrite ? { afterWrite } : {}),
@@ -88,21 +148,73 @@ async function executeFleetUpCore(command, input, effects = {}) {
88
148
  operationId: submission.operationId,
89
149
  };
90
150
  }
91
- const operation = await launch.client.waitForOperation(submission.operationId, command.timeoutMs === undefined ? {} : { timeoutMs: command.timeoutMs });
151
+ if (command.foreground && command.timeoutMs !== undefined) {
152
+ effects.reportProgress?.('[ebk dev up] --timeout does not apply to a foreground up: its supervisor runs in this process, so the command watches until the outcome.');
153
+ }
154
+ let observed;
155
+ try {
156
+ // No default deadline: watch the supervised up until it settles. Only an
157
+ // explicit --timeout caps a detached wait, and in detached mode an
158
+ // interrupt ends observation; neither cancels the operation. A foreground
159
+ // up takes neither: its supervisor lives in this process, so a cap could
160
+ // only abandon it, and an interrupt instead drives the owned down above,
161
+ // which this wait then observes fail.
162
+ const operation = await launch.client.waitForOperationOutcome(submission.operationId, {
163
+ ...(command.foreground || command.timeoutMs === undefined
164
+ ? {}
165
+ : { timeoutMs: command.timeoutMs }),
166
+ ...(command.foreground ? {} : { signal: input.signal }),
167
+ ...(effects.reportProgress
168
+ ? {
169
+ onProgress: fleetOperationProgressReporter('up', effects.reportProgress),
170
+ }
171
+ : {}),
172
+ });
173
+ observed =
174
+ operation.state === 'failed'
175
+ ? { operation }
176
+ : { operation, slices: (await launch.client.status()).slices };
177
+ }
178
+ catch (error) {
179
+ if (afterWrite && !input.signal.aborted) {
180
+ effects.reportProgress?.('[ebk dev up] The foreground Fleet supervisor keeps running in this process. Interrupt to run owned cleanup, or run ebk dev down --all from another terminal.');
181
+ }
182
+ throw holdForegroundSettlement(error, afterWrite);
183
+ }
184
+ const { operation } = observed;
92
185
  if (operation.state === 'failed') {
93
- if (!launch.attached) {
94
- await launch.client.shutdown(idempotencyKey()).catch(() => undefined);
186
+ // After a foreground interrupt the owned down and shutdown already run; a
187
+ // second shutdown here would only be refused while that down runs.
188
+ if (!launch.attached && !(afterWrite && input.signal.aborted)) {
189
+ await launch.client.shutdown(idempotencyKey()).catch((error) => {
190
+ reportCleanupFailure(`Fleet up failed and the supervisor it launched refused to shut down, so it keeps running and keeps its claims: ${error instanceof Error ? error.message : String(error)} ${afterWrite
191
+ ? 'It runs in this process: interrupt to retry owned cleanup, or run ebk dev down --all from another terminal.'
192
+ : 'Check ebk dev status, then run ebk dev down --all to retry cleanup.'}`);
193
+ });
95
194
  }
96
- throw new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'internal-error', `Fleet up failed after transactional rollback (${operation.failureClassification ?? 'internal-error'}).`);
195
+ throw holdForegroundSettlement(new errors_js_1.LocalDevContractError(operation.failureClassification ?? 'internal-error', `Fleet up failed after transactional rollback (${operation.failureClassification ?? 'internal-error'}).${fleetOperationFailureDetail(operation)}`), afterWrite);
97
196
  }
98
- const state = await launch.client.status();
99
197
  return {
100
198
  ...(afterWrite ? { afterWrite } : {}),
101
199
  attached: launch.attached,
102
200
  fleetId: launch.fleetId,
103
201
  operation,
104
202
  operationId: submission.operationId,
105
- slices: state.slices,
203
+ slices: observed.slices,
106
204
  };
107
205
  }
206
+ /** A foreground supervisor lives in this process and can outlive a failed
207
+ * wait: a refused shutdown keeps it, and an interrupt's owned down may still
208
+ * run. Attach its settlement so the caller reports the failure, then holds
209
+ * interrupt handling until owned cleanup settles instead of leaving Node's
210
+ * default handler to kill the process. */
211
+ function holdForegroundSettlement(error, afterWrite) {
212
+ if (!afterWrite)
213
+ return error;
214
+ const failure = error instanceof Error
215
+ ? error
216
+ : new errors_js_1.LocalDevContractError('internal-error', String(error));
217
+ foregroundSettlements.set(failure, afterWrite);
218
+ return failure;
219
+ }
108
220
  //# sourceMappingURL=dev-command-core.js.map
@@ -1,6 +1,24 @@
1
- export declare const LOCAL_DEV_FAILURE_CLASSIFICATIONS: readonly ["invalid-input", "personal-identity-unavailable", "fleet-in-use", "fleet-ownership-ambiguous", "local-context-missing", "local-context-invalid", "local-context-conflict", "unknown-slice", "invalid-adapter", "slice-active-elsewhere", "slice-claim-ambiguous", "not-running", "invalid-state", "configuration-mismatch", "protocol-mismatch", "environment-deficient", "prerequisite-unavailable", "foundation-unavailable", "operation-conflict", "readiness-timeout", "provisioning-timeout", "startup-interrupted", "cleanup-incomplete", "supervisor-unreachable", "stale-state", "stale-state-ambiguous", "log-query-invalid", "internal-error"];
1
+ export declare const LOCAL_DEV_FAILURE_CLASSIFICATIONS: readonly ["invalid-input", "personal-identity-unavailable", "fleet-in-use", "fleet-ownership-ambiguous", "local-context-missing", "local-context-invalid", "local-context-conflict", "unknown-slice", "invalid-adapter", "slice-active-elsewhere", "slice-claim-ambiguous", "not-running", "invalid-state", "configuration-mismatch", "protocol-mismatch", "environment-deficient", "prerequisite-unavailable", "foundation-unavailable", "operation-conflict", "readiness-timeout", "readiness-stalled", "health-check-failing", "provisioning-timeout", "process-exited", "startup-interrupted", "observation-timeout", "observation-interrupted", "cleanup-incomplete", "supervisor-unreachable", "stale-state", "stale-state-ambiguous", "log-query-invalid", "internal-error"];
2
2
  export type LocalDevFailureClassification = (typeof LOCAL_DEV_FAILURE_CLASSIFICATIONS)[number];
3
3
  export declare class LocalDevContractError extends Error {
4
4
  readonly classification: LocalDevFailureClassification;
5
5
  constructor(classification: LocalDevFailureClassification, message: string);
6
6
  }
7
+ /** What the client knew when it stopped watching an accepted operation that
8
+ * had not settled. The operation itself may still be running. */
9
+ export interface FleetObservationDetail {
10
+ elapsedMs: number;
11
+ /** The last non-terminal state read; absent when no read succeeded. */
12
+ lastObservedState?: 'accepted' | 'running';
13
+ operationId: string;
14
+ /** The explicit cap, present only for `observation-timeout`. */
15
+ timeoutMs?: number;
16
+ }
17
+ /** Observation of an accepted operation ended without a terminal record: at an
18
+ * explicit cap, on interruption, or because the supervisor stopped answering.
19
+ * The CLI renders `detail` in the failure envelope, with the operation ID at
20
+ * the envelope level, so a script can keep following the same operation. */
21
+ export declare class FleetObservationError extends LocalDevContractError {
22
+ readonly detail: FleetObservationDetail;
23
+ constructor(classification: 'observation-interrupted' | 'observation-timeout' | 'supervisor-unreachable', message: string, detail: FleetObservationDetail);
24
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LocalDevContractError = exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = void 0;
3
+ exports.FleetObservationError = exports.LocalDevContractError = exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = void 0;
4
4
  exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = [
5
5
  'invalid-input',
6
6
  'personal-identity-unavailable',
@@ -22,8 +22,13 @@ exports.LOCAL_DEV_FAILURE_CLASSIFICATIONS = [
22
22
  'foundation-unavailable',
23
23
  'operation-conflict',
24
24
  'readiness-timeout',
25
+ 'readiness-stalled',
26
+ 'health-check-failing',
25
27
  'provisioning-timeout',
28
+ 'process-exited',
26
29
  'startup-interrupted',
30
+ 'observation-timeout',
31
+ 'observation-interrupted',
27
32
  'cleanup-incomplete',
28
33
  'supervisor-unreachable',
29
34
  'stale-state',
@@ -40,4 +45,16 @@ class LocalDevContractError extends Error {
40
45
  }
41
46
  }
42
47
  exports.LocalDevContractError = LocalDevContractError;
48
+ /** Observation of an accepted operation ended without a terminal record: at an
49
+ * explicit cap, on interruption, or because the supervisor stopped answering.
50
+ * The CLI renders `detail` in the failure envelope, with the operation ID at
51
+ * the envelope level, so a script can keep following the same operation. */
52
+ class FleetObservationError extends LocalDevContractError {
53
+ detail;
54
+ constructor(classification, message, detail) {
55
+ super(classification, message);
56
+ this.detail = detail;
57
+ }
58
+ }
59
+ exports.FleetObservationError = FleetObservationError;
43
60
  //# sourceMappingURL=errors.js.map
@@ -1,19 +1,67 @@
1
1
  import { type FleetMutationCommand } from './control-contract.js';
2
+ import { LocalDevContractError } from './errors.js';
2
3
  import { sendFleetControlRequest } from './fleet-ipc.js';
3
- import { type FleetOperationStateRecord, type FleetStateRecord } from './fleet-state.js';
4
+ import { type FleetOperationStateRecord, type FleetSliceState, type FleetStateRecord } from './fleet-state.js';
4
5
  import { type PrivateStateSecurity } from './private-state.js';
5
6
  import type { FleetRuntimePaths } from './runtime-paths.js';
6
7
  export interface FleetClientEffects {
8
+ /** Monotonic-enough wall clock for observation bookkeeping (tests inject). */
9
+ now?: () => number;
7
10
  requestId?: () => string;
8
11
  send?: typeof sendFleetControlRequest;
12
+ /** Poll pause; resolves early when the signal aborts (tests inject). */
13
+ sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
9
14
  }
15
+ /** A supervisor whose authenticated status keeps answering with the same
16
+ * heartbeat for this long is not making progress. The supervisor writes its
17
+ * heartbeat every 5 s; Windows identity captures can block its loop longer.
18
+ * The window must also span enough consecutive status reads (see
19
+ * `waitForOperationOutcome`), so a host sleep that jumps the clock cannot
20
+ * trip it before the supervisor's next heartbeat. */
21
+ export declare const FLEET_SUPERVISOR_HEARTBEAT_STALE_MS: Readonly<{
22
+ default: 30000;
23
+ win32: 60000;
24
+ }>;
25
+ /** Defaults for watching a supervised operation. None of them is an operation
26
+ * deadline: without an explicit cap the client waits for the real outcome. */
27
+ export declare const FLEET_OPERATION_OBSERVATION: Readonly<{
28
+ pollMs: 200;
29
+ /** Progress is reported on every Slice change and at least this often. */
30
+ progressIntervalMs: 30000;
31
+ /** Status (heartbeat and Slice progress) is read at most this often. */
32
+ statusIntervalMs: 2000;
33
+ /** Consecutive unreachable polls tolerated before the wait fails. */
34
+ unreachableLimit: 6;
35
+ }>;
36
+ export interface FleetOperationProgress {
37
+ elapsedMs: number;
38
+ operation: FleetOperationStateRecord;
39
+ /** Live state of the operation's Slices from the latest status read. */
40
+ slices: FleetSliceState[];
41
+ }
42
+ export interface FleetOperationWaitOptions {
43
+ onProgress?: (progress: FleetOperationProgress) => void;
44
+ pollMs?: number;
45
+ /** Ends observation (never the operation) when aborted. */
46
+ signal?: AbortSignal;
47
+ /** Explicit observation cap. Absent means wait for the terminal state. */
48
+ timeoutMs?: number;
49
+ }
50
+ export interface FleetOperationObservationTuning {
51
+ progressIntervalMs?: number;
52
+ staleHeartbeatMs?: number;
53
+ statusIntervalMs?: number;
54
+ unreachableLimit?: number;
55
+ }
56
+ /** Terminal failure as an error that keeps the supervisor's persisted cause. */
57
+ export declare function fleetOperationFailure(operation: FleetOperationStateRecord): LocalDevContractError;
10
58
  export interface FleetOperationSubmission {
11
59
  operationId: string;
12
60
  state: 'accepted';
13
61
  }
14
62
  export declare class FleetClient {
15
63
  #private;
16
- constructor(paths: FleetRuntimePaths, security?: PrivateStateSecurity, effects?: FleetClientEffects);
64
+ constructor(paths: FleetRuntimePaths, security?: PrivateStateSecurity, effects?: FleetClientEffects, observation?: FleetOperationObservationTuning);
17
65
  ping(): Promise<{
18
66
  fleetId: string;
19
67
  protocolVersion: number;
@@ -25,9 +73,19 @@ export declare class FleetClient {
25
73
  slices: readonly string[];
26
74
  }, idempotencyKey?: `${string}-${string}-${string}-${string}-${string}`): Promise<FleetOperationSubmission>;
27
75
  operationStatus(operationId: string): Promise<FleetOperationStateRecord>;
28
- waitForOperation(operationId: string, options?: {
29
- pollMs?: number;
30
- timeoutMs?: number;
31
- }): Promise<FleetOperationStateRecord>;
76
+ /** Wait for a terminal operation record, `succeeded` or `failed`.
77
+ *
78
+ * There is no default deadline: an operation that is still progressing is
79
+ * observed until it settles. The wait ends early only on an explicit
80
+ * `timeoutMs` cap (`observation-timeout`), on `signal` abort
81
+ * (`observation-interrupted`), or when the supervisor itself stops making
82
+ * progress (`supervisor-unreachable`: repeated unreachable polls or a
83
+ * heartbeat that no longer advances). Ending observation never cancels the
84
+ * supervised operation.
85
+ */
86
+ waitForOperationOutcome(operationId: string, options?: FleetOperationWaitOptions): Promise<FleetOperationStateRecord>;
87
+ /** Throwing form: resolves only on `succeeded`; a terminal `failed` record
88
+ * becomes an error carrying its persisted classification and cause. */
89
+ waitForOperation(operationId: string, options?: FleetOperationWaitOptions): Promise<FleetOperationStateRecord>;
32
90
  shutdown(idempotencyKey?: `${string}-${string}-${string}-${string}-${string}`): Promise<void>;
33
91
  }