argocd-axi 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Laszlo Horanszky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,244 @@
1
- # Temporary Holding Version
1
+ # argocd-axi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Read-only Argo CD for agents. `argocd-axi` wraps the `argocd` CLI and answers
4
+ the questions an agent asks during a deployment problem (which apps need
5
+ attention and why, what differs from Git, what was deployed when, what the
6
+ pods are logging, whether a sync is allowed right now) in compact
7
+ [TOON](https://toonformat.dev/) output that follows the
8
+ [AXI](https://github.com/kunchenguid/axi) conventions.
9
+
10
+ ```
11
+ $ argocd-axi
12
+ bin: ~/.local/bin/argocd-axi
13
+ description: Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows
14
+ context: prod
15
+ server: argocd.example.com
16
+ auth: argocd CLI session for this context
17
+ user: alice
18
+ apps: "142 total, 3 need attention, 1 operation running"
19
+ writableContexts: none
20
+ attention[3]{app,project,sync,health,why}:
21
+ payments-api,payments,OutOfSync,Degraded,"OutOfSync; Degraded: Deployment \"payments-api\" exceeded its progress deadline; last sync Failed: ..."
22
+ legacy,default,Unknown,Unknown,"sync Unknown; health Unknown; ComparisonError: ..."
23
+ checkout,shop,Synced,Progressing,Progressing for 25m
24
+ operations[1]{app,phase,started}:
25
+ checkout,Running,25m ago
26
+ help[2]:
27
+ Run `argocd-axi apps view <app>` to diagnose an application
28
+ Run `argocd-axi apps` to list every application
29
+ ```
30
+
31
+ ## Why
32
+
33
+ The raw `argocd` CLI is built for people. An agent using it has to cope with
34
+ wide tables or very large JSON, `app diff` exiting 1 when it finds drift,
35
+ `app history` having no JSON output, prompts and browser logins, `logs -f` and
36
+ `app wait` that can hang, and Secret data in `app manifests --source live`.
37
+ argocd-axi turns that into small, definitive answers:
38
+
39
+ - **Problems first.** Every list sorts the apps that need attention to the
40
+ top and says why, in one line, with `count: N of M total`.
41
+ - **Summary first.** Diffs and manifests come back as a per-resource list;
42
+ the raw text is one `--full` away.
43
+ - **Read-only.** v1 cannot sync, roll back, refresh, delete or change
44
+ anything. Those verbs answer `REFUSED` with the raw `argocd` command a human
45
+ can run. Writes are on the [roadmap](ROADMAP.md).
46
+ - **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
47
+ parameters are redacted and counted. No flag reveals them.
48
+ - **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
49
+ block because argocd never gets a stdin.
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ npm install -g argocd-axi
55
+ ```
56
+
57
+ Or run it without installing: `npx -y argocd-axi`.
58
+
59
+ Requirements:
60
+
61
+ - Node.js 20 or newer.
62
+ - The [`argocd` CLI](https://argo-cd.readthedocs.io/en/stable/cli_installation/)
63
+ on `PATH`, ideally matching your server's version. Tested with argocd
64
+ v3.3.6. There is no hard minimum; `ctx` and the home view warn when the CLI
65
+ and server differ by a major version.
66
+
67
+ ## Credentials
68
+
69
+ argocd-axi adds no auth of its own. It uses whatever the `argocd` CLI already
70
+ has: the current context's session, or `ARGOCD_SERVER` and
71
+ `ARGOCD_AUTH_TOKEN` from the environment. It never starts a login, never
72
+ prompts, and never reads the argocd config file (`~/.config/argocd/config`,
73
+ which holds clear-text tokens). When a session is missing or expired, it
74
+ answers `AUTH_REQUIRED` and tells the agent that a human runs
75
+ `argocd login <server> --sso` or `argocd relogin`.
76
+
77
+ An agent that uses your own SSO session has all of your Argo CD permissions.
78
+ The recommended agent credential is a **short-lived project-role token**,
79
+ scoped to read (and later, sync only where you want it), so the server bounds
80
+ what the agent can do whatever any tool does:
81
+
82
+ ```sh
83
+ # A human runs these, once per project and role:
84
+ argocd proj role create payments agent-read
85
+ argocd proj role add-policy payments agent-read --action get --permission allow --object '*'
86
+ argocd proj role add-policy payments agent-read --action get --permission allow --object '*' --resource logs
87
+ export ARGOCD_AUTH_TOKEN="$(argocd proj role create-token payments agent-read --expires-in 8h --token-only)"
88
+ ```
89
+
90
+ A project role only adds permissions. Argo CD also grants every token the
91
+ server's `policy.default` role (from `argocd-rbac-cm`), so with
92
+ `policy.default: role:readonly` this token can read every project, not just
93
+ `payments`. For a token that is really project-scoped, keep `policy.default`
94
+ empty or minimal and review the effective policies in `argocd-rbac-cm`.
95
+ `can-i` checks one object literally (`'other-project/*'` is not "any app in
96
+ other-project"), so a `no` there proves nothing about other apps; spot-check
97
+ concrete apps instead, e.g. `argocd-axi can-i get applications
98
+ 'other-project/some-app'`.
99
+
100
+ argocd-axi itself refuses `proj role create-token`, `account generate-token`
101
+ and every other command that prints or changes a credential.
102
+
103
+ ## Agent integration
104
+
105
+ Pick one; you do not need both.
106
+
107
+ - **Session hook (recommended).** `argocd-axi setup hooks` installs a
108
+ SessionStart hook for Claude Code, Codex and OpenCode. The hook runs
109
+ `argocd-axi-ctx`, which is local only: current context, server, CLI version
110
+ and the cached server version. It makes no network call, triggers no SSO
111
+ refresh and reads no credential, so a dead context can never hang a session
112
+ start. It needs a persistent install (`npm install -g argocd-axi`); run from
113
+ an npx cache, it answers `REFUSED` because the hook would point at a
114
+ temporary path.
115
+ - **Skill.** `npx skills add Dematom1/argocd-axi --skill argocd-axi`
116
+ installs [`skills/argocd-axi/SKILL.md`](skills/argocd-axi/SKILL.md), which
117
+ is generated from the CLI's own help and loads when a task is about Argo CD.
118
+
119
+ ## Usage
120
+
121
+ ```
122
+ argocd-axi live overview (needs the server)
123
+ argocd-axi ctx [--context <c>] current context, server, CLI version (local only)
124
+ argocd-axi ctx use <context> switch the argocd CLI's current context
125
+ argocd-axi whoami user, issuer and groups
126
+ argocd-axi can-i <action> <resource> <obj> ask Argo CD RBAC, e.g. can-i sync applications 'payments/payments-api'
127
+ argocd-axi apps [--problems --project --selector --repo --cluster --sync --health --limit --fields]
128
+ argocd-axi apps view <app> [--full]
129
+ argocd-axi apps resources <app> [--problems | --orphaned]
130
+ argocd-axi apps diff <app> [--revision <rev>] [--full]
131
+ argocd-axi apps history <app> [--limit <n>]
132
+ argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
133
+ argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [-p]
134
+ argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
135
+ argocd-axi apps actions <app> [--kind <k>]
136
+ argocd-axi projects | projects view <p> | projects windows <p>
137
+ argocd-axi appsets | appsets view <name> | appsets generate <file>
138
+ argocd-axi repos | clusters
139
+ argocd-axi setup hooks
140
+ argocd-axi update [--check]
141
+ ```
142
+
143
+ Every command takes `--context <name>` to target another Argo CD context, and
144
+ `--help` for its flags and examples. Flags always come after the command.
145
+ Apps in other namespaces are addressed as `namespace/name`.
146
+
147
+ ### What "needs attention" means
148
+
149
+ An app needs attention when its sync status is OutOfSync or Unknown; its
150
+ health is Degraded, Missing or Unknown; it has been Progressing for more than
151
+ 10 minutes; its last sync Failed or Errored; or it has conditions. Both axes
152
+ and the reason are always shown, and Progressing is never folded into
153
+ healthy. When Argo CD does not report when health turned Progressing, the
154
+ duration is shown as unknown and the app is not counted as needing attention.
155
+
156
+ ### Sync windows
157
+
158
+ `projects windows <project>` lists each window, whether it is active now, and
159
+ when an inactive one next opens, then says whether automated and manual syncs
160
+ are allowed right now. It evaluates the windows the way Argo CD does (5-field
161
+ cron schedule or a descriptor such as `@daily` or `@every 1h`, duration, the
162
+ window's time zone or UTC). When a window that could change the outcome cannot
163
+ be evaluated, the verdict is `unknown` and names that window. The verdict assumes
164
+ every window applies; check each window's apps, namespaces and clusters for a
165
+ specific app.
166
+
167
+ ## Output and exit codes
168
+
169
+ - Output is TOON on stdout, errors included: `error`, `code`, and `help` with
170
+ the next command to run. There is no `--json`; the raw `argocd` CLI is the
171
+ fallback for anything argocd-axi does not cover.
172
+ - Exit 0 means the command did what was asked. An OutOfSync or Degraded app,
173
+ drift found by `apps diff`, a `can-i` answer of no, and an `apps wait` that
174
+ timed out are all data with exit 0.
175
+ - Exit 1 is an error, exit 2 a usage error (unknown flag, missing argument).
176
+ Unknown flags are rejected before any argocd call, with the valid flags
177
+ listed.
178
+
179
+ | Code | Meaning |
180
+ | --------------------------------- | -------------------------------------------------------------------------------------- |
181
+ | `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
182
+ | `AUTH_REQUIRED` | No session or an expired one; a human logs in |
183
+ | `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
184
+ | `NOT_FOUND` | No such app, project or ApplicationSet |
185
+ | `RATE_LIMITED` | The server asked to slow down |
186
+ | `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
187
+ | `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
188
+ | `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
189
+ | `TIMEOUT` | argocd did not answer in time |
190
+ | `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
191
+ | `CONFIG_INVALID` | argocd-axi's own config file is malformed |
192
+ | `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
193
+
194
+ ## Safety model
195
+
196
+ - **One subprocess seam.** Every argocd call goes through `src/argocd.ts`,
197
+ which accepts only operations from the allowlist registry in
198
+ `src/registry.ts`. Every registered operation is a server read or a
199
+ local-config operation. The seam also refuses streaming, refresh, prompt,
200
+ local-file and delete flags whatever a command passes, gives argocd no
201
+ stdin, and enforces a timeout and an output cap. Tests fail if a new
202
+ operation becomes reachable without updating the registry.
203
+ - **A refusal registry.** Known writes and credential commands answer
204
+ `REFUSED` with a reason and the raw command a human can run instead of
205
+ failing blind.
206
+ - **Redaction.** Kubernetes Secret `data`, `stringData` and their
207
+ last-applied annotation, values under sensitive-looking keys, and env-var or
208
+ Helm-parameter pairs with sensitive-looking names are replaced with
209
+ `[redacted]`, and the output says how many values were hidden. Free text
210
+ (log lines, diff text) is redacted line by line on a best-effort basis:
211
+ `key=value` pairs, bearer tokens, JWTs and URL passwords. Repository and
212
+ cluster rows are built from an explicit field list, so credentials never
213
+ reach them.
214
+ - **Writable contexts.** argocd-axi's own config file,
215
+ `~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
216
+ holds the allowlist of contexts where the future write tier may execute.
217
+ It defaults to none, and v1 only displays it:
218
+
219
+ ```yaml
220
+ writable-contexts:
221
+ - staging
222
+ ```
223
+
224
+ - **Version cache.** The server version is checked at most once every 12
225
+ hours per context and server, and cached in `~/.cache/argocd-axi/`
226
+ (`$XDG_CACHE_HOME`). This file holds version strings only.
227
+
228
+ ## Development
229
+
230
+ ```sh
231
+ npm install
232
+ npm run check # format, lint, typecheck, tests, skill drift check
233
+ ```
234
+
235
+ Tests never touch a real Argo CD server, cluster, kubeconfig or credential.
236
+ They run the built CLI as a user would, with a fake `argocd`
237
+ (`test/fake-argocd/`) as the only executable on `PATH`. The fake answers from
238
+ the recorded fixtures in `test/fixtures/`, and each test gets a throwaway
239
+ `HOME`. After changing help text, run `npm run build:skill` to regenerate the
240
+ skill.
241
+
242
+ ## License
243
+
244
+ [MIT](LICENSE)
package/ROADMAP.md ADDED
@@ -0,0 +1,96 @@
1
+ # Roadmap
2
+
3
+ argocd-axi v1 is read-only by design: the read surface settles first, and the
4
+ two commands that can hurt production (sync and rollback) come next, behind a
5
+ gate. This file records what is planned and the rules each step must follow.
6
+
7
+ ## Next: the write tier
8
+
9
+ These verbs exist in v1 only as structured `REFUSED` answers that name the raw
10
+ `argocd` command a human can run. The write tier turns them into commands:
11
+
12
+ | Command | Wraps | Notes |
13
+ | ------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `apps sync <app> --context <c> [--prune] [--revision <rev>] [--resource <group:kind:name>]` | `argocd app sync` | Preview from `--dry-run`, the per-resource diff, the sync-window verdict and an RBAC check (`account can-i sync`) |
15
+ | `apps rollback <app> <history-id> --context <c>` | `argocd app rollback` | No native dry run, so the preview is synthesized: target history entry against the current one. Refused while auto-sync is enabled, as Argo CD requires |
16
+ | `apps terminate-op <app> --context <c>` | `argocd app terminate-op` | No-op with exit 0 when no operation is running |
17
+ | `apps refresh <app> [--hard] --context <c>` | `argocd app get --refresh` | Triggers a reconcile, so it is a write, not a read |
18
+
19
+ Later, a third tier: `apps action run <app> <action> --kind <kind>` for
20
+ resource actions such as restart, and Argo Rollouts promote and abort
21
+ through the same path.
22
+
23
+ ### Rules every write follows
24
+
25
+ The gate is already built and unit-tested in `src/gate.ts`, with its
26
+ allowlist loaded from `src/config.ts`. The write tier only adds the commands
27
+ and calls the gate.
28
+
29
+ 1. **Preview by default.** Without the command's execute flag, nothing is
30
+ sent. The preview shows the target context and server, the object, the
31
+ change set and the preflight checks, plus the exact command that executes
32
+ it. Previews work against any context.
33
+ 2. **A local allowlist of writable contexts, default none.** Executing needs
34
+ the context listed in argocd-axi's own config file,
35
+ `~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`):
36
+
37
+ ```yaml
38
+ writable-contexts:
39
+ - staging
40
+ ```
41
+
42
+ A production context stays read-only until a human opts it in locally.
43
+ `ctx` and the home view already show `writableContexts: none`.
44
+
45
+ 3. **An explicit target.** Every write names `--context <name>`; the current
46
+ context is never used implicitly, and the result echoes the context and
47
+ server.
48
+ 4. **Argo CD's own vocabulary.** The execute flag and every other flag are
49
+ named after Argo CD's terms for each command, not a shared cross-tool
50
+ vocabulary. The exact execute-flag spelling is settled when each command is
51
+ built.
52
+ 5. **Idempotent.** Asking for a state that already exists (syncing an app
53
+ that is already synced to the target revision, terminating when nothing
54
+ runs) is a no-op with exit 0. Exit 1 only when the intent cannot be met:
55
+ permission denied (`FORBIDDEN`), a sync window is closed
56
+ (`SYNC_WINDOW_BLOCKED`), auto-sync blocks a rollback.
57
+ 6. **Attributed.** Every sync carries `--info initiated-by=argocd-axi`, so
58
+ the app's audit trail shows agent syncs. Whether `--info` is accepted
59
+ under restricted RBAC is checked in the first real verification pass.
60
+ 7. **Bounded.** A write that waits does so for about 120 seconds (`--timeout`)
61
+ and then returns the current operation state with a hint to wait again.
62
+ Nothing streams.
63
+ 8. **Registered.** Each new argocd invocation is added to the allowlist
64
+ registry in `src/registry.ts` under a write tier, and the registry test is
65
+ updated in the same change.
66
+
67
+ Everything else that writes (app create, set, patch, edit and delete,
68
+ resource patch and delete, project, repository, cluster, certificate and GPG
69
+ changes, credential and token commands, `admin`) stays refused.
70
+
71
+ ## Before output shapes lock: a real verification pass
72
+
73
+ All tests run against a fake `argocd` with recorded fixtures. Before the
74
+ output shapes are considered stable, one pass against a real, disposable
75
+ Argo CD (a local kind or k3d cluster) checks what the fixtures could not:
76
+
77
+ - the JSON field coverage of `app get -o json` and `app list -o json`;
78
+ - the text layout of `app diff` section headers and the `app resources
79
+ --orphaned` table;
80
+ - the JSON shape of `app actions list -o json` and `proj windows list -o json`;
81
+ - how argocd reports sync-window blocks and expired sessions;
82
+ - `--info` under restricted RBAC.
83
+
84
+ Each such pass is a separate task that asks the maintainer first.
85
+
86
+ ## Tuning once instance facts are known
87
+
88
+ Default list limits, the attention threshold for Progressing apps
89
+ (10 minutes) and which features need first-class handling (ApplicationSets,
90
+ app-of-apps, apps in any namespace) are tuned once the target Argo CD
91
+ instances (version, SSO provider, app count) are known.
92
+
93
+ ## Publishing
94
+
95
+ npm publication and submission to the
96
+ [AXI community catalog](https://github.com/kunchenguid/axi) come after v1.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ // Dedicated SessionStart-hook entrypoint (decision S17). The SDK hook runner
3
+ // spawns a bare command with no arguments; pointing it here instead of at the
4
+ // networked home view guarantees ambient context never calls the server,
5
+ // never triggers an SSO refresh and never reads a credential.
6
+ import { ctxView } from "../src/commands/ctx.js";
7
+ try {
8
+ process.stdout.write(`${await ctxView([])}\n`);
9
+ }
10
+ catch {
11
+ // Ambient context must never break session start; degrade to silence.
12
+ process.exitCode = 0;
13
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { tryFastPath } from "axi-sdk-js/fast-path";
3
+ import { VERSION } from "../src/version.js";
4
+ if (!tryFastPath(process.argv.slice(2), { version: VERSION })) {
5
+ const { main } = await import("../src/cli.js");
6
+ await main();
7
+ }
@@ -0,0 +1,118 @@
1
+ import { spawn } from "node:child_process";
2
+ import { CODES, axiError, mapArgocdFailure, } from "./errors.js";
3
+ import { FORBIDDEN_ARGS, OPERATIONS } from "./registry.js";
4
+ /**
5
+ * The one subprocess seam (design decisions A1 and S5). Every argocd call in
6
+ * argocd-axi goes through `runArgocd`, which only accepts operations from the
7
+ * allowlist registry, refuses forbidden arguments, never connects stdin (so
8
+ * no prompt can ever block), and enforces a hard timeout and output cap.
9
+ */
10
+ const MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
11
+ const KILL_GRACE_MS = 2_000;
12
+ export function buildArgv(op, args, context) {
13
+ const operation = OPERATIONS[op];
14
+ for (const arg of args) {
15
+ const name = arg.startsWith("--") ? arg.split("=")[0] : arg;
16
+ if (name !== undefined && FORBIDDEN_ARGS.includes(name)) {
17
+ throw axiError(`internal guard: argument ${name} is never passed to argocd`, CODES.REFUSED);
18
+ }
19
+ }
20
+ const argv = [...operation.argv, ...args];
21
+ if (context !== undefined) {
22
+ argv.push("--argocd-context", context);
23
+ }
24
+ return argv;
25
+ }
26
+ export async function runArgocd(op, args, options = {}) {
27
+ if (!Object.hasOwn(OPERATIONS, op)) {
28
+ throw axiError(`internal guard: ${String(op)} is not a registered argocd operation`, CODES.REFUSED);
29
+ }
30
+ const argv = buildArgv(op, args, options.context);
31
+ const timeoutMs = options.timeoutMs ?? OPERATIONS[op].timeoutMs;
32
+ const result = await spawnCollect("argocd", argv, timeoutMs);
33
+ if (result.timedOut) {
34
+ throw axiError(`argocd did not answer within ${Math.round(timeoutMs / 1000)}s`, CODES.TIMEOUT, [
35
+ "Run `argocd-axi ctx` to confirm which server this context targets",
36
+ "Retry the command; if it keeps timing out, check network access to the server",
37
+ ]);
38
+ }
39
+ if (result.exitCode !== 0 && !options.allowFailure) {
40
+ throw mapArgocdFailure(result.stderr || result.stdout, options.failure);
41
+ }
42
+ return result;
43
+ }
44
+ /** Run an operation whose stdout is JSON and parse it. */
45
+ export async function argocdJson(op, args, options = {}) {
46
+ const result = await runArgocd(op, args, options);
47
+ try {
48
+ return JSON.parse(result.stdout);
49
+ }
50
+ catch {
51
+ throw axiError(`argocd returned output that is not JSON for ${op}`, CODES.UPSTREAM_ERROR, [
52
+ "Check that the argocd CLI version matches the server (`argocd-axi ctx`)",
53
+ ]);
54
+ }
55
+ }
56
+ function spawnCollect(bin, argv, timeoutMs) {
57
+ return new Promise((resolve, reject) => {
58
+ const child = spawn(bin, argv, {
59
+ stdio: ["ignore", "pipe", "pipe"],
60
+ env: process.env,
61
+ windowsHide: true,
62
+ });
63
+ const stdout = [];
64
+ const stderr = [];
65
+ let bytes = 0;
66
+ let timedOut = false;
67
+ let overflow = false;
68
+ let settled = false;
69
+ const kill = () => {
70
+ child.kill("SIGTERM");
71
+ setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
72
+ };
73
+ const timer = setTimeout(() => {
74
+ timedOut = true;
75
+ kill();
76
+ }, timeoutMs);
77
+ const collect = (sink) => (chunk) => {
78
+ bytes += chunk.length;
79
+ if (bytes > MAX_OUTPUT_BYTES) {
80
+ overflow = true;
81
+ kill();
82
+ return;
83
+ }
84
+ sink.push(chunk);
85
+ };
86
+ child.stdout.on("data", collect(stdout));
87
+ child.stderr.on("data", collect(stderr));
88
+ child.on("error", (error) => {
89
+ clearTimeout(timer);
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ if (error.code === "ENOENT") {
94
+ reject(axiError("The argocd CLI is not installed or not on PATH", CODES.ARGOCD_NOT_INSTALLED, [
95
+ "A human installs the argocd CLI, ideally the version matching the server: https://argo-cd.readthedocs.io/en/stable/cli_installation/",
96
+ ]));
97
+ return;
98
+ }
99
+ reject(axiError(`could not start argocd: ${error.message}`, CODES.UPSTREAM_ERROR));
100
+ });
101
+ child.on("close", (code) => {
102
+ clearTimeout(timer);
103
+ if (settled)
104
+ return;
105
+ settled = true;
106
+ if (overflow) {
107
+ reject(axiError(`argocd produced more than ${MAX_OUTPUT_BYTES / 1024 / 1024} MB of output`, CODES.UPSTREAM_ERROR, ["Narrow the request (for example with --project or --kind)"]));
108
+ return;
109
+ }
110
+ resolve({
111
+ stdout: Buffer.concat(stdout).toString("utf8"),
112
+ stderr: Buffer.concat(stderr).toString("utf8"),
113
+ exitCode: code ?? 1,
114
+ timedOut,
115
+ });
116
+ });
117
+ });
118
+ }