@moinulmoin/eden 0.1.4 → 0.2.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.
@@ -9,7 +9,7 @@ import { fileURLToPath, } from "node:url";
9
9
  import { promisify, } from "node:util";
10
10
  import { EveCliError, } from "./eve.js";
11
11
  import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, } from "./eve-packaging.js";
12
- import { buildEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
12
+ import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
13
13
  import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
14
14
  const execFileAsync = promisify(execFile);
15
15
  const require = createRequire(import.meta.url);
@@ -29,6 +29,8 @@ export const DEFAULT_EVE_NODE_IMAGE = Object.freeze({
29
29
  reference: "node:24.17.0-bookworm-slim",
30
30
  digest: "sha256:862263c612aa437e3037674b85419622a9d93bff80aa1eee5398dfe686375532",
31
31
  });
32
+ /** The single tag Eden pushes to the managed Cloudflare registry per generation. */
33
+ const EVE_REGISTRY_IMAGE_TAG = "candidate";
32
34
  function safeText(value) {
33
35
  return redactEveRuntimeOutput(value)
34
36
  .replace(/(?:^|[\s"'=])(?:Bearer|Token)\s+\S+/giu, "$1[redacted]")
@@ -73,6 +75,113 @@ function blockedAfterFailure(id, message) {
73
75
  function allChecksClear(checks) {
74
76
  return checks.every((value) => value.status === "passed" || value.status === "skipped");
75
77
  }
78
+ /** True when the caller asked for the human progress/summary output. */
79
+ function humanOutput(options) {
80
+ return options.outputFormat === "human";
81
+ }
82
+ function emitLine(options, line) {
83
+ (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(line));
84
+ }
85
+ /** Short stage markers so a multi-minute run shows what it is doing. */
86
+ function eveProgress(options, stage) {
87
+ if (humanOutput(options))
88
+ emitLine(options, `... ${stage}`);
89
+ }
90
+ function eveCheckLines(checks) {
91
+ const lines = [];
92
+ for (const value of checks) {
93
+ const glyph = value.status === "passed"
94
+ ? "✓"
95
+ : value.status === "skipped"
96
+ ? "-"
97
+ : "✗";
98
+ lines.push(`${glyph} ${value.id} — ${value.message}`);
99
+ if ((value.status === "failed" || value.status === "blocked") &&
100
+ value.remediation !== undefined) {
101
+ lines.push(` → ${value.remediation}`);
102
+ }
103
+ }
104
+ return lines;
105
+ }
106
+ function formatEveElapsed(startedAt) {
107
+ const elapsed = (Date.now() - startedAt) / 1000;
108
+ return `${elapsed.toFixed(elapsed < 10 ? 1 : 0)}s`;
109
+ }
110
+ /**
111
+ * Human summary lines for a preflight or deploy result: every check with a
112
+ * status glyph, then a compact tail with the outcome.
113
+ */
114
+ function humanEveResultLines(result, startedAt, eveVersion) {
115
+ const lines = [...eveCheckLines(result.checks)];
116
+ const elapsed = formatEveElapsed(startedAt);
117
+ const commandLabel = result.command === "eve deploy" ? "deploy" : "preflight";
118
+ if (result.ok) {
119
+ if (result.command === "eve deploy") {
120
+ const url = result.deployment?.stableWorkersDevOrigin;
121
+ lines.push("", `✓ deployed ${result.name} (${result.environment})` +
122
+ (eveVersion === undefined ? "" : ` · eve ${eveVersion}`));
123
+ if (url !== undefined)
124
+ lines.push(url);
125
+ lines.push(`done in ${elapsed}`);
126
+ }
127
+ else {
128
+ lines.push("", `✓ ${result.name} (${result.environment}) — preflight passed in ${elapsed}`);
129
+ }
130
+ return lines;
131
+ }
132
+ const firstFailure = result.checks.find((value) => value.status === "failed" || value.status === "blocked");
133
+ if (result.deployment !== undefined) {
134
+ lines.push("", `✗ ${commandLabel} ${result.deployment.status}`, ` → ${result.deployment.status === "indeterminate"
135
+ ? "The outcome is indeterminate; inspect the retained ownership evidence before retrying the same exact target."
136
+ : result.deployment.status === "deployed"
137
+ ? "The deployment is live; only local runtime-image cleanup failed, so do not retry the deployment."
138
+ : "The deployment failed before promotion; the exact target was not claimed."}`);
139
+ }
140
+ else if (firstFailure !== undefined) {
141
+ lines.push("", `✗ ${commandLabel} failed — first failing check: ${firstFailure.id}`);
142
+ }
143
+ else {
144
+ lines.push("", `✗ ${commandLabel} failed`);
145
+ }
146
+ lines.push(`stopped after ${elapsed}`);
147
+ return lines;
148
+ }
149
+ function emitEveResult(options, result, startedAt, eveVersion) {
150
+ if (humanOutput(options)) {
151
+ for (const line of humanEveResultLines(result, startedAt, eveVersion)) {
152
+ emitLine(options, line);
153
+ }
154
+ return;
155
+ }
156
+ emitLine(options, JSON.stringify(result));
157
+ }
158
+ /**
159
+ * Human summary lines for a destroy outcome: removed resources, anything
160
+ * retained, and the outcome tail.
161
+ */
162
+ function humanEveDestroyLines(outcome, startedAt) {
163
+ const lines = [...eveCheckLines(outcome.checks)];
164
+ const elapsed = formatEveElapsed(startedAt);
165
+ if (outcome.ok) {
166
+ lines.push("", outcome.status === "absent"
167
+ ? `✓ ${outcome.name} (${outcome.environment}) is already absent — nothing removed`
168
+ : `✓ destroyed ${outcome.name} (${outcome.environment}) in ${elapsed}`, " retained: local deployment records under .eden/eve-deploy");
169
+ return lines;
170
+ }
171
+ const firstFailure = outcome.checks.find((value) => value.status === "failed" || value.status === "blocked");
172
+ lines.push("", `✗ destroy ${outcome.name} ${outcome.status}` +
173
+ (firstFailure === undefined ? "" : ` — first failing check: ${firstFailure.id}`));
174
+ return lines;
175
+ }
176
+ function emitEveDestroyOutcome(options, outcome, startedAt) {
177
+ if (humanOutput(options)) {
178
+ for (const line of humanEveDestroyLines(outcome, startedAt)) {
179
+ emitLine(options, line);
180
+ }
181
+ return;
182
+ }
183
+ emitLine(options, JSON.stringify(outcome));
184
+ }
76
185
  function runtimeConfigExclusion(config) {
77
186
  if (config === undefined)
78
187
  return undefined;
@@ -1018,12 +1127,13 @@ async function runEveDeployment(request, options, collected) {
1018
1127
  }
1019
1128
  }
1020
1129
  else {
1130
+ eveProgress(options, "pushing container image to the Cloudflare registry");
1021
1131
  const localImageReference = runtimeEvidence.imageReference;
1022
1132
  if (localImageReference === undefined || localImageReference.length === 0) {
1023
1133
  throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not return an exact local image handle for publication.");
1024
1134
  }
1025
1135
  const imageRepository = `eden-eve-${targetKey}-${candidate.generationId}`;
1026
- const imageTag = `${imageRepository}:candidate`;
1136
+ const imageTag = `${imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
1027
1137
  const targetImageReference = `registry.cloudflare.com/${accountId}/${imageRepository}@${imageDigest}`;
1028
1138
  const imagePublication = await (options.publishImage ??
1029
1139
  defaultImagePublicationRunner)({
@@ -1062,6 +1172,7 @@ async function runEveDeployment(request, options, collected) {
1062
1172
  }
1063
1173
  let runtimeInjection;
1064
1174
  if (collected.runtimeConfig !== undefined) {
1175
+ eveProgress(options, "registering runtime variables");
1065
1176
  try {
1066
1177
  runtimeInjection = await prepareEveRuntimeInjection(collected.runtimeConfig, {
1067
1178
  mode: "deploy",
@@ -1143,6 +1254,7 @@ async function runEveDeployment(request, options, collected) {
1143
1254
  };
1144
1255
  let publication;
1145
1256
  try {
1257
+ eveProgress(options, "publishing Worker and Container application");
1146
1258
  publication = await (options.publish ?? defaultPublicationRunner())(publicationRequest);
1147
1259
  }
1148
1260
  catch {
@@ -1217,6 +1329,7 @@ async function runEveDeployment(request, options, collected) {
1217
1329
  }
1218
1330
  let health;
1219
1331
  try {
1332
+ eveProgress(options, `verifying health at ${stableWorkersDevOrigin}`);
1220
1333
  health = await (options.health ?? defaultHealthRunner())({
1221
1334
  identity,
1222
1335
  origin: stableWorkersDevOrigin,
@@ -1341,15 +1454,44 @@ async function runEveDeployment(request, options, collected) {
1341
1454
  });
1342
1455
  await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), targetKey, candidate.generationRoot);
1343
1456
  await options.afterPromotion?.();
1457
+ let runtimeImageCleanup;
1458
+ if (runtimeEvidence.cleanup.imageRetained) {
1459
+ if (options.retainRuntimeImage === true) {
1460
+ runtimeImageCleanup = check("VAL-CROSS-004", "skipped", "The exact local runtime image was intentionally retained by the programmatic caller.");
1461
+ }
1462
+ else {
1463
+ let discarded = false;
1464
+ try {
1465
+ discarded = await (options.discardRuntimeImage ??
1466
+ discardEveRuntimeImage)({
1467
+ imageId: imageDigest,
1468
+ generationId: candidate.generationId,
1469
+ generationRoot: candidate.generationRoot,
1470
+ });
1471
+ }
1472
+ catch {
1473
+ discarded = false;
1474
+ }
1475
+ runtimeImageCleanup = discarded
1476
+ ? check("VAL-CROSS-004", "passed", "The exact local runtime image and its publication tags were removed after the healthy deployment was promoted.")
1477
+ : check("VAL-CROSS-004", "failed", "The remote deployment is healthy, but exact local runtime-image cleanup could not be verified.", `Remove only the recorded image ${imageDigest}; do not retry or run broad Docker cleanup.`);
1478
+ }
1479
+ }
1344
1480
  return {
1345
1481
  ...collected.result,
1346
- ok: true,
1482
+ ok: runtimeImageCleanup?.status !== "failed",
1483
+ ...(runtimeImageCleanup === undefined
1484
+ ? {}
1485
+ : { checks: [...collected.result.checks, runtimeImageCleanup] }),
1347
1486
  deployment: deploymentMetadata(identity, targetKey, "deployed"),
1348
1487
  };
1349
1488
  }
1350
1489
  async function collectEvePreflight(request, options) {
1351
1490
  let runtimeConfig;
1352
1491
  const checks = [];
1492
+ if (request.envFile !== undefined) {
1493
+ eveProgress(options, "validating environment file");
1494
+ }
1353
1495
  try {
1354
1496
  if (request.envFile === undefined) {
1355
1497
  checks.push(check("VAL-SEC-001", "skipped", "No explicit --env-file was supplied; no implicit project environment source was read."), check("VAL-SEC-002", "skipped", "No explicit runtime names were supplied; reserved host-name validation was not needed."));
@@ -1378,6 +1520,7 @@ async function collectEvePreflight(request, options) {
1378
1520
  runtimeConfig,
1379
1521
  };
1380
1522
  }
1523
+ eveProgress(options, "packaging project — snapshot, frozen install, eve build");
1381
1524
  const nodeImage = options.nodeImage ?? DEFAULT_EVE_NODE_IMAGE;
1382
1525
  const artifactRoot = options.artifactRoot ??
1383
1526
  join(request.projectRoot, ".eden", "eve-deploy", "generations", `preflight-${randomUUID()}`);
@@ -1474,6 +1617,7 @@ async function collectEvePreflight(request, options) {
1474
1617
  let cloudflare;
1475
1618
  let publicOrigin;
1476
1619
  if (request.command === "deploy") {
1620
+ eveProgress(options, "checking Cloudflare access and exact target");
1477
1621
  try {
1478
1622
  cloudflare = await readEveCloudflareTarget({
1479
1623
  projectId: packaging.project?.projectId ?? "unknown-project",
@@ -1547,6 +1691,7 @@ async function collectEvePreflight(request, options) {
1547
1691
  let runtimeEvidence;
1548
1692
  let runtimeInjection;
1549
1693
  if (hostRequirements !== undefined && hostRequirementsValid) {
1694
+ eveProgress(options, "building linux/amd64 runtime image, booting eve, checking health");
1550
1695
  try {
1551
1696
  if (runtimeConfig !== undefined && request.command === "preflight") {
1552
1697
  runtimeInjection = await prepareEveRuntimeInjection(runtimeConfig, {
@@ -1620,6 +1765,7 @@ async function collectEvePreflight(request, options) {
1620
1765
  };
1621
1766
  }
1622
1767
  if (cloudflare === undefined) {
1768
+ eveProgress(options, "checking Cloudflare access and exact target");
1623
1769
  try {
1624
1770
  cloudflare = await readEveCloudflareTarget({
1625
1771
  projectId: packaging.project?.projectId ?? "unknown-project",
@@ -1665,6 +1811,10 @@ async function collectEvePreflight(request, options) {
1665
1811
  };
1666
1812
  }
1667
1813
  export async function runEveControlPlane(request, options = {}) {
1814
+ const startedAt = Date.now();
1815
+ if (humanOutput(options)) {
1816
+ emitLine(options, `eden ${request.command} — ${request.name} (${request.environment})`);
1817
+ }
1668
1818
  const lock = request.command === "deploy"
1669
1819
  ? await acquireEveTargetLock(request.projectRoot, request.environment, request.name)
1670
1820
  : undefined;
@@ -1674,8 +1824,9 @@ export async function runEveControlPlane(request, options = {}) {
1674
1824
  let lockReleased = true;
1675
1825
  try {
1676
1826
  collected = await collectEvePreflight(request, options);
1827
+ const eveVersion = collected.candidate?.eveVersion;
1677
1828
  if (!collected.result.ok) {
1678
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(collected.result)));
1829
+ emitEveResult(options, collected.result, startedAt, eveVersion);
1679
1830
  throw new EveCliError({
1680
1831
  code: request.command === "deploy"
1681
1832
  ? "EVE_DEPLOY_CHECKS_FAILED"
@@ -1686,20 +1837,24 @@ export async function runEveControlPlane(request, options = {}) {
1686
1837
  });
1687
1838
  }
1688
1839
  if (request.command === "preflight") {
1689
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(collected.result)));
1840
+ emitEveResult(options, collected.result, startedAt, eveVersion);
1690
1841
  }
1691
1842
  else {
1692
1843
  const deploymentResult = await runEveDeployment(request, options, collected);
1693
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(deploymentResult)));
1844
+ emitEveResult(options, deploymentResult, startedAt, eveVersion);
1694
1845
  if (!deploymentResult.ok) {
1695
1846
  const status = deploymentResult.deployment?.status;
1696
1847
  throw new EveCliError({
1697
1848
  code: status === "indeterminate"
1698
1849
  ? "DEPLOY_INDETERMINATE"
1699
- : "EVE_DEPLOYMENT_FAILED",
1850
+ : status === "deployed"
1851
+ ? "EVE_RUNTIME_IMAGE_CLEANUP_FAILED"
1852
+ : "EVE_DEPLOYMENT_FAILED",
1700
1853
  message: status === "indeterminate"
1701
1854
  ? "The Eve deployment outcome is indeterminate; inspect the retained exact ownership evidence before retrying."
1702
- : "The Eve deployment failed before exact target promotion.",
1855
+ : status === "deployed"
1856
+ ? "The Eve deployment is healthy and promoted, but exact local runtime-image cleanup failed; do not retry the deployment."
1857
+ : "The Eve deployment failed before exact target promotion.",
1703
1858
  });
1704
1859
  }
1705
1860
  }
@@ -1802,9 +1957,28 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
1802
1957
  if (identity.environment !== environment || identity.name !== name) {
1803
1958
  return undefined;
1804
1959
  }
1960
+ const verified = {
1961
+ workerName: identity.workerName,
1962
+ containerApplicationName: identity.containerApplicationName,
1963
+ stableContainerInstanceName: identity.stableContainerInstanceName,
1964
+ accountId: identity.accountId,
1965
+ environment,
1966
+ name,
1967
+ ...(typeof identity.projectId === "string" && identity.projectId.length > 0
1968
+ ? { projectId: identity.projectId }
1969
+ : {}),
1970
+ ...(typeof identity.generationId === "string" &&
1971
+ identity.generationId.length > 0
1972
+ ? { generationId: identity.generationId }
1973
+ : {}),
1974
+ ...(typeof identity.containerImage === "string" &&
1975
+ identity.containerImage.length > 0
1976
+ ? { containerImage: identity.containerImage }
1977
+ : {}),
1978
+ };
1805
1979
  return {
1806
1980
  recordPath,
1807
- identity: identity,
1981
+ identity: verified,
1808
1982
  };
1809
1983
  }
1810
1984
  async function defaultDestroyTargetRead(request) {
@@ -1828,14 +2002,57 @@ async function defaultDestroyTargetRead(request) {
1828
2002
  (exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
1829
2003
  const entries = exactContainerEntries(containers.value, request.containerApplicationName);
1830
2004
  const firstContainer = entries === undefined ? undefined : entries[0];
2005
+ const ownedImages = request.registryImages;
2006
+ let registryImagesPresent;
2007
+ if (ownedImages !== undefined && ownedImages.length > 0) {
2008
+ const images = await readWranglerJson([
2009
+ "containers",
2010
+ "images",
2011
+ "list",
2012
+ "--json",
2013
+ ]);
2014
+ const present = registryImageNames(images.value);
2015
+ if (!images.failed && present !== undefined) {
2016
+ registryImagesPresent = ownedImages.filter((image) => present.has(image));
2017
+ }
2018
+ }
1831
2019
  return {
1832
2020
  workerExists,
1833
2021
  ...(firstContainer === undefined
1834
2022
  ? {}
1835
2023
  : { containerApplicationId: firstContainer.id }),
1836
2024
  ...(accountId === undefined ? {} : { accountId }),
2025
+ ...(registryImagesPresent === undefined
2026
+ ? {}
2027
+ : { registryImagesPresent }),
1837
2028
  };
1838
2029
  }
2030
+ /**
2031
+ * `wrangler containers images list --json` reports `[{name, tags}]` entries
2032
+ * with the account prefix already stripped from the repository name. The set
2033
+ * carries `repository:tag` refs for the owned tags Eden publishes.
2034
+ */
2035
+ function registryImageNames(value) {
2036
+ const collection = jsonCollection(value);
2037
+ if (collection === undefined)
2038
+ return undefined;
2039
+ const names = new Set();
2040
+ for (const entry of collection) {
2041
+ if (typeof entry !== "object" || entry === null || !("name" in entry)) {
2042
+ return undefined;
2043
+ }
2044
+ const name = entry.name;
2045
+ const tags = "tags" in entry ? entry.tags : undefined;
2046
+ if (typeof name !== "string" || !Array.isArray(tags))
2047
+ return undefined;
2048
+ for (const tag of tags) {
2049
+ if (typeof tag !== "string")
2050
+ return undefined;
2051
+ names.add(`${name}:${tag}`);
2052
+ }
2053
+ }
2054
+ return names;
2055
+ }
1839
2056
  async function defaultWorkerDelete(request) {
1840
2057
  const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
1841
2058
  if (result.exitCode === 0)
@@ -1854,19 +2071,114 @@ async function defaultContainerDelete(request) {
1854
2071
  }
1855
2072
  return "indeterminate";
1856
2073
  }
1857
- async function verifyEveAbsence(request, options) {
1858
- const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
1859
- const observed = await reader({
1860
- workerName: request.workerName,
1861
- containerApplicationName: request.containerApplicationName,
1862
- });
1863
- return !observed.workerExists &&
1864
- observed.containerApplicationId === undefined;
2074
+ async function defaultRegistryImageDelete(request) {
2075
+ const result = await runWranglerWithInput(["containers", "images", "delete", request.image, "--skip-confirmation"], process.cwd());
2076
+ if (result.exitCode === 0)
2077
+ return "deleted";
2078
+ if (/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2079
+ return "absent";
2080
+ }
2081
+ return "indeterminate";
2082
+ }
2083
+ /**
2084
+ * The exact `<repository>:${EVE_REGISTRY_IMAGE_TAG}` ref this generation's
2085
+ * deployment record proves Eden pushed, or undefined. Ownership holds only
2086
+ * when the recorded digest reference points at the repository derived from
2087
+ * the same identity that produced it; a digest-bearing reference to any other
2088
+ * repository was published outside Eden's push and is never owned here.
2089
+ */
2090
+ function ownedRegistryImageRef(identity, environment, name, accountId) {
2091
+ if (identity.projectId === undefined ||
2092
+ identity.generationId === undefined ||
2093
+ identity.containerImage === undefined ||
2094
+ identity.accountId !== accountId) {
2095
+ return undefined;
2096
+ }
2097
+ const repository = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-${identity.generationId}`;
2098
+ const expected = `registry.cloudflare.com/${identity.accountId}/${repository}@`;
2099
+ if (!identity.containerImage.startsWith(expected) ||
2100
+ imageDigestFromReference(identity.containerImage) === undefined) {
2101
+ return undefined;
2102
+ }
2103
+ return `${repository}:${EVE_REGISTRY_IMAGE_TAG}`;
2104
+ }
2105
+ /**
2106
+ * The exact image ref an indeterminate deploy attempt provably pushed. The
2107
+ * retained attempt evidence names the target's repository; the account and
2108
+ * target key must match the verified destroy record.
2109
+ */
2110
+ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2111
+ const raw = await readFile(attemptPath, "utf8").catch(() => undefined);
2112
+ if (raw === undefined)
2113
+ return undefined;
2114
+ let parsed;
2115
+ try {
2116
+ parsed = JSON.parse(raw);
2117
+ }
2118
+ catch {
2119
+ return undefined;
2120
+ }
2121
+ if (typeof parsed !== "object" || parsed === null)
2122
+ return undefined;
2123
+ const prefix = `eden-eve-${targetKey}-`;
2124
+ if ("imageRepository" in parsed &&
2125
+ typeof parsed.imageRepository === "string" &&
2126
+ parsed.imageRepository.startsWith(prefix) &&
2127
+ (!("targetKey" in parsed) || parsed.targetKey === targetKey) &&
2128
+ "accountId" in parsed &&
2129
+ parsed.accountId === accountId) {
2130
+ return `${parsed.imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
2131
+ }
2132
+ if ("imageReference" in parsed &&
2133
+ typeof parsed.imageReference === "string" &&
2134
+ imageDigestFromReference(parsed.imageReference) !== undefined) {
2135
+ const repository = parsed.imageReference.split("@")[0];
2136
+ const expected = `registry.cloudflare.com/${accountId}/${prefix}`;
2137
+ if (repository !== undefined &&
2138
+ repository.startsWith(expected) &&
2139
+ (!("targetKey" in parsed) || parsed.targetKey === targetKey)) {
2140
+ return `${repository.slice(`registry.cloudflare.com/${accountId}/`.length)}:${EVE_REGISTRY_IMAGE_TAG}`;
2141
+ }
2142
+ }
2143
+ return undefined;
2144
+ }
2145
+ /**
2146
+ * Every registry image tag this exact target provably owns: the recorded
2147
+ * digest references in matching deployment records plus the repository
2148
+ * evidence retained by indeterminate pushes. Never derived by prefix-matching
2149
+ * the registry; each ref is computed from the immutable generation records.
2150
+ */
2151
+ async function collectEveDestroyRegistryImages(projectRoot, environment, name, record) {
2152
+ const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
2153
+ const entries = await readdir(generationsRoot).catch(() => []);
2154
+ const images = new Set();
2155
+ const targetKey = record.projectId === undefined
2156
+ ? undefined
2157
+ : stableDeploymentKey(record.projectId, record.accountId, environment, name);
2158
+ for (const entry of entries) {
2159
+ const generationRoot = join(generationsRoot, entry);
2160
+ const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2161
+ const deployedImage = parsed === undefined
2162
+ ? undefined
2163
+ : ownedRegistryImageRef(parsed.identity, environment, name, record.accountId);
2164
+ if (deployedImage !== undefined)
2165
+ images.add(deployedImage);
2166
+ if (targetKey === undefined)
2167
+ continue;
2168
+ const attemptImage = await attemptRegistryImageRef(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
2169
+ if (attemptImage !== undefined)
2170
+ images.add(attemptImage);
2171
+ }
2172
+ return [...images];
1865
2173
  }
1866
2174
  export async function runEveDestroy(request, options = {}) {
2175
+ const startedAt = Date.now();
2176
+ if (humanOutput(options)) {
2177
+ emitLine(options, `eden destroy — ${request.name} (${request.environment})`);
2178
+ }
1867
2179
  const checks = [];
1868
2180
  const emit = (outcome) => {
1869
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(outcome)));
2181
+ emitEveDestroyOutcome(options, outcome, startedAt);
1870
2182
  };
1871
2183
  const fail = (code, message) => {
1872
2184
  throw new EveCliError({ code, message, source: "eve destroy" });
@@ -1909,15 +2221,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1909
2221
  fail("EVE_DESTROY_RECORD_MISMATCH", "The immutable deployment record does not match the selected target.");
1910
2222
  }
1911
2223
  checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
1912
- const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
1913
- const before = await reader({
2224
+ eveProgress(options, "verifying the immutable ownership record");
2225
+ const ownedRegistryImages = await collectEveDestroyRegistryImages(request.projectRoot, request.environment, request.name, record.identity);
2226
+ const readTarget = () => Promise.resolve(reader({
1914
2227
  workerName: expectedWorker,
1915
2228
  containerApplicationName: expectedContainer,
1916
- });
2229
+ registryImages: ownedRegistryImages,
2230
+ }));
2231
+ const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
2232
+ eveProgress(options, "reading the exact remote inventory");
2233
+ const before = await readTarget();
1917
2234
  if (before.accountId !== record.identity.accountId) {
1918
2235
  fail("EVE_DESTROY_OWNERSHIP_UNPROVEN", "The authenticated account does not own the recorded deployment account.");
1919
2236
  }
1920
- if (!before.workerExists && before.containerApplicationId === undefined) {
2237
+ if (!before.workerExists &&
2238
+ before.containerApplicationId === undefined &&
2239
+ (ownedRegistryImages.length === 0 ||
2240
+ (before.registryImagesPresent !== undefined &&
2241
+ before.registryImagesPresent.length === 0))) {
1921
2242
  emit({
1922
2243
  ok: true,
1923
2244
  status: "absent",
@@ -1932,6 +2253,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1932
2253
  }
1933
2254
  const deleteWorker = options.deleteWorker ?? defaultWorkerDelete;
1934
2255
  if (before.workerExists) {
2256
+ eveProgress(options, `deleting Worker ${expectedWorker}`);
1935
2257
  const workerResult = await deleteWorker({ name: expectedWorker });
1936
2258
  if (workerResult === "indeterminate") {
1937
2259
  emit({
@@ -1952,6 +2274,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1952
2274
  }
1953
2275
  const deleteContainer = options.deleteContainer ?? defaultContainerDelete;
1954
2276
  if (before.containerApplicationId !== undefined) {
2277
+ eveProgress(options, `deleting Container application ${expectedContainer}`);
1955
2278
  const containerResult = await deleteContainer({
1956
2279
  applicationId: before.containerApplicationId,
1957
2280
  });
@@ -1972,11 +2295,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1972
2295
  ? "The exact owned Container application was deleted."
1973
2296
  : "The exact Container application was already absent."));
1974
2297
  }
1975
- const absent = await verifyEveAbsence({
1976
- workerName: expectedWorker,
1977
- containerApplicationName: expectedContainer,
1978
- }, options);
1979
- if (!absent) {
2298
+ const imageDeletes = new Map();
2299
+ if (ownedRegistryImages.length > 0) {
2300
+ const deleteRegistryImage = options.deleteRegistryImage ?? defaultRegistryImageDelete;
2301
+ const deletable = before.registryImagesPresent === undefined
2302
+ ? ownedRegistryImages
2303
+ : ownedRegistryImages.filter((image) => before.registryImagesPresent?.includes(image) === true);
2304
+ if (deletable.length > 0) {
2305
+ eveProgress(options, `deleting ${deletable.length} registry image${deletable.length === 1 ? "" : "s"}`);
2306
+ }
2307
+ for (const image of deletable) {
2308
+ imageDeletes.set(image, await deleteRegistryImage({ image }));
2309
+ }
2310
+ }
2311
+ eveProgress(options, "verifying the exact target is absent");
2312
+ const after = await readTarget();
2313
+ if (after.workerExists || after.containerApplicationId !== undefined) {
1980
2314
  emit({
1981
2315
  ok: false,
1982
2316
  status: "failed",
@@ -1989,6 +2323,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1989
2323
  });
1990
2324
  fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
1991
2325
  }
2326
+ const retainedImages = ownedRegistryImages.filter((image) => after.registryImagesPresent === undefined
2327
+ ? imageDeletes.get(image) === "indeterminate"
2328
+ : after.registryImagesPresent.includes(image));
2329
+ if (ownedRegistryImages.length > 0) {
2330
+ checks.push(retainedImages.length === 0
2331
+ ? check("VAL-LIFE-006-REGISTRY", "passed", "Every registry image tag recorded for this exact target was deleted or proven absent.")
2332
+ : check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image(s) remain after destroy: ${retainedImages.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed image refs with wrangler containers images delete."));
2333
+ }
2334
+ if (retainedImages.length > 0) {
2335
+ emit({
2336
+ ok: false,
2337
+ status: "failed",
2338
+ environment: request.environment,
2339
+ name: request.name,
2340
+ checks,
2341
+ });
2342
+ fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image(s) remain: ${retainedImages.join(", ")}.`);
2343
+ }
1992
2344
  checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
1993
2345
  const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");
1994
2346
  await rm(currentPath, { force: true });
@@ -252,6 +252,13 @@ export declare class EvePackagingError extends Error {
252
252
  readonly remediation: string;
253
253
  });
254
254
  }
255
+ /**
256
+ * The authored Eve project source the runtime stage must carry so `eve start`
257
+ * can resolve the project context (agent/, agents/ workspaces, flat layouts,
258
+ * and the manifest). Returns validated top-level snapshot entries with every
259
+ * capture-time exclusion still applied; generated roots are never source.
260
+ */
261
+ export declare function eveSnapshotSourceEntries(snapshotRoot: string): Promise<readonly string[]>;
255
262
  export declare function buildEveProjectSnapshot(options: EveProjectSnapshotOptions): Promise<EveProjectPackagingResult>;
256
263
  export declare function revalidateEveProjectCandidateInputs(candidate: EveProjectBuildCandidate, runtimeConfig?: EveRuntimeConfigExclusion): Promise<void>;
257
264
  export declare function createDockerEveProjectBuilder(options: {