hammerkit 1.25.0 → 1.27.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 (123) hide show
  1. package/build.schema.json +53 -1
  2. package/dist/cache/backends/local-cache-backend.js +10 -2
  3. package/dist/cache/explain.js +13 -11
  4. package/dist/cli.js +3 -0
  5. package/dist/docker/item-network.js +471 -0
  6. package/dist/docker/pull.js +4 -2
  7. package/dist/docker/remove-orphaned-containers.js +13 -6
  8. package/dist/environment/replace-env-variables.js +8 -6
  9. package/dist/executer/await-completed-dependencies.js +2 -1
  10. package/dist/executer/check-readiness.js +2 -2
  11. package/dist/executer/container-task-state.js +8 -6
  12. package/dist/executer/docker-service.js +24 -13
  13. package/dist/executer/docker-task.js +18 -29
  14. package/dist/executer/event-cache.js +4 -0
  15. package/dist/executer/execute-command.js +18 -5
  16. package/dist/executer/execute-docker.js +4 -1
  17. package/dist/executer/execution-steps.js +33 -7
  18. package/dist/executer/get-docker-executor.js +14 -7
  19. package/dist/executer/scheduler/enqueue-next.js +17 -3
  20. package/dist/executer/watch-loop.js +2 -2
  21. package/dist/executer/watch-state-key.js +1 -1
  22. package/dist/file/within-root.js +42 -0
  23. package/dist/harness/connect.js +99 -0
  24. package/dist/harness/format.js +73 -0
  25. package/dist/harness/mcp-command.js +59 -0
  26. package/dist/harness/mcp.js +432 -0
  27. package/dist/harness/opencode.js +492 -0
  28. package/dist/harness/pi.js +217 -0
  29. package/dist/harness/sandbox-client.js +373 -0
  30. package/dist/index.js +33 -36
  31. package/dist/kubernetes/apply.js +31 -1
  32. package/dist/kubernetes/await-running-state.js +33 -1
  33. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +44 -22
  34. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
  35. package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
  36. package/dist/kubernetes/ensure-persistent-data.js +48 -7
  37. package/dist/kubernetes/get-service-ip.js +15 -1
  38. package/dist/kubernetes/job-placement.js +99 -0
  39. package/dist/kubernetes/job-secrets.js +306 -0
  40. package/dist/kubernetes/kept-claims.js +191 -0
  41. package/dist/kubernetes/kubernetes-instance.js +10 -2
  42. package/dist/kubernetes/network-gate.js +71 -0
  43. package/dist/kubernetes/network-policy.js +526 -0
  44. package/dist/kubernetes/resources.js +13 -2
  45. package/dist/kubernetes/restore-kubernetes-data.js +19 -4
  46. package/dist/kubernetes/secrets.js +2 -0
  47. package/dist/kubernetes/store-kubernetes-data.js +1 -1
  48. package/dist/kubernetes/volumes.js +100 -11
  49. package/dist/main.js +41 -0
  50. package/dist/network/egress-proxy.js +368 -0
  51. package/dist/network/port-relay.js +35 -0
  52. package/dist/optimizer/get-work-cache-stats.js +16 -0
  53. package/dist/optimizer/work-task-cache-description.js +3 -2
  54. package/dist/planner/service-definition.js +8 -2
  55. package/dist/planner/utils/append-work-environment.js +2 -0
  56. package/dist/planner/utils/append-work-service.js +4 -0
  57. package/dist/planner/utils/append-work-task.js +17 -1
  58. package/dist/planner/utils/assert-item-paths.js +50 -0
  59. package/dist/planner/utils/hosted-volumes.js +35 -0
  60. package/dist/planner/utils/task-arguments.js +155 -0
  61. package/dist/planner/work-cache-id.js +15 -1
  62. package/dist/planner/work-network.js +193 -0
  63. package/dist/planner/work-runtime-docker.js +43 -0
  64. package/dist/planner/work-runtime-kubernetes.js +336 -102
  65. package/dist/planner/work-runtime-local.js +16 -8
  66. package/dist/planner/work-service.js +7 -3
  67. package/dist/planner/work-state-record.js +30 -0
  68. package/dist/program.js +80 -7
  69. package/dist/remote/annotations.js +103 -0
  70. package/dist/remote/bundle-text.js +24 -0
  71. package/dist/remote/bundle.js +9 -1
  72. package/dist/remote/remote-commands.js +4 -1
  73. package/dist/remote/remote-work-commands.js +182 -69
  74. package/dist/remote/server-client.js +18 -5
  75. package/dist/remote/source-manifest.js +15 -2
  76. package/dist/remote/token-source.js +67 -0
  77. package/dist/schema/build-file-base-task-schema.js +3 -0
  78. package/dist/schema/build-file-container-service-schema.js +6 -0
  79. package/dist/schema/build-file-container-task-schema.js +3 -0
  80. package/dist/schema/build-file-environment-schema.js +9 -0
  81. package/dist/schema/network-schema.js +78 -0
  82. package/dist/schema/reference-parser.js +1 -0
  83. package/dist/schema/schema-parser.js +16 -3
  84. package/dist/schema/work-scope-parser.js +13 -0
  85. package/dist/secrets/command-secret-provider.js +73 -10
  86. package/dist/secrets/github-app-token-command.js +23 -0
  87. package/dist/secrets/github-app-token.js +191 -0
  88. package/dist/server/accounts-api.js +9 -2
  89. package/dist/server/definitions-api.js +69 -47
  90. package/dist/server/definitions.js +76 -51
  91. package/dist/server/executor.js +156 -41
  92. package/dist/server/git-checkout.js +72 -8
  93. package/dist/server/kubernetes-backend.js +554 -0
  94. package/dist/server/kubernetes-sandbox-storage.js +996 -0
  95. package/dist/server/oauth.js +1 -0
  96. package/dist/server/plan.js +51 -1
  97. package/dist/server/policy.js +104 -15
  98. package/dist/server/proposals-api.js +24 -40
  99. package/dist/server/proposals.js +12 -25
  100. package/dist/server/repo-access.js +37 -6
  101. package/dist/server/run-logs.js +217 -0
  102. package/dist/server/run-plan.js +51 -2
  103. package/dist/server/run-request.js +23 -10
  104. package/dist/server/run-store.js +5 -0
  105. package/dist/server/run-worker-command.js +13 -1
  106. package/dist/server/run-worker.js +121 -24
  107. package/dist/server/runs.js +184 -10
  108. package/dist/server/sandbox-docker.js +62 -0
  109. package/dist/server/sandbox-files-api.js +269 -0
  110. package/dist/server/sandbox-files.js +1054 -0
  111. package/dist/server/sandbox-patch.js +481 -0
  112. package/dist/server/sandbox-pod-command.js +15 -0
  113. package/dist/server/sandbox-pod.js +146 -0
  114. package/dist/server/sandbox-storage.js +462 -0
  115. package/dist/server/sandboxes-api.js +210 -0
  116. package/dist/server/sandboxes.js +519 -0
  117. package/dist/server/server-config.js +412 -22
  118. package/dist/server/server.js +204 -39
  119. package/dist/utils/run-id.js +12 -0
  120. package/harness/opencode.ts +11 -0
  121. package/harness/pi.ts +7 -0
  122. package/package.json +8 -2
  123. package/dist/remote/text-diff.js +0 -89
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getHealthcheckCommand = exports.getServiceCommand = exports.getServiceWorkingDir = exports.isContainerWorkService = void 0;
4
+ const task_arguments_1 = require("./utils/task-arguments");
4
5
  const isContainerWorkService = (svc) => 'image' in svc && svc.type === 'container-service';
