hammerkit 1.27.0-next.8 → 2.0.0-next.1

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 (129) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +76 -17
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-http-route.js +1 -1
  36. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  38. package/dist/kubernetes/ensure-persistent-data.js +212 -10
  39. package/dist/kubernetes/follow-pod-logs.js +182 -0
  40. package/dist/kubernetes/get-service-ip.js +2 -2
  41. package/dist/kubernetes/job-placement.js +36 -12
  42. package/dist/kubernetes/job-secrets.js +9 -6
  43. package/dist/kubernetes/kept-claims.js +22 -10
  44. package/dist/kubernetes/kubernetes-instance.js +1 -0
  45. package/dist/kubernetes/network-policy.js +16 -8
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/remove-stale-items.js +46 -0
  48. package/dist/kubernetes/resources.js +25 -3
  49. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  50. package/dist/kubernetes/sandbox-runner.js +628 -0
  51. package/dist/kubernetes/secrets.js +7 -11
  52. package/dist/kubernetes/service-rollouts.js +74 -0
  53. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  54. package/dist/kubernetes/volumes.js +96 -11
  55. package/dist/main.js +2 -0
  56. package/dist/optimizer/calculate-checksum.js +3 -2
  57. package/dist/optimizer/check-key.js +94 -0
  58. package/dist/optimizer/check-keys.js +64 -0
  59. package/dist/optimizer/get-work-cache-stats.js +17 -3
  60. package/dist/optimizer/work-task-cache-description.js +49 -1
  61. package/dist/planner/service-definition.js +9 -4
  62. package/dist/planner/utils/append-work-dependencies.js +5 -3
  63. package/dist/planner/utils/append-work-service.js +18 -1
  64. package/dist/planner/utils/append-work-task.js +13 -3
  65. package/dist/planner/utils/get-work-runtime.js +6 -1
  66. package/dist/planner/utils/normalize-path.js +19 -1
  67. package/dist/planner/utils/parse-work-mount.js +7 -1
  68. package/dist/planner/utils/parse-work-port.js +4 -1
  69. package/dist/planner/validate.js +22 -0
  70. package/dist/planner/work-network.js +3 -1
  71. package/dist/planner/work-runtime-aggregate.js +32 -0
  72. package/dist/planner/work-runtime-kubernetes.js +153 -26
  73. package/dist/planner/work-task.js +6 -1
  74. package/dist/program.js +27 -9
  75. package/dist/remote/device-login.js +7 -2
  76. package/dist/remote/host-config.js +69 -3
  77. package/dist/remote/remote-commands.js +33 -7
  78. package/dist/remote/remote-status.js +112 -0
  79. package/dist/remote/remote-work-commands.js +562 -45
  80. package/dist/remote/server-client.js +15 -2
  81. package/dist/remote/token-source.js +64 -26
  82. package/dist/run-program.js +9 -0
  83. package/dist/schema/build-file-base-task-schema.js +3 -3
  84. package/dist/schema/build-file-container-service-schema.js +10 -0
  85. package/dist/schema/build-file-container-task-schema.js +16 -1
  86. package/dist/schema/build-file-include-schema.js +18 -4
  87. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  88. package/dist/schema/build-file-task-schema.js +5 -1
  89. package/dist/schema/capabilities-schema.js +56 -0
  90. package/dist/schema/reference-parser.js +119 -11
  91. package/dist/schema/removed-properties.js +20 -0
  92. package/dist/schema/schema-parser.js +94 -41
  93. package/dist/schema/work-scope-parser.js +33 -10
  94. package/dist/secrets/command-secret-provider.js +3 -1
  95. package/dist/secrets/github-app-token-command.js +3 -1
  96. package/dist/secrets/secret-catalog.js +43 -19
  97. package/dist/server/accounts-api.js +4 -1
  98. package/dist/server/approvals-api.js +1 -1
  99. package/dist/server/capacity-format.js +36 -0
  100. package/dist/server/capacity.js +300 -0
  101. package/dist/server/checks-api.js +83 -0
  102. package/dist/server/checks.js +116 -0
  103. package/dist/server/dashboard.js +196 -0
  104. package/dist/server/definitions-api.js +66 -3
  105. package/dist/server/endpoints.js +794 -0
  106. package/dist/server/executor.js +164 -9
  107. package/dist/server/kubernetes-backend.js +478 -70
  108. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  109. package/dist/server/overview.js +271 -0
  110. package/dist/server/plan.js +170 -121
  111. package/dist/server/policy.js +34 -2
  112. package/dist/server/proposals-api.js +17 -8
  113. package/dist/server/repo-access.js +5 -0
  114. package/dist/server/run-logs.js +4 -0
  115. package/dist/server/run-plan.js +38 -4
  116. package/dist/server/run-request.js +58 -3
  117. package/dist/server/run-worker.js +262 -13
  118. package/dist/server/runs.js +969 -36
  119. package/dist/server/sandbox-pod.js +28 -0
  120. package/dist/server/sandbox-storage.js +23 -0
  121. package/dist/server/sandboxes.js +8 -0
  122. package/dist/server/server-config.js +246 -10
  123. package/dist/server/server.js +429 -32
  124. package/dist/server/service-runs.js +82 -0
  125. package/dist/server/timings.js +132 -0
  126. package/dist/server/wait-reason.js +21 -0
  127. package/dist/utils/tar.js +28 -1
  128. package/dist/utils/terminal-prompt.js +53 -0
  129. package/package.json +8 -2
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.registerWorkCommands = exports.describePlan = exports.describeApproval = void 0;
6
+ exports.registerWorkCommands = exports.describePlan = exports.describeApproval = exports.keepRuns = exports.describeListedRun = exports.describeRunEnd = void 0;
7
7
  const commander_1 = require("commander");
