hammerkit 1.27.0-next.9 → 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 (122) 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 +49 -1
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
  36. package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
  37. package/dist/kubernetes/ensure-persistent-data.js +211 -6
  38. package/dist/kubernetes/follow-pod-logs.js +182 -0
  39. package/dist/kubernetes/job-placement.js +36 -12
  40. package/dist/kubernetes/kubernetes-instance.js +1 -0
  41. package/dist/kubernetes/network-policy.js +2 -1
  42. package/dist/kubernetes/remove-stale-items.js +46 -0
  43. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  44. package/dist/kubernetes/sandbox-runner.js +628 -0
  45. package/dist/kubernetes/service-rollouts.js +74 -0
  46. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  47. package/dist/kubernetes/volumes.js +94 -9
  48. package/dist/main.js +2 -0
  49. package/dist/optimizer/calculate-checksum.js +3 -2
  50. package/dist/optimizer/check-key.js +94 -0
  51. package/dist/optimizer/check-keys.js +64 -0
  52. package/dist/optimizer/get-work-cache-stats.js +17 -3
  53. package/dist/optimizer/work-task-cache-description.js +49 -1
  54. package/dist/planner/service-definition.js +9 -4
  55. package/dist/planner/utils/append-work-dependencies.js +5 -3
  56. package/dist/planner/utils/append-work-service.js +18 -1
  57. package/dist/planner/utils/append-work-task.js +13 -3
  58. package/dist/planner/utils/get-work-runtime.js +6 -1
  59. package/dist/planner/utils/normalize-path.js +19 -1
  60. package/dist/planner/utils/parse-work-mount.js +7 -1
  61. package/dist/planner/utils/parse-work-port.js +4 -1
  62. package/dist/planner/validate.js +22 -0
  63. package/dist/planner/work-network.js +3 -1
  64. package/dist/planner/work-runtime-aggregate.js +32 -0
  65. package/dist/planner/work-runtime-kubernetes.js +136 -13
  66. package/dist/planner/work-task.js +6 -1
  67. package/dist/program.js +27 -9
  68. package/dist/remote/device-login.js +7 -2
  69. package/dist/remote/host-config.js +69 -3
  70. package/dist/remote/remote-commands.js +33 -7
  71. package/dist/remote/remote-status.js +112 -0
  72. package/dist/remote/remote-work-commands.js +562 -45
  73. package/dist/remote/server-client.js +15 -2
  74. package/dist/remote/token-source.js +64 -26
  75. package/dist/run-program.js +9 -0
  76. package/dist/schema/build-file-base-task-schema.js +3 -3
  77. package/dist/schema/build-file-container-service-schema.js +10 -0
  78. package/dist/schema/build-file-container-task-schema.js +16 -1
  79. package/dist/schema/build-file-include-schema.js +18 -4
  80. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  81. package/dist/schema/build-file-task-schema.js +5 -1
  82. package/dist/schema/capabilities-schema.js +56 -0
  83. package/dist/schema/reference-parser.js +119 -11
  84. package/dist/schema/removed-properties.js +20 -0
  85. package/dist/schema/schema-parser.js +94 -41
  86. package/dist/schema/work-scope-parser.js +33 -10
  87. package/dist/secrets/command-secret-provider.js +3 -1
  88. package/dist/secrets/github-app-token-command.js +3 -1
  89. package/dist/secrets/secret-catalog.js +43 -19
  90. package/dist/server/accounts-api.js +4 -1
  91. package/dist/server/approvals-api.js +1 -1
  92. package/dist/server/capacity-format.js +36 -0
  93. package/dist/server/capacity.js +300 -0
  94. package/dist/server/checks-api.js +83 -0
  95. package/dist/server/checks.js +116 -0
  96. package/dist/server/dashboard.js +196 -0
  97. package/dist/server/definitions-api.js +66 -3
  98. package/dist/server/endpoints.js +794 -0
  99. package/dist/server/executor.js +164 -9
  100. package/dist/server/kubernetes-backend.js +415 -48
  101. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  102. package/dist/server/overview.js +271 -0
  103. package/dist/server/plan.js +170 -121
  104. package/dist/server/policy.js +34 -2
  105. package/dist/server/proposals-api.js +17 -8
  106. package/dist/server/repo-access.js +5 -0
  107. package/dist/server/run-logs.js +4 -0
  108. package/dist/server/run-plan.js +38 -4
  109. package/dist/server/run-request.js +58 -3
  110. package/dist/server/run-worker.js +262 -13
  111. package/dist/server/runs.js +962 -36
  112. package/dist/server/sandbox-pod.js +28 -0
  113. package/dist/server/sandbox-storage.js +23 -0
  114. package/dist/server/sandboxes.js +8 -0
  115. package/dist/server/server-config.js +246 -10
  116. package/dist/server/server.js +429 -32
  117. package/dist/server/service-runs.js +82 -0
  118. package/dist/server/timings.js +132 -0
  119. package/dist/server/wait-reason.js +21 -0
  120. package/dist/utils/tar.js +28 -1
  121. package/dist/utils/terminal-prompt.js +53 -0
  122. package/package.json +8 -2
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deviceLogin = void 0;
3
+ exports.deviceLogin = exports.NoSignInError = void 0;
4
4
  const server_client_1 = require("./server-client");
