argocd-axi 0.1.5 → 0.1.6

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
@@ -48,8 +48,8 @@ argocd-axi turns that into small, definitive answers:
48
48
  the raw text is one `--full` away.
49
49
  - **Opt-in writes.** `apps sync <app>` previews the per-resource diff and
50
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.
51
+ `--prune` is explicit. Resource actions and automated-sync changes also
52
+ preview by default and execute only on configured writable contexts.
53
53
  - **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
54
54
  parameters are redacted and counted. No flag reveals them.
55
55
  - **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
@@ -146,11 +146,13 @@ argocd-axi apps view <app> [--full]
146
146
  argocd-axi apps resources <app> [--problems | --orphaned]
147
147
  argocd-axi apps diff <app> [--revision <rev>] [--full]
148
148
  argocd-axi apps sync <app> [--confirm] [--prune] [--timeout <s>] --context <c>
149
+ argocd-axi apps auto-sync <app> <on|off> [--confirm] --context <c>
149
150
  argocd-axi apps history <app> [--limit <n>]
150
151
  argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
151
152
  argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [--full] [-p]
152
153
  argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
153
154
  argocd-axi apps actions <app> [--kind <k>]
155
+ argocd-axi apps actions run <app> <action> --kind <k> --name <resource> [--confirm] --context <c>
154
156
  argocd-axi projects | projects view <p> | projects windows <p>
155
157
  argocd-axi appsets | appsets view <name> | appsets generate <file>
156
158
  argocd-axi repos | clusters
@@ -219,7 +221,7 @@ specific app.
219
221
  local-file and delete flags whatever a command passes, gives argocd no
220
222
  stdin, and enforces a timeout and an output cap. Tests fail if a new
221
223
  operation becomes reachable without updating the registry.
222
- - **Opt-in Application writes.** Sync, rollback, refresh and terminate-op require an explicit context, preview and `--confirm`; execution is limited by `writable-contexts` and Argo CD RBAC. Other writes and credential commands answer `REFUSED`.
224
+ - **Opt-in Application writes.** Sync, rollback, refresh, terminate-op, resource actions and automated-sync changes require an explicit context, preview and `--confirm`; execution is limited by `writable-contexts` and Argo CD RBAC. Other writes and credential commands answer `REFUSED`.
223
225
  - **Redaction.** Kubernetes Secret `data`, `stringData` and their
224
226
  last-applied annotation, values under sensitive-looking keys, and env-var or
225
227
  Helm-parameter pairs with sensitive-looking names are replaced with
@@ -231,9 +233,11 @@ specific app.
231
233
  - **Writable contexts.** argocd-axi's own config file,
