@apex-actions/cli 0.67.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 (81) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +84 -0
  3. package/THIRD-PARTY-NOTICES.md +1226 -0
  4. package/dist/cli/actions/main.js +193 -0
  5. package/dist/cli/approval/main.js +76 -0
  6. package/dist/cli/artifact/main.js +115 -0
  7. package/dist/cli/cache/main.js +104 -0
  8. package/dist/cli/deployment/main.js +73 -0
  9. package/dist/cli/dispatch/main.js +47 -0
  10. package/dist/cli/environment/main.js +139 -0
  11. package/dist/cli/login/main.js +73 -0
  12. package/dist/cli/logout/main.js +43 -0
  13. package/dist/cli/logs/main.js +125 -0
  14. package/dist/cli/lsp/main.js +46 -0
  15. package/dist/cli/main.js +88 -0
  16. package/dist/cli/migrate/main.js +275 -0
  17. package/dist/cli/notify/main.js +257 -0
  18. package/dist/cli/oidc/main.js +103 -0
  19. package/dist/cli/reproduce/main.js +73 -0
  20. package/dist/cli/rerun/main.js +100 -0
  21. package/dist/cli/run/main.js +50 -0
  22. package/dist/cli/runner/main.js +238 -0
  23. package/dist/cli/schedule/main.js +54 -0
  24. package/dist/cli/search/main.js +123 -0
  25. package/dist/cli/secret/main.js +102 -0
  26. package/dist/cli/secret/shared.js +104 -0
  27. package/dist/cli/spend/main.js +81 -0
  28. package/dist/cli/terms/main.js +85 -0
  29. package/dist/cli/tests/main.js +122 -0
  30. package/dist/cli/timing/main.js +90 -0
  31. package/dist/cli/validate/main.js +48 -0
  32. package/dist/cli/variable/main.js +83 -0
  33. package/dist/cli/whoami/main.js +63 -0
  34. package/dist/cli/why/main.js +106 -0
  35. package/dist/src/application/authenticate.js +56 -0
  36. package/dist/src/application/dispatch-workflow.js +23 -0
  37. package/dist/src/application/explain-runs.js +40 -0
  38. package/dist/src/application/index.js +2 -0
  39. package/dist/src/application/inspect-runs.js +29 -0
  40. package/dist/src/application/manage-actions.js +26 -0
  41. package/dist/src/application/manage-approvals.js +24 -0
  42. package/dist/src/application/manage-artifacts.js +79 -0
  43. package/dist/src/application/manage-caches.js +41 -0
  44. package/dist/src/application/manage-environments.js +136 -0
  45. package/dist/src/application/manage-notifications.js +20 -0
  46. package/dist/src/application/manage-runners.js +89 -0
  47. package/dist/src/application/manage-secrets.js +83 -0
  48. package/dist/src/application/manage-terms.js +34 -0
  49. package/dist/src/application/read-logs.js +40 -0
  50. package/dist/src/application/report-spend.js +15 -0
  51. package/dist/src/application/rerun-runs.js +34 -0
  52. package/dist/src/application/resolve-run.js +19 -0
  53. package/dist/src/application/run-workflow.js +17 -0
  54. package/dist/src/application/scan-migration.js +76 -0
  55. package/dist/src/application/search-logs.js +24 -0
  56. package/dist/src/application/serve-language-server.js +15 -0
  57. package/dist/src/application/validate-workflows.js +12 -0
  58. package/dist/src/domain/credentials.js +82 -0
  59. package/dist/src/domain/fleet-history.js +136 -0
  60. package/dist/src/domain/index.js +2 -0
  61. package/dist/src/domain/insight.js +42 -0
  62. package/dist/src/domain/logsearch.js +40 -0
  63. package/dist/src/domain/migration.js +122 -0
  64. package/dist/src/domain/notify.js +64 -0
  65. package/dist/src/domain/outcomediff.js +40 -0
  66. package/dist/src/domain/provenance.js +63 -0
  67. package/dist/src/domain/runs.js +90 -0
  68. package/dist/src/domain/secrets.js +39 -0
  69. package/dist/src/domain/spend.js +50 -0
  70. package/dist/src/domain/supplychain.js +48 -0
  71. package/dist/src/domain/terms.js +16 -0
  72. package/dist/src/domain/validation.js +4 -0
  73. package/dist/src/infrastructure/control-plane-http.js +458 -0
  74. package/dist/src/infrastructure/credentials.js +134 -0
  75. package/dist/src/infrastructure/engine-binary.js +164 -0
  76. package/dist/src/infrastructure/github-api.js +154 -0
  77. package/dist/src/infrastructure/index.js +2 -0
  78. package/dist/src/infrastructure/loopback-login.js +136 -0
  79. package/dist/src/infrastructure/runner-binary.js +74 -0
  80. package/dist/src/infrastructure/secret-input.js +52 -0
  81. package/package.json +54 -0
