hammerkit 1.6.0 → 1.7.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.
Files changed (256) hide show
  1. package/README.md +4 -4
  2. package/build.schema.json +49 -2
  3. package/dist/cache/backends/local-cache-backend.js +58 -1
  4. package/dist/cache/backends/registry/registry-client.js +264 -0
  5. package/dist/cache/backends/registry/registry-credentials.js +81 -0
  6. package/dist/cache/backends/registry/registry-reference.js +36 -0
  7. package/dist/cache/backends/registry-cache-backend.js +145 -0
  8. package/dist/cache/backends/s3-cache-backend.js +99 -46
  9. package/dist/cache/cache-backend-registry.js +2 -0
  10. package/dist/cache/cache-inventory.js +84 -0
  11. package/dist/cache/cache-sync.js +77 -0
  12. package/dist/cache/explain.js +214 -0
  13. package/dist/cache/last-resolved.js +48 -0
  14. package/dist/cache/read-only.js +15 -0
  15. package/dist/cache/retention.js +61 -0
  16. package/dist/cli.js +43 -0
  17. package/dist/docker/using-container.js +5 -1
  18. package/dist/executer/archive-task-entry.js +21 -0
  19. package/dist/executer/await-completed-dependencies.js +32 -14
  20. package/dist/executer/cache-metadata.js +9 -2
  21. package/dist/executer/docker-service.js +2 -1
  22. package/dist/executer/docker-task.js +2 -1
  23. package/dist/executer/dry-run.js +73 -0
  24. package/dist/executer/event-cache.js +6 -5
  25. package/dist/executer/execute-command.js +21 -3
  26. package/dist/executer/execute-work-service.js +10 -1
  27. package/dist/executer/execute-work-task.js +95 -25
  28. package/dist/executer/execute-work-tree.js +26 -1
  29. package/dist/executer/get-scheduler-execute-result.js +4 -1
  30. package/dist/executer/lazy-resolver.js +15 -3
  31. package/dist/executer/reset-work-tree.js +2 -0
  32. package/dist/executer/run-summary.js +102 -0
  33. package/dist/executer/scheduler/enqueue-next.js +15 -60
  34. package/dist/executer/scheduler/state-key.js +78 -0
  35. package/dist/kubernetes/await-running-state.js +73 -19
  36. package/dist/log.js +16 -6
  37. package/dist/logging/live-logger.js +2 -1
  38. package/dist/optimizer/calculate-checksum.js +8 -2
  39. package/dist/optimizer/get-work-cache-stats.js +66 -16
  40. package/dist/optimizer/work-task-cache-description.js +34 -5
  41. package/dist/planner/project-root.js +26 -0
  42. package/dist/planner/utils/append-work-service.js +2 -1
  43. package/dist/planner/utils/append-work-task.js +7 -1
  44. package/dist/planner/utils/parse-work-source.js +35 -30
  45. package/dist/planner/utils/portable-path.js +30 -0
  46. package/dist/planner/work-cache-id.js +2 -2
  47. package/dist/planner/work-graph-serializer.js +103 -0
  48. package/dist/planner/work-instance-id.js +15 -0
  49. package/dist/planner/work-runtime-docker.js +53 -15
  50. package/dist/planner/work-runtime-kubernetes.js +35 -3
  51. package/dist/planner/work-runtime-local.js +9 -3
  52. package/dist/planner/work-service-id.js +16 -4
  53. package/dist/program.js +235 -1
  54. package/dist/schema/build-file-base-task-schema.js +4 -0
  55. package/dist/schema/cache-schema.js +21 -2
  56. package/dist/schema/duration-schema.js +18 -0
  57. package/dist/schema/reference-parser.js +3 -0
  58. package/dist/schema/size-schema.js +18 -0
  59. package/dist/schema/work-scope-parser.js +15 -2
  60. package/dist/utils/units.js +87 -0
  61. package/package.json +12 -1
  62. package/.agents/FEEDBACK.md +0 -74
  63. package/.agents/skills/before-commit/SKILL.md +0 -101
  64. package/.agents/skills/do-the-work/SKILL.md +0 -64
  65. package/.agents/skills/fix-ci/SKILL.md +0 -98
  66. package/.agents/skills/speckit-git-commit/SKILL.md +0 -53
  67. package/.agents/skills/speckit-git-feature/SKILL.md +0 -72
  68. package/.agents/skills/speckit-git-initialize/SKILL.md +0 -54
  69. package/.agents/skills/speckit-git-remote/SKILL.md +0 -50
  70. package/.agents/skills/speckit-git-validate/SKILL.md +0 -54
  71. package/.devcontainer/Dockerfile +0 -5
  72. package/.devcontainer/devcontainer.json +0 -40
  73. package/.gitbook.yaml +0 -5
  74. package/.specify/extensions/.registry +0 -37
  75. package/.specify/extensions/git/README.md +0 -100
  76. package/.specify/extensions/git/commands/speckit.git.commit.md +0 -48
  77. package/.specify/extensions/git/commands/speckit.git.feature.md +0 -67
  78. package/.specify/extensions/git/commands/speckit.git.initialize.md +0 -49
  79. package/.specify/extensions/git/commands/speckit.git.remote.md +0 -45
  80. package/.specify/extensions/git/commands/speckit.git.validate.md +0 -49
  81. package/.specify/extensions/git/config-template.yml +0 -62
  82. package/.specify/extensions/git/extension.yml +0 -140
  83. package/.specify/extensions/git/git-config.yml +0 -62
  84. package/.specify/extensions.yml +0 -149
  85. package/.specify/feature.json +0 -3
  86. package/.specify/init-options.json +0 -10
  87. package/.specify/integration.json +0 -15
  88. package/.specify/integrations/claude.manifest.json +0 -16
  89. package/.specify/integrations/speckit.manifest.json +0 -17
  90. package/.specify/memory/constitution.md +0 -162
  91. package/.specify/templates/checklist-template.md +0 -40
  92. package/.specify/templates/constitution-template.md +0 -50
  93. package/.specify/templates/plan-template.md +0 -113
  94. package/.specify/templates/spec-template.md +0 -131
  95. package/.specify/templates/tasks-template.md +0 -252
  96. package/.specify/workflows/speckit/workflow.yml +0 -77
  97. package/.specify/workflows/workflow-registry.json +0 -13
  98. package/AGENTS.md +0 -94
  99. package/Dockerfile +0 -32
  100. package/FOLLOWUPS.md +0 -34
  101. package/SUMMARY.md +0 -68
  102. package/VALUES.md +0 -53
  103. package/dist/cache/auto-pull.spec.js +0 -123
  104. package/dist/cache/backends/local-cache-backend.spec.js +0 -72
  105. package/dist/cache/backends/s3-cache-backend.spec.js +0 -118
  106. package/dist/cache/cache-backend-registry.spec.js +0 -22
  107. package/dist/cache/resolve-cache.spec.js +0 -52
  108. package/dist/cli.spec.js +0 -23
  109. package/dist/console/get-logger.spec.js +0 -23
  110. package/dist/docker/get-default-platform.spec.js +0 -29
  111. package/dist/docker/longest-common-prefix.spec.js +0 -26
  112. package/dist/docker/package.spec.js +0 -145
  113. package/dist/docker/remove-container.spec.js +0 -32
  114. package/dist/docker/using-container.spec.js +0 -90
  115. package/dist/environment/merge-environment-variables.spec.js +0 -24
  116. package/dist/environment/replace-env-variables.spec.js +0 -47
  117. package/dist/executer/abort.spec.js +0 -18
  118. package/dist/executer/environment-mock.spec.js +0 -22
  119. package/dist/executer/execute.spec.js +0 -51
  120. package/dist/executer/get-service-containers.spec.js +0 -59
  121. package/dist/executer/get-service-env-hints.spec.js +0 -65
  122. package/dist/executer/label-values.spec.js +0 -61
  123. package/dist/executer/lazy-resolver.spec.js +0 -20
  124. package/dist/executer/process-manager.spec.js +0 -54
  125. package/dist/executer/scheduler/enqueue-next.spec.js +0 -136
  126. package/dist/executer/state-resolver.spec.js +0 -37
  127. package/dist/executer/state.spec.js +0 -66
  128. package/dist/executer/states.spec.js +0 -14
  129. package/dist/file/get-file-context.spec.js +0 -178
  130. package/dist/file/move-files.spec.js +0 -48
  131. package/dist/kubernetes/ensure-http-route.spec.js +0 -70
  132. package/dist/kubernetes/ensure-namespace.spec.js +0 -39
  133. package/dist/kubernetes/resolve-pod-name.spec.js +0 -81
  134. package/dist/log.spec.js +0 -131
  135. package/dist/logging/grouped-logger.spec.js +0 -84
  136. package/dist/logging/interactive-logger.spec.js +0 -57
  137. package/dist/logging/live-logger.spec.js +0 -37
  138. package/dist/logging/loggers.spec.js +0 -154
  139. package/dist/optimizer/calculate-checksum.spec.js +0 -34
  140. package/dist/optimizer/get-cache-directory.spec.js +0 -59
  141. package/dist/optimizer/get-work-cache-stats.spec.js +0 -79
  142. package/dist/optimizer/work-task-cache-description.spec.js +0 -45
  143. package/dist/parser/default-build-file.spec.js +0 -48
  144. package/dist/parser/parse-label-arguments.spec.js +0 -27
  145. package/dist/parser/read-build-file.spec.js +0 -89
  146. package/dist/parser/read-env-file.spec.js +0 -42
  147. package/dist/planner/utils/append-work-service.spec.js +0 -71
  148. package/dist/planner/utils/get-container-user.spec.js +0 -30
  149. package/dist/planner/utils/get-default-kube-config.spec.js +0 -11
  150. package/dist/planner/utils/normalize-path.spec.js +0 -24
  151. package/dist/planner/utils/parse-work-command.spec.js +0 -21
  152. package/dist/planner/utils/parse-work-generate.spec.js +0 -45
  153. package/dist/planner/utils/parse-work-mount.spec.js +0 -62
  154. package/dist/planner/utils/parse-work-mounts.spec.js +0 -19
  155. package/dist/planner/utils/parse-work-port.spec.js +0 -24
  156. package/dist/planner/utils/parse-work-ports.spec.js +0 -24
  157. package/dist/planner/utils/parse-work-source.spec.js +0 -58
  158. package/dist/planner/utils/parse-work-volume.spec.js +0 -18
  159. package/dist/planner/utils/plan-work-tasks.spec.js +0 -30
  160. package/dist/planner/utils/plan-work-volume.spec.js +0 -33
  161. package/dist/planner/utils/split-name.spec.js +0 -16
  162. package/dist/planner/utils/template-value.spec.js +0 -14
  163. package/dist/planner/validate.spec.js +0 -251
  164. package/dist/planner/work-cache-id.spec.js +0 -173
  165. package/dist/planner/work-runtime-local.spec.js +0 -193
  166. package/dist/program-commands.spec.js +0 -84
  167. package/dist/program.spec.js +0 -43
  168. package/dist/schema/build-file-schema.spec.js +0 -61
  169. package/dist/schema/doc-build-files.spec.js +0 -223
  170. package/dist/schema/parse-error.spec.js +0 -18
  171. package/dist/schema/reference-parser.spec.js +0 -83
  172. package/dist/schema/schema-parser.spec.js +0 -80
  173. package/dist/schema/type-guards.spec.js +0 -23
  174. package/dist/schema/work-scope-parser.spec.js +0 -66
  175. package/dist/service/update-service-status.spec.js +0 -27
  176. package/dist/testing/ensure-local-registry.js +0 -134
  177. package/dist/testing/example-test-suite.js +0 -63
  178. package/dist/testing/expect.js +0 -82
  179. package/dist/testing/get-test-suite.js +0 -9
  180. package/dist/testing/http-get.js +0 -21
  181. package/dist/testing/integration/cache.spec.js +0 -49
  182. package/dist/testing/integration/cancellation.spec.js +0 -45
  183. package/dist/testing/integration/clean.spec.js +0 -43
  184. package/dist/testing/integration/cmd.spec.js +0 -15
  185. package/dist/testing/integration/concurrency.spec.js +0 -19
  186. package/dist/testing/integration/docker-package-registry.spec.js +0 -44
  187. package/dist/testing/integration/docker-service-crash.spec.js +0 -36
  188. package/dist/testing/integration/docker.spec.js +0 -18
  189. package/dist/testing/integration/env.spec.js +0 -48
  190. package/dist/testing/integration/environment.spec.js +0 -97
  191. package/dist/testing/integration/error.spec.js +0 -19
  192. package/dist/testing/integration/export.spec.js +0 -22
  193. package/dist/testing/integration/extend.spec.js +0 -29
  194. package/dist/testing/integration/glob.spec.js +0 -40
  195. package/dist/testing/integration/include.spec.js +0 -31
  196. package/dist/testing/integration/invalid.spec.js +0 -17
  197. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  198. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  199. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  200. package/dist/testing/integration/kubernetes.spec.js +0 -99
  201. package/dist/testing/integration/labels.spec.js +0 -42
  202. package/dist/testing/integration/local.spec.js +0 -14
  203. package/dist/testing/integration/monorepo.spec.js +0 -15
  204. package/dist/testing/integration/multi-line.spec.js +0 -17
  205. package/dist/testing/integration/reference.spec.js +0 -21
  206. package/dist/testing/integration/services.spec.js +0 -38
  207. package/dist/testing/integration/store.spec.js +0 -52
  208. package/dist/testing/integration/unknown.spec.js +0 -28
  209. package/dist/testing/integration/validate.spec.js +0 -160
  210. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  211. package/dist/testing/integration/watch.spec.js +0 -45
  212. package/dist/testing/requires-kubernetes.js +0 -18
  213. package/dist/testing/requires-linux-containers.js +0 -18
  214. package/dist/testing/requires-windows-containers.js +0 -18
  215. package/dist/testing/test-case.js +0 -75
  216. package/dist/testing/test-streams.js +0 -28
  217. package/dist/testing/test-suite-setup.js +0 -3
  218. package/dist/testing/test-suite.js +0 -3
  219. package/dist/testing/testing-timeout.js +0 -32
  220. package/dist/utils/abort-event.spec.js +0 -46
  221. package/dist/utils/buffer-context.spec.js +0 -38
  222. package/dist/utils/ci.spec.js +0 -53
  223. package/dist/utils/debouncer.spec.js +0 -58
  224. package/dist/utils/emitter.spec.js +0 -34
  225. package/dist/utils/empty-writable.spec.js +0 -18
  226. package/dist/utils/environment-config.spec.js +0 -26
  227. package/dist/utils/fail-never.spec.js +0 -9
  228. package/dist/utils/sleep.spec.js +0 -22
  229. package/dist/version.spec.js +0 -12
  230. package/specs/build-file/caches/spec.md +0 -114
  231. package/specs/build-file/environment-variables/spec.md +0 -82
  232. package/specs/build-file/includes/spec.md +0 -60
  233. package/specs/build-file/references/spec.md +0 -56
  234. package/specs/build-file/spec.md +0 -62
  235. package/specs/cli/clean/spec.md +0 -85
  236. package/specs/cli/down/spec.md +0 -85
  237. package/specs/cli/execute/spec.md +0 -101
  238. package/specs/cli/init/spec.md +0 -64
  239. package/specs/cli/ls/spec.md +0 -66
  240. package/specs/cli/package/spec.md +0 -110
  241. package/specs/cli/spec.md +0 -73
  242. package/specs/cli/store-restore/spec.md +0 -74
  243. package/specs/cli/up/spec.md +0 -80
  244. package/specs/cli/validate/spec.md +0 -70
  245. package/specs/labels/spec.md +0 -91
  246. package/specs/service/container/spec.md +0 -120
  247. package/specs/service/kubernetes/spec.md +0 -97
  248. package/specs/service/spec.md +0 -105
  249. package/specs/task/caching/spec.md +0 -107
  250. package/specs/task/container/spec.md +0 -118
  251. package/specs/task/dependencies/spec.md +0 -70
  252. package/specs/task/extending/spec.md +0 -74
  253. package/specs/task/kubernetes/spec.md +0 -125
  254. package/specs/task/needs/spec.md +0 -59
  255. package/specs/task/spec.md +0 -87
  256. package/specs/task/watching/spec.md +0 -69
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRegistryCacheBackend = exports.entryTag = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ const tar_1 = require("tar");
8
+ const registry_reference_1 = require("./registry/registry-reference");
9
+ const registry_client_1 = require("./registry/registry-client");
10
+ const MAX_TAG_LENGTH = 128;
11
+ // The deterministic tag for one entry. A task id (sha1) plus a state key (md5)
12
+ // fits comfortably; anything longer is hashed rather than rejected.
13
+ function entryTag(taskId, stateKey) {
14
+ const tag = `${taskId}-${stateKey}`;
15
+ if (tag.length <= MAX_TAG_LENGTH && /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(tag)) {
16
+ return tag;
17
+ }
18
+ return `${taskId.substring(0, 40)}-${(0, crypto_1.createHash)('sha256').update(stateKey).digest('hex').substring(0, 64)}`;
19
+ }
20
+ exports.entryTag = entryTag;
21
+ async function withTempDir(environment, fn) {
22
+ const dir = (0, path_1.join)((0, os_1.tmpdir)(), `hammerkit-registry-${(0, crypto_1.randomUUID)()}`);
23
+ await environment.file.createDirectory(dir);
24
+ try {
25
+ return await fn(dir);
26
+ }
27
+ finally {
28
+ await environment.file.remove(dir);
29
+ }
30
+ }
31
+ // Stores each cache entry as a standard single-layer OCI image (ADR-0005): the
32
+ // layer is an uncompressed tar of the entry directory (whose archives are
33
+ // already gzipped), tagged `<taskId>-<stateKey>`. The manifest is written only
34
+ // after both blobs are committed, so a partial upload is never visible to
35
+ // has()/pull(); a re-push of the same content is an identical overwrite.
36
+ function createRegistryCacheBackend(spec) {
37
+ const reference = (0, registry_reference_1.parseRegistryReference)(spec.repository, spec.insecure);
38
+ const clients = new WeakMap();
39
+ const client = (environment) => {
40
+ let instance = clients.get(environment);
41
+ if (!instance) {
42
+ instance = new registry_client_1.RegistryClient(reference, environment);
43
+ clients.set(environment, instance);
44
+ }
45
+ return instance;
46
+ };
47
+ return {
48
+ type: 'registry',
49
+ async has(taskId, stateKey, environment) {
50
+ return client(environment).manifestExists(entryTag(taskId, stateKey));
51
+ },
52
+ async pull(taskId, stateKey, into, environment) {
53
+ const registry = client(environment);
54
+ const manifest = await registry.getManifest(entryTag(taskId, stateKey));
55
+ if (!manifest) {
56
+ return false;
57
+ }
58
+ if (manifest.layers.length !== 1) {
59
+ throw new Error(`cache entry ${entryTag(taskId, stateKey)} is not a single-layer image`);
60
+ }
61
+ return withTempDir(environment, async (dir) => {
62
+ const layerFile = (0, path_1.join)(dir, 'layer.tar');
63
+ await registry.downloadBlob(manifest.layers[0], layerFile);
64
+ await environment.file.createDirectory(into);
65
+ // node-tar strips absolute paths and `..` segments by default, so a
66
+ // crafted layer cannot write outside `into`
67
+ await (0, tar_1.extract)({ file: layerFile, cwd: into });
68
+ return true;
69
+ });
70
+ },
71
+ async push(taskId, stateKey, from, environment) {
72
+ const registry = client(environment);
73
+ await withTempDir(environment, async (dir) => {
74
+ const layerFile = (0, path_1.join)(dir, 'layer.tar');
75
+ const files = (await environment.file.listFiles(from)).sort();
76
+ // portable + mtime-free so identical entries produce identical digests
77
+ // `noMtime` is supported by tar 6 but missing from its type definitions
78
+ await (0, tar_1.create)({ file: layerFile, cwd: from, portable: true, noMtime: true }, files);
79
+ const layer = await (0, registry_client_1.digestFile)(layerFile);
80
+ const config = Buffer.from(JSON.stringify({
81
+ architecture: 'amd64',
82
+ os: 'linux',
83
+ created: new Date().toISOString(),
84
+ rootfs: { type: 'layers', diff_ids: [layer.digest] },
85
+ config: { Labels: { 'dev.hammerkit.task-id': taskId, 'dev.hammerkit.state-key': stateKey } },
86
+ }));
87
+ const configDigest = (0, registry_client_1.digestBuffer)(config);
88
+ await registry.uploadBlob(layer.digest, layer.size, { file: layerFile });
89
+ await registry.uploadBlob(configDigest.digest, configDigest.size, { buffer: config });
90
+ const manifest = {
91
+ schemaVersion: 2,
92
+ mediaType: registry_client_1.OCI_MANIFEST,
93
+ config: { mediaType: registry_client_1.OCI_CONFIG, ...configDigest },
94
+ layers: [{ mediaType: registry_client_1.OCI_LAYER_TAR, ...layer }],
95
+ };
96
+ await registry.putManifest(entryTag(taskId, stateKey), manifest);
97
+ });
98
+ },
99
+ async clear(taskId, environment) {
100
+ const registry = client(environment);
101
+ const tags = (await registry.listTags()).filter((tag) => tag.startsWith(`${taskId}-`));
102
+ for (const tag of tags) {
103
+ const digest = await registry.manifestDigest(tag);
104
+ if (digest) {
105
+ await registry.deleteManifest(digest);
106
+ }
107
+ }
108
+ },
109
+ // Entries from the repository's tags: size from the manifest, creation time
110
+ // from the image config. Registries keep no last-use marker, so retention
111
+ // works from age and size.
112
+ async list(environment) {
113
+ const registry = client(environment);
114
+ const entries = [];
115
+ for (const tag of await registry.listTags()) {
116
+ const match = /^([0-9a-f]{40})-(.+)$/.exec(tag);
117
+ if (!match) {
118
+ continue;
119
+ }
120
+ const manifest = await registry.getManifest(tag);
121
+ if (!manifest) {
122
+ continue;
123
+ }
124
+ const config = await registry.getBlobJson(manifest.config.digest);
125
+ entries.push({
126
+ taskId: match[1],
127
+ stateKey: match[2],
128
+ size: manifest.config.size + manifest.layers.reduce((sum, layer) => sum + layer.size, 0),
129
+ createdAt: config.created ? Date.parse(config.created) : null,
130
+ lastAccessedAt: null,
131
+ });
132
+ }
133
+ return entries;
134
+ },
135
+ async remove(taskId, stateKey, environment) {
136
+ const registry = client(environment);
137
+ const digest = await registry.manifestDigest(entryTag(taskId, stateKey));
138
+ if (digest) {
139
+ await registry.deleteManifest(digest);
140
+ }
141
+ },
142
+ };
143
+ }
144
+ exports.createRegistryCacheBackend = createRegistryCacheBackend;
145
+ //# sourceMappingURL=registry-cache-backend.js.map
@@ -14,6 +14,14 @@ function isWithin(into, filename) {
14
14
  const target = (0, path_1.resolve)(base, filename);
15
15
  return target === base || target.startsWith(base + path_1.sep);
16
16
  }