232
234
  `~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
233
235
  holds the allowlist of contexts where Application writes may execute.
234
- It defaults to none. To allow writes on a context, a human runs
235
- `argocd-axi ctx writable add <context>` in a terminal and confirms the exact
236
- context name. Use `argocd-axi ctx writable remove <context>` to revoke access.
236
+ It defaults to none. To allow writes, a human runs
237
+ `argocd-axi ctx writable add` in a terminal to select a configured context,
238
+ or passes `<context>` directly; either path requires an explicit y/yes
239
+ confirmation. Non-interactive runs are refused. Use
240
+ `argocd-axi ctx writable remove <context>` to revoke access.
237
241
 
238
242
  - **Version cache.** The server version is checked at most once every 12
239
243
  hours per context and server, and cached in `~/.cache/argocd-axi/`
@@ -1,8 +1,9 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { setTimeout as delay } from "node:timers/promises";
2
3
  import { parseAllDocuments, stringify } from "yaml";
3
4
  import { argocdJson, runArgocd } from "../argocd.js";
4
5
  import { loadConfig } from "../config.js";
5
- import { evaluateWriteGate } from "../gate.js";
6
+ import { attributionArgs, evaluateWriteGate } from "../gate.js";
6
7
  import { CONTEXT_FLAG, getBool, getInt, getList, getString, parseArgs, parseDurationSeconds, validateName, } from "../args.js";
7
8
  import { CODES, axiError, mapArgocdFailure, validationError, } from "../errors.js";
8
9
  import { appName, appSources, attentionReasonPreview, attentionReasons, autoSyncSummary, destinationSummary, displayNames, formatAge, healthStatus, isOperationRunning, isResourceProblem, isUnhealthy, needsAttention, shortRevision, sortApps, syncRevision, syncStatus, plural, truncate, } from "../model.js";
@@ -1149,9 +1150,7 @@ export const APPS_ACTIONS_SPEC = {
1149
1150
  "argocd-axi apps actions guestbook",
1150
1151
  "argocd-axi apps actions guestbook --kind Rollout",
1151
1152
  ],
1152
- notes: [
1153
- "Running an action is planned for a later write tier; this command only lists them",
1154
- ],
1153
+ notes: ["List actions before running one with `apps actions run`"],
1155
1154
  };
1156
1155
  function pick(record, key) {
1157
1156
  const found = Object.keys(record).find((k) => k.toLowerCase() === key);
@@ -1159,7 +1158,7 @@ function pick(record, key) {
1159
1158
  }
1160
1159
  export async function appsActions(args) {
1161
1160
  if (args[0] === "run")
1162
- refuseActionRun();
1161
+ return appsActionRun(args.slice(1));
1163
1162
  const parsed = parseArgs(APPS_ACTIONS_SPEC, args);
1164
1163
  const name = validateName("app", parsed.positionals[0]);
1165
1164
  const argv = [name, "-o", "json"];
@@ -1185,10 +1184,126 @@ export async function appsActions(args) {
1185
1184
  }));
1186
1185
  return finish([encodeObject({ count: plural(rows.length, "action"), actions: rows })], undefined);
1187
1186
  }
1188
- function refuseActionRun() {
1189
- throw axiError("running resource actions is planned for the write tier, behind preview-by-default and the writable-contexts allowlist; not in v1 (see ROADMAP.md)", CODES.REFUSED, [
1190
- "A human can run one with the raw CLI: `argocd app actions run <app> <action> --kind <kind>`",
1191
- ]);
1187
+ const APPS_ACTION_RUN_SPEC = {
1188
+ path: "apps actions run",
1189
+ summary: "Preview and optionally run one action on one resource",
1190
+ positionals: [
1191
+ APP_ARG,
1192
+ { name: "action", required: true, description: "Listed resource action" },
1193
+ ],
1194
+ flags: [
1195
+ {
1196
+ name: "--kind",
1197
+ kind: "value",
1198
+ valueName: "kind",
1199
+ description: "Resource kind",
1200
+ },
1201
+ {
1202
+ name: "--name",
1203
+ kind: "value",
1204
+ valueName: "resource",
1205
+ description: "Resource name",
1206
+ },
1207
+ {
1208
+ name: "--group",
1209
+ kind: "value",
1210
+ valueName: "group",
1211
+ description: "Resource API group when needed to disambiguate",
1212
+ },
1213
+ {
1214
+ name: "--namespace",
1215
+ kind: "value",
1216
+ valueName: "namespace",
1217
+ description: "Resource namespace when needed to disambiguate",
1218
+ },
1219
+ { name: "--confirm", kind: "bool", description: "Execute after preview" },
1220
+ {
1221
+ name: "--timeout",
1222
+ kind: "value",
1223
+ valueName: "s",
1224
+ description: "Maximum wait in seconds (default 120)",
1225
+ },
1226
+ CONTEXT_FLAG,
1227
+ ],
1228
+ examples: [
1229
+ "argocd-axi apps actions run guestbook restart --kind Deployment --name guestbook --context staging",
1230
+ ],
1231
+ };
1232
+ async function appsActionRun(args) {
1233
+ const parsed = parseArgs(APPS_ACTION_RUN_SPEC, args);
1234
+ const appNameArg = validateName("app", parsed.positionals[0]);
1235
+ const action = validateName("action", parsed.positionals[1]);
1236
+ const kind = getString(parsed, "--kind");
1237
+ const resourceName = getString(parsed, "--name");
1238
+ const group = getString(parsed, "--group");
1239
+ const namespace = getString(parsed, "--namespace");
1240
+ const timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
1241
+ if (!kind || !resourceName)
1242
+ throw validationError("apps actions run needs --kind and --name");
1243
+ const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm");
1244
+ const gate = evaluateWriteGate({
1245
+ command: "apps actions run",
1246
+ context,
1247
+ execute: confirm,
1248
+ executeFlag: "--confirm",
1249
+ }, loadConfig().config);
1250
+ const options = { context: gate.context };
1251
+ const app = await getApp(appNameArg, options);
1252
+ const resources = (app.status?.resources ?? []).filter((r) => r.kind === kind &&
1253
+ r.name === resourceName &&
1254
+ (group === undefined || r.group === group) &&
1255
+ (namespace === undefined || r.namespace === namespace));
1256
+ const resource = resources[0];
1257
+ if (!resource)
1258
+ throw axiError(`REFUSED: ${kind}/${resourceName} is not managed by ${appNameArg}`, CODES.REFUSED);
1259
+ if (resources.length > 1)
1260
+ throw axiError(`REFUSED: ${kind}/${resourceName} is ambiguous; candidates: ${resources
1261
+ .map((r) => `${r.group ?? "core"}/${r.kind} ${r.namespace ?? "_cluster"}/${r.name}`)
1262
+ .join(", ")}. Specify --group and/or --namespace`, CODES.REFUSED);
1263
+ const actions = await argocdJson("app actions list", [appNameArg, "--kind", kind, "-o", "json"], options);
1264
+ const matchingActions = actions.filter((entry) => String(pick(entry, "name") ?? "") === resourceName &&
1265
+ String(pick(entry, "group") ?? "") === (resource.group ?? "") &&
1266
+ String(pick(entry, "kind") ?? "") === kind &&
1267
+ String(pick(entry, "action") ?? "") === action);
1268
+ const namespacedActions = matchingActions.filter((entry) => pick(entry, "namespace") !== undefined);
1269
+ const unnamespacedAction = matchingActions.length === 1 ? matchingActions[0] : undefined;
1270
+ const actionAvailable = namespacedActions.length
1271
+ ? namespacedActions.some((entry) => pick(entry, "namespace") === resource.namespace &&
1272
+ pick(entry, "disabled") !== true)
1273
+ : unnamespacedAction !== undefined &&
1274
+ pick(unnamespacedAction, "disabled") !== true;
1275
+ if (!actionAvailable)
1276
+ throw axiError(`REFUSED: action ${action} is unavailable for ${kind}/${resourceName}`, CODES.REFUSED);
1277
+ const blocks = [
1278
+ encodeObject({ action, app: appNameArg, kind, resource: resourceName }),
1279
+ ];
1280
+ if (!confirm)
1281
+ return finish(blocks, undefined, [
1282
+ "No changes made; add --confirm to run the action",
1283
+ ...(gate.mode === "preview" ? gate.help : []),
1284
+ ]);
1285
+ await checkWritePermission(`action/${resource.group ?? ""}/${kind}/${action}`, app, appNameArg, options);
1286
+ await runApplicationWrite("app actions run", [
1287
+ appNameArg,
1288
+ action,
1289
+ "--kind",
1290
+ kind,
1291
+ "--resource-name",
1292
+ resourceName,
1293
+ ...(resource.group ? ["--group", resource.group] : []),
1294
+ ...(resource.namespace ? ["--namespace", resource.namespace] : []),
1295
+ ...attributionArgs(),
1296
+ ], options, appNameArg);
1297
+ const current = await waitForAppState(appNameArg, timeout, options, (fresh) => fresh.status?.resources?.some((r) => r.group === resource.group &&
1298
+ r.kind === resource.kind &&
1299
+ r.namespace === resource.namespace &&
1300
+ r.name === resource.name &&
1301
+ r.health?.status === "Healthy") ?? false);
1302
+ const state = current.status?.resources?.find((r) => r.group === resource.group &&
1303
+ r.kind === resource.kind &&
1304
+ r.namespace === resource.namespace &&
1305
+ r.name === resource.name)?.health?.status ?? "unknown";
1306
+ return finish([...blocks, encodeObject({ result: "submitted", state })], undefined);
1192
1307
  }
1193
1308
  // ---------------------------------------------------------------- app writes
1194
1309
  function writeSpec(path, summary, positionals) {
@@ -1249,6 +1364,15 @@ function writeOutcome(phase, timedOut, completed) {
1249
1364
  return "waiting";
1250
1365
  return phase ?? "unknown";
1251
1366
  }
1367
+ async function waitForAppState(name, timeout, options, settled) {
1368
+ const deadline = Date.now() + timeout * 1000;
1369
+ let app = await getApp(name, options);
1370
+ while (!settled(app) && Date.now() < deadline) {
1371
+ await delay(Math.min(1000, Math.max(0, deadline - Date.now())));
1372
+ app = await getApp(name, options);
1373
+ }
1374
+ return app;
1375
+ }
1252
1376
  async function waitForWrite(name, timeout, options) {
1253
1377
  try {
1254
1378
  const result = await runArgocd("app wait", [name, "--operation", "--timeout", String(timeout)], { ...options, allowFailure: true, timeoutMs: (timeout + 10) * 1000 });
@@ -1412,6 +1536,61 @@ const SUBCOMMANDS = {
1412
1536
  refresh: { spec: APPS_REFRESH_SPEC, run: appsRefresh },
1413
1537
  "terminate-op": { spec: APPS_TERMINATE_SPEC, run: appsTerminateOp },
1414
1538
  };
1539
+ const APPS_AUTO_SYNC_SPEC = {
1540
+ path: "apps auto-sync",
1541
+ summary: "Preview and optionally enable or disable automated sync",
1542
+ positionals: [
1543
+ APP_ARG,
1544
+ { name: "mode", required: true, description: "on or off" },
1545
+ ],
1546
+ flags: [
1547
+ { name: "--confirm", kind: "bool", description: "Execute after preview" },
1548
+ {
1549
+ name: "--timeout",
1550
+ kind: "value",
1551
+ valueName: "s",
1552
+ description: "Maximum wait in seconds (default 120)",
1553
+ },
1554
+ CONTEXT_FLAG,
1555
+ ],
1556
+ examples: ["argocd-axi apps auto-sync guestbook on --context staging"],
1557
+ };
1558
+ async function appsAutoSync(args) {
1559
+ const parsed = parseArgs(APPS_AUTO_SYNC_SPEC, args);
1560
+ const name = validateName("app", parsed.positionals[0]);
1561
+ const mode = parsed.positionals[1];
1562
+ if (mode !== "on" && mode !== "off")
1563
+ throw validationError("mode must be on or off");
1564
+ const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
1565
+ const gate = evaluateWriteGate({
1566
+ command: "apps auto-sync",
1567
+ context,
1568
+ execute: confirm,
1569
+ executeFlag: "--confirm",
1570
+ }, loadConfig().config);
1571
+ const options = { context: gate.context }, app = await getApp(name, options);
1572
+ const current = autoSyncSummary(app), blocks = [encodeObject({ autoSync: name, current, target: mode })];
1573
+ if (!confirm)
1574
+ return finish(blocks, undefined, [
1575
+ "No changes made; add --confirm to change automated sync",
1576
+ ...(gate.mode === "preview" ? gate.help : []),
1577
+ ]);
1578
+ await checkWritePermission("update", app, name, options);
1579
+ await runApplicationWrite("app set", [
1580
+ name,
1581
+ "--sync-policy",
1582
+ mode === "on" ? "automated" : "none",
1583
+ ...attributionArgs(),
1584
+ ], options, name);
1585
+ const observed = await waitForAppState(name, timeout, options, (fresh) => mode === "on"
1586
+ ? autoSyncSummary(fresh).startsWith("automated")
1587
+ : autoSyncSummary(fresh) === "manual");
1588
+ return finish([
1589
+ ...blocks,
1590
+ encodeObject({ result: "submitted", state: autoSyncSummary(observed) }),
1591
+ ], undefined);
1592
+ }
1593
+ SUBCOMMANDS["auto-sync"] = { spec: APPS_AUTO_SYNC_SPEC, run: appsAutoSync };
1415
1594
  export const APPS_SPECS = [
1416
1595
  APPS_LIST_SPEC,
1417
1596
  ...Object.values(SUBCOMMANDS).map((s) => s.spec),
@@ -150,12 +150,15 @@ const WRITABLE_ADD_SPEC = {
150
150
  positionals: [
151
151
  {
152
152
  name: "context",
153
- required: true,
153
+ required: false,
154
154
  description: "Configured Argo CD context",
155
155
  },
156
156
  ],
157
157
  flags: [],
158
- examples: ["argocd-axi ctx writable add staging"],
158
+ examples: [
159
+ "argocd-axi ctx writable add",
160
+ "argocd-axi ctx writable add staging",
161
+ ],
159
162
  };
160
163
  const WRITABLE_REMOVE_SPEC = {
161
164
  path: "ctx writable remove",
@@ -177,44 +180,92 @@ const WRITABLE_LIST_SPEC = {
177
180
  flags: [],
178
181
  examples: ["argocd-axi ctx writable list"],
179
182
  };
183
+ export async function readConfirmation(input, output, prompt) {
184
+ const rl = createInterface({ input, output });
185
+ const closed = new Promise((resolve) => rl.once("close", () => resolve("")));
186
+ try {
187
+ return await Promise.race([rl.question(prompt), closed]);
188
+ }
189
+ finally {
190
+ rl.close();
191
+ }
192
+ }
193
+ export async function confirmWritableContext(name, server, input, output) {
194
+ const answer = await readConfirmation(input, output, `Allow argocd-axi to sync, rollback, refresh, and terminate operations on context ${name} (${server})? [y/N] `);
195
+ return /^y(?:es)?$/i.test(answer.trim());
196
+ }
197
+ export async function contextDetails(contexts, lookup = async (name) => (await argocdJson("account get-user-info", ["-o", "json"], { context: name, timeoutMs: 1_500 })).username) {
198
+ const result = [];
199
+ for (let offset = 0; offset < contexts.length; offset += 3) {
200
+ const batch = await Promise.all(contexts.slice(offset, offset + 3).map(async (context) => ({
201
+ ...context,
202
+ username: (await lookup(context.name).catch(() => undefined)) ?? "unknown",
203
+ })));
204
+ result.push(...batch);
205
+ }
206
+ return result;
207
+ }
208
+ export function addWritableContextIfConfirmed(name, confirmed) {
209
+ return confirmed ? addWritableContext(name) : false;
210
+ }
211
+ export async function chooseContext(contexts, writable, input, output) {
212
+ output.write(contexts
213
+ .map((context, index) => `${index + 1}. ${context.name} - ${context.server} - ${context.username}${writable.includes(context.name) ? " (already writable)" : ""}`)
214
+ .join("\n") + "\n");
215
+ const selected = await readConfirmation(input, output, "Select a context by number: ");
216
+ const index = Number(selected);
217
+ const context = Number.isInteger(index) && index >= 1 ? contexts[index - 1] : undefined;
218
+ if (!context)
219
+ throw axiError("invalid context selection; no changes made", CODES.REFUSED);
220
+ return context.name;
221
+ }
180
222
  async function writableAdd(args) {
181
223
  const parsed = parseArgs(WRITABLE_ADD_SPEC, args);
182
- const name = validateContextName(parsed.positionals[0]);
224
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
225
+ throw axiError("writable context changes require an interactive terminal; run this command in a terminal", CODES.REFUSED);
183
226
  const listing = await listContexts();
184
227
  if (listing.notInstalled)
185
228
  throw listing.notInstalled;
186
229
  if (listing.unavailable) {
187
230
  throw axiError(`cannot list Argo CD contexts: ${listing.unavailable}`, CODES.NO_CONTEXT, LOGIN_HELP);
188
231
  }
189
- const match = listing.contexts.find((context) => context.name === name);
232
+ const writable = loadConfig().config.writableContexts;
233
+ let name = parsed.positionals[0]
234
+ ? validateContextName(parsed.positionals[0])
235
+ : undefined;
236
+ let username;
237
+ if (name === undefined) {
238
+ if (listing.contexts.length === 0)
239
+ throw axiError("no configured Argo CD contexts to select", CODES.NO_CONTEXT);
240
+ const details = await contextDetails(listing.contexts);
241
+ name = await chooseContext(details, writable, process.stdin, process.stdout);
242
+ username = details.find((context) => context.name === name)?.username;
243
+ }
244
+ const selectedName = name;
245
+ const match = listing.contexts.find((context) => context.name === selectedName);
190
246
  if (!match)
191
247
  throw axiError(`Argo CD context ${name} is not configured`, CODES.CONTEXT_NOT_FOUND);
192
- if (loadConfig().config.writableContexts.includes(name))
248
+ if (writable.includes(selectedName))
193
249
  return finish([
194
250
  encodeObject({
195
- writableContext: `${name} is already writable (no change)`,
251
+ writableContext: `${selectedName} is already writable (no change)`,
252
+ }),
253
+ ], undefined);
254
+ username ??= (await argocdJson("account get-user-info", ["-o", "json"], { context: selectedName })).username;
255
+ process.stdout.write(`Context: ${selectedName}\nServer: ${match.server}\nUser: ${username ?? "unknown"}\n`);
256
+ const confirmed = await confirmWritableContext(selectedName, match.server, process.stdin, process.stdout);
257
+ const changed = addWritableContextIfConfirmed(selectedName, confirmed);
258
+ if (!confirmed)
259
+ return finish([
260
+ encodeObject({
261
+ writableContext: `${selectedName} remains read-only (no change)`,
196
262
  }),
197
263
  ], undefined);
198
- if (!process.stdin.isTTY || !process.stdout.isTTY)
199
- throw axiError("writable context changes require an interactive terminal", CODES.REFUSED);
200
- const info = await argocdJson("account get-user-info", ["-o", "json"], { context: name });
201
- process.stdout.write(`Context: ${name}\nServer: ${match.server}\nUser: ${info.username ?? "unknown"}\nType ${name} to allow writes: `);
202
- const rl = createInterface({ input: process.stdin, output: process.stdout });
203
- let answer;
204
- try {
205
- answer = await rl.question("");
206
- }
207
- finally {
208
- rl.close();
209
- }
210
- if (answer !== name)
211
- throw axiError("confirmation did not match; no changes made", CODES.REFUSED);
212
- const changed = addWritableContext(name);
213
264
  return finish([
214
265
  encodeObject({
215
266
  writableContext: changed
216
- ? `added ${name}`
217
- : `${name} is already writable (no change)`,
267
+ ? `added ${selectedName}`
268
+ : `${selectedName} is already writable (no change)`,
218
269
  }),
219
270
  ], undefined);
220
271
  }
package/dist/src/help.js CHANGED
@@ -16,11 +16,13 @@ export const COMMAND_LINES = [
16
16
  "apps rollback <app> <history-id> [--confirm] [--timeout <s>]: preview revision, then optionally roll back",
17
17
  "apps refresh <app> [--hard] [--confirm]: preview, then optionally refresh one app",
18
18
  "apps terminate-op <app> [--confirm] [--timeout <s>]: preview, then optionally stop a running operation",
19
+ "apps auto-sync <app> <on|off> [--confirm]: preview, then optionally change automated sync",
20
+ "apps actions run <app> <action> --kind <kind> --name <resource> [--confirm]: preview, then optionally run one listed resource action",
19
21
  "apps history <app>: deployments, newest first",
20
22
  "apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted",
21
23
  "apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows",
22
24
  "apps wait <app> [--sync --health --operation] [--timeout <s>]: bounded wait, returns the state either way",
23
- "apps actions <app>: resource actions available (listing only)",
25
+ "apps actions <app>: list available resource actions",
24
26
  "projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now",
25
27
  "appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate",
26
28
  "repos | clusters: repositories and destination clusters with connection status",
@@ -42,7 +44,7 @@ export const TOP_HELP = `${renderBlocks([
42
44
  "argocd-axi apps diff payments-api",
43
45
  ]),
44
46
  renderHelpBlock("notes", [
45
- "Read-only except opt-in writes on writable contexts: apps sync, rollback, refresh and terminate-op require --confirm; sync never prunes unless --prune is given",
47
+ "Writes on writable contexts require --confirm: apps sync, rollback, refresh, terminate-op, auto-sync and actions run; sync never prunes unless --prune is given",
46
48
  "Output is TOON on stdout, errors included; exit 0 = success (an unhealthy app is data, not an error), 1 = error, 2 = usage",
47
49
  "Secret-looking values are redacted and counted; no flag reveals them",
48
50
  "Uses your existing argocd CLI session or ARGOCD_AUTH_TOKEN; never logs in, never reads the argocd config file",
@@ -50,6 +50,18 @@ export const OPERATIONS = {
50
50
  network: true,
51
51
  timeoutMs: READ_TIMEOUT_MS,
52
52
  },
53
+ "app actions run": {
54
+ argv: ["app", "actions", "run"],
55
+ tier: "write",
56
+ network: true,
57
+ timeoutMs: READ_TIMEOUT_MS,
58
+ },
59
+ "app set": {
60
+ argv: ["app", "set"],
61
+ tier: "write",
62
+ network: true,
63
+ timeoutMs: READ_TIMEOUT_MS,
64
+ },
53
65
  "app resources": {
54
66
  argv: ["app", "resources"],
55
67
  tier: "read",
@@ -190,20 +202,11 @@ export const FORBIDDEN_ARGS = [
190
202
  "--port-forward",
191
203
  "--prompts-enabled",
192
204
  ];
193
- const PLANNED = "planned for the write tier, behind preview-by-default and the writable-contexts allowlist; not in v1 (see ROADMAP.md)";
194
205
  /**
195
206
  * Writes the design names for the next tier (decision A2): known verbs that
196
207
  * answer REFUSED with the raw argocd route a human can take today.
197
208
  */
198
- export const PLANNED_WRITES = {
199
- "apps action": {
200
- reason: `running resource actions is ${PLANNED}`,
201
- help: [
202
- "List available actions with `argocd-axi apps actions <app>`",
203
- "A human can run one with the raw CLI: `argocd app actions run <app> <action> --kind <kind>`",
204
- ],
205
- },
206
- };
209
+ export const PLANNED_WRITES = {};
207
210
  const HUMAN_ONLY = "A human runs this with the raw argocd CLI";
208
211
  /**
209
212
  * Known verbs that argocd-axi never wraps (design decisions S7 and A2), each
package/dist/src/skill.js CHANGED
@@ -33,7 +33,7 @@ For anything argocd-axi does not cover, the raw \`argocd\` CLI is the fallback.
33
33
 
34
34
  ## Setup
35
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\`. To enable writes on a context, a human runs \`argocd-axi ctx writable add <context>\` in a terminal and confirms the context name.
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\`. See the README's writable-context instructions before enabling writes.
37
37
 
38
38
  ## Workflow
39
39
 
@@ -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.5"; // x-release-please-version
3
+ export const VERSION = "0.1.6"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argocd-axi",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",
@@ -20,7 +20,7 @@ For anything argocd-axi does not cover, the raw `argocd` CLI is the fallback.
20
20
 
21
21
  ## Setup
22
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`. To enable writes on a context, a human runs `argocd-axi ctx writable add <context>` in a terminal and confirms the context name.
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`. See the README's writable-context instructions before enabling writes.
24
24
 
25
25
  ## Workflow
26
26
 
@@ -34,7 +34,7 @@ Run `npx -y argocd-axi auth guide` for offline instructions. A human operator lo
34
34
  ## Commands
35
35
 
36
36
  ```
37
- commands[23]:
37
+ commands[25]:
38
38
  (no command): live overview - context, server, user, apps needing attention with the reason, running operations
39
39
  ctx [--context <c>]: current context, server and CLI version, local only (what the session hook prints)
40
40
  ctx use <context>: switch the argocd CLI's current context (local config only)
@@ -49,11 +49,13 @@ commands[23]:
49
49
  apps rollback <app> <history-id> [--confirm] [--timeout <s>]: preview revision, then optionally roll back
50
50
  apps refresh <app> [--hard] [--confirm]: preview, then optionally refresh one app
51
51
  apps terminate-op <app> [--confirm] [--timeout <s>]: preview, then optionally stop a running operation
52
+ apps auto-sync <app> <on|off> [--confirm]: preview, then optionally change automated sync
53
+ apps actions run <app> <action> --kind <kind> --name <resource> [--confirm]: preview, then optionally run one listed resource action
52
54
  apps history <app>: deployments, newest first
53
55
  apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted
54
56
  apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows
55
57
  apps wait <app> [--sync --health --operation] [--timeout <s>]: bounded wait, returns the state either way
56
- apps actions <app>: resource actions available (listing only)
58
+ apps actions <app>: list available resource actions
57
59
  projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now
58
60
  appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate
59
61
  repos | clusters: repositories and destination clusters with connection status