5
6
  exports.isContainerWorkService = isContainerWorkService;
6
7
  // The container's working directory: the declared `workdir`, else the build
@@ -10,14 +11,17 @@ exports.getServiceWorkingDir = getServiceWorkingDir;
10
11
  // The container's entrypoint and command for a service: with a `shell`, its
11
12
  // `cmd` runs as `<shell> -c "<cmd>"` in place of the image entrypoint; without
12
13
  // one the tokenized `cmd` becomes the command and the image entrypoint stays.
13
- function getServiceCommand(service) {
14
+ // For Kubernetes, the arguments after -- are escaped so they stay text there.
15
+ function getServiceCommand(service, options = {}) {
14
16
  if (!service.cmd) {
15
17
  return { entrypoint: null, cmd: null };
16
18
  }
17
19
  if (service.shell) {
18
- return { entrypoint: [service.shell, '-c'], cmd: [service.cmd.cmd] };
20
+ const cmd = options.kubernetes ? (0, task_arguments_1.getKubernetesCommandText)(service.cmd) : service.cmd.cmd;
21
+ return { entrypoint: [service.shell, '-c'], cmd: [cmd] };
19
22
  }
20
- return { entrypoint: null, cmd: [service.cmd.parsed.command, ...service.cmd.parsed.args] };
23
+ const args = options.kubernetes ? (0, task_arguments_1.getKubernetesCommandArgs)(service.cmd) : service.cmd.parsed.args;
24
+ return { entrypoint: null, cmd: [service.cmd.parsed.command, ...args] };
21
25
  }
22
26
  exports.getServiceCommand = getServiceCommand;
23
27
  // The healthcheck as an exec command, through the service's `shell` if it has one.
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseStateRecord = exports.formatStateRecord = exports.getStateRecordName = void 0;
4
+ const crypto_1 = require("crypto");
5
+ // The state record says "the outputs at this task's paths were made by this
6
+ // variant of the task (its id), from this state key". It is kept per task
7
+ // *name*, because every variant of a task (other arguments after --, another
8
+ // --set value) writes the same output paths: a record per id would let a run of
9
+ // one variant overwrite the outputs while the record of another one still
10
+ // claims them. The id stored with the key makes the next run of that other
11
+ // variant a miss.
12
+ function getStateRecordName(task) {
13
+ return (0, crypto_1.createHash)('sha1').update(`${task.name}\0${task.data.projectRoot}`).digest('hex');
14
+ }
15
+ exports.getStateRecordName = getStateRecordName;
16
+ function formatStateRecord(task, stateKey) {
17
+ return `${task.id()}:${stateKey}`;
18
+ }
19
+ exports.formatStateRecord = formatStateRecord;
20
+ // The state key of the record when this variant made the outputs, else null.
21
+ function parseStateRecord(task, content) {
22
+ const record = content.trim();
23
+ const separator = record.indexOf(':');
24
+ if (separator < 0 || record.slice(0, separator) !== task.id()) {
25
+ return null;
26
+ }
27
+ return record.slice(separator + 1) || null;
28
+ }
29
+ exports.parseStateRecord = parseStateRecord;
30
+ //# sourceMappingURL=work-state-record.js.map
package/dist/program.js CHANGED
@@ -28,6 +28,9 @@ const cache_inventory_1 = require("./cache/cache-inventory");
28
28
  const remote_commands_1 = require("./remote/remote-commands");
29
29
  const server_command_1 = require("./server/server-command");
30
30
  const run_worker_command_1 = require("./server/run-worker-command");
31
+ const github_app_token_command_1 = require("./secrets/github-app-token-command");
32
+ const work_network_1 = require("./planner/work-network");
33
+ const task_arguments_1 = require("./planner/utils/task-arguments");
31
34
  async function createCli(fileName, environment, workScope) {
32
35
  const { ctx, scope } = await (0, schema_parser_1.createParseContext)(fileName, environment);
33
36
  const referencedScope = await (0, reference_parser_1.parseReferences)(ctx, scope, environment);
@@ -80,6 +83,31 @@ function printTaskDefinition(environment, definition) {
80
83
  (0, log_1.printProperty)(environment, `env ${reference.name}`, `from $${reference.from} ${colors_1.default.grey(reference.available ? '(set)' : '(not set)')}`);
81
84
  }
82
85
  }
86
+ // `ls --json`: what a caller (an agent picking a task) needs to know, with the
87
+ // command that arguments after `--` are appended to (null: it takes none).
88
+ function getListJson(tasks, services) {
89
+ return {
90
+ tasks: tasks.map(({ item }) => ({
91
+ name: item.name,
92
+ description: item.data.description,
93
+ type: item.data.type,
94
+ ...(item.data.type === 'container-task' ? { image: item.data.image } : {}),
95
+ deps: item.deps.map((d) => d.name),
96
+ needs: item.needs.map((n) => n.name),
97
+ labels: item.data.labels,
98
+ command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
99
+ })),
100
+ services: services.map(({ item }) => ({
101
+ name: item.name,
102
+ description: item.data.description,
103
+ type: item.data.type,
104
+ ...(item.data.type === 'container-service' ? { image: item.data.image } : {}),
105
+ ports: item.data.ports.map((p) => ({ host: p.hostPort, container: p.containerPort })),
106
+ labels: item.data.labels,
107
+ command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
108
+ })),
109
+ };
110
+ }
83
111
  function parseWorkLabelScope(options) {
84
112
  const scope = {
85
113
  environmentName: null,
@@ -103,30 +131,57 @@ function parseWorkLabelScope(options) {
103
131
  }
104
132
  return scope;
105
133
  }
134
+ // A task given arguments after -- skips the cache, unless the caller asks for a
135
+ // cache method on the command line.
136
+ function keepsCache(command) {
137
+ return command.getOptionValueSource('cache') === 'cli';
138
+ }
106
139
  async function getProgram(environment, argv) {
107
140
  const program = new commander_1.Command();
108
141
  (0, server_command_1.registerServerCommand)(program, environment);
109
142
  (0, run_worker_command_1.registerRunWorkerCommand)(program, environment);
110
- const parsed = (0, parse_set_arguments_1.parseSetArguments)(argv);
143
+ (0, run_worker_command_1.registerSandboxPodCommand)(program, environment);
144
+ (0, github_app_token_command_1.registerGithubAppTokenCommand)(program, environment);
145
+ // What follows `--` belongs to the one task or service of a run or up, not to
146
+ // hammerkit: a `--set` or `--file` there is an argument like any other.
147
+ const separator = argv.indexOf('--');
148
+ const passthrough = separator >= 0 ? argv.slice(separator + 1) : [];
149
+ const parsed = (0, parse_set_arguments_1.parseSetArguments)(separator >= 0 ? argv.slice(0, separator) : argv);
111
150
  const args = parsed.args;
112
151
  environment.processEnvs = { ...environment.processEnvs, ...parsed.values };
152
+ const remote = args.indexOf('remote');
153
+ if (remote >= 0 && args[remote + 1] === 'mcp') {
154
+ // `remote mcp` speaks its protocol on stdout: a warning there would break it
155
+ environment.console = (0, log_1.consoleContext)(environment.stderr);
156
+ }
113
157
  const fileIndex = args.indexOf('--file');
114
158
  const fileName = fileIndex >= 0 ? (0, path_1.join)(environment.cwd, args[fileIndex + 1]) : await (0, default_build_file_1.getBuildFilename)(environment.cwd, environment);
115
159
  if (fileIndex >= 0) {
116
160
  args.splice(fileIndex, 2);
117
161
  }
118
162
  (0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
163
+ program.hook('preAction', (_, actionCommand) => {
164
+ const takesArguments = actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name());
165
+ if (passthrough.length > 0 && !takesArguments) {
166
+ program.error(`${actionCommand.name()} takes no arguments after --, only run and up do`, { exitCode: 1 });
167
+ }
168
+ });
119
169
  if (await environment.file.exists(fileName)) {
120
170
  program
121
171
  .command('ls')
122
172
  .description('list all tasks')
123
173
  .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
124
174
  .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
175
+ .addOption(new commander_1.Option('--json', 'emit the tasks and services as JSON').default(false))
125
176
  .action(async (options) => {
126
177
  const cli = await createCli(fileName, environment, parseWorkLabelScope(options));
127
178
  const items = cli.ls();
128
179
  const tasks = items.filter(cli_1.isCliTask);
129
180
  const services = items.filter(cli_1.isCliService);
181
+ if (options.json) {
182
+ environment.stdout.write(`${JSON.stringify(getListJson(tasks, services), null, 2)}\n`);
183
+ return;
184
+ }
130
185
  if (services.length > 0) {
131
186
  (0, log_1.printTitle)(environment, 'Services');
132
187
  for (const service of services) {
@@ -527,7 +582,8 @@ async function getProgram(environment, argv) {
527
582
  });
528
583
  program
529
584
  .command('up')
530
- .description('start services(s)')
585
+ .description("start services(s), with the arguments after -- appended to the selected service's cmd")
586
+ .usage('[options] [-- args...]')
531
587
  .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
532
588
  .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
533
589
  .addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
@@ -546,9 +602,13 @@ async function getProgram(environment, argv) {
546
602
  .choices(['checksum', 'modify-date', 'none']))
547
603
  .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
548
604
  .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
549
- .action(async (options) => {
605
+ .addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
606
+ .action(async (options, command) => {
550
607
  try {
551
- const scope = parseWorkLabelScope(options);
608
+ const scope = {
609
+ ...parseWorkLabelScope(options),
610
+ args: { values: passthrough, services: true, cache: keepsCache(command) },
611
+ };
552
612
  const cli = await createCli(fileName, environment, scope);
553
613
  const result = await cli.runUp({
554
614
  cacheDefault: options.cache,
@@ -560,6 +620,7 @@ async function getProgram(environment, argv) {
560
620
  wait: options.wait,
561
621
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
562
622
  timeout: options.timeout ?? null,
623
+ network: options.network ?? null,
563
624
  });
564
625
  if (!result.success) {
565
626
  program.error('Up was not successful', { exitCode: 1 });
@@ -601,7 +662,8 @@ async function getProgram(environment, argv) {
601
662
  });
602
663
  program
603
664
  .command('run [task]', { isDefault: true })
604
- .description('execute task')
665
+ .description('execute task, with the arguments after -- appended to its last command')
666
+ .usage('[options] [task] [-- args...]')
605
667
  .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
606
668
  .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
607
669
  .addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
@@ -620,15 +682,17 @@ async function getProgram(environment, argv) {
620
682
  .addOption(new commander_1.Option('--dry-run', 'print the execution plan with predicted cache hits/misses without running').default(false))
621
683
  .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
622
684
  .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
685
+ .addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
623
686
  .addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
624
- .action(async (task, options) => {
687
+ .action(async (task, options, command) => {
625
688
  try {
626
689
  // For machine-readable JSON, suppress the human progress logger (which
627
690
  // also writes to stdout) so the only thing on stdout is the JSON.
628
691
  const runEnvironment = options.summaryJson
629
692
  ? { ...environment, stdout: (0, empty_writable_1.emptyWritable)(), console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()) }
630
693
  : environment;
631
- const cli = await createCli(fileName, runEnvironment, task ? { taskName: task, environmentName: options.env } : parseWorkLabelScope(options));
694
+ const args = { values: passthrough, services: false, cache: keepsCache(command) };
695
+ const cli = await createCli(fileName, runEnvironment, task ? { taskName: task, environmentName: options.env, args } : { ...parseWorkLabelScope(options), args });
632
696
  if (cli.tasks().length === 0) {
633
697
  program.error('No tasks found', { exitCode: 127 });
634
698
  return;
@@ -652,6 +716,7 @@ async function getProgram(environment, argv) {
652
716
  explain: options.explain,
653
717
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
654
718
  timeout: options.timeout ?? null,
719
+ network: options.network ?? null,
655
720
  skipDeps: options.skipDeps !== false,
656
721
  });
657
722
  // Reporting only: the summary never changes the exit code or behavior.
@@ -723,4 +788,12 @@ tasks:
723
788
  return { program, args };
724
789
  }
725
790
  exports.getProgram = getProgram;
791
+ function parseNetworkArgument(value) {
792
+ try {
793
+ return (0, work_network_1.parseNetworkOption)(value);
794
+ }
795
+ catch (e) {
796
+ throw new commander_1.InvalidArgumentError((0, log_1.getErrorMessage)(e));
797
+ }
798
+ }
726
799
  //# sourceMappingURL=program.js.map
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAnnotations = exports.collectAnnotations = exports.remoteRepo = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ const util_1 = require("util");
8
+ const repo_1 = require("../server/repo");
9
+ const bundle_1 = require("./bundle");
10
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
11
+ async function git(cwd, ...args) {
12
+ try {
13
+ const value = (await execFileAsync('git', args, { cwd, timeout: 5000 })).stdout.trim();
14
+ return value || undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ // `host/path` of a remote, or nothing: a url that is neither https, ssh nor
21
+ // `user@host:path` is left out, so a credential in it never leaves the machine.
22
+ function remoteRepo(url) {
23
+ const scp = /^[\w.-]+@([\w.-]+):(?!\/)(.+)$/.exec(url);
24
+ if (scp) {
25
+ return (0, repo_1.normalizeRepo)(`${scp[1]}/${scp[2]}`) ?? undefined;
26
+ }
27
+ try {
28
+ const parsed = new URL(url);
29
+ if (!['https:', 'ssh:'].includes(parsed.protocol)) {
30
+ return undefined;
31
+ }
32
+ return (0, repo_1.normalizeRepo)(`${parsed.hostname}${parsed.pathname}`) ?? undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ exports.remoteRepo = remoteRepo;
39
+ // Where a build file is published from: its path in its repository (or on this
40
+ // machine, outside of one), the repository and commit, this machine and its user.
41
+ // `files` are the paths of the bundle, relative to the build file's directory: when
42
+ // one of them differs from the commit, `dirty=true` says the version is not that
43
+ // commit's. What can not be found is left out.
44
+ async function collectAnnotations(fileName, files = [(0, path_1.basename)(fileName)]) {
45
+ const path = (0, path_1.resolve)(fileName);
46
+ const cwd = (0, path_1.dirname)(path);
47
+ const annotations = {};
48
+ // git says where the directory is in the repository: comparing paths would trip on
49
+ // what the file system calls the same directory differently (symlinks, 8.3 names)
50
+ const inside = (await git(cwd, 'rev-parse', '--is-inside-work-tree')) === 'true';
51
+ const prefix = inside ? (await git(cwd, 'rev-parse', '--show-prefix')) ?? '' : undefined;
52
+ annotations.path = prefix !== undefined ? `${prefix}${(0, path_1.basename)(path)}` : path;
53
+ if (inside) {
54
+ const remote = await git(cwd, 'remote', 'get-url', 'origin');
55
+ const repo = remote ? remoteRepo(remote) : undefined;
56
+ const commit = await git(cwd, 'rev-parse', 'HEAD');
57
+ if (repo) {
58
+ annotations.repo = repo;
59
+ }
60
+ if (commit) {
61
+ annotations.commit = commit;
62
+ const changed = await git(cwd, '--literal-pathspecs', 'status', '--porcelain', '--untracked-files=all', '--', ...files);
63
+ if (changed) {
64
+ annotations.dirty = 'true';
65
+ }
66
+ }
67
+ }
68
+ annotations.machine = (0, os_1.hostname)();
69
+ try {
70
+ annotations.user = (0, os_1.userInfo)().username;
71
+ }
72
+ catch {
73
+ // a user without an entry in the password database
74
+ }
75
+ return Object.fromEntries(Object.entries(annotations).filter(([, value]) => value));
76
+ }
77
+ exports.collectAnnotations = collectAnnotations;
78
+ // `--annotation key=value` sets one, `key=` leaves it out.
79
+ function applyAnnotations(annotations, options) {
80
+ const result = { ...annotations };
81
+ for (const option of options) {
82
+ const index = option.indexOf('=');
83
+ if (index <= 0) {
84
+ throw new Error(`${option} is not an annotation, write it as key=value`);
85
+ }
86
+ const key = option.slice(0, index);
87
+ const value = option.slice(index + 1);
88
+ if (value) {
89
+ result[key] = value;
90
+ }
91
+ else {
92
+ delete result[key];
93
+ }
94
+ }
95
+ const parsed = bundle_1.annotationsSchema.safeParse(result);
96
+ if (!parsed.success) {
97
+ const issue = parsed.error.issues[0];
98
+ throw new Error(`${issue.path.length > 0 ? `annotation ${issue.path.join('.')}` : 'annotations'}: ${issue.message}`);
99
+ }
100
+ return parsed.data;
101
+ }
102
+ exports.applyAnnotations = applyAnnotations;
103
+ //# sourceMappingURL=annotations.js.map
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.showBundle = exports.visible = void 0;
4
+ // Text from a proposal is shown to somebody who decides on it, so nothing in it may
5
+ // rewrite the terminal or hide what it says: control and format characters (which
6
+ // include the ones that reorder text) are shown as `?`. Long lines are not cut,
7
+ // what is hidden behind a cut is what a reviewer needs to see.
8
+ function visible(line) {
9
+ return line.replace(/\t/g, ' ').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?');
10
+ }
11
+ exports.visible = visible;
12
+ const lines = (text) => (text === '' ? [] : text.replace(/\n$/, '').split('\n'));
13
+ // Every file of a version, whole: a version has no other one it could be read
14
+ // against, whoever decides on it reads all of it.
15
+ function showBundle(bundle) {
16
+ return Object.keys(bundle.files)
17
+ .sort()
18
+ .flatMap((path) => [
19
+ `--- ${visible(path)}${path === bundle.entry ? ' (entry)' : ''}`,
20
+ ...lines(bundle.files[path]).map((line) => ` ${visible(line)}`),
21
+ ]);
22
+ }
23
+ exports.showBundle = showBundle;
24
+ //# sourceMappingURL=bundle-text.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HASH_PATTERN = exports.hashBundle = exports.bundleSchema = exports.isBundlePath = exports.MAX_BUNDLE_BYTES = exports.MAX_BUNDLE_FILES = exports.DEFAULT_ENTRY = void 0;
3
+ exports.HASH_PATTERN = exports.hashBundle = exports.bundleSchema = exports.annotationsSchema = exports.MAX_ANNOTATIONS = exports.isBundlePath = exports.MAX_BUNDLE_BYTES = exports.MAX_BUNDLE_FILES = exports.DEFAULT_ENTRY = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const zod_1 = require("zod");
6
6
  exports.DEFAULT_ENTRY = '.hammerkit.yaml';
@@ -16,6 +16,14 @@ function isBundlePath(path) {
16
16
  !path.split('/').some((part) => part === '' || part === '.' || part === '..'));
17
17
  }
18
18
  exports.isBundlePath = isBundlePath;
19
+ exports.MAX_ANNOTATIONS = 20;
20
+ exports.annotationsSchema = zod_1.z
21
+ .record(zod_1.z
22
+ .string()
23
+ .regex(/^[a-z][a-z0-9._-]{0,63}$/, 'use lower case letters, digits, ".", "_" and "-", starting with a letter'), zod_1.z.string().min(1).max(500))
24
+ .refine((annotations) => Object.keys(annotations).length <= exports.MAX_ANNOTATIONS, {
25
+ message: `at most ${exports.MAX_ANNOTATIONS} annotations`,
26
+ });
19
27
  exports.bundleSchema = zod_1.z
20
28
  .object({
21
29
  entry: zod_1.z.string().default(exports.DEFAULT_ENTRY),
@@ -10,8 +10,10 @@ const log_1 = require("../log");
10
10
  const host_config_1 = require("./host-config");
11
11
  const device_login_1 = require("./device-login");
12
12
  const credentials_1 = require("./credentials");
13
+ const token_source_1 = require("./token-source");
13
14
  const server_client_1 = require("./server-client");
14
15
  const remote_work_commands_1 = require("./remote-work-commands");
16
+ const mcp_command_1 = require("../harness/mcp-command");
15
17
  // Host-level commands: they only touch the machine's config file, so they work
16
18
  // with or without a build file in the working directory.
17
19
  function registerRemoteCommands(program, environment, buildFile) {
@@ -129,7 +131,7 @@ function registerRemoteCommands(program, environment, buildFile) {
129
131
  .description('show who you are on a server and which service accounts you may use')
130
132
  .action(guarded('Remote accounts was not successful', async (name) => {
131
133
  const chosen = await getServer(name);
132
- const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
134
+ const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
133
135
  const me = await (0, server_client_1.getMe)(chosen.server, token);
134
136
  const { body } = await (0, server_client_1.callServer)(chosen.server, token, 'GET', '/v1/accounts');
135
137
  environment.stdout.write(`${chosen.name}: ${(0, server_client_1.printable)(me.sub)} (${(0, server_client_1.printable)(me.iss)})\n`);
@@ -153,6 +155,7 @@ function registerRemoteCommands(program, environment, buildFile) {
153
155
  }
154
156
  }));
155
157
  (0, remote_work_commands_1.registerWorkCommands)(remote, { environment, buildFile, getServer, guarded });
158
+ (0, mcp_command_1.registerMcpCommand)(remote, environment);
156
159
  }
157
160
  exports.registerRemoteCommands = registerRemoteCommands;
158
161
  //# sourceMappingURL=remote-commands.js.map