5
5
  const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
6
6
  const DEFAULT_INTERVAL_SECONDS = 5;
@@ -8,6 +8,11 @@ const SLOW_DOWN_SECONDS = 5;
8
8
  const FETCH_TIMEOUT_MS = 10000;
9
9
  const MIN_INTERVAL_SECONDS = 1;
10
10
  const MAX_LIFETIME_SECONDS = 30 * 60;
11
+ // The server takes tokens of this issuer but offers no device sign-in for it: the token has
12
+ // to come from elsewhere, such as a token command.
13
+ class NoSignInError extends server_client_1.ServerError {
14
+ }
15
+ exports.NoSignInError = NoSignInError;
11
16
  const isLoopbackHost = (hostname) => ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
12
17
  // An endpoint must use the scheme of the issuer: loopback http is for a test
13
18
  // identity provider on this machine only, never beside an https issuer.
@@ -89,7 +94,7 @@ async function deviceLogin(server, options) {
89
94
  throw new server_client_1.ServerError(`${server.url} does not accept tokens from ${server.issuer} (it accepts ${accepted})`);
90
95
  }
91
96
  if (!offered.clientId) {
92
- throw new server_client_1.ServerError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
97
+ throw new NoSignInError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
93
98
  }
94
99
  // What was pinned (by `remote add` or an earlier login) wins; the server can
95
100
  // only offer the same, never something else to sign in with.
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
3
+ exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.getRegisteredServer = exports.getTokenCommand = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
4
4
  const path_1 = require("path");
5
5
  const zod_1 = require("zod");
6
6
  const yaml_1 = require("yaml");
7
7
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
8
+ const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
8
9
  // Names with a meaning of their own in `run.on` / `--on`
9
10
  exports.RESERVED_SERVER_NAMES = ['local', 'auto'];
10
11
  const serverNamePattern = /^[a-z0-9][a-z0-9._-]*$/i;
@@ -28,12 +29,25 @@ exports.hostServerSchema = zod_1.z
28
29
  scope: zod_1.z.string().min(1).optional(),
29
30
  })
30
31
  .strict();
32
+ // A command that prints the token for a server instead of a login, see token-source.ts.
33
+ // It is bound to the url and issuer it was registered for, like a sign-in, and lives
34
+ // beside `servers` rather than in it: hammerkit before 1.27 refuses unknown keys of a
35
+ // server, but keeps unknown top-level keys. It is only ever read from the host config,
36
+ // a build file can't name one.
37
+ const tokenCommandSchema = zod_1.z
38
+ .object({ url: zod_1.z.string(), issuer: zod_1.z.string(), command: zod_1.z.string().trim().min(1) })
39
+ .passthrough();
31
40
  // Unknown top-level keys are kept when the file is written back, so a newer
