hammerkit 1.24.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.
@@ -1,9 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProposalRegistry = exports.ProposalError = void 0;
3
+ exports.ProposalRegistry = exports.StaleProposalError = exports.CooldownError = exports.ProposalError = void 0;
4
+ const units_1 = require("../utils/units");
4
5
  class ProposalError extends Error {
6
+ constructor() {
7
+ super(...arguments);
8
+ // what the caller is told
9
+ this.status = 503;
10
+ }
5
11
  }
6
12
  exports.ProposalError = ProposalError;
13
+ // The caller proposes too soon after a rejection
14
+ class CooldownError extends ProposalError {
15
+ constructor() {
16
+ super(...arguments);
17
+ this.status = 429;
18
+ }
19
+ }
20
+ exports.CooldownError = CooldownError;
21
+ // The build file moved on since the proposal was made
22
+ class StaleProposalError extends Error {
23
+ constructor(base, latest) {
24
+ super(`the build file changed since this was proposed (it follows ${base ?? 'nothing'}, the latest is ${latest ?? 'nothing'}), propose it again on top of the latest`);
25
+ this.base = base;
26
+ this.latest = latest;
27
+ }
28
+ }
29
+ exports.StaleProposalError = StaleProposalError;
7
30
  // The proposals of the server, in memory: a restart forgets what waits, and
8
31
  // whoever proposed it proposes it again. Nothing in here is runnable.
