hammerkit 1.6.0 → 1.8.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 (277) hide show
  1. package/README.md +7 -7
  2. package/build.schema.json +59 -15
  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 +17 -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 +18 -3
  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-docker.js +18 -2
  27. package/dist/executer/execute-work-service.js +10 -1
  28. package/dist/executer/execute-work-task.js +111 -26
  29. package/dist/executer/execute-work-tree.js +26 -1
  30. package/dist/executer/execution-steps.js +3 -8
  31. package/dist/executer/get-container-binds.js +13 -1
  32. package/dist/executer/get-scheduler-execute-result.js +4 -1
  33. package/dist/executer/lazy-resolver.js +15 -3
  34. package/dist/executer/reset-work-tree.js +2 -0
  35. package/dist/executer/run-summary.js +102 -0
  36. package/dist/executer/scheduler/enqueue-next.js +15 -60
  37. package/dist/executer/scheduler/state-key.js +78 -0
  38. package/dist/index.js +14 -1
  39. package/dist/kubernetes/apply.js +6 -7
  40. package/dist/kubernetes/await-running-state.js +100 -60
  41. package/dist/kubernetes/ensure-http-route.js +7 -10
  42. package/dist/kubernetes/ensure-namespace.js +9 -10
  43. package/dist/kubernetes/ensure-persistent-data.js +6 -5
  44. package/dist/kubernetes/get-service-ip.js +2 -2
  45. package/dist/kubernetes/kubernetes-instance.js +0 -4
  46. package/dist/kubernetes/remove-persistent-data.js +16 -6
  47. package/dist/kubernetes/resolve-pod-name.js +7 -7
  48. package/dist/log.js +16 -6
  49. package/dist/logging/live-logger.js +2 -1
  50. package/dist/optimizer/calculate-checksum.js +8 -2
  51. package/dist/optimizer/get-work-cache-stats.js +66 -16
  52. package/dist/optimizer/work-task-cache-description.js +34 -5
  53. package/dist/planner/project-root.js +26 -0
  54. package/dist/planner/utils/append-work-environment.js +1 -1
  55. package/dist/planner/utils/append-work-service.js +2 -1
  56. package/dist/planner/utils/append-work-task.js +7 -1
  57. package/dist/planner/utils/get-outputs-to-reset.js +20 -0
  58. package/dist/planner/utils/parse-work-generate.js +2 -2
  59. package/dist/planner/utils/parse-work-source.js +35 -30
  60. package/dist/planner/utils/portable-path.js +30 -0
  61. package/dist/planner/work-cache-id.js +2 -2
  62. package/dist/planner/work-graph-serializer.js +103 -0
  63. package/dist/planner/work-instance-id.js +15 -0
  64. package/dist/planner/work-runtime-docker.js +58 -15
  65. package/dist/planner/work-runtime-kubernetes.js +95 -45
  66. package/dist/planner/work-runtime-local.js +31 -8
  67. package/dist/planner/work-service-id.js +16 -4
  68. package/dist/program.js +235 -1
  69. package/dist/schema/build-file-base-task-schema.js +4 -0
  70. package/dist/schema/build-file-environment-schema-http-route.js +15 -0
  71. package/dist/schema/build-file-environment-schema.js +8 -2
  72. package/dist/schema/build-file-schema.js +1 -1
  73. package/dist/schema/cache-schema.js +21 -2
  74. package/dist/schema/duration-schema.js +18 -0
  75. package/dist/schema/envs-schema.js +1 -1
  76. package/dist/schema/reference-parser.js +3 -0
  77. package/dist/schema/size-schema.js +18 -0
  78. package/dist/schema/work-scope-parser.js +15 -2
  79. package/dist/utils/units.js +87 -0
  80. package/package.json +20 -9
  81. package/.agents/FEEDBACK.md +0 -74
  82. package/.agents/skills/before-commit/SKILL.md +0 -101
  83. package/.agents/skills/do-the-work/SKILL.md +0 -64
  84. package/.agents/skills/fix-ci/SKILL.md +0 -98
  85. package/.agents/skills/speckit-git-commit/SKILL.md +0 -53
  86. package/.agents/skills/speckit-git-feature/SKILL.md +0 -72
  87. package/.agents/skills/speckit-git-initialize/SKILL.md +0 -54
  88. package/.agents/skills/speckit-git-remote/SKILL.md +0 -50
  89. package/.agents/skills/speckit-git-validate/SKILL.md +0 -54
  90. package/.devcontainer/Dockerfile +0 -5
  91. package/.devcontainer/devcontainer.json +0 -40
  92. package/.gitbook.yaml +0 -5
  93. package/.specify/extensions/.registry +0 -37
  94. package/.specify/extensions/git/README.md +0 -100
  95. package/.specify/extensions/git/commands/speckit.git.commit.md +0 -48
  96. package/.specify/extensions/git/commands/speckit.git.feature.md +0 -67
  97. package/.specify/extensions/git/commands/speckit.git.initialize.md +0 -49
  98. package/.specify/extensions/git/commands/speckit.git.remote.md +0 -45
  99. package/.specify/extensions/git/commands/speckit.git.validate.md +0 -49
  100. package/.specify/extensions/git/config-template.yml +0 -62
  101. package/.specify/extensions/git/extension.yml +0 -140
  102. package/.specify/extensions/git/git-config.yml +0 -62
  103. package/.specify/extensions.yml +0 -149
  104. package/.specify/feature.json +0 -3
  105. package/.specify/init-options.json +0 -10
  106. package/.specify/integration.json +0 -15
  107. package/.specify/integrations/claude.manifest.json +0 -16
  108. package/.specify/integrations/speckit.manifest.json +0 -17
  109. package/.specify/memory/constitution.md +0 -162
  110. package/.specify/templates/checklist-template.md +0 -40
  111. package/.specify/templates/constitution-template.md +0 -50
  112. package/.specify/templates/plan-template.md +0 -113
  113. package/.specify/templates/spec-template.md +0 -131
  114. package/.specify/templates/tasks-template.md +0 -252
  115. package/.specify/workflows/speckit/workflow.yml +0 -77
  116. package/.specify/workflows/workflow-registry.json +0 -13
  117. package/AGENTS.md +0 -94
  118. package/Dockerfile +0 -32
  119. package/FOLLOWUPS.md +0 -34
  120. package/SUMMARY.md +0 -68
  121. package/VALUES.md +0 -53
  122. package/dist/cache/auto-pull.spec.js +0 -123
  123. package/dist/cache/backends/local-cache-backend.spec.js +0 -72
  124. package/dist/cache/backends/s3-cache-backend.spec.js +0 -118
  125. package/dist/cache/cache-backend-registry.spec.js +0 -22
  126. package/dist/cache/resolve-cache.spec.js +0 -52
  127. package/dist/cli.spec.js +0 -23
  128. package/dist/console/get-logger.spec.js +0 -23
  129. package/dist/docker/get-default-platform.spec.js +0 -29
  130. package/dist/docker/longest-common-prefix.spec.js +0 -26
  131. package/dist/docker/package.spec.js +0 -145
  132. package/dist/docker/remove-container.spec.js +0 -32
  133. package/dist/docker/using-container.spec.js +0 -90
  134. package/dist/environment/merge-environment-variables.spec.js +0 -24
  135. package/dist/environment/replace-env-variables.spec.js +0 -47
  136. package/dist/executer/abort.spec.js +0 -18
  137. package/dist/executer/environment-mock.spec.js +0 -22
  138. package/dist/executer/execute.spec.js +0 -51
  139. package/dist/executer/get-service-containers.spec.js +0 -59
  140. package/dist/executer/get-service-env-hints.spec.js +0 -65
  141. package/dist/executer/label-values.spec.js +0 -61
  142. package/dist/executer/lazy-resolver.spec.js +0 -20
  143. package/dist/executer/process-manager.spec.js +0 -54
  144. package/dist/executer/scheduler/enqueue-next.spec.js +0 -136
  145. package/dist/executer/state-resolver.spec.js +0 -37
  146. package/dist/executer/state.spec.js +0 -66
  147. package/dist/executer/states.spec.js +0 -14
  148. package/dist/file/get-file-context.spec.js +0 -178
  149. package/dist/file/move-files.spec.js +0 -48
  150. package/dist/kubernetes/ensure-http-route.spec.js +0 -70
  151. package/dist/kubernetes/ensure-ingress.js +0 -54
  152. package/dist/kubernetes/ensure-namespace.spec.js +0 -39
  153. package/dist/kubernetes/resolve-pod-name.spec.js +0 -81
  154. package/dist/log.spec.js +0 -131
  155. package/dist/logging/grouped-logger.spec.js +0 -84
  156. package/dist/logging/interactive-logger.spec.js +0 -57
  157. package/dist/logging/live-logger.spec.js +0 -37
  158. package/dist/logging/loggers.spec.js +0 -154
  159. package/dist/optimizer/calculate-checksum.spec.js +0 -34
  160. package/dist/optimizer/get-cache-directory.spec.js +0 -59
  161. package/dist/optimizer/get-work-cache-stats.spec.js +0 -79
  162. package/dist/optimizer/work-task-cache-description.spec.js +0 -45
  163. package/dist/parser/default-build-file.spec.js +0 -48
  164. package/dist/parser/parse-label-arguments.spec.js +0 -27
  165. package/dist/parser/read-build-file.spec.js +0 -89
  166. package/dist/parser/read-env-file.spec.js +0 -42
  167. package/dist/planner/utils/append-work-service.spec.js +0 -71
  168. package/dist/planner/utils/get-container-user.spec.js +0 -30
  169. package/dist/planner/utils/get-default-kube-config.spec.js +0 -11
  170. package/dist/planner/utils/normalize-path.spec.js +0 -24
  171. package/dist/planner/utils/parse-work-command.spec.js +0 -21
  172. package/dist/planner/utils/parse-work-generate.spec.js +0 -45
  173. package/dist/planner/utils/parse-work-mount.spec.js +0 -62
  174. package/dist/planner/utils/parse-work-mounts.spec.js +0 -19
  175. package/dist/planner/utils/parse-work-port.spec.js +0 -24
  176. package/dist/planner/utils/parse-work-ports.spec.js +0 -24
  177. package/dist/planner/utils/parse-work-source.spec.js +0 -58
  178. package/dist/planner/utils/parse-work-volume.spec.js +0 -18
  179. package/dist/planner/utils/plan-work-tasks.spec.js +0 -30
  180. package/dist/planner/utils/plan-work-volume.spec.js +0 -33
  181. package/dist/planner/utils/split-name.spec.js +0 -16
  182. package/dist/planner/utils/template-value.spec.js +0 -14
  183. package/dist/planner/validate.spec.js +0 -251
  184. package/dist/planner/work-cache-id.spec.js +0 -173
  185. package/dist/planner/work-runtime-local.spec.js +0 -193
  186. package/dist/program-commands.spec.js +0 -84
  187. package/dist/program.spec.js +0 -43
  188. package/dist/schema/build-file-environment-schema-ingress.js +0 -14
  189. package/dist/schema/build-file-schema.spec.js +0 -61
  190. package/dist/schema/doc-build-files.spec.js +0 -223
  191. package/dist/schema/parse-error.spec.js +0 -18
  192. package/dist/schema/reference-parser.spec.js +0 -83
  193. package/dist/schema/schema-parser.spec.js +0 -80
  194. package/dist/schema/type-guards.spec.js +0 -23
  195. package/dist/schema/work-scope-parser.spec.js +0 -66
  196. package/dist/service/update-service-status.spec.js +0 -27
  197. package/dist/testing/ensure-local-registry.js +0 -134
  198. package/dist/testing/example-test-suite.js +0 -63
  199. package/dist/testing/expect.js +0 -82
  200. package/dist/testing/get-test-suite.js +0 -9
  201. package/dist/testing/http-get.js +0 -21
  202. package/dist/testing/integration/cache.spec.js +0 -49
  203. package/dist/testing/integration/cancellation.spec.js +0 -45
  204. package/dist/testing/integration/clean.spec.js +0 -43
  205. package/dist/testing/integration/cmd.spec.js +0 -15
  206. package/dist/testing/integration/concurrency.spec.js +0 -19
  207. package/dist/testing/integration/docker-package-registry.spec.js +0 -44
  208. package/dist/testing/integration/docker-service-crash.spec.js +0 -36
  209. package/dist/testing/integration/docker.spec.js +0 -18
  210. package/dist/testing/integration/env.spec.js +0 -48
  211. package/dist/testing/integration/environment.spec.js +0 -97
  212. package/dist/testing/integration/error.spec.js +0 -19
  213. package/dist/testing/integration/export.spec.js +0 -22
  214. package/dist/testing/integration/extend.spec.js +0 -29
  215. package/dist/testing/integration/glob.spec.js +0 -40
  216. package/dist/testing/integration/include.spec.js +0 -31
  217. package/dist/testing/integration/invalid.spec.js +0 -17
  218. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  219. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  220. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  221. package/dist/testing/integration/kubernetes.spec.js +0 -99
  222. package/dist/testing/integration/labels.spec.js +0 -42
  223. package/dist/testing/integration/local.spec.js +0 -14
  224. package/dist/testing/integration/monorepo.spec.js +0 -15
  225. package/dist/testing/integration/multi-line.spec.js +0 -17
  226. package/dist/testing/integration/reference.spec.js +0 -21
  227. package/dist/testing/integration/services.spec.js +0 -38
  228. package/dist/testing/integration/store.spec.js +0 -52
  229. package/dist/testing/integration/unknown.spec.js +0 -28
  230. package/dist/testing/integration/validate.spec.js +0 -160
  231. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  232. package/dist/testing/integration/watch.spec.js +0 -45
  233. package/dist/testing/requires-kubernetes.js +0 -18
  234. package/dist/testing/requires-linux-containers.js +0 -18
  235. package/dist/testing/requires-windows-containers.js +0 -18
  236. package/dist/testing/test-case.js +0 -75
  237. package/dist/testing/test-streams.js +0 -28
  238. package/dist/testing/test-suite-setup.js +0 -3
  239. package/dist/testing/test-suite.js +0 -3
  240. package/dist/testing/testing-timeout.js +0 -32
  241. package/dist/utils/abort-event.spec.js +0 -46
  242. package/dist/utils/buffer-context.spec.js +0 -38
  243. package/dist/utils/ci.spec.js +0 -53
  244. package/dist/utils/debouncer.spec.js +0 -58
  245. package/dist/utils/emitter.spec.js +0 -34
  246. package/dist/utils/empty-writable.spec.js +0 -18
  247. package/dist/utils/environment-config.spec.js +0 -26
  248. package/dist/utils/fail-never.spec.js +0 -9
  249. package/dist/utils/sleep.spec.js +0 -22
  250. package/dist/version.spec.js +0 -12
  251. package/specs/build-file/caches/spec.md +0 -114
  252. package/specs/build-file/environment-variables/spec.md +0 -82
  253. package/specs/build-file/includes/spec.md +0 -60
  254. package/specs/build-file/references/spec.md +0 -56
  255. package/specs/build-file/spec.md +0 -62
  256. package/specs/cli/clean/spec.md +0 -85
  257. package/specs/cli/down/spec.md +0 -85
  258. package/specs/cli/execute/spec.md +0 -101
  259. package/specs/cli/init/spec.md +0 -64
  260. package/specs/cli/ls/spec.md +0 -66
  261. package/specs/cli/package/spec.md +0 -110
  262. package/specs/cli/spec.md +0 -73
  263. package/specs/cli/store-restore/spec.md +0 -74
  264. package/specs/cli/up/spec.md +0 -80
  265. package/specs/cli/validate/spec.md +0 -70
  266. package/specs/labels/spec.md +0 -91
  267. package/specs/service/container/spec.md +0 -120
  268. package/specs/service/kubernetes/spec.md +0 -97
  269. package/specs/service/spec.md +0 -105
  270. package/specs/task/caching/spec.md +0 -107
  271. package/specs/task/container/spec.md +0 -118
  272. package/specs/task/dependencies/spec.md +0 -70
  273. package/specs/task/extending/spec.md +0 -74
  274. package/specs/task/kubernetes/spec.md +0 -125
  275. package/specs/task/needs/spec.md +0 -59
  276. package/specs/task/spec.md +0 -87
  277. package/specs/task/watching/spec.md +0 -69
