argocd-axi 0.1.3 → 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 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
- Read-only Argo CD for agents. `argocd-axi` wraps the `argocd` CLI and answers
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: Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows
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,9 +46,10 @@ 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
- - **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).
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
@@ -75,15 +74,22 @@ Requirements:
75
74
  v3.3.6. There is no hard minimum; `ctx` and the home view warn when the CLI
76
75
  and server differ by a major version.
77
76
 
78
- ## Credentials
77
+ ## Setup
79
78
 
80
79
  argocd-axi adds no auth of its own. It uses whatever the `argocd` CLI already
81
80
  has: the current context's session, or `ARGOCD_SERVER` and
82
81
  `ARGOCD_AUTH_TOKEN` from the environment. It never starts a login, never
83
82
  prompts, and never reads the argocd config file (`~/.config/argocd/config`,
84
- which holds clear-text tokens). When a session is missing or expired, it
85
- answers `AUTH_REQUIRED` and tells the agent that a human runs
86
- `argocd login <server> --sso` or `argocd relogin`.
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
87
93
 
88
94
  An agent that uses your own SSO session has all of your Argo CD permissions.
89
95
  The recommended agent credential is a **short-lived project-role token**,
@@ -139,9 +145,10 @@ argocd-axi apps [--problems --project --selector --repo --cluster --sync --healt
139
145
  argocd-axi apps view <app> [--full]
140
146
  argocd-axi apps resources <app> [--problems | --orphaned]
141
147
  argocd-axi apps diff <app> [--revision <rev>] [--full]
148
+ argocd-axi apps sync <app> [--confirm] [--prune] [--timeout <s>] --context <c>
142
149
  argocd-axi apps history <app> [--limit <n>]
143
150
  argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
144
- 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]
145
152
  argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
146
153
  argocd-axi apps actions <app> [--kind <k>]
147
154
  argocd-axi projects | projects view <p> | projects windows <p>
@@ -187,20 +194,21 @@ specific app.
187
194
  Unknown flags are rejected before any argocd call, with the valid flags
188
195
  listed.
189
196
 
190
- | Code | Meaning |
191
- | --------------------------------- | -------------------------------------------------------------------------------------- |
192
- | `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
193
- | `AUTH_REQUIRED` | No session or an expired one; a human logs in |
194
- | `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
195
- | `NOT_FOUND` | No such app, project or ApplicationSet |
196
- | `RATE_LIMITED` | The server asked to slow down |
197
- | `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
198
- | `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
199
- | `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
200
- | `TIMEOUT` | argocd did not answer in time |
201
- | `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
202
- | `CONFIG_INVALID` | argocd-axi's own config file is malformed |
203
- | `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
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 |
204
212
 
205
213
  ## Safety model
206
214
 
package/ROADMAP.md CHANGED
@@ -1,20 +1,19 @@
1
1
  # Roadmap
2
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.
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 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:
9
+ These write verbs still answer `REFUSED` and name the raw `argocd` command a
10
+ human can run:
11
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 |
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
@@ -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
- throw mapArgocdFailure(result.stderr || result.stdout, options.failure);
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
- row.why = truncate(attentionReasons(app, now, redactor).join("; "), 160);
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
- if (full) {
754
- const yaml = objects.map((obj) => stringify(obj).trimEnd()).join("\n---\n");
755
- blocks.push(renderTextBlock("yaml", yaml.split("\n")));
756
- }
757
- return finish(blocks, redactor, full
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 (chars + line.length + 1 > LOG_CHAR_CAP && kept.length > 0)
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 newest = kept[0];
861
- const lineCut = newest.length > LOG_CHAR_CAP;
862
- if (lineCut)
863
- kept[0] = newest.slice(-LOG_CHAR_CAP);
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 ${LOG_CHAR_CAP} of the newest line's ${newest.length} chars`
888
+ ? ` and only the last ${charCap} of the newest line's ${cutLineChars} chars`
866
889
  : "";
867
- const header = kept.length < redacted.length || lineCut
868
- ? `showing the newest ${kept.length} of ${redacted.length} lines (output capped at ${LOG_CHAR_CAP} chars)${cutNote}`
869
- : `${kept.length} lines (--tail ${tail})`;
870
- return finish([encodeObject({ logs: header }), renderTextBlock("lines", kept)], redactor, []);
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 || text.length <= max)
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, ${text.length} chars total)`,
94
+ text: `${text.slice(0, max)}... (truncated, ${totalChars} chars total)`,
95
95
  truncated: true,
96
96
  };
97
97
  }
@@ -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` (or `argocd relogin`); argocd-axi never starts a login",
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|no session information|unauthenticated|please login|login required|failed to get token|session (?:has )?expired/i.test(cleaned)) {
112
- return axiError(`Not authenticated to Argo CD: ${detail}`, CODES.AUTH_REQUIRED, LOGIN_HELP);
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
- * The write gate (design decisions S3, S8, S15 and A7), built ahead of the
6
- * write tier so that tier only adds commands. No v1 command calls it to run
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 = "Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows";
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: v1 never syncs, rolls back, refreshes or deletes; those verbs answer REFUSED with the raw argocd route (writes are on the roadmap)",
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
- export function attentionReasons(app, now = Date.now(), redactor = new Redactor()) {
70
- const clip = (text) => truncate(redactor.text(text), REASON_CHARS);
71
- const reasons = [];
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
- reasons.push("OutOfSync");
76
+ yield "OutOfSync";
75
77
  else if (sync !== "Synced")
76
- reasons.push(`sync ${sync}`);
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
- reasons.push(healthMessage ? `${label}: ${clip(healthMessage)}` : label);
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
- reasons.push(`Progressing for ${formatMinutes(minutes)}`);
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
- reasons.push(op.message
92
- ? `last sync ${op.phase}: ${clip(op.message)}`
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
- reasons.push(condition.message
97
- ? `${condition.type ?? "Condition"}: ${clip(condition.message)}`
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 reasons;
119
+ return { text, totalChars };
101
120
  }
102
121
  export function needsAttention(app, now = Date.now()) {
103
122
  return attentionReasons(app, now).length > 0;
@@ -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;
@@ -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, all read-only. " +
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 \`AUTH_REQUIRED\`, ask the user to run \`argocd login <server> --sso\` (or \`argocd relogin\`) themselves.
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 **read-only**: v1 cannot sync, roll back, refresh, delete or change anything in Argo CD.
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 renderHelpBlock("help", lines);
23
+ return encodeObject({ help: lines });
28
24
  }
29
- /** Render help sections as readable indented lines, not data arrays. */
25
+ /** Render help sections as TOON list items. */
30
26
  export function renderHelpBlock(label, lines) {
31
- if (lines.length === 0)
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) {
@@ -1,3 +1,3 @@
1
1
  // Leaf module: node builtins only, so the bin fast path can answer
2
2
  // `--version` without loading the command graph.
3
- export const VERSION = "0.1.3"; // x-release-please-version
3
+ export const VERSION = "0.1.4"; // x-release-please-version
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "argocd-axi",
3
- "version": "0.1.3",
4
- "description": "Agent-ergonomic, read-only Argo CD CLI - AXI-compliant argocd wrapper with token-efficient TOON output",
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, all read-only. 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."
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
- Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows.
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 `AUTH_REQUIRED`, ask the user to run `argocd login <server> --sso` (or `argocd relogin`) themselves.
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 **read-only**: v1 cannot sync, roll back, refresh, delete or change anything in Argo CD.
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[18]:
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