32
41
  // hammerkit's settings survive an older one editing the file.
33
42
  exports.hostConfigSchema = zod_1.z
34
43
  .object({
35
44
  servers: zod_1.z.record(exports.hostServerSchema).default({}),
45
+ tokenCommands: zod_1.z.record(tokenCommandSchema).optional(),
36
46
  run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
47
+ // how this machine reads secrets, beside what the build files declare (see
48
+ // collectSecretCatalog); a server reads only its own config
49
+ secretProviders: build_file_secret_provider_schema_1.buildFileSecretProvidersSchema.optional(),
50
+ secretAccounts: build_file_secret_provider_schema_1.buildFileSecretAccountsSchema.optional(),
37
51
  })
38
52
  .passthrough();
39
53
  // Names like "constructor" must not find inherited members.
@@ -41,6 +55,35 @@ function hasServer(servers, name) {
41
55
  return Object.prototype.hasOwnProperty.call(servers, name);
42
56
  }
43
57
  exports.hasServer = hasServer;
58
+ // The token command of a server, unless it was registered for another url or issuer (an
59
+ // older hammerkit that pointed the name elsewhere leaves it behind).
60
+ function getTokenCommand(config, name) {
61
+ const commands = config.tokenCommands ?? {};
62
+ const entry = Object.prototype.hasOwnProperty.call(commands, name) ? commands[name] : undefined;
63
+ const server = hasServer(config.servers, name) ? config.servers[name] : undefined;
64
+ return entry && server && entry.url === server.url && entry.issuer === server.issuer ? entry.command : undefined;
65
+ }
66
+ exports.getTokenCommand = getTokenCommand;
67
+ function getRegisteredServer(config, name) {
68
+ const tokenCommand = getTokenCommand(config, name);
69
+ return { ...config.servers[name], ...(tokenCommand ? { tokenCommand } : {}) };
70
+ }
71
+ exports.getRegisteredServer = getRegisteredServer;
72
+ function setTokenCommand(config, name, entry) {
73
+ const commands = { ...(config.tokenCommands ?? {}) };
74
+ if (entry) {
75
+ commands[name] = entry;
76
+ }
77
+ else {
78
+ delete commands[name];
79
+ }
80
+ if (Object.keys(commands).length > 0) {
81
+ config.tokenCommands = commands;
82
+ }
83
+ else {
84
+ delete config.tokenCommands;
85
+ }
86
+ }
44
87
  function getHostConfigPath(environment) {
45
88
  return environment.processEnvs.HAMMERKIT_CONFIG ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'config.yaml');
46
89
  }
@@ -121,16 +164,38 @@ async function addServer(environment, name, url, options) {
121
164
  server[key] = options[key];
122
165
  }
123
166
  }
167
+ if (typeof options.tokenCommand === 'string' && !options.tokenCommand.trim()) {
168
+ throw new Error('the token command must not be empty, use --no-token-command to remove it');
169
+ }
124
170
  const config = await readHostConfig(environment);
125
171
  const existing = hasServer(config.servers, name) ? config.servers[name] : undefined;
126
- const changed = (a, b) => a.url !== b.url || a.issuer !== b.issuer || PINNED.some((key) => a[key] !== b[key]);
172
+ const sameServer = (a, b) => a.url === b.url && a.issuer === b.issuer;
173
+ // Adding the same server again keeps what was pinned (by a login, too) unless it is given,
174
+ // so a later `remote add ... --token-command` needs no --force.
175
+ if (existing && sameServer(existing, server)) {
176
+ for (const key of PINNED) {
177
+ if (!server[key] && existing[key]) {
178
+ server[key] = existing[key];
179
+ }
180
+ }
181
+ }
182
+ const changed = (a, b) => !sameServer(a, b) || PINNED.some((key) => a[key] !== b[key]);
127
183
  if (existing && changed(existing, server) && !options.force) {
128
184
  throw new Error(`server "${name}" is already registered as ${existing.url} (issuer ${existing.issuer}); ` +
129
185
  `use --force to point it somewhere else`);
130
186
  }