package/dist/program.js CHANGED
@@ -10,6 +10,10 @@ const ci_1 = require("./utils/ci");
10
10
  const parse_label_arguments_1 = require("./parser/parse-label-arguments");
11
11
  const cli_1 = require("./cli");
12
12
  const log_1 = require("./log");
13
+ const empty_writable_1 = require("./utils/empty-writable");
14
+ const explain_1 = require("./cache/explain");
15
+ const run_summary_1 = require("./executer/run-summary");
16
+ const dry_run_1 = require("./executer/dry-run");
13
17
  const label_values_1 = require("./executer/label-values");
14
18
  const default_build_file_1 = require("./parser/default-build-file");
15
19
  const schema_parser_1 = require("./schema/schema-parser");
@@ -17,6 +21,9 @@ const work_scope_parser_1 = require("./schema/work-scope-parser");
17
21
  const reference_parser_1 = require("./schema/reference-parser");
18
22
  const colors_1 = __importDefault(require("colors"));
19
23
  const version_1 = require("./version");
24
+ const read_only_1 = require("./cache/read-only");
25
+ const units_1 = require("./utils/units");
26
+ const cache_inventory_1 = require("./cache/cache-inventory");
20
27
  async function createCli(fileName, environment, workScope) {
21
28
  const { ctx, scope } = await (0, schema_parser_1.createParseContext)(fileName, environment);
22
29
  const referencedScope = await (0, reference_parser_1.parseReferences)(ctx, scope, environment);
@@ -233,6 +240,179 @@ async function getProgram(environment, argv) {
233
240
  program.error('Detected errors in the hammerkit configuration', { exitCode: 1 });
234
241
  }
235
242
  });