8
8
  const colors_1 = __importDefault(require("colors"));
9
9
  const token_source_1 = require("./token-source");
@@ -13,8 +13,41 @@ const annotations_1 = require("./annotations");
13
13
  const server_client_1 = require("./server-client");
14
14
  const bundle_text_1 = require("./bundle-text");
15
15
  const sandbox_client_1 = require("../harness/sandbox-client");
16
+ const units_1 = require("../utils/units");
17
+ const service_tools_1 = require("../harness/service-tools");
16
18
  const path_1 = require("path");
19
+ const check_keys_1 = require("../optimizer/check-keys");
20
+ const remote_status_1 = require("./remote-status");
21
+ const timings_1 = require("../server/timings");
22
+ const terminal_prompt_1 = require("../utils/terminal-prompt");
23
+ function describeOutcome(planned) {
24
+ switch (planned.outcome) {
25
+ case 'published':
26
+ return 'publishing it would publish it';
27
+ case 'already published':
28
+ return planned.yanked ? 'it is published already, and yanked' : 'it is published already';
29
+ case 'proposed':
30
+ return planned.missing?.length
31
+ ? `publishing it would propose it, it names accounts you do not use: ${planned.missing
32
+ .map(server_client_1.printable)
33
+ .join(', ')}`
34
+ : 'publishing it would propose it, somebody who may publish approves it';
35
+ default:
36
+ return 'the server accepts it, publishing it needs builds: propose or publish';
37
+ }
38
+ }
17
39
  const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
40
+ // how a check passed, one line
41
+ function describePassed(passed) {
42
+ const from = passed.source.type === 'ref'
43
+ ? `${passed.source.ref} at ${passed.source.commit.slice(0, 12)}`
44
+ : `a check of sandbox ${passed.source.sandbox}, fetched at ${passed.source.commit.slice(0, 12)}`;
45
+ return `passed ${passed.finishedAt}${passed.status === 'cached'
46
+ ? ' from the cache'
47
+ : passed.durationMs !== undefined
48
+ ? ` in ${(0, units_1.formatDuration)(passed.durationMs)}`
49
+ : ''}, ${passed.run} by ${passed.by.sub}, from ${from}`;
50
+ }
18
51
  // what the publisher said about where a version comes from, one line
19
52
  // What a publisher said about where a version comes from: the server takes it as said,
20
53
  // so anybody else is told it is not checked.
@@ -37,6 +70,46 @@ function printableLine(text) {
37
70
  return text.split('\t').map(server_client_1.printableWhole).join('\t');
38
71
  }
39
72
  const collectOption = (value, previous = []) => [...previous, value];
73
+ /**
74
+ * Why a run that did not succeed ended: its error, then each task that failed with its own
75
+ * error or exit code, where the error of the run does not say it already.
76
+ */
77
+ function describeRunEnd(run) {
78
+ const lines = [`${(0, server_client_1.printable)(run.id)} ${(0, server_client_1.printable)(run.state)}${run.error ? `: ${(0, server_client_1.printableWhole)(run.error)}` : ''}`];
79
+ for (const [task, result] of Object.entries(run.results ?? {})) {
80
+ if (result.status !== 'failed') {
81
+ continue;
82
+ }
83
+ const why = result.error ?? (result.exitCode !== undefined ? `exit code ${result.exitCode}` : undefined);
84
+ if (why && !run.error?.includes(`${task}: ${why}`)) {
85
+ lines.push(` ${(0, server_client_1.printable)(task)}: ${(0, server_client_1.printableWhole)(why)}`);
86
+ }
87
+ }
88
+ return lines;
89
+ }
90
+ exports.describeRunEnd = describeRunEnd;
91
+ // What `remote runs` shows of a run, on one line: how long it waited for a place and ran.
92
+ function describeListedRun(run, now = Date.now()) {
93
+ const what = run.up ? `up ${run.up.services.join(', ')}` : run.tasks.join(', ') || 'by labels';
94
+ const until = run.up && (run.state === 'up' || run.state === 'asleep') && run.expiresAt ? ` until ${run.expiresAt}` : '';
95
+ const queue = run.queue
96
+ ? ` (${run.queue.position}. in the queue, waits: ${(0, server_client_1.printable)((0, remote_status_1.describeWait)(run.queue.waitsFor, !run.owner))})`
97
+ : '';
98
+ const waited = (0, timings_1.waitedMs)(run, now);
99
+ const ran = (0, timings_1.ranMs)(run, now);
100
+ const times = [
101
+ waited !== undefined ? `waited ${(0, timings_1.roughly)(waited)}` : '',
102
+ ran !== undefined ? `${run.up ? 'up after' : 'ran'} ${(0, timings_1.roughly)(ran)}` : '',
103
+ ].filter(Boolean);
104
+ return `${(0, server_client_1.printable)(run.id)} ${(0, server_client_1.printable)(run.createdAt)} ${(0, server_client_1.printable)(run.state)}${(0, server_client_1.printable)(until)}${queue} ${(0, server_client_1.printable)(what)}${run.owner ? ` of ${(0, server_client_1.printable)(run.owner.sub)}` : ''}${times.length > 0 ? ` ${times.join(', ')}` : ''}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`;
105
+ }
106
+ exports.describeListedRun = describeListedRun;
107
+ // What a server from before the filters of GET /v1/runs answers is filtered here, so an
108
+ // older server never shows task runs as service runs.
109
+ function keepRuns(runs, states, kind) {
110
+ return runs.filter((run) => (!states || states.includes(run.state)) && (!kind || (kind === 'services') === !!run.up));
111
+ }
112
+ exports.keepRuns = keepRuns;
40
113
  // What a person who approves is shown of a run: everything the decision rests on.
