@moinulmoin/eden 0.1.5 → 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.
@@ -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,
@@ -1376,6 +1489,9 @@ async function runEveDeployment(request, options, collected) {
1376
1489
  async function collectEvePreflight(request, options) {
1377
1490
  let runtimeConfig;
1378
1491
  const checks = [];
1492
+ if (request.envFile !== undefined) {
1493
+ eveProgress(options, "validating environment file");
1494
+ }
1379
1495
  try {
1380
1496
  if (request.envFile === undefined) {
1381
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."));
@@ -1404,6 +1520,7 @@ async function collectEvePreflight(request, options) {
1404
1520
  runtimeConfig,
1405
1521
  };
1406
1522
  }
1523
+ eveProgress(options, "packaging project — snapshot, frozen install, eve build");
1407
1524
  const nodeImage = options.nodeImage ?? DEFAULT_EVE_NODE_IMAGE;
1408
1525
  const artifactRoot = options.artifactRoot ??
1409
1526
  join(request.projectRoot, ".eden", "eve-deploy", "generations", `preflight-${randomUUID()}`);
@@ -1500,6 +1617,7 @@ async function collectEvePreflight(request, options) {
1500
1617
  let cloudflare;
1501
1618
  let publicOrigin;
1502
1619
  if (request.command === "deploy") {
1620
+ eveProgress(options, "checking Cloudflare access and exact target");
1503
1621
  try {
1504
1622
  cloudflare = await readEveCloudflareTarget({
1505
1623
  projectId: packaging.project?.projectId ?? "unknown-project",
@@ -1573,6 +1691,7 @@ async function collectEvePreflight(request, options) {
1573
1691
  let runtimeEvidence;
1574
1692
  let runtimeInjection;
1575
1693
  if (hostRequirements !== undefined && hostRequirementsValid) {
1694
+ eveProgress(options, "building linux/amd64 runtime image, booting eve, checking health");
1576
1695
  try {
1577
1696
  if (runtimeConfig !== undefined && request.command === "preflight") {
1578
1697
  runtimeInjection = await prepareEveRuntimeInjection(runtimeConfig, {
@@ -1646,6 +1765,7 @@ async function collectEvePreflight(request, options) {
1646
1765
  };
1647
1766
  }
1648
1767
  if (cloudflare === undefined) {
1768
+ eveProgress(options, "checking Cloudflare access and exact target");
1649
1769
  try {
1650
1770
  cloudflare = await readEveCloudflareTarget({
1651
1771
  projectId: packaging.project?.projectId ?? "unknown-project",
@@ -1691,6 +1811,10 @@ async function collectEvePreflight(request, options) {
1691
1811
  };
1692
1812
  }
1693
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
+ }
1694
1818
  const lock = request.command === "deploy"
1695
1819
  ? await acquireEveTargetLock(request.projectRoot, request.environment, request.name)
1696
1820
  : undefined;
@@ -1700,8 +1824,9 @@ export async function runEveControlPlane(request, options = {}) {
1700
1824
  let lockReleased = true;
1701
1825
  try {
1702
1826
  collected = await collectEvePreflight(request, options);
1827
+ const eveVersion = collected.candidate?.eveVersion;
1703
1828
  if (!collected.result.ok) {
1704
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(collected.result)));
1829
+ emitEveResult(options, collected.result, startedAt, eveVersion);
1705
1830
  throw new EveCliError({
1706
1831
  code: request.command === "deploy"
1707
1832
  ? "EVE_DEPLOY_CHECKS_FAILED"
@@ -1712,11 +1837,11 @@ export async function runEveControlPlane(request, options = {}) {
1712
1837
  });
1713
1838
  }
1714
1839
  if (request.command === "preflight") {
1715
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(collected.result)));
1840
+ emitEveResult(options, collected.result, startedAt, eveVersion);
1716
1841
  }
1717
1842
  else {
1718
1843
  const deploymentResult = await runEveDeployment(request, options, collected);
1719
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(deploymentResult)));
1844
+ emitEveResult(options, deploymentResult, startedAt, eveVersion);
1720
1845
  if (!deploymentResult.ok) {
1721
1846
  const status = deploymentResult.deployment?.status;
1722
1847
  throw new EveCliError({
@@ -1832,9 +1957,28 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
1832
1957
  if (identity.environment !== environment || identity.name !== name) {
1833
1958
  return undefined;
1834
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
+ };
1835
1979
  return {
1836
1980
  recordPath,
1837
- identity: identity,
1981
+ identity: verified,
1838
1982
  };
1839
1983
  }
1840
1984
  async function defaultDestroyTargetRead(request) {
@@ -1858,14 +2002,57 @@ async function defaultDestroyTargetRead(request) {
1858
2002
  (exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
1859
2003
  const entries = exactContainerEntries(containers.value, request.containerApplicationName);
1860
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
+ }
1861
2019
  return {
1862
2020
  workerExists,
1863
2021
  ...(firstContainer === undefined
1864
2022
  ? {}
1865
2023
  : { containerApplicationId: firstContainer.id }),
1866
2024
  ...(accountId === undefined ? {} : { accountId }),
2025
+ ...(registryImagesPresent === undefined
2026
+ ? {}
2027
+ : { registryImagesPresent }),
1867
2028
  };
1868
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
+ }
1869
2056
  async function defaultWorkerDelete(request) {
1870
2057
  const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
1871
2058
  if (result.exitCode === 0)
@@ -1884,19 +2071,114 @@ async function defaultContainerDelete(request) {
1884
2071
  }
1885
2072
  return "indeterminate";
1886
2073
  }
1887
- async function verifyEveAbsence(request, options) {
1888
- const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
1889
- const observed = await reader({
1890
- workerName: request.workerName,
1891
- containerApplicationName: request.containerApplicationName,
1892
- });
1893
- return !observed.workerExists &&
1894
- 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];
1895
2173
  }
1896
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
+ }
1897
2179
  const checks = [];
1898
2180
  const emit = (outcome) => {
1899
- (options.stdout ?? (() => undefined))(redactEveRuntimeOutput(JSON.stringify(outcome)));
2181
+ emitEveDestroyOutcome(options, outcome, startedAt);
1900
2182
  };
1901
2183
  const fail = (code, message) => {
1902
2184
  throw new EveCliError({ code, message, source: "eve destroy" });
@@ -1939,15 +2221,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1939
2221
  fail("EVE_DESTROY_RECORD_MISMATCH", "The immutable deployment record does not match the selected target.");
1940
2222
  }
1941
2223
  checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
1942
- const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
1943
- 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({
1944
2227
  workerName: expectedWorker,
1945
2228
  containerApplicationName: expectedContainer,
1946
- });
2229
+ registryImages: ownedRegistryImages,
2230
+ }));
2231
+ const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
2232
+ eveProgress(options, "reading the exact remote inventory");
2233
+ const before = await readTarget();
1947
2234
  if (before.accountId !== record.identity.accountId) {
1948
2235
  fail("EVE_DESTROY_OWNERSHIP_UNPROVEN", "The authenticated account does not own the recorded deployment account.");
1949
2236
  }
1950
- 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))) {
1951
2242
  emit({
1952
2243
  ok: true,
1953
2244
  status: "absent",
@@ -1962,6 +2253,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1962
2253
  }
1963
2254
  const deleteWorker = options.deleteWorker ?? defaultWorkerDelete;
1964
2255
  if (before.workerExists) {
2256
+ eveProgress(options, `deleting Worker ${expectedWorker}`);
1965
2257
  const workerResult = await deleteWorker({ name: expectedWorker });
1966
2258
  if (workerResult === "indeterminate") {
1967
2259
  emit({
@@ -1982,6 +2274,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
1982
2274
  }
1983
2275
  const deleteContainer = options.deleteContainer ?? defaultContainerDelete;
1984
2276
  if (before.containerApplicationId !== undefined) {
2277
+ eveProgress(options, `deleting Container application ${expectedContainer}`);
1985
2278
  const containerResult = await deleteContainer({
1986
2279
  applicationId: before.containerApplicationId,
1987
2280
  });
@@ -2002,11 +2295,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2002
2295
  ? "The exact owned Container application was deleted."
2003
2296
  : "The exact Container application was already absent."));
2004
2297
  }
2005
- const absent = await verifyEveAbsence({
2006
- workerName: expectedWorker,
2007
- containerApplicationName: expectedContainer,
2008
- }, options);
2009
- 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) {
2010
2314
  emit({
2011
2315
  ok: false,
2012
2316
  status: "failed",
@@ -2019,6 +2323,24 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2019
2323
  });
2020
2324
  fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
2021
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
+ }
2022
2344
  checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
2023
2345
  const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");
2024
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: {