hammerkit 1.23.0 → 1.25.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 (52) hide show
  1. package/build.schema.json +10 -0
  2. package/dist/cli.js +6 -0
  3. package/dist/executer/execute-command.js +7 -1
  4. package/dist/optimizer/get-cache-directory.js +5 -0
  5. package/dist/planner/utils/append-work-task.js +1 -0
  6. package/dist/planner/work-secret.js +6 -2
  7. package/dist/program.js +6 -0
  8. package/dist/remote/bundle.js +59 -0
  9. package/dist/remote/collect-bundle.js +41 -0
  10. package/dist/remote/credentials.js +93 -0
  11. package/dist/remote/device-login.js +177 -0
  12. package/dist/remote/host-config.js +170 -0
  13. package/dist/remote/location.js +46 -0
  14. package/dist/remote/remote-commands.js +158 -0
  15. package/dist/remote/remote-work-commands.js +224 -0
  16. package/dist/remote/server-client.js +88 -0
  17. package/dist/remote/source-manifest.js +79 -0
  18. package/dist/remote/text-diff.js +89 -0
  19. package/dist/schema/build-file-base-task-schema.js +4 -0
  20. package/dist/schema/build-file-schema.js +3 -0
  21. package/dist/schema/reference-parser.js +9 -2
  22. package/dist/schema/runner-schema.js +13 -0
  23. package/dist/secrets/provider-values.js +4 -0
  24. package/dist/server/accounts-api.js +42 -0
  25. package/dist/server/approvals-api.js +104 -0
  26. package/dist/server/definitions-api.js +163 -0
  27. package/dist/server/definitions.js +160 -0
  28. package/dist/server/discovery.js +9 -0
  29. package/dist/server/executor.js +237 -0
  30. package/dist/server/git-checkout.js +115 -0
  31. package/dist/server/http.js +39 -0
  32. package/dist/server/oauth.js +226 -0
  33. package/dist/server/plan.js +296 -0
  34. package/dist/server/policy.js +138 -0
  35. package/dist/server/proposals-api.js +145 -0
  36. package/dist/server/proposals.js +173 -0
  37. package/dist/server/repo-access.js +127 -0
  38. package/dist/server/repo.js +94 -0
  39. package/dist/server/run-environment.js +20 -0
  40. package/dist/server/run-plan.js +44 -0
  41. package/dist/server/run-request.js +85 -0
  42. package/dist/server/run-store.js +61 -0
  43. package/dist/server/run-worker-command.js +17 -0
  44. package/dist/server/run-worker-main.js +7 -0
  45. package/dist/server/run-worker.js +179 -0
  46. package/dist/server/runs.js +432 -0
  47. package/dist/server/server-command.js +61 -0
  48. package/dist/server/server-config.js +394 -0
  49. package/dist/server/server.js +434 -0
  50. package/dist/server/sources-api.js +106 -0
  51. package/dist/server/sources.js +258 -0
  52. package/package.json +1 -1