187
+ // The command decides which identity's token goes to the server: changing it does not
188
+ // redirect anything, but a name pointed at another server does not keep the old one.
189
+ const kept = existing && sameServer(existing, server) ? getTokenCommand(config, name) : undefined;
190
+ const tokenCommand = typeof options.tokenCommand === 'string'
191
+ ? options.tokenCommand.trim()
192
+ : options.tokenCommand === undefined
193
+ ? kept
194
+ : undefined;
131
195
  config.servers[name] = server;
196
+ setTokenCommand(config, name, tokenCommand ? { url: server.url, issuer: server.issuer, command: tokenCommand } : undefined);
132
197
  await writeHostConfig(environment, config);
133
- return server;
198
+ return { ...server, ...(tokenCommand ? { tokenCommand } : {}) };
134
199
  }
135
200
  exports.addServer = addServer;
136
201
  async function removeServer(environment, name) {
@@ -139,6 +204,7 @@ async function removeServer(environment, name) {
139
204
  throw new Error(`server "${name}" is not registered`);
140
205
  }
141
206
  delete config.servers[name];
207
+ setTokenCommand(config, name, undefined);
142
208
  const resetDefault = config.run.on === name;
143
209
  if (resetDefault) {
144
210
  delete config.run.on;
@@ -40,6 +40,8 @@ passthrough = []) {
40
40
  .addOption(new commander_1.Option('--client-id <id>', 'OAuth client to sign in with (pinned; default: what the server offers)'))
41
41
  .addOption(new commander_1.Option('--audience <audience>', 'audience of the token (pinned; default: what the server offers)'))
42
42
  .addOption(new commander_1.Option('--scope <scope>', 'scopes to request (pinned; default: what the server offers)'))
43
+ .addOption(new commander_1.Option('--token-command <command>', 'a command that prints the token for this server, instead of remote login (e.g. of a service account)'))
44
+ .addOption(new commander_1.Option('--no-token-command', 'remove the token command, sign in with remote login again'))
43
45
  .addOption(new commander_1.Option('--force', 'point an already registered name at a different server').default(false))
44
46
  .action(guarded('Remote add was not successful', async (name, url, options) => {
45
47
  const server = await (0, host_config_1.addServer)(environment, name, url, {
@@ -47,9 +49,13 @@ passthrough = []) {
47
49
  clientId: options.clientId,
48
50
  audience: options.audience,
49
51
  scope: options.scope,
52
+ tokenCommand: options.tokenCommand,
50
53
  force: options.force,
51
54
  });
52
55
  environment.stdout.write(`added ${name}: ${server.url} (issuer ${server.issuer})\n`);
56
+ if (server.tokenCommand) {
57
+ environment.stdout.write(`tokens for ${name} come from: ${server.tokenCommand}\n`);
58
+ }
53
59
  }));
54
60
  remote
55
61
  .command('list')
@@ -62,9 +68,10 @@ passthrough = []) {
62
68
  return;
63
69
  }
64
70
  for (const name of names) {
65
- const server = config.servers[name];
71
+ const server = (0, host_config_1.getRegisteredServer)(config, name);
66
72
  const marker = config.run.on === name ? colors_1.default.green(' (default)') : '';
67
- environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}\n`);
73
+ const token = server.tokenCommand ? ` token from: ${server.tokenCommand}` : '';
74
+ environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}${token}\n`);
68
75
  }
69
76
  if (config.run.on === 'auto' || config.run.on === 'local') {
70
77
  environment.stdout.write(`default location: ${config.run.on}\n`);
@@ -95,17 +102,33 @@ passthrough = []) {
95
102
  ? `server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`
96
103
  : 'name a server, or set one as the default with: hammerkit remote use <name>');
97
104
  }