17
+ // Only a missing object is a cache miss. Anything else (unreachable endpoint,
18
+ // denied credentials) is rethrown: the inline cache lookup catches it and
19
+ // degrades to a miss with a warning, while an explicit `cache pull`/`push`
20
+ // reports it and fails.
21
+ function isNotFound(e) {
22
+ const err = e;
23
+ return err?.name === 'NotFound' || err?.name === 'NoSuchKey' || err?.$metadata?.httpStatusCode === 404;
24
+ }
17
25
  function createS3CacheBackend(spec) {
18
26
  const client = new client_s3_1.S3Client({
19
27
  region: spec.region,
@@ -30,6 +38,28 @@ function createS3CacheBackend(spec) {
30
38
  function taskPrefix(taskId) {
31
39
  return [prefix, taskId].filter((p) => p.length > 0).join('/') + '/';
32
40
  }
41
+ async function listObjects(listPrefix) {
42
+ const objects = [];
43
+ let token;
44
+ do {
45
+ const listed = await client.send(new client_s3_1.ListObjectsV2Command({ Bucket: spec.bucket, Prefix: listPrefix, ContinuationToken: token }));
46
+ for (const obj of listed.Contents ?? []) {
47
+ if (obj.Key) {
48
+ objects.push({ Key: obj.Key, Size: obj.Size ?? 0, LastModified: obj.LastModified });
49
+ }
50
+ }
51
+ token = listed.IsTruncated ? listed.NextContinuationToken : undefined;
52
+ } while (token);
53
+ return objects;
54
+ }
55
+ async function deleteObjects(keys) {
56
+ for (let i = 0; i < keys.length; i += 1000) {
57
+ await client.send(new client_s3_1.DeleteObjectsCommand({
58
+ Bucket: spec.bucket,
59
+ Delete: { Objects: keys.slice(i, i + 1000).map((Key) => ({ Key })) },
60
+ }));
61
+ }
62
+ }
33
63
  return {
34
64
  type: 's3',
35
65
  async has(taskId, stateKey) {
@@ -40,49 +70,50 @@ function createS3CacheBackend(spec) {
40
70
  }));
41
71
  return true;
42
72
  }
43
- catch {
44
- return false;
73
+ catch (e) {
74
+ if (isNotFound(e)) {
75
+ return false;
76
+ }
77
+ throw e;
45
78
  }
46
79
  },
47
80
  async pull(taskId, stateKey, into, environment) {
48
81
  try {
49
- const head = await client
50
- .send(new client_s3_1.HeadObjectCommand({
82
+ await client.send(new client_s3_1.HeadObjectCommand({
51
83
  Bucket: spec.bucket,
52
84
  Key: keyFor(taskId, stateKey, 'stats.json'),
53
- }))
54
- .catch(() => null);
55
- if (!head) {
85
+ }));
86
+ }
87
+ catch (e) {
88
+ if (isNotFound(e)) {
56
89
  return false;
57
90
  }
58
- await environment.file.createDirectory(into);
59
- const listed = await client.send(new client_s3_1.ListObjectsV2Command({
91
+ throw e;
92
+ }
93
+ await environment.file.createDirectory(into);
94
+ const listed = await client.send(new client_s3_1.ListObjectsV2Command({
95
+ Bucket: spec.bucket,
96
+ Prefix: dirPrefix(taskId, stateKey),
97
+ }));
98
+ for (const obj of listed.Contents ?? []) {
99
+ if (!obj.Key)
100
+ continue;
101
+ const filename = obj.Key.substring(dirPrefix(taskId, stateKey).length);
102
+ if (!filename)
103
+ continue;
104
+ if (!isWithin(into, filename)) {
105
+ environment.console.warn(`skipping cache object outside restore dir: ${obj.Key}`);
106
+ continue;
107
+ }
108
+ const body = await client.send(new client_s3_1.GetObjectCommand({
60
109
  Bucket: spec.bucket,
61
- Prefix: dirPrefix(taskId, stateKey),
110
+ Key: obj.Key,
62
111
  }));
63
- for (const obj of listed.Contents ?? []) {
64
- if (!obj.Key)
65
- continue;
66
- const filename = obj.Key.substring(dirPrefix(taskId, stateKey).length);
67
- if (!filename)
68
- continue;
69
- if (!isWithin(into, filename)) {
70
- environment.console.warn(`skipping cache object outside restore dir: ${obj.Key}`);
71
- continue;
72
- }
73
- const body = await client.send(new client_s3_1.GetObjectCommand({
74
- Bucket: spec.bucket,
75
- Key: obj.Key,
76
- }));
77
- if (body.Body instanceof stream_1.Readable) {
78
- await environment.file.writeStream((0, path_1.join)(into, filename), body.Body);
79
- }
112
+ if (body.Body instanceof stream_1.Readable) {
113
+ await environment.file.writeStream((0, path_1.join)(into, filename), body.Body);
80
114
  }
81
- return true;
82
- }
83
- catch {
84
- return false;
85
115
  }
116
+ return true;
86
117
  },
87
118
  async push(taskId, stateKey, from, environment) {
88
119
  const files = await environment.file.listFiles(from);
@@ -101,22 +132,44 @@ function createS3CacheBackend(spec) {
101
132
  }
102
133
  },
103
134
  async clear(taskId) {
104
- let token;
105
- do {
106
- const listed = await client.send(new client_s3_1.ListObjectsV2Command({
107
- Bucket: spec.bucket,
108
- Prefix: taskPrefix(taskId),
109
- ContinuationToken: token,
110
- }));
111
- const objects = (listed.Contents ?? []).filter((o) => o.Key).map((o) => ({ Key: o.Key }));
112
- if (objects.length > 0) {
113
- await client.send(new client_s3_1.DeleteObjectsCommand({
114
- Bucket: spec.bucket,
115
- Delete: { Objects: objects },
116
- }));
135
+ await deleteObjects((await listObjects(taskPrefix(taskId))).map((o) => o.Key));
136
+ },
137
+ // Entries from object metadata: size is the sum of the entry's objects,
138
+ // created is when stats.json (written last) landed. S3 keeps no last-use
139
+ // marker, so retention on S3 works from age and size.
140
+ async list() {
141
+ const root = prefix.length > 0 ? `${prefix}/` : '';
142
+ const entries = new Map();
143
+ for (const obj of await listObjects(root)) {
144
+ const [taskId, stateKey, ...rest] = obj.Key.substring(root.length).split('/');
145
+ if (!taskId || !stateKey || rest.length === 0) {
146
+ continue;
147
+ }
148
+ const id = `${taskId}/${stateKey}`;
149
+ const entry = entries.get(id) ?? {
150
+ taskId,
151
+ stateKey,
152
+ size: 0,
153
+ createdAt: null,
154
+ lastAccessedAt: null,
155
+ complete: false,
156
+ };
157
+ entry.size = (entry.size ?? 0) + obj.Size;
158
+ if (rest.join('/') === 'stats.json') {
159
+ entry.complete = true;
160
+ entry.createdAt = obj.LastModified?.getTime() ?? null;
117
161
  }
118
- token = listed.IsTruncated ? listed.NextContinuationToken : undefined;
119
- } while (token);
162
+ entries.set(id, entry);
163
+ }
164
+ return [...entries.values()].filter((entry) => entry.complete).map(({ complete, ...entry }) => entry); // eslint-disable-line @typescript-eslint/no-unused-vars
165
+ },
166
+ async remove(taskId, stateKey) {
167
+ // stats.json first, so a concurrent reader sees the entry as missing
168
+ // rather than half-deleted
169
+ const objects = (await listObjects(dirPrefix(taskId, stateKey))).map((o) => o.Key);
170
+ const stats = objects.filter((key) => key.endsWith('/stats.json'));
171
+ await deleteObjects(stats);
172
+ await deleteObjects(objects.filter((key) => !stats.includes(key)));
120
173
  },
121
174
  };
122
175
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createCacheBackend = exports.registerCacheBackend = void 0;
4
4
  const local_cache_backend_1 = require("./backends/local-cache-backend");
5
5
  const s3_cache_backend_1 = require("./backends/s3-cache-backend");
6
+ const registry_cache_backend_1 = require("./backends/registry-cache-backend");
6
7
  const factories = new Map();
7
8
  function registerCacheBackend(type, factory) {
8
9
  factories.set(type, factory);
@@ -18,4 +19,5 @@ function createCacheBackend(spec) {
18
19
  exports.createCacheBackend = createCacheBackend;
19
20
  registerCacheBackend('local', (spec) => (0, local_cache_backend_1.createLocalCacheBackend)(spec));
20
21
  registerCacheBackend('s3', (spec) => (0, s3_cache_backend_1.createS3CacheBackend)(spec));
22
+ registerCacheBackend('registry', (spec) => (0, registry_cache_backend_1.createRegistryCacheBackend)(spec));
21
23
  //# sourceMappingURL=cache-backend-registry.js.map
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoPrune = exports.pruneCache = exports.hasPolicy = exports.retentionPolicyOf = exports.listCache = void 0;
4
+ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
5
+ const cache_sync_1 = require("./cache-sync");
6
+ const retention_1 = require("./retention");
7
+ const resolve_cache_1 = require("./resolve-cache");
8
+ const units_1 = require("../utils/units");
9
+ function catalogOf(workTree) {
10
+ return workTree.caches ?? (0, resolve_cache_1.withBuiltinCaches)(undefined);
11
+ }
12
+ function taskNames(workTree) {
13
+ const names = new Map();
14
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
15
+ names.set(task.id(), task.name);
16
+ }
17
+ return names;
18
+ }
19
+ // List the entries a named cache (default: the machine-local `default` cache)
20
+ // holds, labelled with the task names of this build file where they match.
21
+ async function listCache(workTree, cacheName, environment) {
22
+ const name = cacheName ?? resolve_cache_1.DEFAULT_CACHE_NAME;
23
+ const backend = (0, cache_sync_1.resolveRemoteBackend)(workTree, name);
24
+ if (!backend.list) {
25
+ throw new Error(`the ${backend.type} backend of cache "${name}" cannot list its entries`);
26
+ }
27
+ const names = taskNames(workTree);
28
+ return (await backend.list(environment)).map((entry) => ({ ...entry, taskName: names.get(entry.taskId) ?? null }));
29
+ }
30
+ exports.listCache = listCache;
31
+ // The retention policy of a cache: its declared `retention` block, with any
32
+ // command-line values taking precedence.
33
+ function retentionPolicyOf(workTree, cacheName, overrides) {
34
+ const declared = catalogOf(workTree)[cacheName ?? resolve_cache_1.DEFAULT_CACHE_NAME]?.retention;
35
+ return {
36
+ maxAge: overrides.maxAge ?? (declared?.maxAge ? (0, units_1.parseDuration)(declared.maxAge) : undefined),
37
+ maxSize: overrides.maxSize ?? (declared?.maxSize ? (0, units_1.parseSize)(declared.maxSize) : undefined),
38
+ keepPerTask: overrides.keepPerTask ?? declared?.keepPerTask,
39
+ };
40
+ }
41
+ exports.retentionPolicyOf = retentionPolicyOf;
42
+ function hasPolicy(policy) {
43
+ return policy.maxAge !== undefined || policy.maxSize !== undefined || policy.keepPerTask !== undefined;
44
+ }
45
+ exports.hasPolicy = hasPolicy;
46
+ // Apply a retention policy to a cache. Only ever touches the cache it is given:
47
+ // a remote is pruned only when named explicitly (ADR-0001).
48
+ async function pruneCache(workTree, cacheName, policy, options, environment) {
49
+ const name = cacheName ?? resolve_cache_1.DEFAULT_CACHE_NAME;
50
+ const backend = (0, cache_sync_1.resolveRemoteBackend)(workTree, name);
51
+ const entries = await listCache(workTree, name, environment);
52
+ const plan = (0, retention_1.planRetention)(entries, policy, Date.now());
53
+ if (!options.dryRun && plan.evict.length > 0) {
54
+ if (!backend.remove) {
55
+ throw new Error(`the ${backend.type} backend of cache "${name}" cannot remove entries`);
56
+ }
57
+ for (const entry of plan.evict) {
58
+ await backend.remove(entry.taskId, entry.stateKey, environment);
59
+ }
60
+ }
61
+ return plan;
62
+ }
63
+ exports.pruneCache = pruneCache;
64
+ // After a successful run: prune every machine-local cache the run used that
65
+ // declares a retention policy. Remote caches are never pruned implicitly.
66
+ async function autoPrune(workTree, environment) {
67
+ const catalog = catalogOf(workTree);
68
+ const used = new Set();
69
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
70
+ used.add(task.data.caching.name);
71
+ }
72
+ const results = [];
73
+ for (const name of used) {
74
+ const spec = catalog[name];
75
+ if (!spec?.retention || spec.backend.type !== 'local') {
76
+ continue;
77
+ }
78
+ const policy = retentionPolicyOf(workTree, name, {});
79
+ results.push({ cacheName: name, plan: await pruneCache(workTree, name, policy, { dryRun: false }, environment) });
80
+ }
81
+ return results;
82
+ }
83
+ exports.autoPrune = autoPrune;
84
+ //# sourceMappingURL=cache-inventory.js.map
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.syncCache = exports.resolveRemoteBackend = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
8
+ const state_key_1 = require("../executer/scheduler/state-key");
9
+ const resolve_cache_1 = require("./resolve-cache");
10
+ const archive_task_entry_1 = require("../executer/archive-task-entry");
11
+ function resolveRemoteBackend(workTree, remote) {
12
+ // resolveCache throws a clear "cache ... is not defined" error for unknown names
13
+ return (0, resolve_cache_1.resolveCache)({ name: remote }, workTree.caches ?? (0, resolve_cache_1.withBuiltinCaches)(undefined), `--remote ${remote}`).backend;
14
+ }
15
+ exports.resolveRemoteBackend = resolveRemoteBackend;
16
+ // Move cache entries between each in-scope task's own cache and a named remote,
17
+ // executing nothing (ADR-0001). The entries moved are those for the tasks'
18
+ // *current* state keys, computed by the same engine the scheduler uses, so a
19
+ // pull pre-warms exactly what the next build looks up. The work tree already
20
+ // contains the transitive dependencies of the selected scope.
21
+ //
22
+ // Source absence is best-effort ('missing'); any backend error propagates, so
23
+ // an explicit pull/push reports an unreachable remote instead of degrading to a
24
+ // miss like the inline cache lookup does.
25
+ async function syncCache(workTree, options, environment) {
26
+ const remote = resolveRemoteBackend(workTree, options.remote);
27
+ const results = [];
28
+ for (const item of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
29
+ const { stateKey, resolved, provable } = await (0, state_key_1.computeStateKey)(item, options.cacheDefault, environment);
30
+ const local = resolved.backend;
31
+ const result = (status) => ({
32
+ taskId: item.id(),
33
+ taskName: item.name,
34
+ stateKey,
35
+ status,
36
+ });
37
+ if (resolved.method === 'none' || local === remote || !provable) {
38
+ results.push(result('skipped'));
39
+ continue;
40
+ }
41
+ const [source, destination] = options.direction === 'pull' ? [remote, local] : [local, remote];
42
+ if (await destination.has(item.id(), stateKey, environment)) {
43
+ results.push(result('present'));
44
+ continue;
45
+ }
46
+ // Outputs that are up to date in this checkout but were never stored in a
47
+ // backend (e.g. restored by the runtime's own state) are packaged directly.
48
+ if (options.direction === 'push' && !(await local.has(item.id(), stateKey, environment))) {
49
+ if ((await item.runtime.currentStateKey(environment)) !== stateKey) {
50
+ results.push(result('missing'));
51
+ continue;
52
+ }
53
+ await remote.push(item.id(), stateKey, await (0, archive_task_entry_1.archiveTaskEntry)(item, environment), environment);
54
+ results.push(result('transferred'));
55
+ continue;
56
+ }
57
+ // Stage through a private directory: both backends write their completion
58
+ // marker (stats.json) last, so an interrupted transfer is never visible as a
59
+ // complete entry on the destination.
60
+ const staging = (0, path_1.join)((0, os_1.tmpdir)(), `hammerkit-cache-sync-${(0, crypto_1.randomUUID)()}`);
61
+ try {
62
+ const pulled = await source.pull(item.id(), stateKey, staging, environment);
63
+ if (!pulled) {
64
+ results.push(result('missing'));
65
+ continue;
66
+ }
67
+ await destination.push(item.id(), stateKey, staging, environment);
68
+ results.push(result('transferred'));
69
+ }
70
+ finally {
71
+ await environment.file.remove(staging);
72
+ }
73
+ }
74
+ return results;
75
+ }
76
+ exports.syncCache = syncCache;
77
+ //# sourceMappingURL=cache-sync.js.map