@@ -0,0 +1,139 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { deleteEnvironment, environmentSummary, listEnvironments, parseReviewer, saveEnvironment, } from '../../src/application/manage-environments.js';
4
+ import { splitRepository } from '../../src/application/manage-caches.js';
5
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
6
+ /** Build the API client, or explain what is missing and stop. */
7
+ const api = () => {
8
+ try {
9
+ return new HttpControlPlane();
10
+ }
11
+ catch (err) {
12
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
13
+ process.exitCode = 2;
14
+ return undefined;
15
+ }
16
+ };
17
+ const repository = (slug) => {
18
+ const parsed = splitRepository(slug);
19
+ if (parsed)
20
+ return parsed;
21
+ console.error(chalk.red(`expected owner/repo, got ${JSON.stringify(slug)}`));
22
+ process.exitCode = 2;
23
+ return undefined;
24
+ };
25
+ const printEnvironments = (environments) => {
26
+ if (environments.length === 0) {
27
+ console.log(chalk.yellow('No environments are protected. A workflow may still name one; nothing will gate it.'));
28
+ return;
29
+ }
30
+ console.log(chalk.bold(['ENVIRONMENT', 'RULES'].join(' ')));
31
+ for (const e of environments)
32
+ console.log([e.name, environmentSummary(e)].join(' '));
33
+ };
34
+ const listCommand = new Command('list')
35
+ .description('Environments this repository protects, and how')
36
+ .argument('<repository>', 'owner/repo')
37
+ .option('--json', 'machine-readable output')
38
+ .action(async (slug, opts) => {
39
+ const target = repository(slug);
40
+ if (!target)
41
+ return;
42
+ const client = api();
43
+ if (!client)
44
+ return;
45
+ const outcome = await listEnvironments(client, target.owner, target.repo);
46
+ if (outcome.kind === 'refused') {
47
+ console.error(chalk.red(outcome.detail));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ if (opts.json === true) {
52
+ console.log(JSON.stringify({ environments: outcome.environments }, null, 2));
53
+ return;
54
+ }
55
+ printEnvironments(outcome.environments);
56
+ });
57
+ const setCommand = new Command('set')
58
+ .description('Create or update an environment protection rule (repository admin)')
59
+ .argument('<repository>', 'owner/repo')
60
+ .argument('<name>', 'environment name, as the workflow spells it')
61
+ .option('--wait-timer <minutes>', 'delay before the job may start; 0 removes it', (v) => Number(v))
62
+ .option('--reviewer <who...>', 'user:<login>, team:<org/slug>, or a bare login (a user)')
63
+ .option('--branch-pattern <glob...>', 'refs permitted to deploy here (main, releases/*, v?.0)')
64
+ .option('--prevent-self-review', 'refuse an approval from whoever triggered the run')
65
+ .option('--allow-self-review', 'permit it (the default)')
66
+ .action(async (slug, name, opts) => {
67
+ const target = repository(slug);
68
+ if (!target)
69
+ return;
70
+ if (opts.waitTimer !== undefined && (!Number.isSafeInteger(opts.waitTimer) || opts.waitTimer < 0)) {
71
+ console.error(chalk.red('--wait-timer takes a whole number of minutes'));
72
+ process.exitCode = 2;
73
+ return;
74
+ }
75
+ let reviewers;
76
+ if (opts.reviewer !== undefined) {
77
+ reviewers = [];
78
+ for (const raw of opts.reviewer) {
79
+ const parsed = parseReviewer(raw);
80
+ if (!parsed) {
81
+ console.error(chalk.red(`expected user:<login>, team:<org/slug> or a bare login, got ${JSON.stringify(raw)}`));
82
+ process.exitCode = 2;
83
+ return;
84
+ }
85
+ reviewers.push(parsed);
86
+ }
87
+ }
88
+ // Unspecified stays unspecified: the API keeps whatever is stored, so
89
+ // adding a wait timer does not silently clear the reviewers.
90
+ const input = {
91
+ ...(opts.waitTimer !== undefined ? { waitTimerMinutes: opts.waitTimer } : {}),
92
+ ...(reviewers !== undefined ? { reviewers } : {}),
93
+ ...(opts.branchPattern !== undefined ? { branchPatterns: opts.branchPattern } : {}),
94
+ ...(opts.preventSelfReview === true ? { preventSelfReview: true } : opts.allowSelfReview === true ? { preventSelfReview: false } : {}),
95
+ };
96
+ const client = api();
97
+ if (!client)
98
+ return;
99
+ const outcome = await saveEnvironment(client, target.owner, target.repo, name, input);
100
+ if (outcome.kind === 'refused') {
101
+ console.error(chalk.red(outcome.detail));
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ console.log(`${chalk.green('✔')} ${outcome.environment.name}: ${environmentSummary(outcome.environment)}`);
106
+ });
107
+ const deleteCommand = new Command('delete')
108
+ .description('Remove an environment protection rule (repository admin)')
109
+ .argument('<repository>', 'owner/repo')
110
+ .argument('<name>', 'environment name')
111
+ .action(async (slug, name) => {
112
+ const target = repository(slug);
113
+ if (!target)
114
+ return;
115
+ const client = api();
116
+ if (!client)
117
+ return;
118
+ const outcome = await deleteEnvironment(client, target.owner, target.repo, name);
119
+ switch (outcome.kind) {
120
+ case 'not-found':
121
+ console.error(chalk.red(`no environment ${name} in ${slug}`));
122
+ process.exitCode = 1;
123
+ return;
124
+ case 'refused':
125
+ console.error(chalk.red(outcome.detail));
126
+ process.exitCode = 1;
127
+ return;
128
+ case 'deleted':
129
+ // Anything already waiting stays waiting: it was gated by rules that
130
+ // existed when it was gated (control-plane ADR-0018).
131
+ console.log(`${chalk.green('✔')} ${name} is no longer protected`);
132
+ console.log(chalk.dim('Deployments already waiting stay waiting; approve or reject them.'));
133
+ }
134
+ });
135
+ export default new Command('environment')
136
+ .description('Environments: the protection rules a workflow can only name')
137
+ .addCommand(listCommand)
138
+ .addCommand(setCommand)
139
+ .addCommand(deleteCommand);
@@ -0,0 +1,73 @@
1
+ import { hostname } from 'node:os';
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { login } from '../../src/application/authenticate.js';
5
+ import { TOKENS_PAGE, webBaseFor } from '../../src/domain/credentials.js';
6
+ import { FileCredentialStore, TOKEN_ENV, WEB_URL_ENV, apiUrlFrom, credentialsPath } from '../../src/infrastructure/credentials.js';
7
+ import { HttpLoginExchange, listenForCallback, newPkcePair, openInBrowser } from '../../src/infrastructure/loopback-login.js';
8
+ /**
9
+ * `apex login`: sign the CLI in with the browser a person already uses
10
+ * (control-plane ADR-0064, RFC 8252 + PKCE). The token it mints is the same
11
+ * one the dashboard mints, saved `0600` for this user alone.
12
+ *
13
+ * It opens a browser only because it was asked to — this command is the
14
+ * asking — and prints the URL either way, for a machine where none opens.
15
+ */
16
+ export default new Command('login')
17
+ .description('Sign the CLI in through the browser; the token is saved for this user only')
18
+ .option('--no-browser', 'print the URL to open instead of opening a browser')
19
+ .action(async (opts) => {
20
+ const apiUrl = apiUrlFrom();
21
+ const webBase = webBaseFor(apiUrl, process.env[WEB_URL_ENV]);
22
+ const store = new FileCredentialStore(credentialsPath());
23
+ let outcome;
24
+ try {
25
+ outcome = await login({
26
+ pkce: newPkcePair,
27
+ listen: (state) => listenForCallback(state),
28
+ openBrowser: opts.browser ? (url) => openInBrowser(url) : undefined,
29
+ announce: (url) => {
30
+ console.log(opts.browser ? 'Opening the browser to approve this login. If it does not open, visit:' : 'Open this URL to approve this login:');
31
+ console.log(` ${url}`);
32
+ console.log(chalk.dim('Waiting for the approval (5 minutes)…'));
33
+ },
34
+ exchange: new HttpLoginExchange(apiUrl),
35
+ store,
36
+ apiUrl,
37
+ webBase,
38
+ name: hostname().slice(0, 100) || 'apex CLI',
39
+ });
40
+ }
41
+ catch (err) {
42
+ console.error(chalk.red(`login failed: ${err instanceof Error ? err.message : String(err)}`));
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ switch (outcome.kind) {
47
+ case 'logged-in':
48
+ console.log(`${chalk.green('✔')} signed in as ${outcome.login} — the token expires ${outcome.expiresAt}`);
49
+ console.log(chalk.dim(`Saved to ${outcome.location}. Revoke it any time at ${webBase}${TOKENS_PAGE}.`));
50
+ if (process.env[TOKEN_ENV])
51
+ console.log(chalk.yellow(`${TOKEN_ENV} is set in this shell and is used before the saved login.`));
52
+ return;
53
+ case 'denied':
54
+ console.error(chalk.red(`the login was not approved: ${outcome.detail}`));
55
+ process.exitCode = 1;
56
+ return;
57
+ case 'state-mismatch':
58
+ console.error(chalk.red('a callback arrived that this login did not ask for (its state did not match), so nothing was saved; run `apex login` again'));
59
+ process.exitCode = 1;
60
+ return;
61
+ case 'malformed':
62
+ console.error(chalk.red('the callback carried no code, so nothing was saved; run `apex login` again'));
63
+ process.exitCode = 1;
64
+ return;
65
+ case 'timeout':
66
+ console.error(chalk.red('no approval arrived within 5 minutes; run `apex login` again'));
67
+ process.exitCode = 1;
68
+ return;
69
+ case 'refused':
70
+ console.error(chalk.red(`the control plane refused the login code: ${outcome.detail}`));
71
+ process.exitCode = 1;
72
+ }
73
+ });
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { logout } from '../../src/application/authenticate.js';
4
+ import { TOKENS_PAGE, webBaseFor } from '../../src/domain/credentials.js';
5
+ import { HttpControlPlane } from '../../src/infrastructure/control-plane-http.js';
6
+ import { FileCredentialStore, TOKEN_ENV, WEB_URL_ENV, apiUrlFrom, credentialsPath } from '../../src/infrastructure/credentials.js';
7
+ /**
8
+ * `apex logout`: revoke the token `apex login` saved, then forget it.
9
+ *
10
+ * The token is revoked at the control plane that minted it — never at whatever
11
+ * `APEX_API_URL` says now — with `DELETE /api/v1/me/token`, the one thing a
12
+ * token may do to itself (control-plane ADR-0064). The file goes whether or not
13
+ * the server could be reached; when it could not, the command says the token
14
+ * may still be valid and where to revoke it, rather than implying it is dead.
15
+ */
16
+ export default new Command('logout')
17
+ .description('Revoke the token `apex login` saved, and forget it')
18
+ .action(async () => {
19
+ const store = new FileCredentialStore(credentialsPath());
20
+ const tokensPage = `${webBaseFor(apiUrlFrom(), process.env[WEB_URL_ENV])}${TOKENS_PAGE}`;
21
+ let outcome;
22
+ try {
23
+ outcome = await logout(store, (saved) => new HttpControlPlane({ baseUrl: saved.apiUrl, token: saved.token }).revokeCurrentToken());
24
+ }
25
+ catch (err) {
26
+ console.error(chalk.red(`could not remove ${store.location}: ${err instanceof Error ? err.message : String(err)}`));
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ if (outcome.kind === 'nothing') {
31
+ console.log(`No saved login at ${outcome.location}; nothing to forget.`);
32
+ }
33
+ else if (outcome.revoked === true) {
34
+ console.log(`${chalk.green('✔')} revoked the saved token and removed ${outcome.location}`);
35
+ }
36
+ else {
37
+ console.log(`${chalk.green('✔')} removed ${outcome.location}`);
38
+ console.log(chalk.yellow(`The token could not be revoked (${outcome.revoked.failed}) and may still be valid — revoke it at ${tokensPage}.`));
39
+ process.exitCode = 1;
40
+ }
41
+ if (process.env[TOKEN_ENV])
42
+ console.log(chalk.yellow(`${TOKEN_ENV} is still set in this shell, and the CLI still uses it.`));
43
+ });
@@ -0,0 +1,125 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { readLogs } from '../../src/application/read-logs.js';
4
+ import { readLogView } from '../../src/application/search-logs.js';
5
+ import { HttpControlPlane, NotConfiguredError, RealSleeper } from '../../src/infrastructure/control-plane-http.js';
6
+ import { authorityNote, totalsPhrase } from '../../src/domain/logsearch.js';
7
+ import { parseRepository, unknownRunMessage } from '../../src/domain/runs.js';
8
+ /**
9
+ * The structured reading of one job's log.
10
+ *
11
+ * Every line printed here carries the log line it was read from, because this
12
+ * is a table of contents into the log rather than a replacement for it: it can
13
+ * be wrong, and `apex logs --job … | sed -n 'Np'` is the check.
14
+ */
15
+ const printView = (name, view) => {
16
+ console.log(chalk.bold(`\n── ${name} ──`));
17
+ if (view.views.length === 0) {
18
+ // No summary line from any runner we know: saying nothing is the answer,
19
+ // because a structured summary that is wrong looks authoritative.
20
+ console.log(chalk.dim(' no test runner recognised in this log'));
21
+ return;
22
+ }
23
+ for (const tool of view.views) {
24
+ console.log(` ${chalk.bold(tool.tool)} ${totalsPhrase(tool)} ${chalk.dim(`log line ${tool.line}`)}`);
25
+ for (const failure of tool.failures) {
26
+ const where = failure.where === undefined ? '' : chalk.dim(` ${failure.where}`);
27
+ console.log(` ${chalk.red('×')} ${failure.name}${where} ${chalk.dim(`L${failure.line}`)}`);
28
+ if (failure.message !== undefined)
29
+ console.log(chalk.dim(` ${failure.message}`));
30
+ }
31
+ if (tool.moreFailures !== undefined)
32
+ console.log(chalk.dim(` … ${tool.moreFailures} more failures`));
33
+ }
34
+ const note = authorityNote(view);
35
+ if (note !== undefined)
36
+ console.log(chalk.dim(` note: ${note}`));
37
+ };
38
+ const handler = async (run, options) => {
39
+ const repository = options.repo === undefined ? undefined : parseRepository(options.repo);
40
+ if (options.repo !== undefined && repository === undefined) {
41
+ console.error(chalk.red(`--repo must be owner/repo, got ${options.repo}`));
42
+ process.exitCode = 2;
43
+ return;
44
+ }
45
+ let api;
46
+ try {
47
+ api = new HttpControlPlane();
48
+ }
49
+ catch (err) {
50
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
51
+ process.exitCode = 2;
52
+ return;
53
+ }
54
+ let outcome;
55
+ try {
56
+ outcome = await readLogs(api, new RealSleeper(), {
57
+ run: run ?? 'latest',
58
+ repository,
59
+ job: options.job,
60
+ follow: options.follow,
61
+ });
62
+ }
63
+ catch (err) {
64
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
65
+ process.exitCode = 2;
66
+ return;
67
+ }
68
+ switch (outcome.kind) {
69
+ case 'no-runs':
70
+ console.error(chalk.yellow('no runs found'));
71
+ process.exitCode = 2;
72
+ return;
73
+ case 'unknown-run':
74
+ console.error(chalk.red(unknownRunMessage(outcome.selector)));
75
+ process.exitCode = 2;
76
+ return;
77
+ case 'unknown-job':
78
+ console.error(chalk.red(`no job matches ${outcome.selector}`));
79
+ console.error(chalk.dim(`jobs in this run: ${outcome.available.join(', ')}`));
80
+ process.exitCode = 2;
81
+ return;
82
+ case 'logs': {
83
+ if (options.summary === true) {
84
+ const views = [];
85
+ for (const section of outcome.sections) {
86
+ const read = await readLogView(api, section.job.id);
87
+ if (read.kind === 'refused') {
88
+ console.error(chalk.red(read.message));
89
+ process.exitCode = 2;
90
+ return;
91
+ }
92
+ views.push(read.view);
93
+ if (options.json !== true)
94
+ printView(section.job.name, read.view);
95
+ }
96
+ if (options.json === true)
97
+ console.log(JSON.stringify({ run: outcome.run, views }, null, 2));
98
+ process.exitCode = outcome.exitCode;
99
+ return;
100
+ }
101
+ if (options.json === true) {
102
+ console.log(JSON.stringify({ run: outcome.run, jobs: outcome.sections.map((s) => ({ job: s.job, log: s.text })) }, null, 2));
103
+ }
104
+ else {
105
+ const many = outcome.sections.length > 1;
106
+ for (const section of outcome.sections) {
107
+ // A header only where it disambiguates; one job's log stays pipeable.
108
+ if (many)
109
+ console.log(chalk.bold(`\n── ${section.job.name} (${section.job.conclusion ?? section.job.status}) ──`));
110
+ process.stdout.write(section.text);
111
+ }
112
+ }
113
+ process.exitCode = outcome.exitCode;
114
+ }
115
+ }
116
+ };
117
+ export default new Command('logs')
118
+ .description('Stream or fetch logs for a run or job')
119
+ .argument('[run]', 'run id, #number, or "latest"', 'latest')
120
+ .option('--repo <owner/repo>', 'limit the run search to one repository')
121
+ .option('--job <name>', 'one job by id, name, or name prefix (default: every job)')
122
+ .option('--follow', 'keep reading until the run completes')
123
+ .option('--summary', 'print what the log says about the tests it ran, instead of the log')
124
+ .option('--json', 'print the run and its logs as JSON')
125
+ .action(handler);
@@ -0,0 +1,46 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { serveLanguageServer } from '../../src/application/serve-language-server.js';
4
+ import { EngineBinaryLanguageServer } from '../../src/infrastructure/engine-binary.js';
5
+ /**
6
+ * `apex lsp` — the language server, for an editor to start.
7
+ *
8
+ * It takes no arguments and prints nothing to stdout, because stdout *is* the
9
+ * protocol: an editor spawns this and talks JSON-RPC down the pipe. Every
10
+ * message a person is meant to read goes to stderr, which the editor collects
11
+ * as the server's log.
12
+ *
13
+ * There is no `--port`: a language server is a child process, not a service.
14
+ * Running one for a whole machine would mean deciding whose file it is looking
15
+ * at, which is a question editors already answered by not asking it.
16
+ */
17
+ /**
18
+ * How to wire it in. Exported so a test can hold it to its promises: this is
19
+ * the only documentation most people will read about the feature, and an
20
+ * editor name that stops being right is worse than no example.
21
+ */
22
+ export const EDITOR_HELP = `
23
+ Configure your editor to run this for YAML files under .github/workflows.
24
+ It needs no control plane and no network: the engine parses the buffer in
25
+ front of you.
26
+
27
+ VS Code contributes.languages + a client that spawns \`apex lsp\`
28
+ Neovim vim.lsp.start({ cmd = { 'apex', 'lsp' } })
29
+ Helix [language-server.apex] command = "apex", args = ["lsp"]
30
+
31
+ Set APEX_ENGINE_BIN if apex-engine is not on your PATH.`;
32
+ export default new Command('lsp')
33
+ .description('Run the workflow language server on stdio, for an editor to start as a child process')
34
+ .addHelpText('after', EDITOR_HELP)
35
+ .action(async () => {
36
+ const outcome = await serveLanguageServer(new EngineBinaryLanguageServer());
37
+ if (outcome.kind === 'engine-missing') {
38
+ // Named as itself: an editor shows a non-zero exit as "the server
39
+ // crashed", which sends somebody looking in entirely the wrong place.
40
+ console.error(chalk.red(outcome.message));
41
+ process.exitCode = 2;
42
+ return;
43
+ }
44
+ if (outcome.code !== 0)
45
+ process.exitCode = outcome.code;
46
+ });
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { Command } from 'commander';
5
+ import Run from './run/main.js';
6
+ import Validate from './validate/main.js';
7
+ import Migrate from './migrate/main.js';
8
+ import Logs from './logs/main.js';
9
+ import Search from './search/main.js';
10
+ import Why from './why/main.js';
11
+ import Reproduce from './reproduce/main.js';
12
+ import Timing from './timing/main.js';
13
+ import Tests from './tests/main.js';
14
+ import Rerun from './rerun/main.js';
15
+ import Spend from './spend/main.js';
16
+ import Dispatch from './dispatch/main.js';
17
+ import Runner from './runner/main.js';
18
+ import Cache from './cache/main.js';
19
+ import Artifact from './artifact/main.js';
20
+ import Environment from './environment/main.js';
21
+ import Deployment from './deployment/main.js';
22
+ import Approval from './approval/main.js';
23
+ import Schedule from './schedule/main.js';
24
+ import Oidc from './oidc/main.js';
25
+ import Actions from './actions/main.js';
26
+ import Terms from './terms/main.js';
27
+ import Notify from './notify/main.js';
28
+ import Lsp from './lsp/main.js';
29
+ import Login from './login/main.js';
30
+ import Logout from './logout/main.js';
31
+ import Whoami from './whoami/main.js';
32
+ import Secret from './secret/main.js';
33
+ import Variable from './variable/main.js';
34
+ /**
35
+ * The version, from the nearest `package.json` rather than from `VERSION`.
36
+ *
37
+ * `VERSION` is the source of truth in the repository, and it is not in the published package —
38
+ * nor should it be. What ships is `package.json`, npm guarantees it is there, and `version:check`
39
+ * guarantees it agrees with `VERSION`. Reading it by walking up also survives the layout changing
40
+ * underfoot: `cli/main.ts` under tsx and `dist/cli/main.js` from a tarball sit at different depths,
41
+ * and the previous `../VERSION` was correct for exactly one of them.
42
+ */
43
+ const readVersion = () => {
44
+ for (let dir = import.meta.dirname;; dir = dirname(dir)) {
45
+ try {
46
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
47
+ }
48
+ catch {
49
+ if (dirname(dir) === dir)
50
+ return '0.0.0-unknown';
51
+ }
52
+ }
53
+ };
54
+ const version = readVersion();
55
+ const program = new Command()
56
+ .name('apex')
57
+ .description('Apex Actions — developer and operator CLI')
58
+ .version(version)
59
+ .addCommand(Run)
60
+ .addCommand(Validate)
61
+ .addCommand(Migrate)
62
+ .addCommand(Logs)
63
+ .addCommand(Search)
64
+ .addCommand(Why)
65
+ .addCommand(Reproduce)
66
+ .addCommand(Timing)
67
+ .addCommand(Tests)
68
+ .addCommand(Rerun)
69
+ .addCommand(Spend)
70
+ .addCommand(Dispatch)
71
+ .addCommand(Runner)
72
+ .addCommand(Cache)
73
+ .addCommand(Artifact)
74
+ .addCommand(Environment)
75
+ .addCommand(Deployment)
76
+ .addCommand(Approval)
77
+ .addCommand(Schedule)
78
+ .addCommand(Oidc)
79
+ .addCommand(Actions)
80
+ .addCommand(Terms)
81
+ .addCommand(Notify)
82
+ .addCommand(Login)
83
+ .addCommand(Logout)
84
+ .addCommand(Whoami)
85
+ .addCommand(Secret)
86
+ .addCommand(Variable)
87
+ .addCommand(Lsp);
88
+ program.parse(process.argv);