9
32
  class ProposalRegistry {
@@ -13,10 +36,11 @@ class ProposalRegistry {
13
36
  this.maxWaiting = options.maxWaiting ?? 100;
14
37
  this.timeout = options.timeout ?? 60 * 60 * 1000;
15
38
  this.keep = options.keep ?? 100;
39
+ this.cooldown = options.cooldown ?? 0;
16
40
  this.now = options.now ?? Date.now;
17
41
  }
18
42
  // Proposing what already waits is not an error and changes nothing.
19
- propose(version, bundle, proposer) {
43
+ propose(version, bundle, proposer, missing = [], base = null) {
20
44
  if (this.closed) {
21
45
  throw new ProposalError('the server is shutting down');
22
46
  }
@@ -25,6 +49,7 @@ class ProposalRegistry {
25
49
  if (existing && existing.proposal.state === 'waiting') {
26
50
  return { proposal: { ...existing.proposal }, created: false };
27
51
  }
52
+ this.checkCooldown(version, proposer);
28
53
  if (this.waiting().length >= this.maxWaiting) {
29
54
  throw new ProposalError('too many build file versions are waiting for an approval');
30
55
  }
@@ -34,6 +59,10 @@ class ProposalRegistry {
34
59
  entry: version.entry,
35
60
  tasks: version.tasks,
36
61
  services: version.services,
62
+ accounts: version.accounts ?? [],
63
+ missing,
64
+ plan: version.plan ?? {},
65
+ base,
37
66
  proposedBy: { iss: proposer.iss, sub: proposer.sub },
38
67
  createdAt: new Date(this.now()).toISOString(),
39
68
  expiresAt: new Date(this.now() + this.timeout).toISOString(),
@@ -67,7 +96,7 @@ class ProposalRegistry {
67
96
  // decides on their own proposal.
68
97
  async decide(decider, name, hash, outcome, mayDecide, publish, reason) {
69
98
  const entry = this.entries.get(this.key(name, hash));
70
- if (!entry || !mayDecide(name)) {
99
+ if (!entry || !mayDecide(entry.proposal)) {
71
100
  return { status: 'not-found' };
72
101
  }
73
102
  if (entry.proposal.proposedBy.iss === decider.iss && entry.proposal.proposedBy.sub === decider.sub) {
@@ -112,6 +141,24 @@ class ProposalRegistry {
112
141
  }
113
142
  entry.bundle = undefined;
114
143
  }
144
+ // A rejection is a no; a proposal that only ran out of time is not.
145
+ checkCooldown(version, proposer) {
146
+ if (this.cooldown <= 0) {
147
+ return;
148
+ }
149
+ for (const { proposal } of this.entries.values()) {
150
+ if (proposal.state !== 'rejected' || proposal.name !== version.name) {
151
+ continue;
152
+ }
153
+ const left = Date.parse(proposal.decidedAt ?? '') + this.cooldown - this.now();
154
+ const sameVersion = proposal.hash === version.hash;
155
+ const sameProposer = proposal.proposedBy.iss === proposer.iss && proposal.proposedBy.sub === proposer.sub;
156
+ if (left > 0 && (sameVersion || sameProposer)) {
157
+ throw new CooldownError(`${sameVersion ? 'this version was' : 'a version of this build file by you was'} rejected, ` +
158
+ `propose again in ${(0, units_1.formatDuration)(Math.ceil(left / 1000) * 1000)}`);
159
+ }
160
+ }
161
+ }
115
162
  evict() {
116
163
  const decided = [...this.entries.entries()].filter(([, e]) => e.proposal.state !== 'waiting');
117
164
  for (const [key] of decided.slice(0, Math.max(0, decided.length - this.keep))) {
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RepoAccess = void 0;
4
+ const redact_1 = require("../utils/redact");
5
+ const provider_values_1 = require("../secrets/provider-values");
6
+ const git_checkout_1 = require("./git-checkout");
7
+ const repo_1 = require("./repo");
8
+ // A token is read again after this long, so a short-lived one (a GitHub App
9
+ // installation token minted by the provider command) is not used up and a revoked one
10
+ // stops working soon.
11
+ const TOKEN_TTL_MS = 60 * 1000;
12
+ // A run waits for the repository at submit, not forever
13
+ const RESOLVE_TIMEOUT_MS = 30 * 1000;
14
+ /**
15
+ * The repositories a server can run code from. What may be read is the operator's
16
+ * `sources` list; who may name which of them is the entitlement's `repos`. The read
17
+ * credential is a secret of a provider, read as a service account that nobody can use
18
+ * in a task. It is handed to git for one fetch through the environment of that git
19
+ * process (never an argument, never `.git/config`, never the environment of a task)
20
+ * and masked in whatever the run logs.
21
+ */
22
+ class RepoAccess {
23
+ constructor(config,
24
+ // the base environment of the server's runs, with `hosted` set
25
+ environment, options = {}) {
26
+ this.config = config;
27
+ this.environment = environment;
28
+ this.options = options;
29
+ this.tokens = new Map();
30
+ this.now = options.now ?? Date.now;
31
+ }
32
+ // The first source that covers the repository: patterns are listed from the most
33
+ // specific to the least.
34
+ source(repo) {
35
+ return this.config.sources.find((source) => (0, repo_1.matchesAnyRepo)([source.match], repo));
36
+ }
37
+ readable(repo) {
38
+ return this.source(repo) !== undefined;
39
+ }
40
+ url(repo) {
41
+ return this.options.url ? this.options.url(repo) : `https://${repo}.git`;
42
+ }
43
+ // The commit a ref points to now, or an error that says what is wrong with the ref.
44
+ async resolve(repo, ref, signal) {
45
+ const timeout = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
46
+ const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
47
+ if (this.options.resolve) {
48
+ return this.options.resolve(repo, ref, abort);
49
+ }
50
+ const credentials = await this.credentials(repo);
51
+ try {
52
+ return await (0, git_checkout_1.resolveRef)(this.url(repo), ref, credentials.env, abort);
53
+ }
54
+ catch (e) {
55
+ throw this.scrub(e, credentials.values);
56
+ }
57
+ }
58
+ /**
59
+ * The environment a git process needs to read the repository: the allowlisted
60
+ * variables of the server, and the token as a header of requests to that host. The
61
+ * values are what the run has to mask.
62
+ */
63
+ async credentials(repo) {
64
+ const env = {};
65
+ for (const [key, value] of Object.entries(this.environment.processEnvs)) {
66
+ if (value !== undefined) {
67
+ env[key] = value;
68
+ }
69
+ }
70
+ const secret = this.source(repo)?.secret;
71
+ if (!secret) {
72
+ return { env, values: [] };
73
+ }
74
+ const token = await this.token(secret);
75
+ const host = repo.split('/')[0];
76
+ const header = `Authorization: Basic ${Buffer.from(`${secret.username}:${token}`).toString('base64')}`;
77
+ env.GIT_CONFIG_COUNT = '1';
78
+ env.GIT_CONFIG_KEY_0 = `http.https://${host}/.extraHeader`;
79
+ env.GIT_CONFIG_VALUE_0 = header;
80
+ return { env, values: [token, header] };
81
+ }
82
+ async token(secret) {
83
+ const key = `${secret.account}\0${secret.from}`;
84
+ const known = this.tokens.get(key);
85
+ if (known && this.now() - known.at < TOKEN_TTL_MS) {
86
+ return known.value;
87
+ }
88
+ const catalog = this.environment.hosted?.catalog;
89
+ const kind = secret.from.substring(0, secret.from.indexOf(':'));
90
+ const ref = secret.from.substring(secret.from.indexOf(':') + 1);
91
+ const provider = catalog.providers[kind];
92
+ const account = catalog.accounts[secret.account];
93
+ const credentials = account?.providers[kind];
94
+ if (!provider || !credentials) {
95
+ throw new Error(`the source credential ${secret.from} as ${secret.account} is not defined`);
96
+ }
97
+ // a fresh environment per read: values are kept here and for a short time only
98
+ const registry = (0, redact_1.createSecretRegistry)();
99
+ const environment = {
100
+ ...this.environment,
101
+ secrets: registry,
102
+ hosted: { catalog, accounts: [secret.account] },
103
+ };
104
+ let value;
105
+ try {
106
+ value = await (0, provider_values_1.fetchProviderValue)({ definition: provider, account: { name: secret.account, env: credentials.env } }, ref, environment);
107
+ }
108
+ catch (e) {
109
+ throw new Error(`could not read the source credential ${secret.from}: ${e instanceof Error ? e.message : e}`);
110
+ }
111
+ if (value.length === 0) {
112
+ throw new Error(`the source credential ${secret.from} is empty`);
113
+ }
114
+ this.tokens.set(key, { value, at: this.now() });
115
+ return value;
116
+ }
117
+ // An error as the caller may read it: without the token.
118
+ scrub(error, values) {
119
+ const registry = (0, redact_1.createSecretRegistry)();
120
+ values.forEach((value) => registry.register(value));
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ const scrubbed = new (error instanceof Error ? error.constructor : Error)(registry.redact(message));
123
+ return scrubbed;
124
+ }
125
+ }
126
+ exports.RepoAccess = RepoAccess;
127
+ //# sourceMappingURL=repo-access.js.map
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ // Repositories are named `host/path`, such as `github.com/corp/payments` or
3
+ // `gitlab.corp.com/platform/team/tool`, so one pattern can cover a GitHub
4
+ // organisation or a GitLab group with its subgroups. Names are compared in
5
+ // lower case: both hosts treat them that way.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.matchesRef = exports.matchesAnyRepo = exports.matchesRepo = exports.normalizeRepoPattern = exports.isRepoPattern = exports.normalizeRepo = void 0;
8
+ const SEGMENT = /^[a-z0-9._-]+$/;
9
+ const PATTERN_SEGMENT = /^[a-z0-9._*-]+$/;
10
+ // `host/path` of what a caller or an operator wrote, or null when it is not a
11
+ // plain repository address: no credentials, port, query, option-like names or
12
+ // `..`. A scheme (`https://`) and a trailing `.git` are accepted and dropped.
13
+ function normalizeRepo(input) {
14
+ let value = input.trim().toLowerCase();
15
+ value = value.replace(/^https:\/\//, '');
16
+ value = value.replace(/\.git$/, '').replace(/\/+$/, '');
17
+ const parts = value.split('/');
18
+ if (parts.length < 2 || parts.length > 12) {
19
+ return null;
20
+ }
21
+ const [host, ...path] = parts;
22
+ if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(host) || host.includes('..')) {
23
+ return null;
24
+ }
25
+ if (path.some((part) => !SEGMENT.test(part) || part === '.' || part === '..' || part.startsWith('-'))) {
26
+ return null;
27
+ }
28
+ return [host, ...path].join('/');
29
+ }
30
+ exports.normalizeRepo = normalizeRepo;
31
+ // A pattern is a repository where a segment may hold `*` (any characters but `/`),
32
+ // and the last one may be `**`: everything below, subgroups included.
33
+ function isRepoPattern(input) {
34
+ const value = input
35
+ .trim()
36
+ .toLowerCase()
37
+ .replace(/^https:\/\//, '')
38
+ .replace(/\/+$/, '');
39
+ const parts = value.split('/');
40
+ if (parts.length < 2 || !/^[a-z0-9*]([a-z0-9.*-]*[a-z0-9*])?$/.test(parts[0])) {
41
+ return false;
42
+ }
43
+ return parts.slice(1).every((part, index, rest) => {
44
+ if (part === '**') {
45
+ return index === rest.length - 1;
46
+ }
47
+ return PATTERN_SEGMENT.test(part) && part !== '.' && part !== '..' && !part.startsWith('-') && !part.includes('**');
48
+ });
49
+ }
50
+ exports.isRepoPattern = isRepoPattern;
51
+ function normalizeRepoPattern(input) {
52
+ return input
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/^https:\/\//, '')
56
+ .replace(/\/+$/, '');
57
+ }
58
+ exports.normalizeRepoPattern = normalizeRepoPattern;
59
+ function matchesRepo(pattern, repo) {
60
+ const parts = normalizeRepoPattern(pattern).split('/');
61
+ const target = repo.split('/');
62
+ const last = parts[parts.length - 1];
63
+ if (last === '**') {
64
+ const fixed = parts.slice(0, -1);
65
+ return target.length > fixed.length && fixed.every((part, index) => segmentMatches(part, target[index]));
66
+ }
67
+ return parts.length === target.length && parts.every((part, index) => segmentMatches(part, target[index]));
68
+ }
69
+ exports.matchesRepo = matchesRepo;
70
+ function segmentMatches(pattern, segment) {
71
+ const regex = pattern
72
+ .split('*')
73
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
74
+ .join('[^/]*');
75
+ return new RegExp(`^${regex}$`).test(segment);
76
+ }
77
+ function matchesAnyRepo(patterns, repo) {
78
+ return patterns.some((pattern) => matchesRepo(pattern, repo));
79
+ }
80
+ exports.matchesAnyRepo = matchesAnyRepo;
81
+ // A ref is matched as written: `main`, `release/*` (one level) or `release/**`.
82
+ function matchesRef(patterns, ref) {
83
+ return patterns.some((pattern) => {
84
+ const parts = pattern.split('/');
85
+ const target = ref.split('/');
86
+ if (parts[parts.length - 1] === '**') {
87
+ const fixed = parts.slice(0, -1);
88
+ return target.length > fixed.length && fixed.every((part, index) => segmentMatches(part, target[index]));
89
+ }
90
+ return parts.length === target.length && parts.every((part, index) => segmentMatches(part, target[index]));
91
+ });
92
+ }
93
+ exports.matchesRef = matchesRef;
94
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runEnvironment = void 0;
4
+ // What a run sees of the server's environment: the variables the operator listed
5
+ // in `runs.env.allow`, and the fixed ones of `runs.env.set`. The credentials of a
6
+ // run are not among them, they exist only as the secrets of an account.
7
+ function runEnvironment(config, processEnvs) {
8
+ const passed = {};
9
+ for (const name of config.runs.env.allow) {
10
+ // Windows names are not case sensitive
11
+ const windows = process.platform === 'win32';
12
+ const key = Object.keys(processEnvs).find((k) => k === name || (windows && k.toLowerCase() === name.toLowerCase()));
13
+ if (key !== undefined && processEnvs[key] !== undefined) {
14
+ passed[name] = processEnvs[key];
15
+ }
16
+ }
17
+ return { ...passed, ...config.runs.env.set };
18
+ }
19
+ exports.runEnvironment = runEnvironment;
20
+ //# sourceMappingURL=run-environment.js.map
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planRun = exports.getRunScopes = void 0;
4
+ const parse_label_arguments_1 = require("../parser/parse-label-arguments");
5
+ const plan_1 = require("./plan");
6
+ function labels(values) {
7
+ return values && values.length > 0 ? (0, parse_label_arguments_1.parseLabelArguments)(values) : undefined;
8
+ }
9
+ // The tasks in the order they were named, one scope each; by labels when none is.
10
+ function getRunScopes(tasks, options) {
11
+ return tasks.length > 0
12
+ ? tasks.map((task) => ({
13
+ name: task,
14
+ scope: { taskName: task, environmentName: options.env ?? null },
15
+ }))
16
+ : [
17
+ {
18
+ name: 'labels',
19
+ scope: {
20
+ filterLabels: labels(options.filter),
21
+ excludeLabels: labels(options.exclude),
22
+ environmentName: options.env ?? null,
23
+ },
24
+ },
25
+ ];
26
+ }
27
+ exports.getRunScopes = getRunScopes;
28
+ /**
29
+ * What a run will do, read with the engine from the files of the version it is
30
+ * pinned to and with the environment a run has: the same function answers at
31
+ * submit, and again in the executor, so what was approved is what runs.
32
+ */
33
+ async function planRun(bundle, run, environment, policy) {
34
+ const plan = {};
35
+ for (const { scope } of getRunScopes(run.tasks, run.options)) {
36
+ Object.assign(plan, (await (0, plan_1.planBundle)(bundle, environment, { scope, policy })).plan);
37
+ }
38
+ return {
39
+ plan,
40
+ accounts: [...new Set(Object.values(plan).flatMap((entry) => entry.accounts))].sort(),
41
+ };
42
+ }
43
+ exports.planRun = planRun;
44
+ //# sourceMappingURL=run-plan.js.map
@@ -5,9 +5,11 @@ const zod_1 = require("zod");
5
5
  const units_1 = require("../utils/units");
6
6
  const bundle_1 = require("../remote/bundle");
7
7
  const server_config_1 = require("./server-config");
8
+ const repo_1 = require("./repo");
8
9
  const name = zod_1.z.string().min(1).max(200);
9
- // What `POST /v1/runs` takes. Who is asking, which accounts they may use and
10
- // where code may come from is decided from the token, never from this body.
10
+ // What `POST /v1/runs` takes. Who is asking and which accounts they may use is
11
+ // decided from the token, never from this body; the accounts a run uses are those
12
+ // its secrets are read as, which the server derives from the published build file.
11
13
  const duration = zod_1.z.string().refine((value) => {
12
14
  try {
13
15
  (0, units_1.parseDuration)(value);
@@ -50,10 +52,26 @@ exports.runRequestSchema = zod_1.z
50
52
  // tasks by their name in the build file, as on the command line; labels in
51
53
  // the options can select tasks instead
52
54
  tasks: zod_1.z.array(name).max(100).default([]),
53
- // the service account to use; the caller's default when left out
54
- account: name.optional(),
55
55
  source: zod_1.z.discriminatedUnion('type', [
56
- zod_1.z.object({ type: zod_1.z.literal('ref'), ref: name }).strict(),
56
+ zod_1.z
57
+ .object({
58
+ type: zod_1.z.literal('ref'),
59
+ // `host/path`, such as github.com/corp/payments; the server reads it from its own
60
+ // sources, a url is never taken from a request
61
+ repo: zod_1.z.string().transform((value, ctx) => {
62
+ const repo = (0, repo_1.normalizeRepo)(value);
63
+ if (!repo) {
64
+ ctx.addIssue({
65
+ code: zod_1.z.ZodIssueCode.custom,
66
+ message: 'expected a repository like github.com/corp/payments',
67
+ });
68
+ return zod_1.z.NEVER;
69
+ }
70
+ return repo;
71
+ }),
72
+ ref: name,
73
+ })
74
+ .strict(),
57
75
  // the name of a tree announced with POST /v1/sources
58
76
  zod_1.z.object({ type: zod_1.z.literal('upload'), snapshot: zod_1.z.string().regex(bundle_1.HASH_PATTERN) }).strict(),
59
77
  ]),
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileRunStore = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ const runs_1 = require("./runs");
7
+ /**
8
+ * One file per run, written to a temporary file and renamed, so a crash leaves the
9
+ * old record and not half a new one. A record holds what the server already shows
10
+ * the caller: the state, the plan, the logs as they were masked. Only the user of the
11
+ * server can read the files.
12
+ */
13
+ class FileRunStore {
14
+ constructor(directory) {
15
+ this.directory = directory;
16
+ }
17
+ async save(record) {
18
+ await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
19
+ const file = (0, path_1.join)(this.directory, `${record.run.id}.json`);
20
+ const temporary = `${file}.${process.pid}.tmp`;
21
+ await (0, promises_1.writeFile)(temporary, JSON.stringify(record), { mode: 0o600 });
22
+ await (0, promises_1.rename)(temporary, file);
23
+ }
24
+ async remove(id) {
25
+ await (0, promises_1.rm)((0, path_1.join)(this.directory, `${id}.json`), { force: true });
26
+ }
27
+ async load() {
28
+ const records = [];
29
+ const skipped = [];
30
+ let names;
31
+ try {
32
+ names = await (0, promises_1.readdir)(this.directory);
33
+ }
34
+ catch {
35
+ return { records, skipped };
36
+ }
37
+ for (const name of names) {
38
+ if (!name.endsWith('.json')) {
39
+ // a record that was being written when the server stopped
40
+ if (name.endsWith('.tmp')) {
41
+ await (0, promises_1.rm)((0, path_1.join)(this.directory, name), { force: true });
42
+ }
43
+ continue;
44
+ }
45
+ try {
46
+ const record = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(this.directory, name), 'utf8'));
47
+ if (!runs_1.RUN_ID_PATTERN.test(record.run.id) || `${record.run.id}.json` !== name || !Array.isArray(record.events)) {
48
+ throw new Error('not a run record');
49
+ }
50
+ records.push(record);
51
+ }
52
+ catch {
53
+ skipped.push(name);
54
+ }
55
+ }
56
+ records.sort((a, b) => a.run.createdAt.localeCompare(b.run.createdAt));
57
+ return { records, skipped };
58
+ }
59
+ }
60
+ exports.FileRunStore = FileRunStore;
61
+ //# sourceMappingURL=run-store.js.map
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerRunWorkerCommand = void 0;
4
+ const run_worker_1 = require("./run-worker");
5
+ // `hammerkit server-run`: what the server starts for each run. It reads the run on its
6
+ // standard input, executes it in the current directory and reports on its standard output.
7
+ // Not for people: the input comes from the server and says which accounts it may read.
8
+ function registerRunWorkerCommand(program, environment) {
9
+ program
10
+ .command('server-run', { hidden: true })
11
+ .description('execute one run for a hammerkit server (internal)')
12
+ .action(async () => {
13
+ await (0, run_worker_1.workerMain)(process.stdin, environment.stdout, environment.processEnvs, environment.cwd, environment.abortCtrl);
14
+ });
15
+ }
16
+ exports.registerRunWorkerCommand = registerRunWorkerCommand;
17
+ //# sourceMappingURL=run-worker-command.js.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const run_worker_1 = require("./run-worker");
4
+ // The process of a run without the command line, for tests that run from source
5
+ // (`vite-node src/server/run-worker-main.ts`). A build uses `hammerkit server-run`.
6
+ (0, run_worker_1.workerMain)(process.stdin, process.stdout, process.env, process.cwd(), new AbortController()).then(() => process.exit(0), () => process.exit(1));
7
+ //# sourceMappingURL=run-worker-main.js.map