argocd-axi 0.1.5 → 0.1.7
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 +26 -15
- package/dist/src/cli.js +2 -0
- package/dist/src/commands/apps.js +188 -9
- package/dist/src/commands/context.js +83 -0
- package/dist/src/commands/ctx.js +74 -23
- package/dist/src/commands/setup.js +26 -3
- package/dist/src/help.js +7 -3
- package/dist/src/registry.js +13 -10
- package/dist/src/session-hook.js +117 -0
- package/dist/src/sessionlog.js +347 -0
- package/dist/src/skill.js +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/argocd-axi/SKILL.md +8 -4
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
|
|
52
|
-
|
|
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
|
|
@@ -121,14 +121,20 @@ and every other command that prints or changes a credential.
|
|
|
121
121
|
|
|
122
122
|
Pick one; you do not need both.
|
|
123
123
|
|
|
124
|
-
- **Session hook (recommended).** `argocd-axi setup hooks` installs
|
|
125
|
-
SessionStart
|
|
126
|
-
`argocd-axi-ctx`, which is local only:
|
|
127
|
-
and the cached server version. It makes
|
|
128
|
-
refresh and reads no credential, so a dead
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
- **Session hook (recommended).** `argocd-axi setup hooks` installs
|
|
125
|
+
SessionStart hooks for Claude Code, Codex and OpenCode, plus a Claude Code
|
|
126
|
+
SessionEnd hook. The start hook runs `argocd-axi-ctx`, which is local only:
|
|
127
|
+
current context, server, CLI version and the cached server version. It makes
|
|
128
|
+
no network call, triggers no SSO refresh and reads no credential, so a dead
|
|
129
|
+
context can never hang a session start. The end hook records only argocd-axi
|
|
130
|
+
command names, their counts and the most-used `--context` value for that
|
|
131
|
+
working directory; it does not save arguments or other values. The session
|
|
132
|
+
log is limited to 100 directories and stored with `0600` permissions. Run
|
|
133
|
+
`argocd-axi context` to view the current directory's last session, or
|
|
134
|
+
`argocd-axi setup status` / `argocd-axi setup uninstall` to inspect or remove
|
|
135
|
+
the Claude Code SessionEnd hook. Hooks need a persistent install
|
|
136
|
+
(`npm install -g argocd-axi`); run from an npx cache, installation is refused
|
|
137
|
+
because hooks would point at a temporary path.
|
|
132
138
|
- **Skill.** `npx skills add Dematom1/argocd-axi --skill argocd-axi`
|
|
133
139
|
installs [`skills/argocd-axi/SKILL.md`](skills/argocd-axi/SKILL.md), which
|
|
134
140
|
is generated from the CLI's own help and loads when a task is about Argo CD.
|
|
@@ -146,15 +152,18 @@ argocd-axi apps view <app> [--full]
|
|
|
146
152
|
argocd-axi apps resources <app> [--problems | --orphaned]
|
|
147
153
|
argocd-axi apps diff <app> [--revision <rev>] [--full]
|
|
148
154
|
argocd-axi apps sync <app> [--confirm] [--prune] [--timeout <s>] --context <c>
|
|
155
|
+
argocd-axi apps auto-sync <app> <on|off> [--confirm] --context <c>
|
|
149
156
|
argocd-axi apps history <app> [--limit <n>]
|
|
150
157
|
argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
|
|
151
158
|
argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [--full] [-p]
|
|
152
159
|
argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
|
|
153
160
|
argocd-axi apps actions <app> [--kind <k>]
|
|
161
|
+
argocd-axi apps actions run <app> <action> --kind <k> --name <resource> [--confirm] --context <c>
|
|
154
162
|
argocd-axi projects | projects view <p> | projects windows <p>
|
|
155
163
|
argocd-axi appsets | appsets view <name> | appsets generate <file>
|
|
156
164
|
argocd-axi repos | clusters
|
|
157
|
-
argocd-axi
|
|
165
|
+
argocd-axi context [end]
|
|
166
|
+
argocd-axi setup hooks | status | uninstall
|
|
158
167
|
argocd-axi update [--check]
|
|
159
168
|
```
|
|
160
169
|
|
|
@@ -219,7 +228,7 @@ specific app.
|
|
|
219
228
|
local-file and delete flags whatever a command passes, gives argocd no
|
|
220
229
|
stdin, and enforces a timeout and an output cap. Tests fail if a new
|
|
221
230
|
operation becomes reachable without updating the registry.
|
|
222
|
-
- **Opt-in Application writes.** Sync, rollback, refresh
|
|
231
|
+
- **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
232
|
- **Redaction.** Kubernetes Secret `data`, `stringData` and their
|
|
224
233
|
last-applied annotation, values under sensitive-looking keys, and env-var or
|
|
225
234
|
Helm-parameter pairs with sensitive-looking names are replaced with
|
|
@@ -231,9 +240,11 @@ specific app.
|
|
|
231
240
|
- **Writable contexts.** argocd-axi's own config file,
|
|
232
241
|
`~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
|
|
233
242
|
holds the allowlist of contexts where Application writes may execute.
|
|
234
|
-
It defaults to none. To allow writes
|
|
235
|
-
`argocd-axi ctx writable add
|
|
236
|
-
|
|
243
|
+
It defaults to none. To allow writes, a human runs
|
|
244
|
+
`argocd-axi ctx writable add` in a terminal to select a configured context,
|
|
245
|
+
or passes `<context>` directly; either path requires an explicit y/yes
|
|
246
|
+
confirmation. Non-interactive runs are refused. Use
|
|
247
|
+
`argocd-axi ctx writable remove <context>` to revoke access.
|
|
237
248
|
|
|
238
249
|
- **Version cache.** The server version is checked at most once every 12
|
|
239
250
|
hours per context and server, and cached in `~/.cache/argocd-axi/`
|
package/dist/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { homeCommand } from "./commands/home.js";
|
|
|
10
10
|
import { clustersCommand, reposCommand } from "./commands/inventory.js";
|
|
11
11
|
import { projectsCommand } from "./commands/projects.js";
|
|
12
12
|
import { setupCommand } from "./commands/setup.js";
|
|
13
|
+
import { contextCommand } from "./commands/context.js";
|
|
13
14
|
import { DESCRIPTION, TOP_HELP } from "./help.js";
|
|
14
15
|
import { NATIVE_NOUNS, REFUSALS } from "./registry.js";
|
|
15
16
|
import { renderBlocks, renderHelp } from "./toon.js";
|
|
@@ -55,6 +56,7 @@ export const COMMANDS = Object.assign(Object.create(null), {
|
|
|
55
56
|
repos: reposCommand,
|
|
56
57
|
clusters: clustersCommand,
|
|
57
58
|
setup: setupCommand,
|
|
59
|
+
context: contextCommand,
|
|
58
60
|
...Object.fromEntries(TOP_LEVEL_REFUSALS.map((key) => [key, refusalCommand(key)])),
|
|
59
61
|
});
|
|
60
62
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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),
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { parseArgs } from "../args.js";
|
|
3
|
+
import { CODES, axiError } from "../errors.js";
|
|
4
|
+
import { encodeObject } from "../toon.js";
|
|
5
|
+
import { finish } from "./common.js";
|
|
6
|
+
import { lastSession, recordSession, sessionText, summarizeTranscript, } from "../sessionlog.js";
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function directory(path) {
|
|
11
|
+
try {
|
|
12
|
+
return realpathSync(path);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function display(summary) {
|
|
19
|
+
return summary ? sessionText(summary) : undefined;
|
|
20
|
+
}
|
|
21
|
+
const CONTEXT_SPEC = {
|
|
22
|
+
path: "context",
|
|
23
|
+
summary: "Show or record session memory",
|
|
24
|
+
positionals: [{ name: "action", required: false, description: "end" }],
|
|
25
|
+
flags: [],
|
|
26
|
+
examples: ["argocd-axi context", "argocd-axi context end"],
|
|
27
|
+
};
|
|
28
|
+
export async function contextCommand(args) {
|
|
29
|
+
const parsed = parseArgs(CONTEXT_SPEC, args);
|
|
30
|
+
const sub = parsed.positionals[0];
|
|
31
|
+
if (sub !== undefined && sub !== "end") {
|
|
32
|
+
throw axiError("unknown context action", CODES.VALIDATION_ERROR, [
|
|
33
|
+
"Run `argocd-axi context` or `argocd-axi context end`",
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
if (sub !== "end") {
|
|
37
|
+
const summary = display(lastSession(directory(process.cwd())));
|
|
38
|
+
return finish([encodeObject({ last_session: summary })], undefined);
|
|
39
|
+
}
|
|
40
|
+
if (process.stdin.isTTY)
|
|
41
|
+
throw axiError("context end reads a SessionEnd hook payload on stdin", CODES.VALIDATION_ERROR, ["It runs from the hook installed by `argocd-axi setup hooks`"]);
|
|
42
|
+
let payload;
|
|
43
|
+
try {
|
|
44
|
+
payload = JSON.parse(await new Promise((resolve, reject) => {
|
|
45
|
+
let text = "";
|
|
46
|
+
process.stdin.setEncoding("utf8");
|
|
47
|
+
process.stdin.on("data", (chunk) => (text += chunk));
|
|
48
|
+
process.stdin.on("end", () => resolve(text));
|
|
49
|
+
process.stdin.on("error", reject);
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return finish([encodeObject({ recorded: "nothing - stdin is not a hook payload" })], undefined);
|
|
54
|
+
}
|
|
55
|
+
if (!isRecord(payload) || typeof payload.transcript_path !== "string")
|
|
56
|
+
return finish([encodeObject({ recorded: "nothing - payload has no transcript_path" })], undefined);
|
|
57
|
+
let summary;
|
|
58
|
+
try {
|
|
59
|
+
summary = await summarizeTranscript(payload.transcript_path);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return finish([encodeObject({ recorded: "nothing - transcript could not be read" })], undefined);
|
|
63
|
+
}
|
|
64
|
+
if (!summary)
|
|
65
|
+
return finish([
|
|
66
|
+
encodeObject({
|
|
67
|
+
recorded: "nothing - no argocd-axi commands this session",
|
|
68
|
+
}),
|
|
69
|
+
], undefined);
|
|
70
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : process.cwd();
|
|
71
|
+
try {
|
|
72
|
+
if (!(await recordSession(directory(cwd), summary)))
|
|
73
|
+
return finish([encodeObject({ recorded: "nothing - session log is busy" })], undefined);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return finish([
|
|
77
|
+
encodeObject({
|
|
78
|
+
recorded: "nothing - session log could not be written",
|
|
79
|
+
}),
|
|
80
|
+
], undefined);
|
|
81
|
+
}
|
|
82
|
+
return finish([encodeObject({ recorded: sessionText(summary) })], undefined);
|
|
83
|
+
}
|
package/dist/src/commands/ctx.js
CHANGED
|
@@ -150,12 +150,15 @@ const WRITABLE_ADD_SPEC = {
|
|
|
150
150
|
positionals: [
|
|
151
151
|
{
|
|
152
152
|
name: "context",
|
|
153
|
-
required:
|
|
153
|
+
required: false,
|
|
154
154
|
description: "Configured Argo CD context",
|
|
155
155
|
},
|
|
156
156
|
],
|
|
157
157
|
flags: [],
|
|
158
|
-
examples: [
|
|
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
|
-
|
|
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
|
|
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 (
|
|
248
|
+
if (writable.includes(selectedName))
|
|
193
249
|
return finish([
|
|
194
250
|
encodeObject({
|
|
195
|
-
writableContext: `${
|
|
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 ${
|
|
217
|
-
: `${
|
|
267
|
+
? `added ${selectedName}`
|
|
268
|
+
: `${selectedName} is already writable (no change)`,
|
|
218
269
|
}),
|
|
219
270
|
], undefined);
|
|
220
271
|
}
|
|
@@ -6,14 +6,16 @@ import { parseArgs, renderCommandHelp } from "../args.js";
|
|
|
6
6
|
import { CODES, axiError, validationError } from "../errors.js";
|
|
7
7
|
import { encodeObject } from "../toon.js";
|
|
8
8
|
import { finish } from "./common.js";
|
|
9
|
+
import { installSessionEndHook, sessionEndHookStatus, uninstallSessionEndHook, } from "../session-hook.js";
|
|
9
10
|
export const SETUP_HOOKS_SPEC = {
|
|
10
11
|
path: "setup hooks",
|
|
11
|
-
summary: "Install or
|
|
12
|
+
summary: "Install, inspect or remove agent session hooks",
|
|
12
13
|
positionals: [],
|
|
13
14
|
flags: [],
|
|
14
15
|
examples: ["argocd-axi setup hooks"],
|
|
15
16
|
notes: [
|
|
16
17
|
"The hook runs `argocd-axi-ctx`, which is local only: current context, server and CLI version, with no network call, no SSO refresh and no credential read",
|
|
18
|
+
"SessionEnd records command names, counts and the most-used --context value only; setup status and setup uninstall inspect or remove that Claude Code hook",
|
|
17
19
|
"Intended for global installs; repeated runs are no-ops",
|
|
18
20
|
],
|
|
19
21
|
};
|
|
@@ -29,16 +31,31 @@ function ctxEntrypoint() {
|
|
|
29
31
|
: join(here, "..", "..", "bin", "argocd-axi-ctx.ts");
|
|
30
32
|
}
|
|
31
33
|
export async function setupCommand(args) {
|
|
32
|
-
|
|
34
|
+
const action = args[0];
|
|
35
|
+
if (action !== "hooks" && action !== "status" && action !== "uninstall") {
|
|
33
36
|
if (args.includes("--help"))
|
|
34
37
|
return renderCommandHelp(SETUP_HOOKS_SPEC);
|
|
35
38
|
throw validationError("unknown setup action", [
|
|
36
|
-
"Run `argocd-axi setup hooks`",
|
|
39
|
+
"Run `argocd-axi setup hooks`, `argocd-axi setup status` or `argocd-axi setup uninstall`",
|
|
37
40
|
]);
|
|
38
41
|
}
|
|
39
42
|
if (args.includes("--help"))
|
|
40
43
|
return renderCommandHelp(SETUP_HOOKS_SPEC);
|
|
41
44
|
parseArgs(SETUP_HOOKS_SPEC, args.slice(1));
|
|
45
|
+
if (action === "status")
|
|
46
|
+
return finish([
|
|
47
|
+
encodeObject({
|
|
48
|
+
sessionEnd: sessionEndHookStatus() ? "installed" : "not installed",
|
|
49
|
+
}),
|
|
50
|
+
], undefined);
|
|
51
|
+
if (action === "uninstall")
|
|
52
|
+
return finish([
|
|
53
|
+
encodeObject({
|
|
54
|
+
sessionEnd: uninstallSessionEndHook()
|
|
55
|
+
? "removed"
|
|
56
|
+
: "not installed (no-op)",
|
|
57
|
+
}),
|
|
58
|
+
], undefined);
|
|
42
59
|
// An npx (or pnpm dlx, bunx) run lives in a cache that later sessions may
|
|
43
60
|
// not have on PATH or may have evicted, so a hook pointing at it would
|
|
44
61
|
// silently stop working. Require a persistent install instead.
|
|
@@ -56,6 +73,12 @@ export async function setupCommand(args) {
|
|
|
56
73
|
distEntrypoints: ["dist/bin/argocd-axi-ctx.js"],
|
|
57
74
|
onError: (message) => errors.push(message),
|
|
58
75
|
});
|
|
76
|
+
try {
|
|
77
|
+
installSessionEndHook();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
81
|
+
}
|
|
59
82
|
if (errors.length > 0) {
|
|
60
83
|
throw axiError(`hook installation reported problems: ${errors.join("; ")}`, CODES.UPSTREAM_ERROR, ["Re-run `argocd-axi setup hooks` after fixing the paths above"]);
|
|
61
84
|
}
|
package/dist/src/help.js
CHANGED
|
@@ -4,6 +4,8 @@ export const DESCRIPTION = "Argo CD for agents: read operations plus opt-in writ
|
|
|
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
|
+
"context: last session's argocd-axi command summary for this directory",
|
|
8
|
+
"context end: record the current Claude Code session summary (SessionEnd hook)",
|
|
7
9
|
"ctx use <context>: switch the argocd CLI's current context (local config only)",
|
|
8
10
|
"auth guide [--context <c>]: offline human login and agent token setup instructions",
|
|
9
11
|
"whoami: user, issuer and groups of the current credential",
|
|
@@ -16,15 +18,17 @@ export const COMMAND_LINES = [
|
|
|
16
18
|
"apps rollback <app> <history-id> [--confirm] [--timeout <s>]: preview revision, then optionally roll back",
|
|
17
19
|
"apps refresh <app> [--hard] [--confirm]: preview, then optionally refresh one app",
|
|
18
20
|
"apps terminate-op <app> [--confirm] [--timeout <s>]: preview, then optionally stop a running operation",
|
|
21
|
+
"apps auto-sync <app> <on|off> [--confirm]: preview, then optionally change automated sync",
|
|
22
|
+
"apps actions run <app> <action> --kind <kind> --name <resource> [--confirm]: preview, then optionally run one listed resource action",
|
|
19
23
|
"apps history <app>: deployments, newest first",
|
|
20
24
|
"apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted",
|
|
21
25
|
"apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows",
|
|
22
26
|
"apps wait <app> [--sync --health --operation] [--timeout <s>]: bounded wait, returns the state either way",
|
|
23
|
-
"apps actions <app>:
|
|
27
|
+
"apps actions <app>: list available resource actions",
|
|
24
28
|
"projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now",
|
|
25
29
|
"appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate",
|
|
26
30
|
"repos | clusters: repositories and destination clusters with connection status",
|
|
27
|
-
"setup hooks:
|
|
31
|
+
"setup hooks | status | uninstall: manage the Claude Code SessionEnd memory hook; setup hooks also installs SessionStart hooks",
|
|
28
32
|
];
|
|
29
33
|
export const TOP_HELP = `${renderBlocks([
|
|
30
34
|
"usage: argocd-axi <command> [args] [flags]",
|
|
@@ -42,7 +46,7 @@ export const TOP_HELP = `${renderBlocks([
|
|
|
42
46
|
"argocd-axi apps diff payments-api",
|
|
43
47
|
]),
|
|
44
48
|
renderHelpBlock("notes", [
|
|
45
|
-
"
|
|
49
|
+
"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
50
|
"Output is TOON on stdout, errors included; exit 0 = success (an unhealthy app is data, not an error), 1 = error, 2 = usage",
|
|
47
51
|
"Secret-looking values are redacted and counted; no flag reveals them",
|
|
48
52
|
"Uses your existing argocd CLI session or ARGOCD_AUTH_TOKEN; never logs in, never reads the argocd config file",
|
package/dist/src/registry.js
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const path = () => join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "settings.json");
|
|
6
|
+
const command = "argocd-axi context end";
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function settings() {
|
|
11
|
+
const file = path();
|
|
12
|
+
if (!existsSync(file))
|
|
13
|
+
return {};
|
|
14
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
throw new Error(`${file} is not a JSON object`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function writeSettings(value) {
|
|
20
|
+
const file = path();
|
|
21
|
+
mkdirSync(join(file, ".."), { recursive: true });
|
|
22
|
+
const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
|
|
23
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode });
|
|
26
|
+
chmodSync(temporary, mode);
|
|
27
|
+
renameSync(temporary, file);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
try {
|
|
31
|
+
unlinkSync(temporary);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* No temporary file to remove. */
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function installSessionEndHook() {
|
|
40
|
+
const value = settings();
|
|
41
|
+
const hooks = isRecord(value.hooks) ? value.hooks : {};
|
|
42
|
+
value.hooks = hooks;
|
|
43
|
+
const groups = Array.isArray(hooks.SessionEnd) ? hooks.SessionEnd : [];
|
|
44
|
+
const next = [];
|
|
45
|
+
let installed = false;
|
|
46
|
+
for (const group of groups) {
|
|
47
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
48
|
+
next.push(group);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const kept = group.hooks.filter((entry) => {
|
|
52
|
+
const ours = isRecord(entry) &&
|
|
53
|
+
(entry.command === command ||
|
|
54
|
+
(typeof entry.command === "string" &&
|
|
55
|
+
entry.command.endsWith("/argocd-axi context end")));
|
|
56
|
+
if (ours && !installed) {
|
|
57
|
+
installed = true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return !ours;
|
|
61
|
+
});
|
|
62
|
+
if (kept.length)
|
|
63
|
+
next.push({ ...group, hooks: kept });
|
|
64
|
+
}
|
|
65
|
+
next.push({
|
|
66
|
+
matcher: "",
|
|
67
|
+
hooks: [{ type: "command", command, timeout: 30 }],
|
|
68
|
+
});
|
|
69
|
+
hooks.SessionEnd = next;
|
|
70
|
+
writeSettings(value);
|
|
71
|
+
}
|
|
72
|
+
export function sessionEndHookStatus() {
|
|
73
|
+
try {
|
|
74
|
+
const hooks = settings().hooks;
|
|
75
|
+
const groups = isRecord(hooks) ? hooks.SessionEnd : undefined;
|
|
76
|
+
return (Array.isArray(groups) &&
|
|
77
|
+
groups.some((group) => isRecord(group) &&
|
|
78
|
+
Array.isArray(group.hooks) &&
|
|
79
|
+
group.hooks.some((entry) => isRecord(entry) &&
|
|
80
|
+
(entry.command === command ||
|
|
81
|
+
(typeof entry.command === "string" &&
|
|
82
|
+
entry.command.endsWith("/argocd-axi context end"))))));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function uninstallSessionEndHook() {
|
|
89
|
+
const value = settings();
|
|
90
|
+
const hooks = value.hooks;
|
|
91
|
+
if (!isRecord(hooks) || !Array.isArray(hooks.SessionEnd))
|
|
92
|
+
return false;
|
|
93
|
+
let removed = false;
|
|
94
|
+
const next = hooks.SessionEnd.flatMap((group) => {
|
|
95
|
+
if (!isRecord(group) || !Array.isArray(group.hooks))
|
|
96
|
+
return [group];
|
|
97
|
+
const entries = group.hooks.filter((entry) => {
|
|
98
|
+
const ours = isRecord(entry) &&
|
|
99
|
+
(entry.command === command ||
|
|
100
|
+
(typeof entry.command === "string" &&
|
|
101
|
+
entry.command.endsWith("/argocd-axi context end")));
|
|
102
|
+
removed ||= ours;
|
|
103
|
+
return !ours;
|
|
104
|
+
});
|
|
105
|
+
return entries.length ? [{ ...group, hooks: entries }] : [];
|
|
106
|
+
});
|
|
107
|
+
if (!removed)
|
|
108
|
+
return false;
|
|
109
|
+
if (next.length)
|
|
110
|
+
hooks.SessionEnd = next;
|
|
111
|
+
else
|
|
112
|
+
delete hooks.SessionEnd;
|
|
113
|
+
if (Object.keys(hooks).length === 0)
|
|
114
|
+
delete value.hooks;
|
|
115
|
+
writeSettings(value);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { chmodSync, closeSync, createReadStream, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { configPath } from "./config.js";
|
|
6
|
+
const MAX_DIRECTORIES = 100;
|
|
7
|
+
const LOCK_WAIT_MS = 2_000;
|
|
8
|
+
const STALE_LOCK_MS = 30_000;
|
|
9
|
+
const MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
10
|
+
const TOOL = "argocd-axi";
|
|
11
|
+
const COMMANDS = new Set([
|
|
12
|
+
"apps",
|
|
13
|
+
"projects",
|
|
14
|
+
"appsets",
|
|
15
|
+
"repos",
|
|
16
|
+
"clusters",
|
|
17
|
+
"ctx",
|
|
18
|
+
"auth",
|
|
19
|
+
"whoami",
|
|
20
|
+
"can-i",
|
|
21
|
+
"setup",
|
|
22
|
+
"context",
|
|
23
|
+
]);
|
|
24
|
+
const CONTEXTS = new Set([
|
|
25
|
+
"apps",
|
|
26
|
+
"projects",
|
|
27
|
+
"appsets",
|
|
28
|
+
"repos",
|
|
29
|
+
"clusters",
|
|
30
|
+
"ctx",
|
|
31
|
+
"auth",
|
|
32
|
+
"whoami",
|
|
33
|
+
"can-i",
|
|
34
|
+
]);
|
|
35
|
+
/** Split shell words without retaining anything in the persisted result. */
|
|
36
|
+
function words(command) {
|
|
37
|
+
const result = [];
|
|
38
|
+
let token = "";
|
|
39
|
+
let quote = "";
|
|
40
|
+
let started = false;
|
|
41
|
+
const end = () => {
|
|
42
|
+
if (started)
|
|
43
|
+
result.push(token);
|
|
44
|
+
token = "";
|
|
45
|
+
started = false;
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i < command.length; i++) {
|
|
48
|
+
const c = command[i];
|
|
49
|
+
if (quote) {
|
|
50
|
+
if (c === quote)
|
|
51
|
+
quote = "";
|
|
52
|
+
else if (c === "\\" && quote === '"' && i + 1 < command.length)
|
|
53
|
+
token += command[++i];
|
|
54
|
+
else
|
|
55
|
+
token += c;
|
|
56
|
+
}
|
|
57
|
+
else if (c === "'" || c === '"') {
|
|
58
|
+
quote = c;
|
|
59
|
+
started = true;
|
|
60
|
+
}
|
|
61
|
+
else if (c === "\\" && i + 1 < command.length) {
|
|
62
|
+
token += command[++i];
|
|
63
|
+
started = true;
|
|
64
|
+
}
|
|
65
|
+
else if (/\s|[;|&()`]/.test(c))
|
|
66
|
+
end();
|
|
67
|
+
else {
|
|
68
|
+
token += c;
|
|
69
|
+
started = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
end();
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function commandParts(line) {
|
|
76
|
+
const parts = [];
|
|
77
|
+
let quote = "";
|
|
78
|
+
let start = 0;
|
|
79
|
+
for (let i = 0; i < line.length; i++) {
|
|
80
|
+
const c = line[i];
|
|
81
|
+
if (quote) {
|
|
82
|
+
if (c === "\\" && quote === '"')
|
|
83
|
+
i++;
|
|
84
|
+
else if (c === quote)
|
|
85
|
+
quote = "";
|
|
86
|
+
}
|
|
87
|
+
else if (c === "\\")
|
|
88
|
+
i++;
|
|
89
|
+
else if (c === "'" || c === '"')
|
|
90
|
+
quote = c;
|
|
91
|
+
else if (/[;|&\n]/.test(c)) {
|
|
92
|
+
parts.push(line.slice(start, i));
|
|
93
|
+
start = i + 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
parts.push(line.slice(start));
|
|
97
|
+
return parts;
|
|
98
|
+
}
|
|
99
|
+
function commandCalls(line) {
|
|
100
|
+
const calls = [];
|
|
101
|
+
for (const part of commandParts(line)) {
|
|
102
|
+
const args = words(part);
|
|
103
|
+
const index = args.findIndex((word) => word.split("/").pop()?.replace(/\.js$/, "") === TOOL);
|
|
104
|
+
if (index < 0)
|
|
105
|
+
continue;
|
|
106
|
+
const argsAfter = args.slice(index + 1);
|
|
107
|
+
const command = argsAfter.find((arg) => !arg.startsWith("-"));
|
|
108
|
+
if (!command || !COMMANDS.has(command))
|
|
109
|
+
continue;
|
|
110
|
+
const contextArg = argsAfter.findIndex((arg) => arg === "--context");
|
|
111
|
+
const contextValue = contextArg >= 0
|
|
112
|
+
? argsAfter[contextArg + 1]
|
|
113
|
+
: argsAfter.find((arg) => arg.startsWith("--context="))?.slice(10);
|
|
114
|
+
calls.push({
|
|
115
|
+
command,
|
|
116
|
+
...(contextValue && CONTEXTS.has(command.split(" ")[0])
|
|
117
|
+
? { context: contextValue }
|
|
118
|
+
: {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return calls;
|
|
122
|
+
}
|
|
123
|
+
function isRecord(value) {
|
|
124
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
125
|
+
}
|
|
126
|
+
function* toolCommands(value) {
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const item of value)
|
|
129
|
+
yield* toolCommands(item);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!isRecord(value))
|
|
133
|
+
return;
|
|
134
|
+
if (value.type === "tool_use") {
|
|
135
|
+
if (isRecord(value.input) && typeof value.input.command === "string")
|
|
136
|
+
yield value.input.command;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const key of ["message", "content"])
|
|
140
|
+
if (key in value)
|
|
141
|
+
yield* toolCommands(value[key]);
|
|
142
|
+
}
|
|
143
|
+
export async function summarizeTranscript(path) {
|
|
144
|
+
if (statSync(path).size > MAX_TRANSCRIPT_BYTES)
|
|
145
|
+
return undefined;
|
|
146
|
+
const counts = {};
|
|
147
|
+
const contexts = [];
|
|
148
|
+
const lines = createInterface({
|
|
149
|
+
input: createReadStream(path, { encoding: "utf8" }),
|
|
150
|
+
crlfDelay: Infinity,
|
|
151
|
+
});
|
|
152
|
+
for await (const line of lines) {
|
|
153
|
+
if (!line.includes(TOOL))
|
|
154
|
+
continue;
|
|
155
|
+
let entry;
|
|
156
|
+
try {
|
|
157
|
+
entry = JSON.parse(line);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const commandLine of toolCommands(entry))
|
|
163
|
+
for (const call of commandCalls(commandLine)) {
|
|
164
|
+
counts[call.command] = (counts[call.command] ?? 0) + 1;
|
|
165
|
+
if (call.context)
|
|
166
|
+
contexts.push(call.context);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (Object.keys(counts).length === 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
const contextCounts = new Map();
|
|
172
|
+
for (const context of contexts)
|
|
173
|
+
contextCounts.set(context, (contextCounts.get(context) ?? 0) + 1);
|
|
174
|
+
const context = [...contextCounts].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
175
|
+
return { commands: counts, ...(context ? { context } : {}) };
|
|
176
|
+
}
|
|
177
|
+
const sessionsPath = () => join(dirname(configPath()), "sessions.json");
|
|
178
|
+
function isSessionSummary(value) {
|
|
179
|
+
return (isRecord(value) &&
|
|
180
|
+
isRecord(value.commands) &&
|
|
181
|
+
Object.entries(value.commands).every(([name, count]) => COMMANDS.has(name) &&
|
|
182
|
+
typeof count === "number" &&
|
|
183
|
+
Number.isSafeInteger(count) &&
|
|
184
|
+
count > 0) &&
|
|
185
|
+
(value.context === undefined ||
|
|
186
|
+
(typeof value.context === "string" && !/[\r\n]/.test(value.context))));
|
|
187
|
+
}
|
|
188
|
+
function readSessions() {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(readFileSync(sessionsPath(), "utf8"));
|
|
191
|
+
if (isRecord(parsed) && isRecord(parsed.directories))
|
|
192
|
+
return Object.fromEntries(Object.entries(parsed.directories).filter((entry) => isSessionSummary(entry[1])));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* Missing or corrupt memory is optional. */
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
function processAlive(pid) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(pid, 0);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return errorCode(error) !== "ESRCH";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function lockPid(token) {
|
|
209
|
+
const pid = Number(token.trim().split(/\s+/, 1)[0]);
|
|
210
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
|
|
211
|
+
}
|
|
212
|
+
function errorCode(error) {
|
|
213
|
+
return error &&
|
|
214
|
+
typeof error === "object" &&
|
|
215
|
+
"code" in error &&
|
|
216
|
+
typeof error.code === "string"
|
|
217
|
+
? error.code
|
|
218
|
+
: undefined;
|
|
219
|
+
}
|
|
220
|
+
async function acquireLock(path) {
|
|
221
|
+
const lockPath = `${path}.lock`;
|
|
222
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
223
|
+
while (Date.now() < deadline) {
|
|
224
|
+
try {
|
|
225
|
+
mkdirSync(lockPath, 0o700);
|
|
226
|
+
const ownerPath = join(lockPath, `owner-${randomUUID()}`);
|
|
227
|
+
const token = `${process.pid} ${randomUUID()}\n`;
|
|
228
|
+
let fd;
|
|
229
|
+
try {
|
|
230
|
+
fd = openSync(ownerPath, "wx", 0o600);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
rmdirSync(lockPath);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
writeFileSync(fd, token);
|
|
238
|
+
return { fd, path: ownerPath, token };
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
closeSync(fd);
|
|
242
|
+
try {
|
|
243
|
+
unlinkSync(ownerPath);
|
|
244
|
+
rmdirSync(lockPath);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* The lock may already be gone. */
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (errorCode(error) !== "EEXIST")
|
|
254
|
+
return undefined;
|
|
255
|
+
if (reclaimStaleLock(lockPath))
|
|
256
|
+
continue;
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function reclaimStaleLock(lockPath) {
|
|
263
|
+
try {
|
|
264
|
+
const owners = readdirSync(lockPath);
|
|
265
|
+
if (owners.length === 0) {
|
|
266
|
+
if (Date.now() - statSync(lockPath).mtimeMs <= STALE_LOCK_MS)
|
|
267
|
+
return false;
|
|
268
|
+
rmdirSync(lockPath);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
for (const ownerFile of owners) {
|
|
272
|
+
const ownerPath = join(lockPath, ownerFile);
|
|
273
|
+
const info = statSync(ownerPath);
|
|
274
|
+
const owner = readFileSync(ownerPath, "utf8");
|
|
275
|
+
const pid = lockPid(owner);
|
|
276
|
+
if (Date.now() - info.mtimeMs <= STALE_LOCK_MS ||
|
|
277
|
+
pid === undefined ||
|
|
278
|
+
processAlive(pid) ||
|
|
279
|
+
readFileSync(ownerPath, "utf8") !== owner)
|
|
280
|
+
continue;
|
|
281
|
+
unlinkSync(ownerPath);
|
|
282
|
+
rmdirSync(lockPath);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
export function releaseSessionLock(lock) {
|
|
292
|
+
closeSync(lock.fd);
|
|
293
|
+
try {
|
|
294
|
+
if (readFileSync(lock.path, "utf8") === lock.token) {
|
|
295
|
+
unlinkSync(lock.path);
|
|
296
|
+
rmdirSync(dirname(lock.path));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* A successor owns the lock, or it is already gone. */
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export async function recordSession(directory, summary) {
|
|
304
|
+
const path = sessionsPath();
|
|
305
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
306
|
+
const lock = await acquireLock(path);
|
|
307
|
+
if (lock === undefined)
|
|
308
|
+
return false;
|
|
309
|
+
try {
|
|
310
|
+
const directories = readSessions();
|
|
311
|
+
delete directories[directory];
|
|
312
|
+
directories[directory] = summary;
|
|
313
|
+
for (const old of Object.keys(directories).slice(0, Math.max(0, Object.keys(directories).length - MAX_DIRECTORIES)))
|
|
314
|
+
delete directories[old];
|
|
315
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
316
|
+
try {
|
|
317
|
+
writeFileSync(temporary, `${JSON.stringify({ directories }, null, 2)}\n`, {
|
|
318
|
+
mode: 0o600,
|
|
319
|
+
});
|
|
320
|
+
chmodSync(temporary, 0o600);
|
|
321
|
+
renameSync(temporary, path);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
try {
|
|
325
|
+
unlinkSync(temporary);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* No temporary file to remove. */
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
releaseSessionLock(lock);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export function lastSession(directory) {
|
|
339
|
+
return readSessions()[directory];
|
|
340
|
+
}
|
|
341
|
+
export function sessionText(summary) {
|
|
342
|
+
const commands = Object.entries(summary.commands)
|
|
343
|
+
.sort((a, b) => b[1] - a[1])
|
|
344
|
+
.map(([name, count]) => `${name} ${count}`)
|
|
345
|
+
.join("; ");
|
|
346
|
+
return `${commands}${summary.context ? ` - context ${JSON.stringify(summary.context)}` : ""}`;
|
|
347
|
+
}
|
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\`.
|
|
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
|
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -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`.
|
|
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,9 +34,11 @@ Run `npx -y argocd-axi auth guide` for offline instructions. A human operator lo
|
|
|
34
34
|
## Commands
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
commands[
|
|
37
|
+
commands[27]:
|
|
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
|
+
context: last session's argocd-axi command summary for this directory
|
|
41
|
+
context end: record the current Claude Code session summary (SessionEnd hook)
|
|
40
42
|
ctx use <context>: switch the argocd CLI's current context (local config only)
|
|
41
43
|
auth guide [--context <c>]: offline human login and agent token setup instructions
|
|
42
44
|
whoami: user, issuer and groups of the current credential
|
|
@@ -49,15 +51,17 @@ commands[23]:
|
|
|
49
51
|
apps rollback <app> <history-id> [--confirm] [--timeout <s>]: preview revision, then optionally roll back
|
|
50
52
|
apps refresh <app> [--hard] [--confirm]: preview, then optionally refresh one app
|
|
51
53
|
apps terminate-op <app> [--confirm] [--timeout <s>]: preview, then optionally stop a running operation
|
|
54
|
+
apps auto-sync <app> <on|off> [--confirm]: preview, then optionally change automated sync
|
|
55
|
+
apps actions run <app> <action> --kind <kind> --name <resource> [--confirm]: preview, then optionally run one listed resource action
|
|
52
56
|
apps history <app>: deployments, newest first
|
|
53
57
|
apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted
|
|
54
58
|
apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows
|
|
55
59
|
apps wait <app> [--sync --health --operation] [--timeout <s>]: bounded wait, returns the state either way
|
|
56
|
-
apps actions <app>:
|
|
60
|
+
apps actions <app>: list available resource actions
|
|
57
61
|
projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now
|
|
58
62
|
appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate
|
|
59
63
|
repos | clusters: repositories and destination clusters with connection status
|
|
60
|
-
setup hooks:
|
|
64
|
+
setup hooks | status | uninstall: manage the Claude Code SessionEnd memory hook; setup hooks also installs SessionStart hooks
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
Run `npx -y argocd-axi --help` for global flags, or `npx -y argocd-axi <command> --help` for per-command usage.
|