41
114
  function describeApproval(run) {
42
115
  const source = run.source.type === 'ref'
@@ -92,33 +165,51 @@ function describePlan(plan) {
92
165
  ...(entry.mounts.length > 0 ? [` mounts: ${entry.mounts.map(server_client_1.printable).join(', ')}`] : []),
93
166
  ...(entry.ports.length > 0 ? [` ports: ${entry.ports.map(server_client_1.printable).join(', ')}`] : []),
94
167
  ...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
168
+ ...(entry.capAdd?.length ? [` capAdd: ${entry.capAdd.map(server_client_1.printable).join(', ')}`] : []),
95
169
  ]);
96
170
  }
97
171
  exports.describePlan = describePlan;
98
172
  function registerWorkCommands(remote, context) {
99
173
  const { environment, buildFile, getServer, guarded, passthrough, fail } = context;
174
+ // the ref a run starts from: what the options name, else the checkout here
175
+ const refSource = async (options) => {
176
+ const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
177
+ const repo = options.repo ?? checkout.repo;
178
+ const ref = options.ref ?? checkout.branch;
179
+ // the commit checked out here is the code meant, when the ref is the branch it is on
180
+ const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
181
+ if (!repo) {
182
+ throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
183
+ }
184
+ if (!ref) {
185
+ throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
186
+ }
187
+ return { type: 'ref', repo, ref, ...(commit ? { commit } : {}) };
188
+ };
100
189
  const connect = async (name) => {
101
190
  const chosen = await getServer(name);
102
191
  // a CI job or an agent brings its own token, a person signs in
103
192
  const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
104
193
  return { ...chosen, token };
105
194
  };
106
- remote
107
- .command('publish [server]')
108
- .description('publish the build file and the files it includes to a server, runs refer to it by its hash')
109
- .addOption(new commander_1.Option('--annotation <key=value>', 'say more about where it comes from, or leave one out with key=; path, repo, commit, machine and user are filled in').argParser(collectOption))
110
- .action(guarded('Remote publish was not successful', async (name, options) => {
111
- const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
112
- const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
113
- const { name: serverName, server, token } = await connect(name);
195
+ // publishes the bundle with where it comes from and says what it is now: published,
196
+ // already published, or only proposed
197
+ const publishBundle = async (serverName, server, token, bundle, annotations) => {
114
198
  const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
115
199
  ...bundle,
116
200
  annotations,
117
201
  });
118
202
  const version = answer.body;
119
203
  const files = Object.keys(bundle.files).length;
120
- const proposed = answer.status === 202;
121
- const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
204
+ // a proposal that already waits answers 200 like a published version, with its state
205
+ const proposed = answer.status === 202 || version.state !== undefined;
206
+ const verb = answer.status === 202
207
+ ? 'proposed'
208
+ : proposed
209
+ ? 'already proposed'
210
+ : answer.status === 201
211
+ ? 'published'
212
+ : 'already published';
122
213
  environment.stdout.write(`${verb} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
123
214
  const names = [...(version.tasks ?? []), ...(version.services ?? [])];
124
215
  environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
@@ -128,6 +219,123 @@ function registerWorkCommands(remote, context) {
128
219
  if (proposed) {
129
220
  environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
130
221
  }
222
+ return { version, proposed };
223
+ };
224
+ // what publishing the bundle would do, undefined from a server that can't say (before 1.27)
225
+ const planBundle = async (server, token, bundle) => {
226
+ try {
227
+ return (await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions/plan', bundle)).body;
228
+ }
229
+ catch (e) {
230
+ if (e instanceof server_client_1.ServerError && e.status === 404) {
231
+ return undefined;
232
+ }
233
+ throw e;
234
+ }
235
+ };
236
+ // The build file a run or up names, and what `--publish` may publish: the hash of the
237
+ // build file here, unless `--build` or HAMMERKIT_BUILD names a version.
238
+ const runDefinition = async (options) => {
239
+ const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
240
+ if (named !== undefined) {
241
+ if (options.publish) {
242
+ throw new Error(`--publish publishes the build file here, it does not go with ${options.build !== undefined ? '--build' : 'HAMMERKIT_BUILD'}`);
243
+ }
244
+ if (!bundle_1.HASH_PATTERN.test(named)) {
245
+ throw new Error(`${(0, server_client_1.printable)(named)} is not the hash of a published version (sha256:...)`);
246
+ }
247
+ return { definition: named };
248
+ }
249
+ const local = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
250
+ return { definition: (0, bundle_1.hashBundle)(local), local };
251
+ };
252
+ // Submits a run of a published version. When the server does not have the build file
253
+ // here, it is published first: with --publish at once, else when the person at the
254
+ // terminal says so. Without a terminal and without --publish the run fails and says how
255
+ // to publish it, so CI never waits on a question.
256
+ const submitPublishing = async (target, request, local, publish) => {
257
+ const { serverName, server, connection } = target;
258
+ try {
259
+ return await (0, sandbox_client_1.submitRun)(connection, request);
260
+ }
261
+ catch (e) {
262
+ const status = e.status;
263
+ if (!local || status !== 400 || !/no published build file/.test(String(e))) {
264
+ throw e;
265
+ }
266
+ }
267
+ const definition = request.definition;
268
+ const notPublished = `the build file here is not published on ${serverName} (${definition})`;
269
+ const token = await connection.token();
270
+ if (!publish) {
271
+ if (!environment.prompt) {
272
+ throw new Error(`${notPublished}, publish it first with: hammerkit remote publish ${serverName}, or pass --publish`);
273
+ }
274
+ // a server before 1.27 can't say what publishing would do: it is asked all the same,
275
+ // and refuses then what the caller may not do
276
+ const planned = await planBundle(server, token, local);
277
+ if (planned && planned.outcome === null) {
278
+ throw new Error(`${notPublished}, and you may only run published build files (builds: run): ` +
279
+ `somebody who may publish publishes it with: hammerkit remote publish ${serverName}`);
280
+ }
281
+ if (planned?.outcome !== 'already published') {
282
+ const files = Object.keys(local.files).length;
283
+ environment.stdout.write(`${notPublished}, ${files} ${files === 1 ? 'file' : 'files'}\n`);
284
+ if (planned) {
285
+ const names = [...(planned.tasks ?? []), ...(planned.services ?? [])];
286
+ environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
287
+ }
288
+ const proposing = planned?.outcome === 'proposed';
289
+ if (proposing) {
290
+ environment.stdout.write(`${describeOutcome(planned)}\n`);
291
+ }
292
+ const answer = await environment.prompt(proposing ? `propose it to ${serverName} now? [y/N] ` : `publish it to ${serverName} now and go on? [y/N] `);
293
+ if (environment.abortCtrl.signal.aborted) {
294
+ throw new Error(`stopped, nothing was published`);
295
+ }
296
+ if (!(0, terminal_prompt_1.isYes)(answer)) {
297
+ throw new Error(`${notPublished}, nothing was published`);
298
+ }
299
+ }
300
+ }
301
+ const annotations = await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(local.files));
302
+ const { version, proposed } = await publishBundle(serverName, server, token, local, annotations).catch((e) => {
303
+ throw e instanceof server_client_1.ServerError && e.status === 403 ? new Error(`${notPublished}, ${e.message}`) : e;
304
+ });
305
+ if (version.hash !== definition) {
306
+ throw new Error(`${serverName} published ${(0, server_client_1.printable)(version.hash)}, not ${definition} as computed here, nothing was started`);
307
+ }
308
+ if (proposed) {
309
+ throw new Error(`${definition} is only proposed on ${serverName}, nothing was started: ` +
310
+ 'run this again once somebody who may publish approves it');
311
+ }
312
+ return (0, sandbox_client_1.submitRun)(connection, request);
313
+ };
314
+ remote
315
+ .command('publish [server]')
316
+ .description('publish the build file and the files it includes to a server, runs refer to it by its hash')
317
+ .addOption(new commander_1.Option('--annotation <key=value>', 'say more about where it comes from, or leave one out with key=; path, repo, commit, machine and user are filled in').argParser(collectOption))
318
+ .addOption(new commander_1.Option('--dry-run', "check it against the server's policy and say what publishing would do, without publishing; builds: run is enough"))
319
+ .action(guarded('Remote publish was not successful', async (name, options) => {
320
+ const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
321
+ if (options.dryRun) {
322
+ const { name: serverName, server, token } = await connect(name);
323
+ const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions/plan', bundle).catch((e) => {
324
+ throw e instanceof server_client_1.ServerError && e.status === 404
325
+ ? new Error(`${serverName} can't check a build file without publishing it, it needs hammerkit 1.27`)
326
+ : e;
327
+ });
328
+ const planned = answer.body;
329
+ const files = Object.keys(bundle.files).length;
330
+ environment.stdout.write(`checked with the policy of ${serverName}: ${(0, server_client_1.printable)(planned.hash)} (${files} ${files === 1 ? 'file' : 'files'}), nothing was published\n`);
331
+ const names = [...(planned.tasks ?? []), ...(planned.services ?? [])];
332
+ environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
333
+ environment.stdout.write(`${describeOutcome(planned)}\n`);
334
+ return;
335
+ }
336
+ const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
337
+ const { name: serverName, server, token } = await connect(name);
338
+ await publishBundle(serverName, server, token, bundle, annotations);
131
339
  }));