@@ -0,0 +1,170 @@
1
+ "use strict";
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;
4
+ const path_1 = require("path");
5
+ const zod_1 = require("zod");
6
+ const yaml_1 = require("yaml");
7
+ const get_cache_directory_1 = require("../optimizer/get-cache-directory");
8
+ // Names with a meaning of their own in `run.on` / `--on`
9
+ exports.RESERVED_SERVER_NAMES = ['local', 'auto'];
10
+ const serverNamePattern = /^[a-z0-9][a-z0-9._-]*$/i;
11
+ const checked = (what) => (value, ctx) => {
12
+ try {
13
+ normalizeServerUrl(value, what);
14
+ }
15
+ catch (e) {
16
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: e.message });
17
+ }
18
+ };
19
+ // `clientId`, `audience` and `scope` are what a login asks the identity provider
20
+ // for. They are pinned the first time a login happens (or given to `remote add`),
21
+ // so a server can't change them later, see device-login.ts.
22
+ exports.hostServerSchema = zod_1.z
23
+ .object({
24
+ url: zod_1.z.string().superRefine(checked('url')),
25
+ issuer: zod_1.z.string().superRefine(checked('issuer')),
26
+ clientId: zod_1.z.string().min(1).optional(),
27
+ audience: zod_1.z.string().min(1).optional(),
28
+ scope: zod_1.z.string().min(1).optional(),
29
+ })
30
+ .strict();
31
+ // Unknown top-level keys are kept when the file is written back, so a newer
32
+ // hammerkit's settings survive an older one editing the file.
33
+ exports.hostConfigSchema = zod_1.z
34
+ .object({
35
+ servers: zod_1.z.record(exports.hostServerSchema).default({}),
36
+ run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
37
+ })
38
+ .passthrough();
39
+ // Names like "constructor" must not find inherited members.
40
+ function hasServer(servers, name) {
41
+ return Object.prototype.hasOwnProperty.call(servers, name);
42
+ }
43
+ exports.hasServer = hasServer;
44
+ function getHostConfigPath(environment) {
45
+ return environment.processEnvs.HAMMERKIT_CONFIG ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'config.yaml');
46
+ }
47
+ exports.getHostConfigPath = getHostConfigPath;
48
+ async function readHostConfig(environment) {
49
+ const path = getHostConfigPath(environment);
50
+ if (!(await environment.file.exists(path))) {
51
+ return exports.hostConfigSchema.parse({});
52
+ }
53
+ let raw;
54
+ try {
55
+ raw = (0, yaml_1.parse)(await environment.file.read(path));
56
+ }
57
+ catch (e) {
58
+ throw new Error(`unable to read ${path}: ${e.message}`);
59
+ }
60
+ const result = exports.hostConfigSchema.safeParse(raw ?? {});
61
+ if (!result.success) {
62
+ const issue = result.error.issues[0];
63
+ throw new Error(`invalid ${path}: ${issue.path.join('.') || 'config'}: ${issue.message}`);
64
+ }
65
+ return result.data;
66
+ }
67
+ exports.readHostConfig = readHostConfig;
68
+ async function writeHostConfig(environment, config) {
69
+ const path = getHostConfigPath(environment);
70
+ await environment.file.createDirectory((0, path_1.join)(path, '..'));
71
+ await environment.file.writeFile(path, (0, yaml_1.stringify)(config));
72
+ }
73
+ exports.writeHostConfig = writeHostConfig;
74
+ function validateServerName(name) {
75
+ if (exports.RESERVED_SERVER_NAMES.includes(name.toLowerCase())) {
76
+ throw new Error(`"${name}" is reserved and cannot be used as a server name`);
77
+ }
78
+ if (!serverNamePattern.test(name)) {
79
+ throw new Error(`invalid server name "${name}": use letters, digits, ".", "_" and "-", starting with a letter or digit`);
80
+ }
81
+ return name;
82
+ }
83
+ exports.validateServerName = validateServerName;
84
+ function isLoopback(hostname) {
85
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
86
+ }
87
+ // A server receives the source of a run and the caller's OAuth token, so it must
88
+ // be reached over https; plain http is only for a server on this machine. The
89
+ // result has no trailing slash, so one server has one spelling.
90
+ // An issuer is compared as a string with the `iss` of tokens, so it keeps the
91
+ // spelling it was given (some providers end it with a slash, some don't).
92
+ function normalizeServerUrl(value, what = 'url') {
93
+ let url;
94
+ try {
95
+ url = new URL(value);
96
+ }
97
+ catch {
98
+ throw new Error(`invalid ${what} "${value}"`);
99
+ }
100
+ if (url.username || url.password) {
101
+ throw new Error(`the ${what} must not contain credentials`);
102
+ }
103
+ if (url.search || url.hash) {
104
+ throw new Error(`the ${what} must not contain a query or fragment`);
105
+ }
106
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback(url.hostname))) {
107
+ throw new Error(`the ${what} must use https (http is only allowed for localhost), got "${value}"`);
108
+ }
109
+ return what === 'issuer' ? value : url.toString().replace(/\/+$/, '');
110
+ }
111
+ exports.normalizeServerUrl = normalizeServerUrl;
112
+ const PINNED = ['clientId', 'audience', 'scope'];
113
+ async function addServer(environment, name, url, options) {
114
+ validateServerName(name);
115
+ const server = {
116
+ url: normalizeServerUrl(url),
117
+ issuer: normalizeServerUrl(options.issuer, 'issuer'),
118
+ };
119
+ for (const key of PINNED) {
120
+ if (options[key]) {
121
+ server[key] = options[key];
122
+ }
123
+ }
124
+ const config = await readHostConfig(environment);
125
+ 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]);
127
+ if (existing && changed(existing, server) && !options.force) {
128
+ throw new Error(`server "${name}" is already registered as ${existing.url} (issuer ${existing.issuer}); ` +
129
+ `use --force to point it somewhere else`);
130
+ }
131
+ config.servers[name] = server;
132
+ await writeHostConfig(environment, config);
133
+ return server;
134
+ }
135
+ exports.addServer = addServer;
136
+ async function removeServer(environment, name) {
137
+ const config = await readHostConfig(environment);
138
+ if (!hasServer(config.servers, name)) {
139
+ throw new Error(`server "${name}" is not registered`);
140
+ }
141
+ delete config.servers[name];
142
+ const resetDefault = config.run.on === name;
143
+ if (resetDefault) {
144
+ delete config.run.on;
145
+ }
146
+ await writeHostConfig(environment, config);
147
+ return { resetDefault };
148
+ }
149
+ exports.removeServer = removeServer;
150
+ async function setDefaultLocation(environment, on) {
151
+ const config = await readHostConfig(environment);
152
+ if (!exports.RESERVED_SERVER_NAMES.includes(on) && !hasServer(config.servers, on)) {
153
+ throw new Error(`server "${on}" is not registered; add it with: hammerkit remote add ${on} <url> --issuer <url>`);
154
+ }
155
+ config.run.on = on;
156
+ await writeHostConfig(environment, config);
157
+ }
158
+ exports.setDefaultLocation = setDefaultLocation;
159
+ // Remember what the first login asked the identity provider for; later logins
160
+ // have to ask for the same.
161
+ async function pinLogin(environment, name, pins) {
162
+ const config = await readHostConfig(environment);
163
+ if (!hasServer(config.servers, name)) {
164
+ return;
165
+ }
166
+ Object.assign(config.servers[name], pins);
167
+ await writeHostConfig(environment, config);
168
+ }
169
+ exports.pinLogin = pinLogin;
170
+ //# sourceMappingURL=host-config.js.map
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRunLocation = void 0;
4
+ const host_config_1 = require("./host-config");
5
+ // Where a run happens, highest precedence first: --on / --local, HAMMERKIT_ON,
6
+ // the runner of the tasks, `run.on` of the host config, local. `auto` (as
7
+ // --on, HAMMERKIT_ON or run.on) means the runner of the tasks, else local.
8
+ // A name that is not registered is an error: a run never falls back to the
9
+ // local machine silently.
10
+ async function resolveRunLocation(workTree, environment, options = {}) {
11
+ if (options.on && options.local) {
12
+ throw new Error('--on and --local contradict each other');
13
+ }
14
+ const config = await (0, host_config_1.readHostConfig)(environment);
15
+ const explicit = options.local ? 'local' : options.on ?? (environment.processEnvs.HAMMERKIT_ON || undefined);
16
+ if (explicit && explicit !== 'auto') {
17
+ return explicit === 'local' ? { type: 'local' } : lookup(config.servers, explicit);
18
+ }
19
+ const runners = getTaskRunners(workTree);
20
+ if (runners.length > 1) {
21
+ throw new Error(`a run goes to one server, but its tasks name ${runners.join(', ')}`);
22
+ }
23
+ const requested = runners[0] ?? (explicit ? undefined : config.run.on);
24
+ if (!requested || requested === 'local' || requested === 'auto') {
25
+ return { type: 'local' };
26
+ }
27
+ return lookup(config.servers, requested);
28
+ }
29
+ exports.resolveRunLocation = resolveRunLocation;
30
+ function lookup(servers, name) {
31
+ const server = (0, host_config_1.hasServer)(servers, name) ? servers[name] : undefined;
32
+ if (!server) {
33
+ throw new Error(`server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`);
34
+ }
35
+ return { type: 'server', name, server };
36
+ }
37
+ function getTaskRunners(workTree) {
38
+ const runners = new Set();
39
+ for (const task of Object.values(workTree.tasks)) {
40
+ if (task.data.runner) {
41
+ runners.add(task.data.runner);
42
+ }
43
+ }
44
+ return [...runners].sort();
45
+ }
46
+ //# sourceMappingURL=location.js.map
@@ -0,0 +1,158 @@
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.registerRemoteCommands = void 0;
7
+ const commander_1 = require("commander");
8
+ const colors_1 = __importDefault(require("colors"));
9
+ const log_1 = require("../log");
10
+ const host_config_1 = require("./host-config");
11
+ const device_login_1 = require("./device-login");
12
+ const credentials_1 = require("./credentials");
13
+ const server_client_1 = require("./server-client");
14
+ const remote_work_commands_1 = require("./remote-work-commands");
15
+ // Host-level commands: they only touch the machine's config file, so they work
16
+ // with or without a build file in the working directory.
17
+ function registerRemoteCommands(program, environment, buildFile) {
18
+ const remote = program.command('remote').description('register the servers runs can be sent to');
19
+ const guarded = (failure, fn) => {
20
+ return async (...args) => {
21
+ try {
22
+ await fn(...args);
23
+ }
24
+ catch (e) {
25
+ if (e instanceof commander_1.CommanderError) {
26
+ throw e;
27
+ }
28
+ program.error(`${failure}: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
29
+ }
30
+ };
31
+ };
32
+ remote
33
+ .command('add <name> <url>')
34
+ .description('register a server under a name that build files and --on can refer to')
35
+ .addOption(new commander_1.Option('--issuer <url>', 'OAuth issuer the server accepts tokens from').makeOptionMandatory())
36
+ .addOption(new commander_1.Option('--client-id <id>', 'OAuth client to sign in with (pinned; default: what the server offers)'))
37
+ .addOption(new commander_1.Option('--audience <audience>', 'audience of the token (pinned; default: what the server offers)'))
38
+ .addOption(new commander_1.Option('--scope <scope>', 'scopes to request (pinned; default: what the server offers)'))
39
+ .addOption(new commander_1.Option('--force', 'point an already registered name at a different server').default(false))
40
+ .action(guarded('Remote add was not successful', async (name, url, options) => {
41
+ const server = await (0, host_config_1.addServer)(environment, name, url, {
42
+ issuer: options.issuer,
43
+ clientId: options.clientId,
44
+ audience: options.audience,
45
+ scope: options.scope,
46
+ force: options.force,
47
+ });
48
+ environment.stdout.write(`added ${name}: ${server.url} (issuer ${server.issuer})\n`);
49
+ }));
50
+ remote
51
+ .command('list')
52
+ .description('list the registered servers')
53
+ .action(guarded('Remote list was not successful', async () => {
54
+ const config = await (0, host_config_1.readHostConfig)(environment);
55
+ const names = Object.keys(config.servers).sort();
56
+ if (names.length === 0) {
57
+ environment.stdout.write(`no servers registered (config: ${(0, host_config_1.getHostConfigPath)(environment)})\n`);
58
+ return;
59
+ }
60
+ for (const name of names) {
61
+ const server = config.servers[name];
62
+ const marker = config.run.on === name ? colors_1.default.green(' (default)') : '';
63
+ environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}\n`);
64
+ }
65
+ if (config.run.on === 'auto' || config.run.on === 'local') {
66
+ environment.stdout.write(`default location: ${config.run.on}\n`);
67
+ }
68
+ }));
69
+ remote
70
+ .command('remove <name>')
71
+ .description('forget a registered server')
72
+ .action(guarded('Remote remove was not successful', async (name) => {
73
+ const { resetDefault } = await (0, host_config_1.removeServer)(environment, name);
74
+ environment.stdout.write(`removed ${name}\n`);
75
+ if (resetDefault) {
76
+ environment.stdout.write('it was the default location, runs are local again\n');
77
+ }
78
+ }));
79
+ remote
80
+ .command('use <location>')
81
+ .description('set where runs happen by default: local, auto or a registered server')
82
+ .action(guarded('Remote use was not successful', async (location) => {
83
+ await (0, host_config_1.setDefaultLocation)(environment, location);
84
+ environment.stdout.write(`default location: ${location}\n`);
85
+ }));
86
+ const getServer = async (name) => {
87
+ const config = await (0, host_config_1.readHostConfig)(environment);
88
+ const chosen = name ?? config.run.on;
89
+ if (!chosen || !(0, host_config_1.hasServer)(config.servers, chosen)) {
90
+ throw new Error(name
91
+ ? `server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`
92
+ : 'name a server, or set one as the default with: hammerkit remote use <name>');
93
+ }
94
+ return { name: chosen, server: config.servers[chosen] };
95
+ };
96
+ remote
97
+ .command('login <name>')
98
+ .description('sign in to a registered server with your identity provider (device code, works headless)')
99
+ .action(guarded('Remote login was not successful', async (name) => {
100
+ const { server } = await getServer(name);
101
+ const { pinned, ...result } = await (0, device_login_1.deviceLogin)(server, {
102
+ prompt: (message) => environment.stdout.write(`${message}\n`),
103
+ signal: environment.abortCtrl.signal,
104
+ });
105
+ await (0, host_config_1.pinLogin)(environment, name, pinned);
106
+ await (0, credentials_1.saveToken)(environment, name, { url: server.url, issuer: server.issuer, ...result });
107
+ try {
108
+ const me = await (0, server_client_1.getMe)(server, result.accessToken);
109
+ environment.stdout.write(`signed in to ${name} as ${me.sub}\n`);
110
+ environment.stdout.write(me.accounts.length > 0
111
+ ? `service accounts: ${me.accounts.map((a) => `${(0, server_client_1.printable)(a.name)} (${a.level})`).join(', ')}\n`
112
+ : 'you have no service accounts on this server\n');
113
+ }
114
+ catch (e) {
115
+ // a token the server does not accept is of no use
116
+ await (0, credentials_1.removeToken)(environment, name);
117
+ throw e;
118
+ }
119
+ }));
120
+ remote
121
+ .command('logout <name>')
122
+ .description('forget the sign-in to a server')
123
+ .action(guarded('Remote logout was not successful', async (name) => {
124
+ const removed = await (0, credentials_1.removeToken)(environment, name);
125
+ environment.stdout.write(removed ? `signed out of ${name}\n` : `not signed in to ${name}\n`);
126
+ }));
127
+ remote
128
+ .command('accounts [name]')
129
+ .description('show who you are on a server and which service accounts you may use')
130
+ .action(guarded('Remote accounts was not successful', async (name) => {
131
+ const chosen = await getServer(name);
132
+ const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
133
+ const me = await (0, server_client_1.getMe)(chosen.server, token);
134
+ const { body } = await (0, server_client_1.callServer)(chosen.server, token, 'GET', '/v1/accounts');
135
+ environment.stdout.write(`${chosen.name}: ${(0, server_client_1.printable)(me.sub)} (${(0, server_client_1.printable)(me.iss)})\n`);
136
+ if (body.accounts.length === 0) {
137
+ environment.stdout.write('no service accounts\n');
138
+ }
139
+ for (const account of body.accounts) {
140
+ const level = account.level === 'use' ? 'use' : 'request, somebody who uses it approves';
141
+ environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(account.name))} ${level}${account.description ? ` ${(0, server_client_1.printable)(account.description)}` : ''} from ${account.sources.map(server_client_1.printable).join(', ')}\n`);
142
+ }
143
+ if (me.builds) {
144
+ const may = {
145
+ run: 'run published build files',
146
+ propose: 'propose build files, which somebody who publishes approves',
147
+ publish: 'publish build files',
148
+ }[me.builds];
149
+ environment.stdout.write(`you may ${may}\n`);
150
+ }
151
+ if (me.repos.length > 0) {
152
+ environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
153
+ }
154
+ }));
155
+ (0, remote_work_commands_1.registerWorkCommands)(remote, { environment, buildFile, getServer, guarded });
156
+ }
157
+ exports.registerRemoteCommands = registerRemoteCommands;
158
+ //# sourceMappingURL=remote-commands.js.map
@@ -0,0 +1,224 @@
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.registerWorkCommands = void 0;
7
+ const commander_1 = require("commander");
8
+ const colors_1 = __importDefault(require("colors"));
9
+ const credentials_1 = require("./credentials");
10
+ const collect_bundle_1 = require("./collect-bundle");
11
+ const bundle_1 = require("./bundle");
12
+ const server_client_1 = require("./server-client");
13
+ const text_diff_1 = require("./text-diff");
14
+ const PROPOSAL_NAME = /^([a-z0-9][a-z0-9._-]{0,63})@(sha256:[0-9a-f]{64})$/i;
15
+ // A proposed version is named like `payments@sha256:...`
16
+ function parseProposal(value) {
17
+ const match = PROPOSAL_NAME.exec(value);
18
+ return match && bundle_1.HASH_PATTERN.test(match[2]) ? { name: match[1], hash: match[2] } : undefined;
19
+ }
20
+ const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
21
+ // What a person who approves is shown of a run: everything the decision rests on.
22
+ function describe(run) {
23
+ const source = run.source.type === 'ref' ? `ref ${run.source.ref}` : `snapshot ${run.source.snapshot}`;
24
+ const options = Object.entries(run.options)
25
+ .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
26
+ .join(' ');
27
+ return [
28
+ `${colors_1.default.bold((0, server_client_1.printable)(run.id))} requested by ${(0, server_client_1.printable)(run.requestedBy.sub)} (${(0, server_client_1.printable)(run.requestedBy.iss)})`,
29
+ ` ${(0, server_client_1.printable)(run.definition.name)} ${(0, server_client_1.printable)(run.definition.hash)}`,
30
+ ` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
31
+ ` accounts: ${run.accounts.length > 0 ? run.accounts.map(server_client_1.printable).join(', ') : 'none'}, source ${(0, server_client_1.printable)(source)}${options ? `, ${(0, server_client_1.printable)(options)}` : ''}`,
32
+ ...(run.approvals ?? []).map((approval) => ` ${(0, server_client_1.printable)(approval.account)}: ${approval.approvedBy
33
+ ? `approved by ${(0, server_client_1.printable)(approval.approvedBy.sub)}`
34
+ : 'waits for somebody who uses it to approve'}`),
35
+ ...flagLines(run),
36
+ ` decide before ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`,
37
+ ];
38
+ }
39
+ // What to look at first, one line.
40
+ function flagLines(run) {
41
+ const flagged = Object.entries(run.plan ?? {}).filter(([, entry]) => entry.flags.length > 0);
42
+ return flagged.length > 0
43
+ ? [
44
+ ` look at: ${flagged
45
+ .map(([name, entry]) => `${(0, server_client_1.printable)(name)} (${entry.flags.map(server_client_1.printable).join(', ')})`)
46
+ .join(', ')}`,
47
+ ]
48
+ : [];
49
+ }
50
+ // The tasks and services the run executes and what each does, never a secret's value.
51
+ function describePlan(plan) {
52
+ return Object.entries(plan).flatMap(([name, entry]) => [
53
+ ` ${colors_1.default.bold((0, server_client_1.printable)(name))} ${entry.kind}, ${(0, server_client_1.printable)(entry.runtime)}${entry.image ? ` ${(0, server_client_1.printable)(entry.image)}` : ''}${entry.flags.length > 0 ? ` [${entry.flags.map(server_client_1.printable).join(', ')}]` : ''}`,
54
+ ...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printable)(cmd)}`),
55
+ ...(entry.deps.length > 0 ? [` after: ${entry.deps.map(server_client_1.printable).join(', ')}`] : []),
56
+ ...(entry.needs.length > 0 ? [` needs: ${entry.needs.map(server_client_1.printable).join(', ')}`] : []),
57
+ ...entry.secrets.map((secret) => ` secret ${(0, server_client_1.printable)(secret.from)}${secret.account ? ` as ${(0, server_client_1.printable)(secret.account)}` : ''} -> ${(0, server_client_1.printable)(secret.target)}`),
58
+ ...(entry.mounts.length > 0 ? [` mounts: ${entry.mounts.map(server_client_1.printable).join(', ')}`] : []),
59
+ ...(entry.ports.length > 0 ? [` ports: ${entry.ports.map(server_client_1.printable).join(', ')}`] : []),
60
+ ...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
61
+ ]);
62
+ }
63
+ function registerWorkCommands(remote, context) {
64
+ const { environment, buildFile, getServer, guarded } = context;
65
+ const connect = async (name) => {
66
+ const chosen = await getServer(name);
67
+ const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
68
+ return { ...chosen, token };
69
+ };
70
+ remote
71
+ .command('publish <definition> [server]')
72
+ .description('publish the build file and the files it includes to a server, under a name runs can refer to')
73
+ .action(guarded('Remote publish was not successful', async (definition, name) => {
74
+ const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
75
+ const { name: serverName, server, token } = await connect(name);
76
+ const answer = await (0, server_client_1.callServer)(server, token, 'PUT', `/v1/definitions/${encodeURIComponent(definition)}`, bundle);
77
+ const version = answer.body;
78
+ const files = Object.keys(bundle.files).length;
79
+ const proposed = answer.status === 202;
80
+ const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
81
+ environment.stdout.write(`${verb} ${definition} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
82
+ const names = [...(version.tasks ?? []), ...(version.services ?? [])];
83
+ environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
84
+ if (proposed) {
85
+ environment.stdout.write(`it can not run before somebody who may publish ${(0, server_client_1.printable)(definition)} approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
86
+ }
87
+ }));
88
+ remote
89
+ .command('approvals [server]')
90
+ .description('list the runs on a server that wait for your approval')
91
+ .action(guarded('Remote approvals was not successful', async (name) => {
92
+ const { name: serverName, server, token } = await connect(name);
93
+ const { body } = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/approvals');
94
+ if (body.approvals.length === 0) {
95
+ environment.stdout.write(`${serverName}: no runs wait for your approval\n`);
96
+ return;
97
+ }
98
+ environment.stdout.write(`${serverName}: ${body.approvals.length} waiting for your approval\n`);
99
+ for (const run of body.approvals) {
100
+ for (const line of describe(run)) {
101
+ environment.stdout.write(`${line}\n`);
102
+ }
103
+ }
104
+ }));
105
+ remote
106
+ .command('show <run> [server]')
107
+ .description('show a run that waits for your approval in full: the commands, secrets and accounts of its tasks')
108
+ .action(guarded('Remote show was not successful', async (run, name) => {
109
+ if (!RUN_ID.test(run)) {
110
+ throw new Error(`${(0, server_client_1.printable)(run)} is not a run id, list them with: hammerkit remote approvals`);
111
+ }
112
+ const { server, token } = await connect(name);
113
+ const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${run}`)).body;
114
+ for (const line of describe(view)) {
115
+ environment.stdout.write(`${line}\n`);
116
+ }
117
+ environment.stdout.write(view.plan ? `${describePlan(view.plan).join('\n')}\n` : 'the server did not record what its tasks do\n');
118
+ }));
119
+ remote
120
+ .command('proposals [server]')
121
+ .description('list the proposed versions of build files you made or may approve')
122
+ .action(guarded('Remote proposals was not successful', async (name) => {
123
+ const { name: serverName, server, token } = await connect(name);
124
+ const { body } = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/proposals');
125
+ if (body.proposals.length === 0) {
126
+ environment.stdout.write(`${serverName}: no proposed versions\n`);
127
+ return;
128
+ }
129
+ environment.stdout.write(`${serverName}: ${body.proposals.length} proposed\n`);
130
+ for (const proposal of body.proposals) {
131
+ const who = proposal.mine ? 'you' : (0, server_client_1.printable)(proposal.proposedBy.sub);
132
+ const state = proposal.canDecide ? colors_1.default.bold('waits for your approval') : (0, server_client_1.printable)(proposal.state);
133
+ environment.stdout.write(`${colors_1.default.bold(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)}`)} proposed by ${who}, ${state}\n`);
134
+ const names = [...proposal.tasks, ...proposal.services];
135
+ environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
136
+ if (proposal.accounts.length > 0) {
137
+ environment.stdout.write(` accounts: ${proposal.accounts.map(server_client_1.printable).join(', ')}${proposal.missing.length > 0
138
+ ? `, the proposer does not use ${proposal.missing.map(server_client_1.printable).join(', ')}`
139
+ : ''}\n`);
140
+ }
141
+ for (const line of flagLines(proposal)) {
142
+ environment.stdout.write(`${line}\n`);
143
+ }
144
+ if (proposal.stale) {
145
+ environment.stdout.write(` stale: ${(0, server_client_1.printable)(proposal.name)} has a newer version, reject it and propose again on top of it\n`);
146
+ }
147
+ if (proposal.state === 'waiting') {
148
+ environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
149
+ }
150
+ else if (proposal.reason) {
151
+ environment.stdout.write(` ${(0, server_client_1.printable)(proposal.state)}: ${(0, server_client_1.printable)(proposal.reason)}\n`);
152
+ }
153
+ }
154
+ }));
155
+ remote
156
+ .command('diff <proposal> [server]')
157
+ .description('show what a proposed version (name@hash) changes against the version it follows')
158
+ .action(guarded('Remote diff was not successful', async (target, name) => {
159
+ const proposal = parseProposal(target);
160
+ if (!proposal) {
161
+ throw new Error(`${(0, server_client_1.printable)(target)} is not a proposal, list them with: hammerkit remote proposals`);
162
+ }
163
+ const { name: serverName, server, token } = await connect(name);
164
+ const path = `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}`;
165
+ const view = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
166
+ const proposed = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
167
+ const base = view.base
168
+ ? (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/definitions/${encodeURIComponent(proposal.name)}/${view.base}/files`)).body
169
+ : undefined;
170
+ environment.stdout.write(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)} on ${serverName}, proposed by ${(0, server_client_1.printable)(view.proposedBy.sub)} (${(0, server_client_1.printable)(view.proposedBy.iss)})\n`);
171
+ environment.stdout.write(base ? `compared with ${(0, server_client_1.printable)(view.base)}\n` : 'no version is published yet, everything is new\n');
172
+ if (view.stale) {
173
+ environment.stdout.write(`warning: ${(0, server_client_1.printable)(proposal.name)} has a newer version (${(0, server_client_1.printable)(view.latest)}), this can only be rejected\n`);
174
+ }
175
+ const lines = (0, text_diff_1.diffBundles)(base, proposed);
176
+ environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
177
+ }));
178
+ for (const action of ['yank', 'unyank']) {
179
+ remote
180
+ .command(`${action} <version> [server]`)
181
+ .description(action === 'yank'
182
+ ? 'pull a published version (name@hash) back: no run starts from it and it is not latest'
183
+ : 'let a yanked version (name@hash) go on')
184
+ .addOption(new commander_1.Option('--reason <text>', 'why, shown to whoever tries to run it').argParser((value) => value))
185
+ .action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
186
+ const version = parseProposal(target);
187
+ if (!version) {
188
+ throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it like payments@sha256:...`);
189
+ }
190
+ const { name: serverName, server, token } = await connect(name);
191
+ await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${encodeURIComponent(version.name)}/${version.hash}/${action}`, options.reason ? { reason: options.reason } : undefined);
192
+ environment.stdout.write(`${action === 'yank' ? 'yanked' : 'let go on'} ${(0, server_client_1.printable)(target)} on ${serverName}\n`);
193
+ }));
194
+ }
195
+ for (const action of ['approve', 'reject']) {
196
+ remote
197
+ .command(`${action} <target> [server]`)
198
+ .description(action === 'approve'
199
+ ? 'let a run (run_...) or a proposed version (name@hash) that waits for your approval go ahead'
200
+ : 'end a run (run_...) or drop a proposed version (name@hash) that waits for your approval')
201
+ .addOption(new commander_1.Option('--reason <text>', 'why, shown to who asked').argParser((value) => value))
202
+ .action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
203
+ const proposal = parseProposal(target);
204
+ if (!proposal && !RUN_ID.test(target)) {
205
+ throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a proposed version, list them with: hammerkit remote approvals or remote proposals`);
206
+ }
207
+ const { name: serverName, server, token } = await connect(name);
208
+ const path = proposal
209
+ ? `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}/${action}`
210
+ : `/v1/approvals/${target}/${action}`;
211
+ const { body } = await (0, server_client_1.callServer)(server, token, 'POST', path, options.reason ? { reason: options.reason } : undefined);
212
+ const rejected = body.state === 'rejected';
213
+ environment.stdout.write(proposal
214
+ ? rejected
215
+ ? `rejected ${target} on ${serverName}\n`
216
+ : `approved ${target} on ${serverName}, it is published\n`
217
+ : rejected
218
+ ? `rejected ${target} on ${serverName}\n`
219
+ : `approved ${target} on ${serverName}, it is queued\n`);
220
+ }));
221
+ }
222
+ }
223
+ exports.registerWorkCommands = registerWorkCommands;
224
+ //# sourceMappingURL=remote-work-commands.js.map