98
- return { name: chosen, server: config.servers[chosen] };
105
+ return { name: chosen, server: (0, host_config_1.getRegisteredServer)(config, chosen) };
99
106
  };
100
107
  remote
101
108
  .command('login <name>')
102
109
  .description('sign in to a registered server with your identity provider (device code, works headless)')
103
110
  .action(guarded('Remote login was not successful', async (name) => {
104
111
  const { server } = await getServer(name);
105
- const { pinned, ...result } = await (0, device_login_1.deviceLogin)(server, {
106
- prompt: (message) => environment.stdout.write(`${message}\n`),
107
- signal: environment.abortCtrl.signal,
108
- });
112
+ if (server.tokenCommand) {
113
+ // a sign-in would never be used: the command's token goes first
114
+ throw new Error(`${name} takes its tokens from its token command (${server.tokenCommand}), it needs no sign-in; ` +
115
+ `to sign in instead, remove it with: hammerkit remote add ${name} ${server.url} --issuer ${server.issuer} --no-token-command`);
116
+ }
117
+ let login;
118
+ try {
119
+ login = await (0, device_login_1.deviceLogin)(server, {
120
+ prompt: (message) => environment.stdout.write(`${message}\n`),
121
+ signal: environment.abortCtrl.signal,
122
+ });
123
+ }
124
+ catch (e) {
125
+ if (e instanceof device_login_1.NoSignInError) {
126
+ throw new Error(`${e.message}: register a command that prints one, such as the token of a service account, with: ` +
127
+ `hammerkit remote add ${name} ${server.url} --issuer ${server.issuer} --token-command '<command>'`);
128
+ }
129
+ throw e;
130
+ }
131
+ const { pinned, ...result } = login;
109
132
  await (0, host_config_1.pinLogin)(environment, name, pinned);
110
133
  await (0, credentials_1.saveToken)(environment, name, { url: server.url, issuer: server.issuer, ...result });
111
134
  try {
@@ -155,6 +178,9 @@ passthrough = []) {
155
178
  if (me.repos.length > 0) {
156
179
  environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
157
180
  }
181
+ if (me.observe) {
182
+ environment.stdout.write('you observe every run of the server, see them with: hammerkit remote runs --all\n');
183
+ }
158
184
  }));
159
185
  (0, remote_work_commands_1.registerWorkCommands)(remote, {
160
186
  environment,
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.describeTaskStats = exports.describeCapacity = exports.describeOverview = exports.describeServiceRun = exports.describeWork = exports.describeWait = void 0;
7
+ const colors_1 = __importDefault(require("colors"));
8
+ const capacity_format_1 = require("../server/capacity-format");
9
+ const timings_1 = require("../server/timings");
10
+ const wait_reason_1 = require("../server/wait-reason");
11
+ Object.defineProperty(exports, "describeWait", { enumerable: true, get: function () { return wait_reason_1.describeWait; } });
12
+ const server_client_1 = require("./server-client");
13
+ // What a run does, in a few words: its tasks, or the services it keeps up.
14
+ function describeWork(run) {
15
+ if (run.up) {
16
+ return `${(0, server_client_1.printable)(run.up.name)}: up ${run.up.services.map(server_client_1.printable).join(', ')}`;
17
+ }
18
+ return run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'by labels';
19
+ }
20
+ exports.describeWork = describeWork;
21
+ function ownerOf(run) {
22
+ return run.owner ? ` of ${(0, server_client_1.printable)(run.owner.sub)}` : '';
23
+ }
24
+ function describeMeter(name, meter) {
25
+ const full = meter.limit !== undefined && meter.used >= meter.limit;
26
+ const count = `${meter.used}${meter.limit !== undefined ? ` of ${meter.limit}` : ''}`;
27
+ return ` ${name.padEnd(9)} ${count.padEnd(9)} ${meter.runs.length > 0 ? meter.runs.map(server_client_1.printable).join(', ') : '-'}${full ? colors_1.default.yellow(' full') : ''}`;
28
+ }
29
+ // A service run on one line, then a line per endpoint: when it ends, when it sleeps.
30
+ function describeServiceRun(run) {
31
+ const times = [
32
+ run.state === 'up' || run.state === 'asleep' ? (run.expiresAt ? `until ${run.expiresAt}` : '') : '',
33
+ run.state === 'up' && run.sleepsAt ? `sleeps at ${run.sleepsAt}` : '',
34
+ run.wakes ? `woken ${run.wakes}×` : '',
35
+ ].filter(Boolean);
36
+ return [
37
+ ` ${(0, server_client_1.printable)(run.id)} ${(0, server_client_1.printable)(run.state)} ${describeWork(run)}${ownerOf(run)}${times.length > 0 ? ` ${(0, server_client_1.printable)(times.join(', '))}` : ''}`,
38
+ ...(run.up?.endpoints ?? []).map((endpoint) => ` ${(0, server_client_1.printable)(endpoint.service)}:${endpoint.port}${endpoint.url ? ` ${(0, server_client_1.printable)(endpoint.url)}` : ''}`),
39
+ ];
40
+ }
41
+ exports.describeServiceRun = describeServiceRun;
42
+ // What `hammerkit remote status` prints: the places, the queue, the caller's limits.
43
+ function describeOverview(server, overview, now = Date.now()) {
44
+ const lines = [
45
+ `${(0, server_client_1.printable)(server)}: signed in as ${(0, server_client_1.printable)(overview.you.sub)}${overview.you.observe ? ', you observe every run' : ''}`,
46
+ `places: ${overview.places.used} of ${overview.places.max} taken (runs.maxConcurrent)`,
47
+ ...overview.places.runs.map((run) => ` ${(0, server_client_1.printable)(run.id)} ${describeWork(run)}${ownerOf(run)}${run.startedAt ? ` since ${(0, server_client_1.printable)(run.startedAt)}, ${(0, timings_1.roughly)((0, timings_1.ranMs)(run, now) ?? 0)}` : ''}`),
48
+ ];
49
+ if (overview.places.others > 0) {
50
+ lines.push(` ${overview.places.others} ${overview.places.others === 1 ? 'run' : 'runs'} of others`);
51
+ }
52
+ // a place a run holds that is not running: a service run waking up, or a place not given back
53
+ const unaccounted = overview.places.used - overview.places.runs.length - overview.places.others;
54
+ if (unaccounted > 0) {
55
+ lines.push(` ${unaccounted} taken by no running run`);
56
+ }
57
+ lines.push(`queue: ${overview.queue.length === 0 ? 'empty' : overview.queue.length}`);
58
+ for (const entry of overview.queue) {
59
+ const waited = entry.run ? (0, timings_1.waitedMs)(entry.run, now) : undefined;
60
+ const what = entry.run
61
+ ? `${(0, server_client_1.printable)(entry.run.id)} ${describeWork(entry.run)}${ownerOf(entry.run)}${waited !== undefined ? ` waiting ${(0, timings_1.roughly)(waited)}` : ''}`
62
+ : 'a run of somebody else';
63
+ lines.push(` ${entry.position}. ${what} waits: ${(0, server_client_1.printable)((0, wait_reason_1.describeWait)(entry.waitsFor, entry.mine))}`);
64
+ }
65
+ lines.push(`waiting for an approval: ${overview.approvals.waiting} of at most ${overview.approvals.max}`);
66
+ for (const run of overview.approvals.runs) {
67
+ const accounts = (run.approvals ?? [])
68
+ .filter((approval) => !approval.approvedBy)
69
+ .map((approval) => approval.account);
70
+ lines.push(` ${(0, server_client_1.printable)(run.id)} ${describeWork(run)}${ownerOf(run)} needs ${accounts
71
+ .map(server_client_1.printable)
72
+ .join(', ')} until ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`);
73
+ }
74
+ lines.push(`service runs: ${overview.services.open}${overview.services.max !== undefined ? ` of ${overview.services.max}` : ''} places taken on the server`);
75
+ for (const run of overview.services.runs) {
76
+ lines.push(...describeServiceRun(run));
77
+ }
78
+ lines.push('your limits:', describeMeter('running', overview.limits.running), describeMeter('queued', overview.limits.queued), describeMeter('approval', overview.limits.waiting), describeMeter('services', overview.limits.services), ...(overview.limits.asleep ? [describeMeter('asleep', overview.limits.asleep)] : []));
79
+ return lines;
80
+ }
81
+ exports.describeOverview = describeOverview;
82
+ // The machines of the server and what runs take of them, for `remote capacity`; `short` for
83
+ // the two lines of `remote status`.
84
+ function describeCapacity(capacity, short = false) {
85
+ const { machines, runs } = capacity;
86
+ const resources = (value) => value ? `${(0, capacity_format_1.formatCpu)(value.cpu)} CPU, ${(0, capacity_format_1.formatMemory)(value.memory)}` : '?';
87
+ const kind = capacity.backend === 'kubernetes' ? (machines?.count === 1 ? 'node' : 'nodes') : 'machine';
88
+ const lines = [
89
+ `capacity: ${machines
90
+ ? `${machines.count} ${kind} with ${resources(machines.allocatable)}`
91
+ : 'what the machines have is not known'}${machines?.used ? `, ${resources(machines.used)} in use` : ''}${machines?.load ? `, load ${machines.load.map((load) => load.toFixed(2)).join(' ')}` : ''}`,
92
+ ` runs: ${runs.pods !== undefined ? `${(0, capacity_format_1.plural)(runs.pods, 'pod')}, ` : ''}${runs.containers !== undefined ? `${runs.containers} containers, ` : ''}${capacity.backend === 'kubernetes' ? `requested ${resources(runs.requested)}` : ''}${runs.used ? `${capacity.backend === 'kubernetes' ? ', ' : ''}in use ${resources(runs.used)}` : ''}`,
93
+ ];
94
+ if (short) {
95
+ return lines;
96
+ }
97
+ for (const group of capacity.groups) {
98
+ lines.push(` ${(0, server_client_1.printable)(group.name)} ${group.kind === 'service-run' ? 'service run' : 'caller'}${group.owner ? ` of ${(0, server_client_1.printable)(group.owner.sub)}` : ''}${group.run ? ` ${(0, server_client_1.printable)(group.run)}` : ''} ${(0, capacity_format_1.plural)(group.pods ?? 0, 'pod')} requested ${resources(group.requested)}${group.quota ? ` of ${resources(group.quota)}` : ''}${group.used ? ` in use ${resources(group.used)}` : ''}`);
99
+ }
100
+ for (const problem of capacity.problems) {
101
+ lines.push(colors_1.default.yellow(` not shown: ${(0, server_client_1.printable)(problem)}`));
102
+ }
103
+ return lines;
104
+ }
105
+ exports.describeCapacity = describeCapacity;
106
+ // What the past runs of each task took, for `remote tasks`.
107
+ function describeTaskStats(stats) {
108
+ const span = (ms) => (ms === undefined ? '-' : (0, timings_1.roughly)(ms));
109
+ return stats.map((entry) => `${(0, server_client_1.printable)(entry.task)} ${(0, server_client_1.printable)(entry.repo)} last ${(0, server_client_1.printable)(entry.last.status)} ${(0, server_client_1.printable)(entry.last.at)}${entry.last.durationMs !== undefined ? ` took ${(0, timings_1.roughly)(entry.last.durationMs)}` : ''} ${(0, capacity_format_1.plural)(entry.runs, 'run')}${entry.failed ? `, ${entry.failed} failed` : ''}${entry.cached ? `, ${entry.cached} cached` : ''} median ${span(entry.medianMs)}, 90% ${span(entry.p90Ms)}, waited ${span(entry.medianWaitMs)}`);
110
+ }
111
+ exports.describeTaskStats = describeTaskStats;
112
+ //# sourceMappingURL=remote-status.js.map