132
340
  remote
133
341
  .command('run <tasks...>')
@@ -139,28 +347,18 @@ function registerWorkCommands(remote, context) {
139
347
  .addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at, else the run is refused; the commit checked out here when --ref is left out'))
140
348
  .addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
141
349
  .addOption(new commander_1.Option('--cache <method>', 'as --cache of run').choices(['checksum', 'modify-date', 'none']))
142
- .addOption(new commander_1.Option('--timeout <duration>', 'stop the run after this long, such as 30m'))
350
+ .addOption(new commander_1.Option('--timeout <duration>', 'as --timeout of run: for tasks without their own, such as 30m'))
351
+ .addOption(new commander_1.Option('--max-timeout <duration>', 'as --max-timeout of run: for every task, also one with its own timeout'))
352
+ .addOption(new commander_1.Option('--detach', 'submit the run and exit, without following it: for a check that CI verifies later'))
353
+ .addOption(new commander_1.Option('--publish', 'publish the build file here first when the server does not have it, without asking; for CI and scripts'))
143
354
  .action(guarded('Remote run was not successful', async (tasks, options) => {
144
- const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
145
- const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
146
- if (!bundle_1.HASH_PATTERN.test(definition)) {
147
- throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
148
- }
149
- const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
150
- const repo = options.repo ?? checkout.repo;
151
- const ref = options.ref ?? checkout.branch;
152
- // the commit checked out here is the code meant, when the ref is the branch it is on
153
- const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
154
- if (!repo) {
155
- throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
156
- }
157
- if (!ref) {
158
- throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
159
- }
355
+ const { definition, local } = await runDefinition(options);
356
+ const source = await refSource(options);
357
+ const ref = source.ref;
160
358
  const { name: serverName, server } = await getServer(options.server);
161
359
  const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
162
360
  const runOptions = {};
163
- for (const key of ['env', 'cache', 'timeout']) {
361
+ for (const key of ['env', 'cache', 'timeout', 'maxTimeout']) {
164
362
  if (options[key] !== undefined) {
165
363
  runOptions[key] = options[key];
166
364
  }
@@ -168,23 +366,13 @@ function registerWorkCommands(remote, context) {
168
366
  if (passthrough.length > 0) {
169
367
  runOptions.args = passthrough;
170
368
  }
171
- let submitted;
172
- try {
173
- submitted = await (0, sandbox_client_1.submitRun)(connection, {
174
- definition,
175
- tasks,
176
- source: { type: 'ref', repo, ref, ...(commit ? { commit } : {}) },
177
- options: runOptions,
178
- });
179
- }
180
- catch (e) {
181
- if (e.status === 400 && !named && /no published build file/.test(String(e))) {
182
- throw new Error(`the build file here is not published on ${serverName} (${definition}), publish it first`);
183
- }
184
- throw e;
185
- }
369
+ const submitted = await submitPublishing({ serverName, server, connection }, { definition, tasks, source, options: runOptions }, local, !!options.publish);
186
370
  const at = submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : '';
187
371
  environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${tasks.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(ref)}${at}\n`);
372
+ if (options.detach) {
373
+ environment.stdout.write(`follow it with: hammerkit remote logs ${(0, server_client_1.printable)(submitted.id)} --follow\n`);
374
+ return;
375
+ }
188
376
  const prefixed = tasks.length > 1;
189
377
  const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
190
378
  signal: environment.abortCtrl.signal,
@@ -208,7 +396,336 @@ function registerWorkCommands(remote, context) {
208
396
  .concat(Object.values(run.results ?? {}))
209
397
  .find((result) => result?.status === 'failed');
210
398
  const exitCode = failed?.exitCode && failed.exitCode > 0 ? failed.exitCode : 1;
211
- fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, exitCode);
399
+ const [said, ...why] = describeRunEnd(run);
400
+ for (const line of why) {
401
+ environment.stdout.write(`${line}\n`);
402
+ }
403
+ environment.stdout.write(`read its log again with: hammerkit remote logs ${(0, server_client_1.printable)(run.id)}\n`);
404
+ fail(said, exitCode);
405
+ }));
406
+ remote
407
+ .command('up <services...>')
408
+ .description('start services of a published build file on a server from a git ref and keep them up until their ttl ends, ' +
409
+ 'with their endpoints at addresses of their own behind the sign-in of the server')
410
+ .addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
411
+ .addOption(new commander_1.Option('--endpoint <service:port>', 'publish this port of a service, such as web:8080; may be given more than once').argParser(collectOption))
412
+ .addOption(new commander_1.Option('--ttl <duration>', 'how long they stay once ready, such as 2h, then they are removed; the server default when left out'))
413
+ .addOption(new commander_1.Option('--sleep-ttl <duration>', 'how long they stay up without a request, such as 30m, then they sleep until the next request wakes them; ' +
414
+ 'the server default when left out'))
415
+ .addOption(new commander_1.Option('--name <name>', 'a later remote up of yours with the same name moves this run to its commit, or replaces it when it ' +
416
+ 'starts other services or another build file; the addresses stay; the branch when left out'))
417
+ .addOption(new commander_1.Option('--build <hash>', 'the published version to run (sha256:...), or HAMMERKIT_BUILD; the hash of the build file here when left out'))
418
+ .addOption(new commander_1.Option('--repo <host/path>', 'the repository; origin of the checkout here when left out'))
419
+ .addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to start; the branch checked out here when left out'))
420
+ .addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at; the commit checked out here when --ref is left out'))
421
+ .addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
422
+ .addOption(new commander_1.Option('--publish', 'publish the build file here first when the server does not have it, without asking; for CI and scripts'))
423
+ .action(guarded('Remote up was not successful', async (services, options) => {
424
+ const { definition, local } = await runDefinition(options);
425
+ const endpoints = (options.endpoint ?? []).map(service_tools_1.parseEndpoint);
426
+ const source = await refSource(options);
427
+ const { name: serverName, server } = await getServer(options.server);
428
+ const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
429
+ const submitted = await submitPublishing({ serverName, server, connection }, {
430
+ definition,
431
+ services,
432
+ endpoints,
433
+ ...(options.ttl ? { ttl: options.ttl } : {}),
434
+ ...(options.sleepTtl ? { sleepTtl: options.sleepTtl } : {}),
435
+ ...(options.name ? { name: options.name } : {}),
436
+ source,
437
+ options: options.env ? { env: options.env } : {},
438
+ }, local, !!options.publish);
439
+ if (submitted.moved) {
440
+ // a run of this name runs the same: it moves to the commit, with its id, addresses and volumes
441
+ environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName} (${(0, server_client_1.printable)(submitted.state)}) moves to ${(0, server_client_1.printable)(source.ref)} at ${(0, server_client_1.printable)(submitted.moving?.commit ?? (submitted.source.type === 'ref' ? submitted.source.commit : '-'))}\n`);
442
+ }
443
+ else {
444
+ environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${services.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(source.ref)}${submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : ''}\n`);
445
+ }
446
+ const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
447
+ signal: environment.abortCtrl.signal,
448
+ untilUp: true,
449
+ onLine: (line) => environment.stdout.write(`[${(0, server_client_1.printable)(line.task)}] ${printableLine(line.text)}\n`),
450
+ onWaiting: (run) => {
451
+ for (const approval of run.approvals ?? []) {
452
+ environment.stdout.write(`waiting until somebody who uses ${(0, server_client_1.printable)(approval.account)} approves it, before ${(0, server_client_1.printable)(approval.expiresAt)}: hammerkit remote approve ${(0, server_client_1.printable)(run.id)}\n`);
453
+ }
454
+ },
455
+ });
456
+ const run = followed.run;
457
+ if (run.state !== 'up' && !(submitted.moved && run.state === 'asleep')) {
458
+ const [said, ...why] = describeRunEnd(run);
459
+ for (const line of why) {
460
+ environment.stdout.write(`${line}\n`);
461
+ }
462
+ environment.stdout.write(`read its log again with: hammerkit remote logs ${(0, server_client_1.printable)(run.id)}\n`);
463
+ fail(said, 1);
464
+ }
465
+ // the outcome of this move, not of one that came after it
466
+ const target = (0, sandbox_client_1.getMoveTarget)(submitted);
467
+ const lastMove = [...(run.moves ?? [])].reverse().find((move) => move.commit === target);
468
+ if (submitted.moved && lastMove?.error) {
469
+ fail(`${(0, server_client_1.printable)(run.id)} did not move to ${(0, server_client_1.printable)(lastMove.commit)}: ${(0, server_client_1.printable)(lastMove.error)}; it stays at ${(0, server_client_1.printable)(run.source.type === 'ref' ? run.source.commit : '-')}`, 1);
470
+ }
471
+ if (run.state === 'asleep') {
472
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} sleeps, at ${(0, server_client_1.printable)(run.source.type === 'ref' ? run.source.commit : '-')} when it wakes, until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}\n`);
473
+ return;
474
+ }
475
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} is up until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}${run.up?.sleepTtl ? `, asleep after ${(0, units_1.formatDuration)(run.up.sleepTtl)} without a request` : ''}\n`);
476
+ for (const endpoint of run.up?.endpoints ?? []) {
477
+ environment.stdout.write(` ${(0, server_client_1.printable)(endpoint.service)}:${endpoint.port} ${(0, server_client_1.printable)(endpoint.url)}\n`);
478
+ }
479
+ environment.stdout.write(`extend it with: hammerkit remote extend ${(0, server_client_1.printable)(run.id)} 8h, stop it with: hammerkit remote stop ${(0, server_client_1.printable)(run.id)}\n`);
480
+ }));
481
+ remote
482
+ .command('status [server]')
483
+ .description('show how a server stands for you: the places that run, the queue and why each run in it waits, your limits and the runs that count against them, your service runs')
484
+ .addOption(new commander_1.Option('--json', 'print the overview as the server answers it, for scripts and agents'))
485
+ .action(guarded('Remote status was not successful', async (name, options) => {
486
+ const { name: serverName, server, token } = await connect(name);
487
+ const overview = (await (0, server_client_1.callServer)(server, token, 'GET', '/v1/overview')).body;
488
+ if (options.json) {
489
+ environment.stdout.write(`${JSON.stringify(overview, null, 2)}\n`);
490
+ return;
491
+ }
492
+ for (const line of (0, remote_status_1.describeOverview)(serverName, overview)) {
493
+ environment.stdout.write(`${line}\n`);
494
+ }
495
+ // a server from before /v1/capacity, or one that can not read it, prints without
496
+ const capacity = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/capacity').catch(() => undefined);
497
+ if (capacity) {
498
+ for (const line of (0, remote_status_1.describeCapacity)(capacity.body, true)) {
499
+ environment.stdout.write(`${line}\n`);
500
+ }
501
+ }
502
+ }));
503
+ remote
504
+ .command('capacity [server]')
505
+ .description('show what the machines of a server have and what its runs take: CPU and memory requested and in use, per namespace on Kubernetes')
506
+ .addOption(new commander_1.Option('--json', 'print it as the server answers it'))
507
+ .action(guarded('Remote capacity was not successful', async (name, options) => {
508
+ const { server, token } = await connect(name);
509
+ const capacity = (await (0, server_client_1.callServer)(server, token, 'GET', '/v1/capacity')).body;
510
+ if (options.json) {
511
+ environment.stdout.write(`${JSON.stringify(capacity, null, 2)}\n`);
512
+ return;
513
+ }
514
+ for (const line of (0, remote_status_1.describeCapacity)(capacity)) {
515
+ environment.stdout.write(`${line}\n`);
516
+ }
517
+ }));
518
+ remote
519
+ .command('tasks [server]')
520
+ .description('show what the past runs of each task took, over the runs the server keeps: how often it ran, failed and came from the cache, the median and 90th percentile of its time, and how long its runs waited for a place')
521
+ .addOption(new commander_1.Option('--mine', 'only your runs'))
522
+ .addOption(new commander_1.Option('--json', 'print it as the server answers it'))
523
+ .action(guarded('Remote tasks was not successful', async (name, options) => {
524
+ const { name: serverName, server, token } = await connect(name);
525
+ const { tasks } = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/tasks?scope=${options.mine ? 'mine' : 'visible'}`)).body;
526
+ if (options.json) {
527
+ environment.stdout.write(`${JSON.stringify(tasks, null, 2)}\n`);
528
+ return;
529
+ }
530
+ if (tasks.length === 0) {
531
+ environment.stdout.write(`${serverName}: no task ran in the runs it keeps\n`);
532
+ return;
533
+ }
534
+ for (const line of (0, remote_status_1.describeTaskStats)(tasks)) {
535
+ environment.stdout.write(`${line}\n`);
536
+ }
537
+ }));
538
+ remote
539
+ .command('dashboard [server]')
540
+ .description('print the address of the dashboard of a server, the pages of remote status, services, runs and logs')
541
+ .action(guarded('Remote dashboard was not successful', async (name) => {
542
+ const { name: serverName, server } = await getServer(name);
543
+ const discovery = await (0, server_client_1.getDiscovery)(server);
544
+ if (!discovery.dashboard) {
545
+ throw new Error(`${serverName} has no dashboard: it needs endpoints with a login in its config, see https://hammerkit.dev/docs/remote-compute/status#dashboard`);
546
+ }
547
+ environment.stdout.write(`${(0, server_client_1.printable)(discovery.dashboard)}\n`);
548
+ }));
549
+ remote
550
+ .command('services [server]')
551
+ .description('list the service runs that are starting, up or asleep: yours, and those whose endpoints you may open, with their addresses and when they end or sleep')
552
+ .addOption(new commander_1.Option('--ended', 'also the ones that ended, as long as the server keeps them'))
553
+ .action(guarded('Remote services was not successful', async (name, options) => {
554
+ const { name: serverName, server, token } = await connect(name);
555
+ const open = ['awaiting-approval', 'queued', 'running', 'up', 'asleep'];
556
+ const states = options.ended ? '' : `&state=${open.join(',')}`;
557
+ const runs = keepRuns((await (0, server_client_1.callServer)(server, token, 'GET', `/v1/runs?scope=visible&kind=services${states}`)).body.runs, options.ended ? undefined : open, 'services');
558
+ if (runs.length === 0) {
559
+ environment.stdout.write(`${serverName}: no service runs${options.ended ? '' : ' are starting, up or asleep'}\n`);
560
+ return;
561
+ }
562
+ for (const run of runs) {
563
+ for (const line of (0, remote_status_1.describeServiceRun)(run)) {
564
+ environment.stdout.write(`${line}\n`);
565
+ }
566
+ }
567
+ }));
568
+ remote
569
+ .command('runs [server]')
570
+ .description('list your runs on a server, newest first: their state, where a queued one stands, and why one failed')
571
+ .addOption(new commander_1.Option('--limit <count>', 'show at most this many').default('20'))
572
+ .addOption(new commander_1.Option('--state <states>', 'only runs in these states, comma separated: awaiting-approval, queued, running, up, asleep, succeeded, failed, cancelled, rejected'))
573
+ .addOption(new commander_1.Option('--services', 'only service runs').conflicts('tasks'))
574
+ .addOption(new commander_1.Option('--tasks', 'only runs of tasks'))
575
+ .addOption(new commander_1.Option('--all', 'also the runs of others you may see: those you may approve, service runs you may open, every run with observe'))
576
+ .action(guarded('Remote runs was not successful', async (name, options) => {
577
+ const limit = Number(options.limit);
578
+ if (!Number.isInteger(limit) || limit < 1) {
579
+ throw new Error('--limit is a number of runs, 1 or more');
580
+ }
581
+ const query = new URLSearchParams();
582
+ if (options.all) {
583
+ query.set('scope', 'visible');
584
+ }
585
+ if (options.state) {
586
+ query.set('state', options.state);
587
+ }
588
+ if (options.services || options.tasks) {
589
+ query.set('kind', options.services ? 'services' : 'tasks');
590
+ }
591
+ const { name: serverName, server, token } = await connect(name);
592
+ const path = `/v1/runs${query.toString() ? `?${query}` : ''}`;
593
+ const runs = keepRuns((await (0, server_client_1.callServer)(server, token, 'GET', path)).body.runs, options.state ? String(options.state).split(',') : undefined, options.services ? 'services' : options.tasks ? 'tasks' : undefined);
594
+ if (runs.length === 0) {
595
+ environment.stdout.write(`${serverName}: ${query.toString() ? 'no runs it keeps are like that' : 'you have no runs it still keeps'}\n`);
596
+ return;
597
+ }
598
+ for (const run of runs.slice(0, limit)) {
599
+ environment.stdout.write(`${describeListedRun(run)}\n`);
600
+ }
601
+ if (runs.length > limit) {
602
+ environment.stdout.write(`… and ${runs.length - limit} older, see them with --limit ${runs.length}\n`);
603
+ }
604
+ }));
605
+ remote
606
+ .command('logs <run> [server]')
607
+ .description('print the log of a run, also of one that ended: what its tasks and services printed, and what the server and the cluster said')
608
+ .addOption(new commander_1.Option('--task <name>', 'only the lines of this task or service'))
609
+ .addOption(new commander_1.Option('--tail <lines>', 'only the last lines, at most 1000').argParser((value) => {
610
+ if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 1000) {
611
+ throw new commander_1.InvalidArgumentError('a number of lines from 1 to 1000');
612
+ }
613
+ return Number(value);
614
+ }))
615
+ .addOption(new commander_1.Option('-f, --follow', 'keep printing what comes until the run ends; ctrl-c stops following, not the run'))
616
+ .action(guarded('Remote logs was not successful', async (id, name, options) => {
617
+ if (!RUN_ID.test(id)) {
618
+ throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...), list them with: hammerkit remote runs`);
619
+ }
620
+ if (options.follow && options.tail !== undefined) {
621
+ throw new Error('--follow prints the whole log, leave --tail out');
622
+ }
623
+ const { name: serverName, server } = await getServer(name);
624
+ const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
625
+ const found = await (0, sandbox_client_1.getRun)(connection, id);
626
+ if (!found) {
627
+ throw new Error(`${serverName} keeps no run ${(0, server_client_1.printable)(id)} you may see, list them with: hammerkit remote runs`);
628
+ }
629
+ let run = found;
630
+ const task = options.task;
631
+ const print = (line) => environment.stdout.write(`${task ? '' : `[${(0, server_client_1.printable)(line.task)}] `}${printableLine(line.text)}\n`);
632
+ if (options.follow) {
633
+ run = (await (0, sandbox_client_1.followRun)(connection, run, {
634
+ task,
635
+ signal: environment.abortCtrl.signal,
636
+ detach: true,
637
+ onLine: print,
638
+ })).run;
639
+ }
640
+ else {
641
+ await (0, sandbox_client_1.readRunLog)(connection, run, { task, tail: options.tail }, print);
642
+ }
643
+ if (run.state === 'succeeded' || run.state === 'up' || run.state === 'asleep') {
644
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} ${(0, server_client_1.printable)(run.state)}\n`);
645
+ }
646
+ else if (sandbox_client_1.FINAL_RUN_STATES.includes(run.state)) {
647
+ for (const line of describeRunEnd(run)) {
648
+ environment.stdout.write(`${line}\n`);
649
+ }
650
+ }
651
+ else {
652
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} is ${(0, server_client_1.printable)(run.state)}, follow it with --follow\n`);
653
+ }
654
+ }));
655
+ remote
656
+ .command('extend <run> <ttl> [server]')
657
+ .description('keep a service run that is up for this long from now, such as 8h, at most the maxTtl of the server')
658
+ .action(guarded('Remote extend was not successful', async (id, ttl, name) => {
659
+ if (!RUN_ID.test(id)) {
660
+ throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...)`);
661
+ }
662
+ const { name: serverName, server, token } = await connect(name);
663
+ const run = (await (0, server_client_1.callServer)(server, token, 'POST', `/v1/runs/${id}/extend`, { ttl })).body;
664
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} on ${serverName} stays up until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}\n`);
665
+ }));
666
+ remote
667
+ .command('stop <run> [server]')
668
+ .description('stop a run: a service run takes its services and endpoints down')
669
+ .action(guarded('Remote stop was not successful', async (id, name) => {
670
+ if (!RUN_ID.test(id)) {
671
+ throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...)`);
672
+ }
673
+ const { name: serverName, server, token } = await connect(name);
674
+ const run = (await (0, server_client_1.callServer)(server, token, 'POST', `/v1/runs/${id}/cancel`)).body;
675
+ environment.stdout.write(`${(0, server_client_1.printable)(run.id)} on ${serverName}: ${run.state === 'up' || run.state === 'running' ? 'stopping' : (0, server_client_1.printable)(run.state)}\n`);
676
+ }));
677
+ remote
678
+ .command('verify <tasks...>')
679
+ .description('check that the server already passed tasks for the files checked out here, without running them; ' +
680
+ 'exits 1 at once for any it did not')
681
+ .addOption(new commander_1.Option('--server <name>', 'the server to ask; the default of remote use when left out'))
682
+ .addOption(new commander_1.Option('--repo <host/path>', 'the repository, such as github.com/corp/payments; origin of the checkout here when left out'))
683
+ .addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
684
+ .action(guarded('Remote verify was not successful', async (tasks, options) => {
685
+ const repo = options.repo ?? (await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile))).repo;
686
+ if (!repo) {
687
+ throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
688
+ }
689
+ const keys = [];
690
+ const unkeyed = [];
691
+ for (const task of tasks) {
692
+ const found = (await (0, check_keys_1.getCheckKeys)(buildFile, environment, { taskName: task, environmentName: options.env ?? null }))[task];
693
+ if (!found) {
694
+ throw new Error(`the build file has no task ${(0, server_client_1.printable)(task)}`);
695
+ }
696
+ if (found.key === null) {
697
+ unkeyed.push({ task, reason: found.reason });
698
+ }
699
+ else {
700
+ keys.push({ task, key: found.key });
701
+ }
702
+ }
703
+ const { name: serverName, server, token } = await connect(options.server);
704
+ const answer = keys.length > 0
705
+ ? await (0, server_client_1.callServer)(server, token, 'POST', '/v1/checks/lookup', { repo, keys })
706
+ : { body: { checks: [] } };
707
+ const missing = [];
708
+ // by what was asked, never by what was answered: a task the answer leaves out, or
709
+ // answers for another key, is not verified
710
+ for (const { task, key } of keys) {
711
+ const check = answer.body.checks.find((answered) => answered.task === task && answered.key === key);
712
+ if (check?.passed) {
713
+ environment.stdout.write(`${(0, server_client_1.printable)(task)}: ${(0, server_client_1.printable)(describePassed(check.passed))}\n`);
714
+ }
715
+ else {
716
+ missing.push(task);
717
+ environment.stdout.write(`${(0, server_client_1.printable)(task)}: no run on ${serverName} passed it for these files${check?.reason ? `, ${(0, server_client_1.printable)(check.reason)}` : ''}\n`);
718
+ }
719
+ }
720
+ for (const { task, reason } of unkeyed) {
721
+ environment.stdout.write(`${(0, server_client_1.printable)(task)}: can not be verified, ${(0, server_client_1.printable)(reason)}\n`);
722
+ }
723
+ if (missing.length > 0 || unkeyed.length > 0) {
724
+ const run = missing.length > 0 ? `; run on the server: hammerkit remote run ${missing.map(server_client_1.printable).join(' ')}` : '';
725
+ // whole names: a task of an include is named like build:all
726
+ const names = [...missing, ...unkeyed.map(({ task }) => task)].map(server_client_1.printable);
727
+ fail(`not verified: ${names.join(', ')}${run}`, 1);
728
+ }
212
729
  }));
213
730
  remote
214
731
  .command('versions [server]')