argocd-axi 0.1.2 → 0.1.4
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/README.md +43 -33
- package/ROADMAP.md +10 -11
- package/dist/src/argocd.js +20 -1
- package/dist/src/cli.js +2 -0
- package/dist/src/commands/apps.js +193 -22
- package/dist/src/commands/auth.js +49 -0
- package/dist/src/commands/common.js +3 -3
- package/dist/src/errors.js +29 -3
- package/dist/src/gate.js +2 -3
- package/dist/src/help.js +4 -2
- package/dist/src/model.js +33 -14
- package/dist/src/redact.js +2 -0
- package/dist/src/registry.js +6 -7
- package/dist/src/skill.js +7 -4
- package/dist/src/toon.js +4 -10
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
- package/skills/argocd-axi/SKILL.md +11 -6
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<a href="https://x.com/dematom1"><img alt="X" src="https://img.shields.io/badge/X-@dematom1-black?style=flat-square" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Argo CD for agents, read-only except opt-in writes on writable contexts. `argocd-axi` wraps the `argocd` CLI and answers
|
|
12
12
|
the questions an agent asks during a deployment problem (which apps need
|
|
13
13
|
attention and why, what differs from Git, what was deployed when, what the
|
|
14
14
|
pods are logging, whether a sync is allowed right now) in compact
|
|
@@ -18,7 +18,7 @@ pods are logging, whether a sync is allowed right now) in compact
|
|
|
18
18
|
```
|
|
19
19
|
$ argocd-axi
|
|
20
20
|
bin: ~/.local/bin/argocd-axi
|
|
21
|
-
description:
|
|
21
|
+
description: Argo CD for agents: read operations plus opt-in writes on writable contexts
|
|
22
22
|
context: prod
|
|
23
23
|
server: argocd.example.com
|
|
24
24
|
auth: argocd CLI session for this context
|
|
@@ -31,9 +31,7 @@ attention[3]{app,project,sync,health,why}:
|
|
|
31
31
|
checkout,shop,Synced,Progressing,Progressing for 25m
|
|
32
32
|
operations[1]{app,phase,started}:
|
|
33
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
|
|
34
|
+
help[2]: Run `argocd-axi apps view <app>` to diagnose an application,Run `argocd-axi apps` to list every application
|
|
37
35
|
```
|
|
38
36
|
|
|
39
37
|
## Why
|
|
@@ -48,21 +46,25 @@ argocd-axi turns that into small, definitive answers:
|
|
|
48
46
|
top and says why, in one line, with `count: N of M total`.
|
|
49
47
|
- **Summary first.** Diffs and manifests come back as a per-resource list;
|
|
50
48
|
the raw text is one `--full` away.
|
|
51
|
-
- **
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
- **Opt-in writes.** `apps sync <app>` previews the per-resource diff and
|
|
50
|
+
makes no change unless `--confirm` is passed. It never prunes unless
|
|
51
|
+
`--prune` is explicit, and execution is limited to configured writable
|
|
52
|
+
contexts. Other writes remain refused.
|
|
54
53
|
- **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
|
|
55
54
|
parameters are redacted and counted. No flag reveals them.
|
|
56
55
|
- **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
|
|
57
56
|
block because argocd never gets a stdin.
|
|
58
57
|
|
|
59
|
-
##
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
Install the published package:
|
|
60
61
|
|
|
61
62
|
```sh
|
|
62
63
|
npm install -g argocd-axi
|
|
64
|
+
argocd-axi setup hooks
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
Or run
|
|
67
|
+
Or run without installing: `npx -y argocd-axi`. The session hook requires a global install.
|
|
66
68
|
|
|
67
69
|
Requirements:
|
|
68
70
|
|
|
@@ -72,15 +74,22 @@ Requirements:
|
|
|
72
74
|
v3.3.6. There is no hard minimum; `ctx` and the home view warn when the CLI
|
|
73
75
|
and server differ by a major version.
|
|
74
76
|
|
|
75
|
-
##
|
|
77
|
+
## Setup
|
|
76
78
|
|
|
77
79
|
argocd-axi adds no auth of its own. It uses whatever the `argocd` CLI already
|
|
78
80
|
has: the current context's session, or `ARGOCD_SERVER` and
|
|
79
81
|
`ARGOCD_AUTH_TOKEN` from the environment. It never starts a login, never
|
|
80
82
|
prompts, and never reads the argocd config file (`~/.config/argocd/config`,
|
|
81
|
-
which holds clear-text tokens).
|
|
82
|
-
answers `
|
|
83
|
-
|
|
83
|
+
which holds clear-text tokens). A missing session answers `AUTH_REQUIRED`; an
|
|
84
|
+
expired or rejected login answers `AUTH_EXPIRED`. When the target server is
|
|
85
|
+
known, help includes the exact operator login command; otherwise it shows the
|
|
86
|
+
login syntax and asks for the server address. Run
|
|
87
|
+
`argocd-axi auth guide` for offline setup instructions. Human operators run
|
|
88
|
+
`argocd login <server> --sso --grpc-web` interactively themselves. Agents and
|
|
89
|
+
CI should set `ARGOCD_SERVER` and `ARGOCD_AUTH_TOKEN` from a scoped account
|
|
90
|
+
token. Verify the selected context with `argocd-axi ctx`. The tool never logs in.
|
|
91
|
+
|
|
92
|
+
### Scoped agent credentials
|
|
84
93
|
|
|
85
94
|
An agent that uses your own SSO session has all of your Argo CD permissions.
|
|
86
95
|
The recommended agent credential is a **short-lived project-role token**,
|
|
@@ -136,9 +145,10 @@ argocd-axi apps [--problems --project --selector --repo --cluster --sync --healt
|
|
|
136
145
|
argocd-axi apps view <app> [--full]
|
|
137
146
|
argocd-axi apps resources <app> [--problems | --orphaned]
|
|
138
147
|
argocd-axi apps diff <app> [--revision <rev>] [--full]
|
|
148
|
+
argocd-axi apps sync <app> [--confirm] [--prune] [--timeout <s>] --context <c>
|
|
139
149
|
argocd-axi apps history <app> [--limit <n>]
|
|
140
150
|
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]
|
|
151
|
+
argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [--full] [-p]
|
|
142
152
|
argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
|
|
143
153
|
argocd-axi apps actions <app> [--kind <k>]
|
|
144
154
|
argocd-axi projects | projects view <p> | projects windows <p>
|
|
@@ -184,20 +194,21 @@ specific app.
|
|
|
184
194
|
Unknown flags are rejected before any argocd call, with the valid flags
|
|
185
195
|
listed.
|
|
186
196
|
|
|
187
|
-
| Code | Meaning
|
|
188
|
-
| --------------------------------- |
|
|
189
|
-
| `VALIDATION_ERROR` | Bad flag or argument (exit 2)
|
|
190
|
-
| `AUTH_REQUIRED` | No session
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
197
|
+
| Code | Meaning |
|
|
198
|
+
| --------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
|
|
200
|
+
| `AUTH_REQUIRED` | No session is configured |
|
|
201
|
+
| `AUTH_EXPIRED` | Argo CD rejected or expired the login; help prints the exact command when the server is known |
|
|
202
|
+
| `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
|
|
203
|
+
| `NOT_FOUND` | No such app, project or ApplicationSet |
|
|
204
|
+
| `RATE_LIMITED` | The server asked to slow down |
|
|
205
|
+
| `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
|
|
206
|
+
| `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
|
|
207
|
+
| `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
|
|
208
|
+
| `TIMEOUT` | argocd did not answer in time |
|
|
209
|
+
| `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
|
|
210
|
+
| `CONFIG_INVALID` | argocd-axi's own config file is malformed |
|
|
211
|
+
| `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
|
|
201
212
|
|
|
202
213
|
## Safety model
|
|
203
214
|
|
|
@@ -235,8 +246,11 @@ specific app.
|
|
|
235
246
|
|
|
236
247
|
## Development
|
|
237
248
|
|
|
249
|
+
Build from source and run checks:
|
|
250
|
+
|
|
238
251
|
```sh
|
|
239
252
|
npm install
|
|
253
|
+
npm run build
|
|
240
254
|
npm run check # format, lint, typecheck, tests, skill drift check
|
|
241
255
|
```
|
|
242
256
|
|
|
@@ -260,10 +274,6 @@ runs, approve the staged version with 2FA using `npm stage approve <id>` or
|
|
|
260
274
|
npmjs.com. The version is not live on npm until approved. `CHANGELOG.md` and
|
|
261
275
|
`.release-please-manifest.json` are generated, so never edit them by hand.
|
|
262
276
|
|
|
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
277
|
If a release was tagged but its publish failed, re-run the publish on that tag
|
|
268
278
|
(the run must be dispatched on the tag so the provenance names its commit):
|
|
269
279
|
|
package/ROADMAP.md
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
argocd-axi
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
argocd-axi is read-only except for confirmed app syncs on contexts explicitly
|
|
4
|
+
listed as writable. This file records the remaining write tier and shared
|
|
5
|
+
safety rules.
|
|
6
6
|
|
|
7
7
|
## Next: the write tier
|
|
8
8
|
|
|
9
|
-
These verbs
|
|
10
|
-
|
|
9
|
+
These write verbs still answer `REFUSED` and name the raw `argocd` command a
|
|
10
|
+
human can run:
|
|
11
11
|
|
|
12
|
-
| Command
|
|
13
|
-
|
|
|
14
|
-
| `apps
|
|
15
|
-
| `apps
|
|
16
|
-
| `apps
|
|
17
|
-
| `apps refresh <app> [--hard] --context <c>` | `argocd app get --refresh` | Triggers a reconcile, so it is a write, not a read |
|
|
12
|
+
| Command | Wraps | Notes |
|
|
13
|
+
| ------------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
14
|
+
| `apps rollback <app> <history-id> --context <c>` | `argocd app rollback` | Preview synthesized from target history and current app state; refuse while auto-sync is enabled |
|
|
15
|
+
| `apps terminate-op <app> --context <c>` | `argocd app terminate-op` | No-op when no operation is running |
|
|
16
|
+
| `apps refresh <app> [--hard] --context <c>` | `argocd app get --refresh` | Triggers a reconcile |
|
|
18
17
|
|
|
19
18
|
Later, a third tier: `apps action run <app> <action> --kind <kind>` for
|
|
20
19
|
resource actions such as restart, and Argo Rollouts promote and abort
|
package/dist/src/argocd.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolveTarget } from "./context.js";
|
|
2
3
|
import { CODES, axiError, mapArgocdFailure, } from "./errors.js";
|
|
3
4
|
import { FORBIDDEN_ARGS, OPERATIONS } from "./registry.js";
|
|
4
5
|
/**
|
|
@@ -37,10 +38,28 @@ export async function runArgocd(op, args, options = {}) {
|
|
|
37
38
|
]);
|
|
38
39
|
}
|
|
39
40
|
if (result.exitCode !== 0 && !options.allowFailure) {
|
|
40
|
-
|
|
41
|
+
const failure = options.failure ?? {};
|
|
42
|
+
const cleaned = result.stderr || result.stdout;
|
|
43
|
+
if (!failure.loginCommand && isAuthenticationFailure(cleaned)) {
|
|
44
|
+
failure.agentCredentials = Boolean(process.env.ARGOCD_AUTH_TOKEN?.trim());
|
|
45
|
+
try {
|
|
46
|
+
const { target } = await resolveTarget(options.context);
|
|
47
|
+
const server = target.envServer ?? target.server;
|
|
48
|
+
failure.loginCommand = server
|
|
49
|
+
? `argocd login '${server.replaceAll("'", "'\\''")}' --sso --grpc-web`
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
failure.loginCommand = undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw mapArgocdFailure(cleaned, failure);
|
|
41
57
|
}
|
|
42
58
|
return result;
|
|
43
59
|
}
|
|
60
|
+
function isAuthenticationFailure(message) {
|
|
61
|
+
return /rpc error: code = Unauthenticated|invalid session|token is expired|token has expired|no session information|unauthenticated|please login|login required|failed to get token|session (?:has )?expired/i.test(message);
|
|
62
|
+
}
|
|
44
63
|
/** Run an operation whose stdout is JSON and parse it. */
|
|
45
64
|
export async function argocdJson(op, args, options = {}) {
|
|
46
65
|
const result = await runArgocd(op, args, options);
|
package/dist/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { appsCommand } from "./commands/apps.js";
|
|
|
5
5
|
import { appsetsCommand } from "./commands/appsets.js";
|
|
6
6
|
import { canICommand, whoamiCommand } from "./commands/account.js";
|
|
7
7
|
import { ctxCommand } from "./commands/ctx.js";
|
|
8
|
+
import { authCommand } from "./commands/auth.js";
|
|
8
9
|
import { homeCommand } from "./commands/home.js";
|
|
9
10
|
import { clustersCommand, reposCommand } from "./commands/inventory.js";
|
|
10
11
|
import { projectsCommand } from "./commands/projects.js";
|
|
@@ -45,6 +46,7 @@ const refusalCommand = (key) => async () => refuse(key);
|
|
|
45
46
|
const TOP_LEVEL_REFUSALS = Object.keys(REFUSALS).filter((key) => !key.includes(" "));
|
|
46
47
|
export const COMMANDS = Object.assign(Object.create(null), {
|
|
47
48
|
ctx: ctxCommand,
|
|
49
|
+
auth: authCommand,
|
|
48
50
|
whoami: whoamiCommand,
|
|
49
51
|
"can-i": canICommand,
|
|
50
52
|
apps: appsCommand,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
2
|
import { parseAllDocuments, stringify } from "yaml";
|
|
3
3
|
import { argocdJson, runArgocd } from "../argocd.js";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { evaluateWriteGate } from "../gate.js";
|
|
4
6
|
import { CONTEXT_FLAG, getBool, getInt, getList, getString, parseArgs, parseDurationSeconds, validateName, } from "../args.js";
|
|
5
7
|
import { CODES, axiError, mapArgocdFailure, validationError, } from "../errors.js";
|
|
6
|
-
import { appName, appSources, attentionReasons, autoSyncSummary, destinationSummary, displayNames, formatAge, healthStatus, isOperationRunning, isResourceProblem, isUnhealthy, needsAttention, shortRevision, sortApps, syncRevision, syncStatus, plural, truncate, } from "../model.js";
|
|
8
|
+
import { appName, appSources, attentionReasonPreview, attentionReasons, autoSyncSummary, destinationSummary, displayNames, formatAge, healthStatus, isOperationRunning, isResourceProblem, isUnhealthy, needsAttention, shortRevision, sortApps, syncRevision, syncStatus, plural, truncate, } from "../model.js";
|
|
7
9
|
import { parseTable } from "../context.js";
|
|
8
10
|
import { Redactor } from "../redact.js";
|
|
9
11
|
import { encodeObject, renderTextBlock } from "../toon.js";
|
|
@@ -16,6 +18,7 @@ const APP_ARG = {
|
|
|
16
18
|
const NOT_FOUND_HELP = ["Run `argocd-axi apps` to list applications"];
|
|
17
19
|
const MESSAGE_CHARS = 300;
|
|
18
20
|
const LOG_CHAR_CAP = 20_000;
|
|
21
|
+
const FULL_LOG_CHAR_CAP = 1_000_000;
|
|
19
22
|
async function getApp(name, options) {
|
|
20
23
|
return argocdJson("app get", [validateName("app", name), "-o", "json"], {
|
|
21
24
|
...options,
|
|
@@ -208,8 +211,10 @@ export async function appsList(args) {
|
|
|
208
211
|
sync: syncStatus(app),
|
|
209
212
|
health: healthStatus(app),
|
|
210
213
|
};
|
|
211
|
-
if (showWhy)
|
|
212
|
-
|
|
214
|
+
if (showWhy) {
|
|
215
|
+
const why = attentionReasonPreview(app, 160, now, redactor);
|
|
216
|
+
row.why = preview(why.text, 160, false, why.totalChars).text;
|
|
217
|
+
}
|
|
213
218
|
for (const field of fields)
|
|
214
219
|
row[field] = fieldValue(app, field);
|
|
215
220
|
return row;
|
|
@@ -750,15 +755,14 @@ export async function appsManifests(args) {
|
|
|
750
755
|
manifests: rows,
|
|
751
756
|
}),
|
|
752
757
|
];
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
? []
|
|
759
|
-
: [
|
|
758
|
+
const yaml = redactor.text(objects.map((obj) => stringify(obj).trimEnd()).join("\n---\n"));
|
|
759
|
+
const yamlPreview = preview(yaml, 800, full);
|
|
760
|
+
blocks.push(renderTextBlock("yaml", yamlPreview.text.split("\n")));
|
|
761
|
+
return finish(blocks, redactor, yamlPreview.truncated
|
|
762
|
+
? [
|
|
760
763
|
`Run \`argocd-axi apps manifests ${name} --full${carriedFlags(parsed)}\` to see the YAML`,
|
|
761
|
-
]
|
|
764
|
+
]
|
|
765
|
+
: []);
|
|
762
766
|
}
|
|
763
767
|
// ---------------------------------------------------------------- apps logs
|
|
764
768
|
export const APPS_LOGS_SPEC = {
|
|
@@ -796,6 +800,11 @@ export const APPS_LOGS_SPEC = {
|
|
|
796
800
|
valueName: "text",
|
|
797
801
|
description: "Only lines containing this text",
|
|
798
802
|
},
|
|
803
|
+
{
|
|
804
|
+
name: "--full",
|
|
805
|
+
kind: "bool",
|
|
806
|
+
description: "Return requested lines up to 5000, capped at 1,000,000 characters after redaction",
|
|
807
|
+
},
|
|
799
808
|
{
|
|
800
809
|
name: "--previous",
|
|
801
810
|
short: "-p",
|
|
@@ -810,7 +819,7 @@ export const APPS_LOGS_SPEC = {
|
|
|
810
819
|
"argocd-axi apps logs guestbook --filter error -p",
|
|
811
820
|
],
|
|
812
821
|
notes: [
|
|
813
|
-
`Output keeps the newest lines and is capped at ${LOG_CHAR_CAP} characters; secret-looking values are redacted on a best-effort basis`,
|
|
822
|
+
`Output keeps the newest lines and is capped at ${LOG_CHAR_CAP} characters; --full returns up to 5000 requested lines, capped at ${FULL_LOG_CHAR_CAP} characters after redaction; secret-looking values are redacted on a best-effort basis`,
|
|
814
823
|
],
|
|
815
824
|
};
|
|
816
825
|
export async function appsLogs(args) {
|
|
@@ -836,6 +845,7 @@ export async function appsLogs(args) {
|
|
|
836
845
|
...contextOption(parsed),
|
|
837
846
|
failure: { notFoundHelp: NOT_FOUND_HELP },
|
|
838
847
|
});
|
|
848
|
+
const full = getBool(parsed, "--full");
|
|
839
849
|
const lines = result.stdout.split("\n");
|
|
840
850
|
if (lines.at(-1) === "")
|
|
841
851
|
lines.pop();
|
|
@@ -848,26 +858,62 @@ export async function appsLogs(args) {
|
|
|
848
858
|
}
|
|
849
859
|
const redactor = new Redactor();
|
|
850
860
|
const redacted = lines.map((line) => redactor.text(line));
|
|
861
|
+
const charCap = full ? FULL_LOG_CHAR_CAP : LOG_CHAR_CAP;
|
|
851
862
|
const kept = [];
|
|
852
863
|
let chars = 0;
|
|
864
|
+
let charLimitCut = false;
|
|
865
|
+
let lineCut = false;
|
|
866
|
+
let cutLineChars = 0;
|
|
853
867
|
for (let i = redacted.length - 1; i >= 0; i--) {
|
|
854
868
|
const line = redacted[i];
|
|
855
|
-
if (
|
|
869
|
+
if (kept.length >= tail)
|
|
856
870
|
break;
|
|
871
|
+
if (chars + line.length + 1 > charCap) {
|
|
872
|
+
charLimitCut = true;
|
|
873
|
+
if (kept.length === 0) {
|
|
874
|
+
kept.unshift(line.slice(-charCap));
|
|
875
|
+
lineCut = true;
|
|
876
|
+
cutLineChars = line.length;
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
857
880
|
kept.unshift(line);
|
|
858
881
|
chars += line.length + 1;
|
|
859
882
|
}
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
883
|
+
const lineLimitCut = redacted.length > tail;
|
|
884
|
+
const omittedByCharCap = charLimitCut
|
|
885
|
+
? Math.max(0, Math.min(tail, redacted.length) - kept.length)
|
|
886
|
+
: 0;
|
|
864
887
|
const cutNote = lineCut
|
|
865
|
-
? ` and only the last ${
|
|
888
|
+
? ` and only the last ${charCap} of the newest line's ${cutLineChars} chars`
|
|
866
889
|
: "";
|
|
867
|
-
const header =
|
|
868
|
-
? `showing the newest ${kept.length} of ${redacted.length} lines (
|
|
869
|
-
:
|
|
870
|
-
|
|
890
|
+
const header = lineLimitCut
|
|
891
|
+
? `showing the newest ${kept.length} of ${redacted.length} merged lines (--tail ${tail} limit)`
|
|
892
|
+
: charLimitCut
|
|
893
|
+
? `showing the newest ${kept.length} of ${redacted.length} lines (output capped at ${charCap} chars)${cutNote}`
|
|
894
|
+
: `${kept.length} lines (--tail ${tail})`;
|
|
895
|
+
const logHeader = charLimitCut
|
|
896
|
+
? [
|
|
897
|
+
header,
|
|
898
|
+
`${plural(omittedByCharCap, "requested line")} omitted by the ${charCap}-character cap`,
|
|
899
|
+
...(lineCut
|
|
900
|
+
? [`${cutLineChars - charCap} chars omitted from the newest line`]
|
|
901
|
+
: []),
|
|
902
|
+
].join("; ")
|
|
903
|
+
: header;
|
|
904
|
+
const help = [];
|
|
905
|
+
const carried = carriedFlags(parsed, ["--tail", "--full", "--filter"]);
|
|
906
|
+
if (lineLimitCut && tail < 5000)
|
|
907
|
+
help.push(`Run \`argocd-axi apps logs ${name} --tail 5000 --full${carried}\` to read more merged lines${filter ? "; reapply --filter manually if needed" : ""}`);
|
|
908
|
+
else if (lineLimitCut)
|
|
909
|
+
help.push("The --tail limit is 5000 lines; no larger tail is supported");
|
|
910
|
+
if (charLimitCut)
|
|
911
|
+
help.push(full
|
|
912
|
+
? `The --full output reached its ${FULL_LOG_CHAR_CAP}-character cap; narrow with --tail, --since, --kind, or --name${filter ? "; reapply --filter manually if needed" : ""}`
|
|
913
|
+
: tail >= 5000
|
|
914
|
+
? `The --full output is capped at ${FULL_LOG_CHAR_CAP} characters; narrow with --since, --kind, or --name${filter ? "; reapply --filter manually if needed" : ""}`
|
|
915
|
+
: `Run \`argocd-axi apps logs ${name} --full${carriedFlags(parsed, ["--full", "--filter"])}\` to see all requested lines (up to ${FULL_LOG_CHAR_CAP} characters); narrow with --tail, --since, --kind, or --name if needed${filter ? "; reapply --filter manually if needed" : ""}`);
|
|
916
|
+
return finish([encodeObject({ logs: logHeader }), renderTextBlock("lines", kept)], redactor, help);
|
|
871
917
|
}
|
|
872
918
|
// ---------------------------------------------------------------- apps wait
|
|
873
919
|
export const APPS_WAIT_SPEC = {
|
|
@@ -959,6 +1005,130 @@ export async function appsWait(args) {
|
|
|
959
1005
|
`Run \`argocd-axi apps wait ${name}${conditions.map((c) => ` ${c}`).join("")} --timeout ${timeout}${contextSuffix(parsed)}\` to wait again`,
|
|
960
1006
|
]);
|
|
961
1007
|
}
|
|
1008
|
+
// ---------------------------------------------------------------- apps sync
|
|
1009
|
+
export const APPS_SYNC_SPEC = {
|
|
1010
|
+
path: "apps sync",
|
|
1011
|
+
summary: "Preview and optionally sync one Application",
|
|
1012
|
+
positionals: [APP_ARG],
|
|
1013
|
+
flags: [
|
|
1014
|
+
{
|
|
1015
|
+
name: "--confirm",
|
|
1016
|
+
kind: "bool",
|
|
1017
|
+
description: "Execute the sync after preview",
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "--prune",
|
|
1021
|
+
kind: "bool",
|
|
1022
|
+
description: "Allow pruning resources in the sync",
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "--timeout",
|
|
1026
|
+
kind: "value",
|
|
1027
|
+
valueName: "s",
|
|
1028
|
+
description: "Maximum wait in seconds (default 120)",
|
|
1029
|
+
},
|
|
1030
|
+
CONTEXT_FLAG,
|
|
1031
|
+
],
|
|
1032
|
+
examples: [
|
|
1033
|
+
"argocd-axi apps sync guestbook --context staging",
|
|
1034
|
+
"argocd-axi apps sync guestbook --context staging --confirm",
|
|
1035
|
+
],
|
|
1036
|
+
notes: [
|
|
1037
|
+
"Never prunes unless --prune is given; requires the context in writable-contexts to execute",
|
|
1038
|
+
],
|
|
1039
|
+
};
|
|
1040
|
+
export async function appsSync(args) {
|
|
1041
|
+
const parsed = parseArgs(APPS_SYNC_SPEC, args);
|
|
1042
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1043
|
+
const context = getString(parsed, "--context");
|
|
1044
|
+
const confirm = getBool(parsed, "--confirm");
|
|
1045
|
+
const prune = getBool(parsed, "--prune");
|
|
1046
|
+
const timeout = getInt(parsed, "--timeout", {
|
|
1047
|
+
default: 120,
|
|
1048
|
+
min: 1,
|
|
1049
|
+
max: 3600,
|
|
1050
|
+
});
|
|
1051
|
+
const gate = evaluateWriteGate({
|
|
1052
|
+
command: "apps sync",
|
|
1053
|
+
context,
|
|
1054
|
+
execute: confirm,
|
|
1055
|
+
executeFlag: "--confirm",
|
|
1056
|
+
}, loadConfig().config);
|
|
1057
|
+
const options = { context: gate.context };
|
|
1058
|
+
const diff = await appsDiff([name, "--context", gate.context]);
|
|
1059
|
+
const app = await getApp(name, options);
|
|
1060
|
+
const project = validateName("project", app.spec?.project ?? "default");
|
|
1061
|
+
const windows = (await argocdJson("proj windows list", [project, "-o", "json"], options)) ?? [];
|
|
1062
|
+
const preview = [diff, encodeObject({ syncWindows: windows })];
|
|
1063
|
+
if (!confirm)
|
|
1064
|
+
return finish(preview, undefined, [
|
|
1065
|
+
"No changes made; add --confirm to sync",
|
|
1066
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1067
|
+
]);
|
|
1068
|
+
const permission = await runArgocd("account can-i", ["sync", "applications", `${app.spec?.project ?? "default"}/${name}`], { ...options, allowFailure: true });
|
|
1069
|
+
if (permission.exitCode !== 0 || !/^yes\s*$/i.test(permission.stdout.trim()))
|
|
1070
|
+
throw axiError(`REFUSED: Argo CD RBAC denies sync for ${name}`, CODES.REFUSED);
|
|
1071
|
+
const syncArgs = [name, "--async", "--info", "initiated-by=argocd-axi"];
|
|
1072
|
+
if (prune)
|
|
1073
|
+
syncArgs.push("--prune");
|
|
1074
|
+
try {
|
|
1075
|
+
await runArgocd("app sync", syncArgs, options);
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
if (error instanceof AxiError &&
|
|
1079
|
+
/sync.?window|operation.*(?:running|progress|active)|(?:running|progress).*operation/i.test(error.message))
|
|
1080
|
+
throw axiError(`REFUSED: ${error.message}`, CODES.REFUSED);
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
let waitTimedOut = false;
|
|
1084
|
+
let waitError;
|
|
1085
|
+
try {
|
|
1086
|
+
const wait = await runArgocd("app wait", [name, "--operation", "--timeout", String(timeout)], { ...options, allowFailure: true, timeoutMs: (timeout + 10) * 1000 });
|
|
1087
|
+
if (wait.exitCode !== 0) {
|
|
1088
|
+
const message = wait.stderr.trim() || wait.stdout.trim() || "app wait failed";
|
|
1089
|
+
if (/timed out|deadline exceeded/i.test(message))
|
|
1090
|
+
waitTimedOut = true;
|
|
1091
|
+
else
|
|
1092
|
+
waitError = message;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
if (!(error instanceof AxiError && error.code === CODES.TIMEOUT))
|
|
1097
|
+
throw error;
|
|
1098
|
+
waitTimedOut = true;
|
|
1099
|
+
}
|
|
1100
|
+
const result = await getApp(name, options);
|
|
1101
|
+
const phase = result.status?.operationState?.phase;
|
|
1102
|
+
const syncResult = phase === "Succeeded"
|
|
1103
|
+
? "complete"
|
|
1104
|
+
: phase === "Failed" || phase === "Error"
|
|
1105
|
+
? "failed"
|
|
1106
|
+
: phase === "Running" && waitTimedOut
|
|
1107
|
+
? "timeout"
|
|
1108
|
+
: "error";
|
|
1109
|
+
return finish([
|
|
1110
|
+
...preview,
|
|
1111
|
+
encodeObject({
|
|
1112
|
+
sync: syncResult,
|
|
1113
|
+
...(syncResult === "error"
|
|
1114
|
+
? {
|
|
1115
|
+
error: waitError ??
|
|
1116
|
+
result.status?.operationState?.message ??
|
|
1117
|
+
`app wait ended with operation phase ${phase ?? "unknown"}`,
|
|
1118
|
+
}
|
|
1119
|
+
: {}),
|
|
1120
|
+
revision: syncRevision(result),
|
|
1121
|
+
health: healthStatus(result),
|
|
1122
|
+
failedResources: (result.status?.resources ?? [])
|
|
1123
|
+
.filter((r) => r.status === "SyncFailed" || r.health?.status === "Degraded")
|
|
1124
|
+
.map((r) => ({
|
|
1125
|
+
kind: r.kind ?? "",
|
|
1126
|
+
name: r.name ?? "",
|
|
1127
|
+
message: r.health?.message ?? r.status ?? "failed",
|
|
1128
|
+
})),
|
|
1129
|
+
}),
|
|
1130
|
+
], undefined);
|
|
1131
|
+
}
|
|
962
1132
|
// ---------------------------------------------------------------- apps actions
|
|
963
1133
|
export const APPS_ACTIONS_SPEC = {
|
|
964
1134
|
path: "apps actions",
|
|
@@ -1028,6 +1198,7 @@ const SUBCOMMANDS = {
|
|
|
1028
1198
|
logs: { spec: APPS_LOGS_SPEC, run: appsLogs },
|
|
1029
1199
|
wait: { spec: APPS_WAIT_SPEC, run: appsWait },
|
|
1030
1200
|
actions: { spec: APPS_ACTIONS_SPEC, run: appsActions },
|
|
1201
|
+
sync: { spec: APPS_SYNC_SPEC, run: appsSync },
|
|
1031
1202
|
};
|
|
1032
1203
|
export const APPS_SPECS = [
|
|
1033
1204
|
APPS_LIST_SPEC,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parseArgs, CONTEXT_FLAG } from "../args.js";
|
|
2
|
+
import { describeTarget, listContexts, resolveTarget } from "../context.js";
|
|
3
|
+
import { validationError } from "../errors.js";
|
|
4
|
+
import { encodeObject } from "../toon.js";
|
|
5
|
+
import { contextOption, finish } from "./common.js";
|
|
6
|
+
const GUIDE_SPEC = {
|
|
7
|
+
path: "auth guide",
|
|
8
|
+
summary: "Offline setup instructions for human and agent credentials",
|
|
9
|
+
positionals: [],
|
|
10
|
+
flags: [CONTEXT_FLAG],
|
|
11
|
+
examples: ["argocd-axi auth guide", "argocd-axi auth guide --context prod"],
|
|
12
|
+
};
|
|
13
|
+
export async function authCommand(args) {
|
|
14
|
+
if (args[0] !== "guide")
|
|
15
|
+
throw validationError("expected auth guide");
|
|
16
|
+
const parsed = parseArgs(GUIDE_SPEC, args.slice(1));
|
|
17
|
+
const explicitContext = contextOption(parsed).context;
|
|
18
|
+
let listing;
|
|
19
|
+
try {
|
|
20
|
+
listing = await listContexts();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
listing = {
|
|
24
|
+
contexts: [],
|
|
25
|
+
unavailable: error instanceof Error ? error.message : String(error),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const { target } = await resolveTarget(explicitContext, listing);
|
|
29
|
+
const server = target.envServer ?? target.server;
|
|
30
|
+
const login = server
|
|
31
|
+
? `argocd login '${server.replaceAll("'", "'\\''")}' --sso --grpc-web`
|
|
32
|
+
: "argocd login <server> --sso --grpc-web";
|
|
33
|
+
return finish([
|
|
34
|
+
encodeObject({
|
|
35
|
+
...describeTarget(target, listing),
|
|
36
|
+
contexts: listing.unavailable
|
|
37
|
+
? `unavailable - ${listing.unavailable}`
|
|
38
|
+
: undefined,
|
|
39
|
+
operator: "Run the interactive command yourself; argocd-axi never logs in",
|
|
40
|
+
login,
|
|
41
|
+
agent: "Set ARGOCD_SERVER and ARGOCD_AUTH_TOKEN using a scoped Argo CD account token",
|
|
42
|
+
verify: "Run `argocd-axi ctx`",
|
|
43
|
+
}),
|
|
44
|
+
], undefined, server
|
|
45
|
+
? []
|
|
46
|
+
: [
|
|
47
|
+
"Obtain the Argo CD server address, then replace `<server>` in the login command",
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
@@ -87,11 +87,11 @@ export function finish(blocks, redactor, help = []) {
|
|
|
87
87
|
]);
|
|
88
88
|
}
|
|
89
89
|
/** Truncate long text per AXI principle 3, reporting the total size. */
|
|
90
|
-
export function preview(text, max, full) {
|
|
91
|
-
if (full ||
|
|
90
|
+
export function preview(text, max, full, totalChars = text.length) {
|
|
91
|
+
if (full || totalChars <= max)
|
|
92
92
|
return { text, truncated: false };
|
|
93
93
|
return {
|
|
94
|
-
text: `${text.slice(0, max)}... (truncated, ${
|
|
94
|
+
text: `${text.slice(0, max)}... (truncated, ${totalChars} chars total)`,
|
|
95
95
|
truncated: true,
|
|
96
96
|
};
|
|
97
97
|
}
|
package/dist/src/errors.js
CHANGED
|
@@ -8,6 +8,7 @@ import { redactText } from "./redact.js";
|
|
|
8
8
|
export const CODES = {
|
|
9
9
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
10
10
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
11
|
+
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
11
12
|
FORBIDDEN: "FORBIDDEN",
|
|
12
13
|
NOT_FOUND: "NOT_FOUND",
|
|
13
14
|
RATE_LIMITED: "RATE_LIMITED",
|
|
@@ -28,7 +29,7 @@ export function validationError(message, help = []) {
|
|
|
28
29
|
return axiError(message, CODES.VALIDATION_ERROR, help);
|
|
29
30
|
}
|
|
30
31
|
export const LOGIN_HELP = [
|
|
31
|
-
"A human runs `argocd login <server> --sso
|
|
32
|
+
"A human runs `argocd login <server> --sso --grpc-web`; argocd-axi never starts a login",
|
|
32
33
|
"For agents, prefer a short-lived project-role token in ARGOCD_AUTH_TOKEN (see the README)",
|
|
33
34
|
];
|
|
34
35
|
const MAX_MESSAGE_CHARS = 300;
|
|
@@ -107,9 +108,34 @@ export function mapArgocdFailure(stderr, context = {}) {
|
|
|
107
108
|
const grpcCode = grpc?.[1];
|
|
108
109
|
const detail = shorten(redactText(grpc?.[2] ?? cleaned).text) ||
|
|
109
110
|
"argocd failed without an error message";
|
|
111
|
+
if (/no session information|please login|login required/i.test(cleaned) &&
|
|
112
|
+
!/invalid session|token is expired|token has expired|session (?:has )?expired/i.test(cleaned)) {
|
|
113
|
+
return axiError(`Not authenticated to Argo CD: ${detail}`, CODES.AUTH_REQUIRED, context.agentCredentials
|
|
114
|
+
? [
|
|
115
|
+
"Check ARGOCD_SERVER and ARGOCD_AUTH_TOKEN; replace the token with a valid scoped account token",
|
|
116
|
+
]
|
|
117
|
+
: context.loginCommand
|
|
118
|
+
? [
|
|
119
|
+
`A human runs \`${context.loginCommand}\`; argocd-axi never logs in`,
|
|
120
|
+
]
|
|
121
|
+
: LOGIN_HELP);
|
|
122
|
+
}
|
|
110
123
|
if (grpcCode === "Unauthenticated" ||
|
|
111
|
-
/invalid session|token is expired|token has expired|
|
|
112
|
-
return axiError(`Not authenticated to Argo CD: ${detail}`, CODES.
|
|
124
|
+
/invalid session|token is expired|token has expired|unauthenticated|failed to get token|session (?:has )?expired/i.test(cleaned)) {
|
|
125
|
+
return axiError(`Not authenticated to Argo CD: ${detail}`, CODES.AUTH_EXPIRED, context.agentCredentials
|
|
126
|
+
? [
|
|
127
|
+
"Check ARGOCD_SERVER and ARGOCD_AUTH_TOKEN; replace the token with a valid scoped account token",
|
|
128
|
+
...(context.loginCommand
|
|
129
|
+
? [
|
|
130
|
+
`A human runs \`${context.loginCommand}\`; argocd-axi never logs in`,
|
|
131
|
+
]
|
|
132
|
+
: []),
|
|
133
|
+
]
|
|
134
|
+
: context.loginCommand
|
|
135
|
+
? [
|
|
136
|
+
`A human runs \`${context.loginCommand}\`; argocd-axi never logs in`,
|
|
137
|
+
]
|
|
138
|
+
: LOGIN_HELP);
|
|
113
139
|
}
|
|
114
140
|
if (/server address unspecified/i.test(cleaned)) {
|
|
115
141
|
return axiError("No Argo CD server is configured for this context", CODES.NO_CONTEXT, [
|
package/dist/src/gate.js
CHANGED
|
@@ -2,9 +2,8 @@ import { configPath } from "./config.js";
|
|
|
2
2
|
import { validateContextName } from "./context.js";
|
|
3
3
|
import { CODES, axiError, validationError } from "./errors.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a write; v1 shows its allowlist in the home view and `ctx`.
|
|
5
|
+
* Shared write gate: commands require an explicit context, preview by default,
|
|
6
|
+
* and the local writable-contexts allowlist before execution.
|
|
8
7
|
*
|
|
9
8
|
* Rules:
|
|
10
9
|
* - A write always names its target with an explicit `--context`; the
|
package/dist/src/help.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { renderBlocks, renderHelpBlock } from "./toon.js";
|
|
2
|
-
export const DESCRIPTION = "
|
|
2
|
+
export const DESCRIPTION = "Argo CD for agents: read operations plus opt-in writes on writable contexts";
|
|
3
3
|
/** The commands block; the skill is generated from it so the two never drift. */
|
|
4
4
|
export const COMMAND_LINES = [
|
|
5
5
|
"(no command): live overview - context, server, user, apps needing attention with the reason, running operations",
|
|
6
6
|
"ctx [--context <c>]: current context, server and CLI version, local only (what the session hook prints)",
|
|
7
7
|
"ctx use <context>: switch the argocd CLI's current context (local config only)",
|
|
8
|
+
"auth guide [--context <c>]: offline human login and agent token setup instructions",
|
|
8
9
|
"whoami: user, issuer and groups of the current credential",
|
|
9
10
|
"can-i <action> <resource> <object>: ask Argo CD RBAC, e.g. can-i sync applications 'payments/payments-api'",
|
|
10
11
|
"apps [--project --selector --repo --cluster --sync --health --problems --limit --fields]: applications, problems first",
|
|
11
12
|
"apps view <app>: source, destination, sync, health, operation, conditions, auto-sync, problem resources",
|
|
12
13
|
"apps resources <app> [--problems --orphaned]: managed resources with sync and health",
|
|
13
14
|
"apps diff <app> [--revision <rev>] [--full]: what differs from Git, per resource; raw diff with --full",
|
|
15
|
+
"apps sync <app> [--confirm] [--prune] [--timeout <s>]: preview, then optionally sync one app",
|
|
14
16
|
"apps history <app>: deployments, newest first",
|
|
15
17
|
"apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted",
|
|
16
18
|
"apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows",
|
|
@@ -37,7 +39,7 @@ export const TOP_HELP = `${renderBlocks([
|
|
|
37
39
|
"argocd-axi apps diff payments-api",
|
|
38
40
|
]),
|
|
39
41
|
renderHelpBlock("notes", [
|
|
40
|
-
"Read-only
|
|
42
|
+
"Read-only except opt-in writes on writable contexts: apps sync requires --confirm and never prunes unless --prune is given; other writes answer REFUSED",
|
|
41
43
|
"Output is TOON on stdout, errors included; exit 0 = success (an unhealthy app is data, not an error), 1 = error, 2 = usage",
|
|
42
44
|
"Secret-looking values are redacted and counted; no flag reveals them",
|
|
43
45
|
"Uses your existing argocd CLI session or ARGOCD_AUTH_TOKEN; never logs in, never reads the argocd config file",
|
package/dist/src/model.js
CHANGED
|
@@ -66,38 +66,57 @@ function formatMinutes(minutes) {
|
|
|
66
66
|
* Why an app needs attention (empty when it does not), per decision A6.
|
|
67
67
|
* Messages are redacted before they are cut, counted on `redactor`.
|
|
68
68
|
*/
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
69
|
+
function* attentionReasonTexts(app, now, redactor, clipMessages = false) {
|
|
70
|
+
const redact = (text) => {
|
|
71
|
+
const redacted = redactor.text(text);
|
|
72
|
+
return clipMessages ? truncate(redacted, REASON_CHARS) : redacted;
|
|
73
|
+
};
|
|
72
74
|
const sync = syncStatus(app);
|
|
73
75
|
if (sync === "OutOfSync")
|
|
74
|
-
|
|
76
|
+
yield "OutOfSync";
|
|
75
77
|
else if (sync !== "Synced")
|
|
76
|
-
|
|
78
|
+
yield `sync ${sync}`;
|
|
77
79
|
const health = healthStatus(app);
|
|
78
80
|
const healthMessage = app.status?.health?.message;
|
|
79
81
|
if (health === "Degraded" || health === "Missing" || health === "Unknown") {
|
|
80
82
|
const label = health === "Unknown" ? "health Unknown" : health;
|
|
81
|
-
|
|
83
|
+
yield healthMessage ? `${label}: ${redact(healthMessage)}` : label;
|
|
82
84
|
}
|
|
83
85
|
else if (health === "Progressing") {
|
|
84
86
|
const minutes = minutesSince(app.status?.health?.lastTransitionTime, now);
|
|
85
87
|
if (minutes !== undefined && minutes > PROGRESSING_ATTENTION_MINUTES) {
|
|
86
|
-
|
|
88
|
+
yield `Progressing for ${formatMinutes(minutes)}`;
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
const op = app.status?.operationState;
|
|
90
92
|
if (op?.phase === "Failed" || op?.phase === "Error") {
|
|
91
|
-
|
|
92
|
-
? `last sync ${op.phase}: ${
|
|
93
|
-
: `last sync ${op.phase}
|
|
93
|
+
yield op.message
|
|
94
|
+
? `last sync ${op.phase}: ${redact(op.message)}`
|
|
95
|
+
: `last sync ${op.phase}`;
|
|
94
96
|
}
|
|
95
97
|
for (const condition of app.status?.conditions ?? []) {
|
|
96
|
-
|
|
97
|
-
? `${condition.type ?? "Condition"}: ${
|
|
98
|
-
: (condition.type ?? "Condition")
|
|
98
|
+
yield condition.message
|
|
99
|
+
? `${condition.type ?? "Condition"}: ${redact(condition.message)}`
|
|
100
|
+
: (condition.type ?? "Condition");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function attentionReasons(app, now = Date.now(), redactor = new Redactor()) {
|
|
104
|
+
return Array.from(attentionReasonTexts(app, now, redactor, true));
|
|
105
|
+
}
|
|
106
|
+
export function attentionReasonPreview(app, max, now = Date.now(), redactor = new Redactor()) {
|
|
107
|
+
let text = "";
|
|
108
|
+
let totalChars = 0;
|
|
109
|
+
for (const reason of attentionReasonTexts(app, now, redactor)) {
|
|
110
|
+
if (totalChars > 0) {
|
|
111
|
+
if (text.length < max)
|
|
112
|
+
text += "; ".slice(0, max - text.length);
|
|
113
|
+
totalChars += 2;
|
|
114
|
+
}
|
|
115
|
+
if (text.length < max)
|
|
116
|
+
text += reason.slice(0, max - text.length);
|
|
117
|
+
totalChars += reason.length;
|
|
99
118
|
}
|
|
100
|
-
return
|
|
119
|
+
return { text, totalChars };
|
|
101
120
|
}
|
|
102
121
|
export function needsAttention(app, now = Date.now()) {
|
|
103
122
|
return attentionReasons(app, now).length > 0;
|
package/dist/src/redact.js
CHANGED
|
@@ -127,6 +127,8 @@ function redactLine(line, count) {
|
|
|
127
127
|
return `${scheme}${space}${REDACTED}`;
|
|
128
128
|
});
|
|
129
129
|
text = maskUserinfo(text, count);
|
|
130
|
+
if (!/passw|passwd|secret|token|api[-_.]?key|private[-_.]?key|credential|access[-_.]?key|client[-_.]?secret/i.test(text))
|
|
131
|
+
return text;
|
|
130
132
|
return text.replace(KEY_VALUE, (match, q1, key, sep, q2, value) => {
|
|
131
133
|
if (REFERENCE_KEY.test(key) || value === REDACTED)
|
|
132
134
|
return match;
|
package/dist/src/registry.js
CHANGED
|
@@ -26,6 +26,12 @@ export const OPERATIONS = {
|
|
|
26
26
|
network: true,
|
|
27
27
|
timeoutMs: READ_TIMEOUT_MS,
|
|
28
28
|
},
|
|
29
|
+
"app sync": {
|
|
30
|
+
argv: ["app", "sync"],
|
|
31
|
+
tier: "write",
|
|
32
|
+
network: true,
|
|
33
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
34
|
+
},
|
|
29
35
|
"app resources": {
|
|
30
36
|
argv: ["app", "resources"],
|
|
31
37
|
tier: "read",
|
|
@@ -172,13 +178,6 @@ const PLANNED = "planned for the write tier, behind preview-by-default and the w
|
|
|
172
178
|
* answer REFUSED with the raw argocd route a human can take today.
|
|
173
179
|
*/
|
|
174
180
|
export const PLANNED_WRITES = {
|
|
175
|
-
"apps sync": {
|
|
176
|
-
reason: `apps sync is ${PLANNED}`,
|
|
177
|
-
help: [
|
|
178
|
-
"Preview what a sync would change with `argocd-axi apps diff <app>`",
|
|
179
|
-
"A human can sync with the raw CLI: `argocd app sync <app>`",
|
|
180
|
-
],
|
|
181
|
-
},
|
|
182
181
|
"apps rollback": {
|
|
183
182
|
reason: `apps rollback is ${PLANNED}`,
|
|
184
183
|
help: [
|
package/dist/src/skill.js
CHANGED
|
@@ -6,7 +6,7 @@ import { COMMAND_LINES, DESCRIPTION } from "./help.js";
|
|
|
6
6
|
* is stale.
|
|
7
7
|
*/
|
|
8
8
|
export const SKILL_DESCRIPTION = "Inspect Argo CD through the argocd-axi CLI - which applications are OutOfSync, Degraded or failing to sync and why, " +
|
|
9
|
-
"per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks,
|
|
9
|
+
"per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks, plus confirmed app syncs on writable contexts. " +
|
|
10
10
|
"Use whenever a task involves Argo CD or GitOps deployments: an app stuck or unhealthy, a sync that failed, " +
|
|
11
11
|
"what changed in a rollout, or whether a sync is allowed right now.";
|
|
12
12
|
export function createSkillMarkdown() {
|
|
@@ -26,12 +26,15 @@ If argocd-axi output suggests a command starting with \`argocd-axi\`, run it as
|
|
|
26
26
|
|
|
27
27
|
argocd-axi wraps the \`argocd\` CLI, which must be on PATH and already logged in (or have \`ARGOCD_AUTH_TOKEN\` set).
|
|
28
28
|
It uses that session as is and adds no auth of its own; it never logs in, never prompts, and never reads the argocd config file.
|
|
29
|
-
If a command answers \`
|
|
29
|
+
Run \`npx -y argocd-axi auth guide\` for setup instructions; the README is the authority for credential setup. The tool never logs in. If a command answers \`AUTH_EXPIRED\`, show the exact operator login command when the server is known; otherwise show the fallback login syntax and ask for the server address. An unreachable server is a separate error.
|
|
30
30
|
|
|
31
|
-
It is
|
|
32
|
-
Those verbs answer \`REFUSED\` with the raw \`argocd\` command a human can run; writes are on the roadmap.
|
|
31
|
+
It is read-only except for opt-in writes on contexts listed in \`writable-contexts\`. \`apps sync\` previews the diff and does nothing unless \`--confirm\` is passed; pruning also requires \`--prune\`. Other writes answer \`REFUSED\`.
|
|
33
32
|
For anything argocd-axi does not cover, the raw \`argocd\` CLI is the fallback.
|
|
34
33
|
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
Run \`npx -y argocd-axi auth guide\` for offline instructions. A human operator logs in interactively with \`argocd login <server> --sso --grpc-web\`; agents and CI use \`ARGOCD_SERVER\` and \`ARGOCD_AUTH_TOKEN\` from a scoped account token. The user owns login; argocd-axi never logs in. Verify access with \`npx -y argocd-axi whoami\`.
|
|
37
|
+
|
|
35
38
|
## Workflow
|
|
36
39
|
|
|
37
40
|
1. Start wide: \`npx -y argocd-axi\` shows the context, server, user, every app needing attention with the reason, and running operations.
|
package/dist/src/toon.js
CHANGED
|
@@ -18,19 +18,13 @@ export function encodeObject(value) {
|
|
|
18
18
|
return "";
|
|
19
19
|
return encode(clean);
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Render next-step suggestions as an indented block. `encode()` inlines
|
|
23
|
-
* primitive arrays on one comma-joined line, which is hard to read for
|
|
24
|
-
* sentences, so help is formatted by hand like other AXI tools do.
|
|
25
|
-
*/
|
|
21
|
+
/** Render next-step suggestions as a TOON list. */
|
|
26
22
|
export function renderHelp(lines) {
|
|
27
|
-
return
|
|
23
|
+
return encodeObject({ help: lines });
|
|
28
24
|
}
|
|
29
|
-
/** Render help sections as
|
|
25
|
+
/** Render help sections as TOON list items. */
|
|
30
26
|
export function renderHelpBlock(label, lines) {
|
|
31
|
-
|
|
32
|
-
return "";
|
|
33
|
-
return `${label}[${lines.length}]:\n${lines.map((line) => ` ${line}`).join("\n")}`;
|
|
27
|
+
return encodeObject({ [label]: lines });
|
|
34
28
|
}
|
|
35
29
|
/** Encode multi-line text (logs, diffs, manifests) as a TOON array. */
|
|
36
30
|
export function renderTextBlock(label, lines) {
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argocd-axi",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Agent-ergonomic
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Agent-ergonomic Argo CD CLI with opt-in writes - AXI-compliant argocd wrapper with token-efficient TOON output",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Laszlo Horanszky",
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: argocd-axi
|
|
3
|
-
description: "Inspect Argo CD through the argocd-axi CLI - which applications are OutOfSync, Degraded or failing to sync and why, per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks,
|
|
3
|
+
description: "Inspect Argo CD through the argocd-axi CLI - which applications are OutOfSync, Degraded or failing to sync and why, per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks, plus confirmed app syncs on writable contexts. Use whenever a task involves Argo CD or GitOps deployments: an app stuck or unhealthy, a sync that failed, what changed in a rollout, or whether a sync is allowed right now."
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# argocd-axi
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Argo CD for agents: read operations plus opt-in writes on writable contexts.
|
|
10
10
|
|
|
11
11
|
You do not need argocd-axi installed globally - invoke it with `npx -y argocd-axi <command>`.
|
|
12
12
|
If argocd-axi output suggests a command starting with `argocd-axi`, run it as `npx -y argocd-axi ...` instead.
|
|
13
13
|
|
|
14
14
|
argocd-axi wraps the `argocd` CLI, which must be on PATH and already logged in (or have `ARGOCD_AUTH_TOKEN` set).
|
|
15
15
|
It uses that session as is and adds no auth of its own; it never logs in, never prompts, and never reads the argocd config file.
|
|
16
|
-
If a command answers `
|
|
16
|
+
Run `npx -y argocd-axi auth guide` for setup instructions; the README is the authority for credential setup. The tool never logs in. If a command answers `AUTH_EXPIRED`, show the exact operator login command when the server is known; otherwise show the fallback login syntax and ask for the server address. An unreachable server is a separate error.
|
|
17
17
|
|
|
18
|
-
It is
|
|
19
|
-
Those verbs answer `REFUSED` with the raw `argocd` command a human can run; writes are on the roadmap.
|
|
18
|
+
It is read-only except for opt-in writes on contexts listed in `writable-contexts`. `apps sync` previews the diff and does nothing unless `--confirm` is passed; pruning also requires `--prune`. Other writes answer `REFUSED`.
|
|
20
19
|
For anything argocd-axi does not cover, the raw `argocd` CLI is the fallback.
|
|
21
20
|
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Run `npx -y argocd-axi auth guide` for offline instructions. A human operator logs in interactively with `argocd login <server> --sso --grpc-web`; agents and CI use `ARGOCD_SERVER` and `ARGOCD_AUTH_TOKEN` from a scoped account token. The user owns login; argocd-axi never logs in. Verify access with `npx -y argocd-axi whoami`.
|
|
24
|
+
|
|
22
25
|
## Workflow
|
|
23
26
|
|
|
24
27
|
1. Start wide: `npx -y argocd-axi` shows the context, server, user, every app needing attention with the reason, and running operations.
|
|
@@ -31,16 +34,18 @@ For anything argocd-axi does not cover, the raw `argocd` CLI is the fallback.
|
|
|
31
34
|
## Commands
|
|
32
35
|
|
|
33
36
|
```
|
|
34
|
-
commands[
|
|
37
|
+
commands[20]:
|
|
35
38
|
(no command): live overview - context, server, user, apps needing attention with the reason, running operations
|
|
36
39
|
ctx [--context <c>]: current context, server and CLI version, local only (what the session hook prints)
|
|
37
40
|
ctx use <context>: switch the argocd CLI's current context (local config only)
|
|
41
|
+
auth guide [--context <c>]: offline human login and agent token setup instructions
|
|
38
42
|
whoami: user, issuer and groups of the current credential
|
|
39
43
|
can-i <action> <resource> <object>: ask Argo CD RBAC, e.g. can-i sync applications 'payments/payments-api'
|
|
40
44
|
apps [--project --selector --repo --cluster --sync --health --problems --limit --fields]: applications, problems first
|
|
41
45
|
apps view <app>: source, destination, sync, health, operation, conditions, auto-sync, problem resources
|
|
42
46
|
apps resources <app> [--problems --orphaned]: managed resources with sync and health
|
|
43
47
|
apps diff <app> [--revision <rev>] [--full]: what differs from Git, per resource; raw diff with --full
|
|
48
|
+
apps sync <app> [--confirm] [--prune] [--timeout <s>]: preview, then optionally sync one app
|
|
44
49
|
apps history <app>: deployments, newest first
|
|
45
50
|
apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted
|
|
46
51
|
apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows
|