243
+ program
244
+ .command('graph [task]')
245
+ .description('serialize the build graph (tasks, services, deps/needs edges) as mermaid or dot')
246
+ .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
247
+ .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
248
+ .addOption(new commander_1.Option('--env <name>', 'environment'))
249
+ .addOption(new commander_1.Option('--format <format>', 'output format').default('mermaid').choices(['mermaid', 'dot']))
250
+ .action(async (task, options) => {
251
+ try {
252
+ const cli = await createCli(fileName, environment, task ? { taskName: task, environmentName: options.env ?? null } : parseWorkLabelScope(options));
253
+ const serialization = cli.graph(options.format);
254
+ environment.stdout.write(`${serialization.output}\n`);
255
+ if (serialization.cycle) {
256
+ environment.console.warn(`cycle detected: ${serialization.cycle.join(' -> ')}`);
257
+ }
258
+ }
259
+ catch (e) {
260
+ if (e instanceof commander_1.CommanderError) {
261
+ throw e;
262
+ }
263
+ environment.console.error((0, log_1.getErrorMessage)(e));
264
+ program.error('Graph was not successful', { exitCode: 1 });
265
+ }
266
+ });
267
+ program
268
+ .command('explain [task]')
269
+ .description('explain whether tasks would be a cache hit or miss, without running them')
270
+ .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
271
+ .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
272
+ .addOption(new commander_1.Option('--env <name>', 'environment'))
273
+ .addOption(new commander_1.Option('--cache <method>', 'caching method to compare')
274
+ .default('checksum')
275
+ .choices(['checksum', 'modify-date', 'none']))
276
+ .addOption(new commander_1.Option('--json', 'emit the explanation as JSON').default(false))
277
+ .action(async (task, options) => {
278
+ try {
279
+ const cli = await createCli(fileName, environment, task ? { taskName: task, environmentName: options.env ?? null } : parseWorkLabelScope(options));
280
+ const explanations = await cli.explain({ cacheDefault: options.cache });
281
+ if (options.json) {
282
+ environment.stdout.write(`${JSON.stringify(explanations, null, 2)}\n`);
283
+ return;
284
+ }
285
+ for (const explanation of explanations) {
286
+ const label = explanation.status === 'hit'
287
+ ? colors_1.default.green('cache hit')
288
+ : explanation.status === 'uncacheable'
289
+ ? colors_1.default.grey('uncacheable')
290
+ : colors_1.default.yellow('cache miss');
291
+ environment.stdout.write(`• ${explanation.taskName}: ${label}\n`);
292
+ for (const cause of explanation.causes) {
293
+ (0, log_1.printProperty)(environment, 'cause', (0, explain_1.describeCause)(cause));
294
+ }
295
+ }
296
+ }
297
+ catch (e) {
298
+ if (e instanceof commander_1.CommanderError) {
299
+ throw e;
300
+ }
301
+ environment.console.error((0, log_1.getErrorMessage)(e));
302
+ program.error('Explain was not successful', { exitCode: 1 });
303
+ }
304
+ });
305
+ const cacheCommand = program.command('cache').description('inspect, prune and move cache entries');
306
+ for (const direction of ['pull', 'push']) {
307
+ cacheCommand
308
+ .command(`${direction} [task]`)
309
+ .description(direction === 'pull'
310
+ ? 'fetch the cache entries of the current state from a remote cache, without running tasks'
311
+ : 'upload the locally cached entries of the current state to a remote cache, without running tasks')
312
+ .addOption(new commander_1.Option('--remote <name>', 'remote cache declared in the caches block').makeOptionMandatory())
313
+ .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
314
+ .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
315
+ .addOption(new commander_1.Option('--env <name>', 'environment'))
316
+ .addOption(new commander_1.Option('--cache <method>', 'caching method to compare')
317
+ .default('checksum')
318
+ .choices(['checksum', 'modify-date', 'none']))
319
+ .action(async (task, options) => {
320
+ try {
321
+ if (direction === 'push' && (0, read_only_1.isCacheReadOnly)(false, environment.processEnvs)) {
322
+ program.error(`cache is read-only (${read_only_1.CACHE_READ_ONLY_ENV} is set), refusing to push`, { exitCode: 1 });
323
+ return;
324
+ }
325
+ const cli = await createCli(fileName, environment, task ? { taskName: task, environmentName: options.env ?? null } : parseWorkLabelScope(options));
326
+ const results = await cli.syncCache({ direction, remote: options.remote, cacheDefault: options.cache });
327
+ const labels = {
328
+ transferred: colors_1.default.green(direction === 'pull' ? 'pulled' : 'pushed'),
329
+ present: colors_1.default.grey('already present'),
330
+ missing: colors_1.default.yellow(direction === 'pull' ? 'not in remote' : 'not in local cache'),
331
+ skipped: colors_1.default.grey('skipped'),
332
+ };
333
+ for (const result of results) {
334
+ environment.stdout.write(`• ${result.taskName}: ${labels[result.status]}\n`);
335
+ }
336
+ const moved = results.filter((r) => r.status === 'transferred').length;
337
+ environment.stdout.write(`${moved}/${results.length} entries ${direction === 'pull' ? 'pulled' : 'pushed'}\n`);
338
+ }
339
+ catch (e) {
340
+ if (e instanceof commander_1.CommanderError) {
341
+ throw e;
342
+ }
343
+ environment.console.error((0, log_1.getErrorMessage)(e));
344
+ program.error(`Cache ${direction} was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
345
+ }
346
+ });
347
+ }
348
+ const describeEntry = (entry) => `${entry.taskName ?? colors_1.default.grey(entry.taskId.substring(0, 12))} ${colors_1.default.grey(entry.stateKey.substring(0, 12))} ` +
349
+ `${entry.size === null ? '?' : (0, units_1.formatSize)(entry.size)}` +
350
+ (entry.lastAccessedAt ?? entry.createdAt
351
+ ? colors_1.default.grey(` last used ${(0, units_1.formatDuration)(Date.now() - (entry.lastAccessedAt ?? entry.createdAt ?? 0))} ago`)
352
+ : '');
353
+ const totalSize = (entries) => (0, units_1.formatSize)(entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0));
354
+ const countOf = (count) => `${count} ${count === 1 ? 'entry' : 'entries'}`;
355
+ cacheCommand
356
+ .command('ls')
357
+ .description('list the entries of a cache (default: the local default cache)')
358
+ .addOption(new commander_1.Option('--remote <name>', 'cache declared in the caches block to list'))
359
+ .addOption(new commander_1.Option('--json', 'emit the entries as JSON').default(false))
360
+ .action(async (options) => {
361
+ try {
362
+ const cli = await createCli(fileName, environment, parseWorkLabelScope({}));
363
+ const entries = await cli.listCache(options.remote);
364
+ if (options.json) {
365
+ environment.stdout.write(`${JSON.stringify(entries, null, 2)}\n`);
366
+ return;
367
+ }
368
+ for (const entry of entries) {
369
+ environment.stdout.write(`• ${describeEntry(entry)}\n`);
370
+ }
371
+ environment.stdout.write(`${countOf(entries.length)}, ${totalSize(entries)} total\n`);
372
+ }
373
+ catch (e) {
374
+ if (e instanceof commander_1.CommanderError) {
375
+ throw e;
376
+ }
377
+ program.error(`Cache ls was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
378
+ }
379
+ });
380
+ cacheCommand
381
+ .command('prune')
382
+ .description('remove cache entries by retention policy (default: the local default cache)')
383
+ .addOption(new commander_1.Option('--remote <name>', 'cache declared in the caches block to prune'))
384
+ .addOption(new commander_1.Option('--max-age <duration>', 'remove entries not used within this long (e.g. 30d)').argParser(units_1.parseDuration))
385
+ .addOption(new commander_1.Option('--max-size <size>', 'remove least recently used entries above this size (e.g. 5Gi)').argParser(units_1.parseSize))
386
+ .addOption(new commander_1.Option('--keep <count>', 'keep only the newest versions per task').argParser((v) => parseInt(v, 10)))
387
+ .addOption(new commander_1.Option('--dry-run', 'show what would be removed without removing it').default(false))
388
+ .action(async (options) => {
389
+ try {
390
+ const cli = await createCli(fileName, environment, parseWorkLabelScope({}));
391
+ const policy = cli.retentionPolicy(options.remote, {
392
+ maxAge: options.maxAge,
393
+ maxSize: options.maxSize,
394
+ keepPerTask: options.keep,
395
+ });
396
+ if (!(0, cache_inventory_1.hasPolicy)(policy)) {
397
+ program.error('no retention policy: pass --max-age, --max-size or --keep, or declare retention on the cache', { exitCode: 1 });
398
+ return;
399
+ }
400
+ const plan = await cli.pruneCache(options.remote, policy, options.dryRun);
401
+ for (const entry of plan.evict) {
402
+ environment.stdout.write(`• ${options.dryRun ? 'would remove' : 'removed'} ${describeEntry(entry)}\n`);
403
+ }
404
+ for (const note of plan.unavailable) {
405
+ environment.console.warn(note);
406
+ }
407
+ environment.stdout.write(`${options.dryRun ? 'would remove' : 'removed'} ${countOf(plan.evict.length)} (${totalSize(plan.evict)}), ` + `kept ${countOf(plan.keep.length)} (${totalSize(plan.keep)})\n`);
408
+ }
409
+ catch (e) {
410
+ if (e instanceof commander_1.CommanderError) {
411
+ throw e;
412
+ }
413
+ program.error(`Cache prune was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
414
+ }
415
+ });
236
416
  program
237
417
  .command('up')
238
418
  .description('start services(s)')
@@ -248,6 +428,8 @@ async function getProgram(environment, argv) {
248
428
  .addOption(new commander_1.Option('--cache <method>', 'caching method to compare')
249
429
  .default('checksum')
250
430
  .choices(['checksum', 'modify-date', 'none']))
431
+ .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
432
+ .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
251
433
  .action(async (options) => {
252
434
  try {
253
435
  const scope = parseWorkLabelScope(options);
@@ -258,6 +440,8 @@ async function getProgram(environment, argv) {
258
440
  workers: options.concurrency,
259
441
  logMode: options.log,
260
442
  daemon: options.daemon,
443
+ cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
444
+ timeout: options.timeout ?? null,
261
445
  });
262
446
  if (!result.success) {
263
447
  program.error('Up was not successful', { exitCode: 1 });
@@ -311,22 +495,72 @@ async function getProgram(environment, argv) {
311
495
  .addOption(new commander_1.Option('--cache <method>', 'caching method to compare')
312
496
  .default('checksum')
313
497
  .choices(['checksum', 'modify-date', 'none']))
498
+ .addOption(new commander_1.Option('--no-summary', 'do not print the end-of-run summary'))
499
+ .addOption(new commander_1.Option('--summary-json', 'emit the end-of-run summary as JSON').default(false))
500
+ .addOption(new commander_1.Option('--explain', 'print the cache-miss cause when a task rebuilds').default(false))
501
+ .addOption(new commander_1.Option('--dry-run', 'print the execution plan with predicted cache hits/misses without running').default(false))
502
+ .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
503
+ .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
504
+ .addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when every task needing them is a cache hit'))
314
505
  .action(async (task, options) => {
315
506
  try {
316
- const cli = await createCli(fileName, environment, task ? { taskName: task, environmentName: options.env } : parseWorkLabelScope(options));
507
+ // For machine-readable JSON, suppress the human progress logger (which
508
+ // also writes to stdout) so the only thing on stdout is the JSON.
509
+ const runEnvironment = options.summaryJson
510
+ ? { ...environment, stdout: (0, empty_writable_1.emptyWritable)(), console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()) }
511
+ : environment;
512
+ const cli = await createCli(fileName, runEnvironment, task ? { taskName: task, environmentName: options.env } : parseWorkLabelScope(options));
317
513
  if (cli.tasks().length === 0) {
318
514
  program.error('No tasks found', { exitCode: 127 });
319
515
  return;
320
516
  }
517
+ if (options.dryRun) {
518
+ const plan = await cli.dryRun({ cacheDefault: options.cache });
519
+ if (plan.cycle) {
520
+ program.error(`task cycle detected ${plan.cycle.join(' -> ')}`, { exitCode: 1 });
521
+ return;
522
+ }
523
+ (0, dry_run_1.printDryRun)(environment, plan);
524
+ return;
525
+ }
526
+ const runStart = Date.now();
321
527
  const result = await cli.runExec({
322
528
  cacheDefault: options.cache,
323
529
  watch: options.watch,
324
530
  workers: options.concurrency,
325
531
  logMode: options.log,
532
+ explain: options.explain,
533
+ cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
534
+ timeout: options.timeout ?? null,
535
+ skipDeps: options.skipDeps !== false,
326
536
  });
537
+ // Reporting only: the summary never changes the exit code or behavior.
538
+ if (!options.watch) {
539
+ const summary = (0, run_summary_1.summarizeRun)(result.state, Date.now() - runStart);
540
+ if (options.summaryJson) {
541
+ environment.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
542
+ }
543
+ else if (options.summary !== false) {
544
+ (0, run_summary_1.printRunSummary)(environment, summary);
545
+ }
546
+ }
327
547
  if (!result.success) {
328
548
  program.error('Execution was not successful', { exitCode: 1 });
329
549
  }
550
+ // Declared retention keeps local caches bounded without anyone having
551
+ // to remember `cache prune`. Never on remotes, never in read-only mode.
552
+ if (!options.watch && !(0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs)) {
553
+ try {
554
+ for (const { cacheName, plan } of await cli.autoPrune()) {
555
+ if (plan.evict.length > 0) {
556
+ runEnvironment.console.info(`pruned ${plan.evict.length} old ${plan.evict.length === 1 ? 'entry' : 'entries'} from cache "${cacheName}"`);
557
+ }
558
+ }
559
+ }
560
+ catch (e) {
561
+ runEnvironment.console.warn(`automatic cache prune failed: ${(0, log_1.getErrorMessage)(e)}`);
562
+ }
563
+ }
330
564
  }
331
565
  catch (e) {
332
566
  if (e instanceof commander_1.CommanderError) {
@@ -8,6 +8,7 @@ const labels_schema_1 = require("./labels-schema");
8
8
  const cache_schema_1 = require("./cache-schema");
9
9
  const build_file_task_command_schema_1 = require("./build-file-task-command-schema");
10
10
  const build_file_volume_schema_1 = require("./build-file-volume-schema");
11
+ const duration_schema_1 = require("./duration-schema");
11
12
  exports.buildFileBaseTaskSchema = (0, zod_1.object)({
12
13
  deps: (0, zod_1.array)((0, zod_1.string)()).optional(),
13
14
  needs: (0, zod_1.array)(build_file_need_schema_1.buildFileNeedSchema).optional(),
@@ -23,5 +24,8 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
23
24
  // A task that watches its own sources and restarts itself (e.g. `ng serve`,
24
25
  // `tsc -w`). In watch mode hammerkit then does not watch/restart it itself.
25
26
  continuous: (0, zod_1.boolean)().optional(),
27
+ // Maximum execution time, e.g. `30s`, `10m`, `1h30m`. The task fails when it
28
+ // runs longer. Does not affect the cache key (ADR-0002).
29
+ timeout: duration_schema_1.durationSchema.optional(),
26
30
  });
27
31
  //# sourceMappingURL=build-file-base-task-schema.js.map
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFileEnvironmentSchemaHttpRoute = void 0;
4
+ const zod_1 = require("zod");
5
+ // A Gateway API HTTPRoute exposing a service of the environment under a hostname.
6
+ exports.buildFileEnvironmentSchemaHttpRoute = (0, zod_1.object)({
7
+ host: (0, zod_1.string)(),
8
+ service: (0, zod_1.string)(),
9
+ servicePort: (0, zod_1.number)().optional(),
10
+ path: (0, zod_1.string)().optional(),
11
+ // the parent Gateway the route attaches to
12
+ gateway: (0, zod_1.string)(),
13
+ gatewayNamespace: (0, zod_1.string)().optional(),
14
+ }).strict();
15
+ //# sourceMappingURL=build-file-environment-schema-http-route.js.map
@@ -2,12 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildFileEnvironmentSchema = exports.buildFileDockerEnvironmentSchema = exports.buildFileKubernetesEnvironmentSchema = void 0;
4
4
  const zod_1 = require("zod");
5
- const build_file_environment_schema_ingress_1 = require("./build-file-environment-schema-ingress");
5
+ const build_file_environment_schema_http_route_1 = require("./build-file-environment-schema-http-route");
6
6
  exports.buildFileKubernetesEnvironmentSchema = (0, zod_1.object)({
7
7
  namespace: (0, zod_1.string)().optional(),
8
8
  context: (0, zod_1.string)(),
9
9
  kubeconfig: (0, zod_1.string)().optional(),
10
- ingresses: (0, zod_1.array)(build_file_environment_schema_ingress_1.buildFileEnvironmentSchemaIngress).optional(),
10
+ httpRoutes: (0, zod_1.array)(build_file_environment_schema_http_route_1.buildFileEnvironmentSchemaHttpRoute).optional(),
11
+ // Ingress support was removed in 1.8.0. Fail instead of dropping the routes.
12
+ ingresses: (0, zod_1.never)({
13
+ errorMap: () => ({
14
+ message: 'ingresses were removed in 1.8.0: declare Gateway API routes under httpRoutes, each with a gateway',
15
+ }),
16
+ }).optional(),
11
17
  });
12
18
  exports.buildFileDockerEnvironmentSchema = (0, zod_1.object)({
13
19
  host: (0, zod_1.string)().optional(),
@@ -19,5 +19,5 @@ exports.buildFileSchema = (0, zod_1.object)({
19
19
  labels: labels_schema_1.labelsSchema.optional(),
20
20
  })
21
21
  .strict()
22
- .describe('Build file with support for containerization\nhttps://no0dles.gitbook.io/hammerkit/build-file');
22
+ .describe('Build file with support for containerization\nhttps://hammerkit.dev/docs/build-file');
23
23
  //# sourceMappingURL=build-file-schema.js.map
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.cacheSchema = exports.taskCacheRefSchema = exports.buildFileCacheSchema = exports.cacheBackendSchema = exports.cacheBackendS3Schema = exports.cacheBackendLocalSchema = exports.cacheMethodSchema = void 0;
3
+ exports.cacheSchema = exports.taskCacheRefSchema = exports.buildFileCacheSchema = exports.cacheRetentionSchema = exports.cacheBackendSchema = exports.cacheBackendRegistrySchema = exports.cacheBackendS3Schema = exports.cacheBackendLocalSchema = exports.cacheMethodSchema = void 0;
4
4
  const zod_1 = require("zod");
5
+ const duration_schema_1 = require("./duration-schema");
6
+ const size_schema_1 = require("./size-schema");
5
7
  exports.cacheMethodSchema = (0, zod_1.union)([(0, zod_1.literal)('checksum'), (0, zod_1.literal)('modify-date'), (0, zod_1.literal)('none')]);
6
8
  exports.cacheBackendLocalSchema = (0, zod_1.object)({
7
9
  type: (0, zod_1.literal)('local'),
@@ -15,10 +17,27 @@ exports.cacheBackendS3Schema = (0, zod_1.object)({
15
17
  prefix: (0, zod_1.string)().optional(),
16
18
  forcePathStyle: zod_1.z.boolean().optional(),
17
19
  }).strict();
18
- exports.cacheBackendSchema = (0, zod_1.union)([exports.cacheBackendLocalSchema, exports.cacheBackendS3Schema]);
20
+ exports.cacheBackendRegistrySchema = (0, zod_1.object)({
21
+ type: (0, zod_1.literal)('registry'),
22
+ // repository to store entries in, e.g. `ghcr.io/org/hammerkit-cache`
23
+ repository: (0, zod_1.string)(),
24
+ // talk plain http (defaults to true only for localhost registries)
25
+ insecure: zod_1.z.boolean().optional(),
26
+ }).strict();
27
+ exports.cacheBackendSchema = (0, zod_1.union)([exports.cacheBackendLocalSchema, exports.cacheBackendS3Schema, exports.cacheBackendRegistrySchema]);
28
+ exports.cacheRetentionSchema = (0, zod_1.object)({
29
+ // evict entries not used within this long, e.g. `30d`
30
+ maxAge: duration_schema_1.durationSchema.optional(),
31
+ // evict least recently used entries until the cache is at most this big, e.g. `5Gi`
32
+ maxSize: size_schema_1.sizeSchema.optional(),
33
+ // keep only the newest N versions per task
34
+ keepPerTask: zod_1.z.number().int().positive().optional(),
35
+ }).strict();
19
36
  exports.buildFileCacheSchema = (0, zod_1.object)({
20
37
  method: exports.cacheMethodSchema,
21
38
  backend: exports.cacheBackendSchema,
39
+ // applied by `cache prune`, and automatically after a run for local caches
40
+ retention: exports.cacheRetentionSchema.optional(),
22
41
  }).strict();
23
42
  exports.taskCacheRefSchema = (0, zod_1.union)([
24
43
  exports.cacheMethodSchema,
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.durationSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const units_1 = require("../utils/units");
6
+ function isDuration(value) {
7
+ try {
8
+ (0, units_1.parseDuration)(value);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ exports.durationSchema = (0, zod_1.string)().refine(isDuration, (value) => ({
16
+ message: `invalid duration "${value}", expected e.g. 30s, 5m, 1h30m or 30d`,
17
+ }));
18
+ //# sourceMappingURL=duration-schema.js.map
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.envsSchema = void 0;
4
4
  const zod_1 = require("zod");
5
- exports.envsSchema = (0, zod_1.record)((0, zod_1.union)([(0, zod_1.string)(), (0, zod_1.number)()])).describe('Environment values for the current build task\nhttps://no0dles.gitbook.io/hammerkit/build-file/environment-variables');
5
+ exports.envsSchema = (0, zod_1.record)((0, zod_1.union)([(0, zod_1.string)(), (0, zod_1.number)()])).describe('Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables');
6
6
  //# sourceMappingURL=envs-schema.js.map
@@ -8,6 +8,7 @@ const read_env_file_1 = require("../parser/read-env-file");
8
8
  const label_values_1 = require("../executer/label-values");
9
9
  const merge_environment_variables_1 = require("../environment/merge-environment-variables");
10
10
  const resolve_cache_1 = require("../cache/resolve-cache");
11
+ const project_root_1 = require("../planner/project-root");
11
12
  function combinePath(namePrefix, name) {
12
13
  return namePrefix.length > 0 ? `${namePrefix}:${name}` : name;
13
14
  }
@@ -20,6 +21,7 @@ async function parseReferences(ctx, mainScope, environment) {
20
21
  envFiles: {},
21
22
  environments: {},
22
23
  caches: {},
24
+ projectRoot: await (0, project_root_1.findProjectRoot)(mainScope.cwd, environment),
23
25
  };
24
26
  for (const file of Object.values(ctx.files)) {
25
27
  if (!reference.envFiles[file.cwd]) {
@@ -97,6 +99,7 @@ function applySchemaExtension(reference, task) {
97
99
  applySchemaExtension(reference, extend);
98
100
  task.schema.shell = task.schema.shell ?? extend.schema.shell;
99
101
  task.schema.cache = task.schema.cache ?? extend.schema.cache;
102
+ task.schema.timeout = task.schema.timeout ?? extend.schema.timeout;
100
103
  task.schema.cmds = extendArray(task.schema.cmds, extend.schema.cmds);
101
104
  task.schema.src = extendArray(task.schema.src, extend.schema.src);
102
105
  task.schema.generates = extendArray(task.schema.generates, extend.schema.generates);
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sizeSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const units_1 = require("../utils/units");
6
+ function isSize(value) {
7
+ try {
8
+ (0, units_1.parseSize)(value);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ exports.sizeSchema = (0, zod_1.string)().refine(isSize, (value) => ({
16
+ message: `invalid size "${value}", expected e.g. 500Mi, 5Gi or 2G`,
17
+ }));
18
+ //# sourceMappingURL=size-schema.js.map
@@ -17,8 +17,20 @@ function defaultEnvironment(context) {
17
17
  }
18
18
  exports.defaultEnvironment = defaultEnvironment;
19
19
  function getWorkContext(context, scope, environment) {
20
- const workTree = { services: {}, environment: defaultEnvironment(context), tasks: {} };
21
- const filteredWorkTree = { services: {}, environment: defaultEnvironment(context), tasks: {} };
20
+ const workTree = {
21
+ services: {},
22
+ environment: defaultEnvironment(context),
23
+ tasks: {},
24
+ caches: context.caches,
25
+ };
26
+ const filteredWorkTree = {
27
+ services: {},
28
+ environment: defaultEnvironment(context),
29
+ tasks: {},
30
+ caches: context.caches,
31
+ };
32
+ const requested = [];
33
+ filteredWorkTree.requested = requested;
22
34
  for (const [envName, env] of Object.entries(context.environments)) {
23
35
  if (envName === scope.environmentName) {
24
36
  filteredWorkTree.environment = (0, append_work_environment_1.appendWorkEnvironment)(env);
@@ -28,6 +40,7 @@ function getWorkContext(context, scope, environment) {
28
40
  for (const task of Object.values(context.tasks)) {
29
41
  const item = (0, append_work_task_1.appendWorkTask)(workTree, task.cwd, task, environment, context);
30
42
  if (matchesTaskWorkScope(item, scope)) {
43
+ requested.push(item.name);
31
44
  applyTask(filteredWorkTree, item);
32
45
  }
33
46
  }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatSize = exports.parseSize = exports.formatDuration = exports.parseDuration = void 0;
4
+ const DURATION_UNITS = {
5
+ ms: 1,
6
+ s: 1000,
7
+ m: 60000,
8
+ h: 3600000,
9
+ d: 86400000,
10
+ };
11
+ // Parse a duration such as `500ms`, `30s`, `5m`, `1h30m` or `30d` into
12
+ // milliseconds. Every component needs a unit, and the total must be positive.
13
+ function parseDuration(value) {
14
+ const pattern = /(\d+(?:\.\d+)?)(ms|s|m|h|d)/y;
15
+ let total = 0;
16
+ let offset = 0;
17
+ while (offset < value.length) {
18
+ pattern.lastIndex = offset;
19
+ const match = pattern.exec(value);
20
+ if (!match) {
21
+ throw new Error(`invalid duration "${value}", expected e.g. 30s, 5m, 1h30m or 30d`);
22
+ }
23
+ total += parseFloat(match[1]) * DURATION_UNITS[match[2]];
24
+ offset = pattern.lastIndex;
25
+ }
26
+ if (total <= 0) {
27
+ throw new Error(`invalid duration "${value}", expected e.g. 30s, 5m, 1h30m or 30d`);
28
+ }
29
+ return Math.round(total);
30
+ }
31
+ exports.parseDuration = parseDuration;
32
+ function formatDuration(ms) {
33
+ if (ms < 1000) {
34
+ return `${ms}ms`;
35
+ }
36
+ let remaining = Math.round(ms / 1000);
37
+ const parts = [];
38
+ for (const [unit, seconds] of [
39
+ ['d', 86400],
40
+ ['h', 3600],
41
+ ['m', 60],
42
+ ['s', 1],
43
+ ]) {
44
+ if (remaining >= seconds) {
45
+ parts.push(`${Math.floor(remaining / seconds)}${unit}`);
46
+ remaining %= seconds;
47
+ }
48
+ }
49
+ return parts.join('');
50
+ }
51
+ exports.formatDuration = formatDuration;
52
+ const SIZE_UNITS = {
53
+ '': 1,
54
+ K: 1000,
55
+ M: 1000 ** 2,
56
+ G: 1000 ** 3,
57
+ T: 1000 ** 4,
58
+ Ki: 1024,
59
+ Mi: 1024 ** 2,
60
+ Gi: 1024 ** 3,
61
+ Ti: 1024 ** 4,
62
+ };
63
+ // Parse a size such as `500Mi`, `5Gi` or `2G` (Kubernetes-style suffixes;
64
+ // binary `Ki/Mi/Gi/Ti`, decimal `K/M/G/T`, none for bytes) into bytes.
65
+ function parseSize(value) {
66
+ const match = /^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|K|M|G|T)?$/.exec(value);
67
+ if (!match) {
68
+ throw new Error(`invalid size "${value}", expected e.g. 500Mi, 5Gi or 2G`);
69
+ }
70
+ return Math.round(parseFloat(match[1]) * SIZE_UNITS[match[2] ?? '']);
71
+ }
72
+ exports.parseSize = parseSize;
73
+ function formatSize(bytes) {
74
+ for (const [unit, factor] of [
75
+ ['Ti', 1024 ** 4],
76
+ ['Gi', 1024 ** 3],
77
+ ['Mi', 1024 ** 2],
78
+ ['Ki', 1024],
79
+ ]) {
80
+ if (bytes >= factor) {
81
+ return `${(bytes / factor).toFixed(1).replace(/\.0$/, '')}${unit}`;
82
+ }
83
+ }
84
+ return `${bytes}B`;
85
+ }
86
+ exports.formatSize = formatSize;
87
+ //# sourceMappingURL=units.js.map