argocd-axi 0.0.0-stage → 0.1.2

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,280 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">argocd-axi</h1>
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
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/argocd-axi"><img alt="npm" src="https://img.shields.io/npm/v/argocd-axi?style=flat-square" /></a>
5
+ <a href="https://github.com/Dematom1/argocd-axi/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/Dematom1/argocd-axi/ci.yml?style=flat-square&label=ci" /></a>
6
+ <a href="https://github.com/Dematom1/argocd-axi/actions/workflows/release-please.yml"><img alt="Release" src="https://img.shields.io/github/actions/workflow/status/Dematom1/argocd-axi/release-please.yml?style=flat-square&label=release" /></a>
7
+ <a href="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue?style=flat-square"><img alt="Platform" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue?style=flat-square" /></a>
8
+ <a href="https://x.com/dematom1"><img alt="X" src="https://img.shields.io/badge/X-@dematom1-black?style=flat-square" /></a>
9
+ </p>
10
+
11
+ Read-only Argo CD for agents. `argocd-axi` wraps the `argocd` CLI and answers
12
+ the questions an agent asks during a deployment problem (which apps need
13
+ attention and why, what differs from Git, what was deployed when, what the
14
+ pods are logging, whether a sync is allowed right now) in compact
15
+ [TOON](https://toonformat.dev/) output that follows the
16
+ [AXI](https://github.com/kunchenguid/axi) conventions.
17
+
18
+ ```
19
+ $ argocd-axi
20
+ bin: ~/.local/bin/argocd-axi
21
+ description: Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows
22
+ context: prod
23
+ server: argocd.example.com
24
+ auth: argocd CLI session for this context
25
+ user: alice
26
+ apps: "142 total, 3 need attention, 1 operation running"
27
+ writableContexts: none
28
+ attention[3]{app,project,sync,health,why}:
29
+ payments-api,payments,OutOfSync,Degraded,"OutOfSync; Degraded: Deployment \"payments-api\" exceeded its progress deadline; last sync Failed: ..."
30
+ legacy,default,Unknown,Unknown,"sync Unknown; health Unknown; ComparisonError: ..."
31
+ checkout,shop,Synced,Progressing,Progressing for 25m
32
+ operations[1]{app,phase,started}:
33
+ checkout,Running,25m ago
34
+ help[2]:
35
+ Run `argocd-axi apps view <app>` to diagnose an application
36
+ Run `argocd-axi apps` to list every application
37
+ ```
38
+
39
+ ## Why
40
+
41
+ The raw `argocd` CLI is built for people. An agent using it has to cope with
42
+ wide tables or very large JSON, `app diff` exiting 1 when it finds drift,
43
+ `app history` having no JSON output, prompts and browser logins, `logs -f` and
44
+ `app wait` that can hang, and Secret data in `app manifests --source live`.
45
+ argocd-axi turns that into small, definitive answers:
46
+
47
+ - **Problems first.** Every list sorts the apps that need attention to the
48
+ top and says why, in one line, with `count: N of M total`.
49
+ - **Summary first.** Diffs and manifests come back as a per-resource list;
50
+ the raw text is one `--full` away.
51
+ - **Read-only.** v1 cannot sync, roll back, refresh, delete or change
52
+ anything. Those verbs answer `REFUSED` with the raw `argocd` command a human
53
+ can run. Writes are on the [roadmap](ROADMAP.md).
54
+ - **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
55
+ parameters are redacted and counted. No flag reveals them.
56
+ - **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
57
+ block because argocd never gets a stdin.
58
+
59
+ ## Install
60
+
61
+ ```sh
62
+ npm install -g argocd-axi
63
+ ```
64
+
65
+ Or run it without installing: `npx -y argocd-axi`.
66
+
67
+ Requirements:
68
+
69
+ - Node.js 22.12 or newer.
70
+ - The [`argocd` CLI](https://argo-cd.readthedocs.io/en/stable/cli_installation/)
71
+ on `PATH`, ideally matching your server's version. Tested with argocd
72
+ v3.3.6. There is no hard minimum; `ctx` and the home view warn when the CLI
73
+ and server differ by a major version.
74
+
75
+ ## Credentials
76
+
77
+ argocd-axi adds no auth of its own. It uses whatever the `argocd` CLI already
78
+ has: the current context's session, or `ARGOCD_SERVER` and
79
+ `ARGOCD_AUTH_TOKEN` from the environment. It never starts a login, never
80
+ prompts, and never reads the argocd config file (`~/.config/argocd/config`,
81
+ which holds clear-text tokens). When a session is missing or expired, it
82
+ answers `AUTH_REQUIRED` and tells the agent that a human runs
83
+ `argocd login <server> --sso` or `argocd relogin`.
84
+
85
+ An agent that uses your own SSO session has all of your Argo CD permissions.
86
+ The recommended agent credential is a **short-lived project-role token**,
87
+ scoped to read (and later, sync only where you want it), so the server bounds
88
+ what the agent can do whatever any tool does:
89
+
90
+ ```sh
91
+ # A human runs these, once per project and role:
92
+ argocd proj role create payments agent-read
93
+ argocd proj role add-policy payments agent-read --action get --permission allow --object '*'
94
+ argocd proj role add-policy payments agent-read --action get --permission allow --object '*' --resource logs
95
+ export ARGOCD_AUTH_TOKEN="$(argocd proj role create-token payments agent-read --expires-in 8h --token-only)"
96
+ ```
97
+
98
+ A project role only adds permissions. Argo CD also grants every token the
99
+ server's `policy.default` role (from `argocd-rbac-cm`), so with
100
+ `policy.default: role:readonly` this token can read every project, not just
101
+ `payments`. For a token that is really project-scoped, keep `policy.default`
102
+ empty or minimal and review the effective policies in `argocd-rbac-cm`.
103
+ `can-i` checks one object literally (`'other-project/*'` is not "any app in
104
+ other-project"), so a `no` there proves nothing about other apps; spot-check
105
+ concrete apps instead, e.g. `argocd-axi can-i get applications
106
+ 'other-project/some-app'`.
107
+
108
+ argocd-axi itself refuses `proj role create-token`, `account generate-token`
109
+ and every other command that prints or changes a credential.
110
+
111
+ ## Agent integration
112
+
113
+ Pick one; you do not need both.
114
+
115
+ - **Session hook (recommended).** `argocd-axi setup hooks` installs a
116
+ SessionStart hook for Claude Code, Codex and OpenCode. The hook runs
117
+ `argocd-axi-ctx`, which is local only: current context, server, CLI version
118
+ and the cached server version. It makes no network call, triggers no SSO
119
+ refresh and reads no credential, so a dead context can never hang a session
120
+ start. It needs a persistent install (`npm install -g argocd-axi`); run from
121
+ an npx cache, it answers `REFUSED` because the hook would point at a
122
+ temporary path.
123
+ - **Skill.** `npx skills add Dematom1/argocd-axi --skill argocd-axi`
124
+ installs [`skills/argocd-axi/SKILL.md`](skills/argocd-axi/SKILL.md), which
125
+ is generated from the CLI's own help and loads when a task is about Argo CD.
126
+
127
+ ## Usage
128
+
129
+ ```
130
+ argocd-axi live overview (needs the server)
131
+ argocd-axi ctx [--context <c>] current context, server, CLI version (local only)
132
+ argocd-axi ctx use <context> switch the argocd CLI's current context
133
+ argocd-axi whoami user, issuer and groups
134
+ argocd-axi can-i <action> <resource> <obj> ask Argo CD RBAC, e.g. can-i sync applications 'payments/payments-api'
135
+ argocd-axi apps [--problems --project --selector --repo --cluster --sync --health --limit --fields]
136
+ argocd-axi apps view <app> [--full]
137
+ argocd-axi apps resources <app> [--problems | --orphaned]
138
+ argocd-axi apps diff <app> [--revision <rev>] [--full]
139
+ argocd-axi apps history <app> [--limit <n>]
140
+ argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
141
+ argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [-p]
142
+ argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
143
+ argocd-axi apps actions <app> [--kind <k>]
144
+ argocd-axi projects | projects view <p> | projects windows <p>
145
+ argocd-axi appsets | appsets view <name> | appsets generate <file>
146
+ argocd-axi repos | clusters
147
+ argocd-axi setup hooks
148
+ argocd-axi update [--check]
149
+ ```
150
+
151
+ Every command takes `--context <name>` to target another Argo CD context, and
152
+ `--help` for its flags and examples. Flags always come after the command.
153
+ Apps in other namespaces are addressed as `namespace/name`.
154
+
155
+ ### What "needs attention" means
156
+
157
+ An app needs attention when its sync status is OutOfSync or Unknown; its
158
+ health is Degraded, Missing or Unknown; it has been Progressing for more than
159
+ 10 minutes; its last sync Failed or Errored; or it has conditions. Both axes
160
+ and the reason are always shown, and Progressing is never folded into
161
+ healthy. When Argo CD does not report when health turned Progressing, the
162
+ duration is shown as unknown and the app is not counted as needing attention.
163
+
164
+ ### Sync windows
165
+
166
+ `projects windows <project>` lists each window, whether it is active now, and
167
+ when an inactive one next opens, then says whether automated and manual syncs
168
+ are allowed right now. It evaluates the windows the way Argo CD does (5-field
169
+ cron schedule or a descriptor such as `@daily` or `@every 1h`, duration, the
170
+ window's time zone or UTC). When a window that could change the outcome cannot
171
+ be evaluated, the verdict is `unknown` and names that window. The verdict assumes
172
+ every window applies; check each window's apps, namespaces and clusters for a
173
+ specific app.
174
+
175
+ ## Output and exit codes
176
+
177
+ - Output is TOON on stdout, errors included: `error`, `code`, and `help` with
178
+ the next command to run. There is no `--json`; the raw `argocd` CLI is the
179
+ fallback for anything argocd-axi does not cover.
180
+ - Exit 0 means the command did what was asked. An OutOfSync or Degraded app,
181
+ drift found by `apps diff`, a `can-i` answer of no, and an `apps wait` that
182
+ timed out are all data with exit 0.
183
+ - Exit 1 is an error, exit 2 a usage error (unknown flag, missing argument).
184
+ Unknown flags are rejected before any argocd call, with the valid flags
185
+ listed.
186
+
187
+ | Code | Meaning |
188
+ | --------------------------------- | -------------------------------------------------------------------------------------- |
189
+ | `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
190
+ | `AUTH_REQUIRED` | No session or an expired one; a human logs in |
191
+ | `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
192
+ | `NOT_FOUND` | No such app, project or ApplicationSet |
193
+ | `RATE_LIMITED` | The server asked to slow down |
194
+ | `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
195
+ | `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
196
+ | `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
197
+ | `TIMEOUT` | argocd did not answer in time |
198
+ | `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
199
+ | `CONFIG_INVALID` | argocd-axi's own config file is malformed |
200
+ | `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
201
+
202
+ ## Safety model
203
+
204
+ - **One subprocess seam.** Every argocd call goes through `src/argocd.ts`,
205
+ which accepts only operations from the allowlist registry in
206
+ `src/registry.ts`. Every registered operation is a server read or a
207
+ local-config operation. The seam also refuses streaming, refresh, prompt,
208
+ local-file and delete flags whatever a command passes, gives argocd no
209
+ stdin, and enforces a timeout and an output cap. Tests fail if a new
210
+ operation becomes reachable without updating the registry.
211
+ - **A refusal registry.** Known writes and credential commands answer
212
+ `REFUSED` with a reason and the raw command a human can run instead of
213
+ failing blind.
214
+ - **Redaction.** Kubernetes Secret `data`, `stringData` and their
215
+ last-applied annotation, values under sensitive-looking keys, and env-var or
216
+ Helm-parameter pairs with sensitive-looking names are replaced with
217
+ `[redacted]`, and the output says how many values were hidden. Free text
218
+ (log lines, diff text) is redacted line by line on a best-effort basis:
219
+ `key=value` pairs, bearer tokens, JWTs and URL passwords. Repository and
220
+ cluster rows are built from an explicit field list, so credentials never
221
+ reach them.
222
+ - **Writable contexts.** argocd-axi's own config file,
223
+ `~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
224
+ holds the allowlist of contexts where the future write tier may execute.
225
+ It defaults to none, and v1 only displays it:
226
+
227
+ ```yaml
228
+ writable-contexts:
229
+ - staging
230
+ ```
231
+
232
+ - **Version cache.** The server version is checked at most once every 12
233
+ hours per context and server, and cached in `~/.cache/argocd-axi/`
234
+ (`$XDG_CACHE_HOME`). This file holds version strings only.
235
+
236
+ ## Development
237
+
238
+ ```sh
239
+ npm install
240
+ npm run check # format, lint, typecheck, tests, skill drift check
241
+ ```
242
+
243
+ Tests never touch a real Argo CD server, cluster, kubeconfig or credential.
244
+ They run the built CLI as a user would, with a fake `argocd`
245
+ (`test/fake-argocd/`) as the only executable on `PATH`. The fake answers from
246
+ the recorded fixtures in `test/fixtures/`, and each test gets a throwaway
247
+ `HOME`. After changing help text, run `npm run build:skill` to regenerate the
248
+ skill.
249
+
250
+ ## Releases
251
+
252
+ Releases are cut by [release-please](https://github.com/googleapis/release-please)
253
+ from conventional commits (`feat:`, `fix:`, `docs:`, ...) on `main`. It keeps
254
+ a release PR open that bumps the package version, updates `src/version.ts`,
255
+ and updates `CHANGELOG.md`; merging
256
+ that PR tags the release and publishes it to npm from
257
+ `.github/workflows/release-please.yml` through npm trusted publishing, with
258
+ provenance and no stored npm token. Each release is staged: after the workflow
259
+ runs, approve the staged version with 2FA using `npm stage approve <id>` or
260
+ npmjs.com. The version is not live on npm until approved. `CHANGELOG.md` and
261
+ `.release-please-manifest.json` are generated, so never edit them by hand.
262
+
263
+ Release PRs show the required "PR must be raised via no-mistakes" check as
264
+ missing, because that check never runs for them; the maintainer merges them
265
+ with an admin bypass.
266
+
267
+ If a release was tagged but its publish failed, re-run the publish on that tag
268
+ (the run must be dispatched on the tag so the provenance names its commit):
269
+
270
+ ```sh
271
+ gh workflow run release-please.yml --ref v0.1.1 -f tag=v0.1.1
272
+ ```
273
+
274
+ A version that is staged but not yet approved does not count as published, so
275
+ re-running would try to stage it again and fail. Approve or discard the staged
276
+ version instead.
277
+
278
+ ## License
279
+
280
+ [MIT](LICENSE)
package/ROADMAP.md ADDED
@@ -0,0 +1,97 @@
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
+ ## AXI community catalog
94
+
95
+ Submission to the [AXI community catalog](https://github.com/kunchenguid/axi)
96
+ comes after v1. npm releases are automated; see the Releases section in
97
+ [README.md](README.md).
@@ -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
+ }