@ricsam/r5dctl 0.0.49 → 0.0.50
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 +6 -0
- package/dist/cjs/cli.cjs +213 -0
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +208 -0
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +39 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -79,12 +79,18 @@ r5dctl ps list
|
|
|
79
79
|
r5dctl -p <project> -b <branch> ps list --worker <label>
|
|
80
80
|
r5dctl -s <session-id> ps list --json
|
|
81
81
|
r5dctl ps stop <run-id>
|
|
82
|
+
|
|
83
|
+
r5dctl k8s usage
|
|
84
|
+
r5dctl k8s usage --namespace default --workload api --container server
|
|
85
|
+
r5dctl k8s usage --workload default/api --window 24h --json
|
|
82
86
|
```
|
|
83
87
|
|
|
84
88
|
`shell` connects to the same live worker PTY as the web Shell tab. Without `-c` it attaches the local terminal interactively. With `-c`/`--command`, it runs the supplied shell expression, streams combined PTY output, and exits with the remote command's status. Because command output uses a PTY, stdout and stderr are combined and programs may emit colors or other terminal control sequences.
|
|
85
89
|
|
|
86
90
|
`ps list` shows active long-running commands managed by r5d. It does not expose the worker host's general OS process table or interactive PTYs. Global project, branch, and session options filter the list, while `--worker` selects a worker label. `ps stop` sends `SIGTERM` to the selected run. Inside an r5d-provided worker shell, the temporary credential can only list and stop runs on that shell's worker.
|
|
87
91
|
|
|
92
|
+
`k8s usage` shows current and 24-hour CPU and memory usage for every workload container in your managed vCluster. Filter with `--namespace`, `--workload`, or `--container`; a workload may be identified by name, `namespace/name`, or Kubernetes UID. Human-readable output summarizes the 24-hour minimum and maximum across replicas, while global `--json` includes the complete five-minute history envelope and per-pod series. The only currently supported `--window` value is `24h`.
|
|
93
|
+
|
|
88
94
|
`get envs` reports names and whether values are set, but never prints the values. Prefer `--from-env-file` for bulk imports so secret values do not appear in shell arguments or history. Empty dotenv values are skipped unless `--include-empty` is passed, and `--only-missing` preserves values that are already set.
|
|
89
95
|
|
|
90
96
|
Conversation output is human-readable by default and excludes both the system prompt and tool definitions. `--raw` keeps the transcript layout but renders structured message and tool parts as JSON, `--tools` includes the provider-shaped tool definitions, and `--system` includes system prompt messages. The global `--json` flag remains separate and prints the complete API response envelope.
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -28,6 +28,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var cli_exports = {};
|
|
30
30
|
__export(cli_exports, {
|
|
31
|
+
collectK8sUsage: () => collectK8sUsage,
|
|
32
|
+
formatK8sCpu: () => formatK8sCpu,
|
|
33
|
+
formatK8sMemory: () => formatK8sMemory,
|
|
31
34
|
formatProcessAge: () => formatProcessAge,
|
|
32
35
|
getCliHelpText: () => getCliHelpText,
|
|
33
36
|
getR5dctlVersion: () => getR5dctlVersion,
|
|
@@ -38,6 +41,7 @@ __export(cli_exports, {
|
|
|
38
41
|
parseEnvFlags: () => parseEnvFlags,
|
|
39
42
|
parseEnvRequestResponseArgs: () => parseEnvRequestResponseArgs,
|
|
40
43
|
parseGlobalArgs: () => parseGlobalArgs,
|
|
44
|
+
parseK8sUsageArgs: () => parseK8sUsageArgs,
|
|
41
45
|
parsePromptArgs: () => parsePromptArgs,
|
|
42
46
|
parsePsHistoryArgs: () => parsePsHistoryArgs,
|
|
43
47
|
parsePsListArgs: () => parsePsListArgs,
|
|
@@ -48,6 +52,7 @@ __export(cli_exports, {
|
|
|
48
52
|
renderConversationOverviewResponse: () => renderConversationOverviewResponse,
|
|
49
53
|
renderConversationResponse: () => renderConversationResponse,
|
|
50
54
|
renderConversationWorkResponse: () => renderConversationWorkResponse,
|
|
55
|
+
renderK8sUsageReport: () => renderK8sUsageReport,
|
|
51
56
|
renderProcessHistory: () => renderProcessHistory,
|
|
52
57
|
renderProcessInspection: () => renderProcessInspection,
|
|
53
58
|
renderProcessList: () => renderProcessList,
|
|
@@ -126,6 +131,13 @@ const PS_HELP_TEXT = [
|
|
|
126
131
|
"Use -p/--project, -b/--branch, or -s/--session to filter list and history.",
|
|
127
132
|
""
|
|
128
133
|
].join("\n");
|
|
134
|
+
const K8S_HELP_TEXT = [
|
|
135
|
+
"Usage:",
|
|
136
|
+
" r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
|
|
137
|
+
"",
|
|
138
|
+
"Show current and 24-hour CPU and memory usage for managed-vCluster workload containers.",
|
|
139
|
+
""
|
|
140
|
+
].join("\n");
|
|
129
141
|
const SHARED_HELP_ENTRIES = [
|
|
130
142
|
{ section: "auth", usage: "auth status", description: "Show the current authenticated user and credential source." },
|
|
131
143
|
{ section: "auth", usage: "auth logout", description: "Revoke the current credential and clear saved auth." },
|
|
@@ -221,6 +233,11 @@ const SHARED_HELP_ENTRIES = [
|
|
|
221
233
|
usage: "-s <session-id> answer-env-request [-e KEY=value] [--context <text>]",
|
|
222
234
|
description: "Answer a pending environment variable request with values, context, or both."
|
|
223
235
|
},
|
|
236
|
+
{
|
|
237
|
+
section: "kubernetes",
|
|
238
|
+
usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
|
|
239
|
+
description: "Show current and 24-hour CPU and memory usage for managed-vCluster workload containers."
|
|
240
|
+
},
|
|
224
241
|
{
|
|
225
242
|
section: "processes",
|
|
226
243
|
usage: "ps list [--worker <label>]",
|
|
@@ -259,6 +276,7 @@ const HELP_SECTION_ORDER = [
|
|
|
259
276
|
"branches",
|
|
260
277
|
"envs",
|
|
261
278
|
"sessions",
|
|
279
|
+
"kubernetes",
|
|
262
280
|
"processes",
|
|
263
281
|
"shell",
|
|
264
282
|
"agents",
|
|
@@ -271,6 +289,7 @@ const HELP_SECTION_TITLES = {
|
|
|
271
289
|
branches: "Branches",
|
|
272
290
|
envs: "Environment variables",
|
|
273
291
|
sessions: "Sessions",
|
|
292
|
+
kubernetes: "Kubernetes",
|
|
274
293
|
processes: "Processes",
|
|
275
294
|
shell: "Shell",
|
|
276
295
|
agents: "Agents",
|
|
@@ -935,6 +954,36 @@ function parseRecentSessionArgs(args) {
|
|
|
935
954
|
if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new Error("--limit must be an integer between 1 and 200");
|
|
936
955
|
return { limit };
|
|
937
956
|
}
|
|
957
|
+
function parseK8sUsageArgs(args) {
|
|
958
|
+
const parsed = { window: "24h" };
|
|
959
|
+
const seen = /* @__PURE__ */ new Set();
|
|
960
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
961
|
+
const arg = args[index];
|
|
962
|
+
if (!arg) continue;
|
|
963
|
+
const option = ["--namespace", "--workload", "--container", "--window"].find(
|
|
964
|
+
(candidate) => arg === candidate || arg.startsWith(`${candidate}=`)
|
|
965
|
+
);
|
|
966
|
+
if (!option) throw new Error(`Unknown k8s usage argument: ${arg}`);
|
|
967
|
+
if (seen.has(option)) throw new Error(`Use ${option} only once`);
|
|
968
|
+
seen.add(option);
|
|
969
|
+
const inlineValue = parseLongOptionWithEquals(arg, option);
|
|
970
|
+
const candidateValue = inlineValue ?? args[index + 1];
|
|
971
|
+
if (!candidateValue || candidateValue.startsWith("--")) throw new Error(`Missing value for ${option}`);
|
|
972
|
+
const value = candidateValue;
|
|
973
|
+
if (inlineValue === void 0) index += 1;
|
|
974
|
+
if (option === "--window") {
|
|
975
|
+
if (value !== "24h") throw new Error("--window currently supports only 24h");
|
|
976
|
+
parsed.window = value;
|
|
977
|
+
} else if (option === "--namespace") {
|
|
978
|
+
parsed.namespace = value;
|
|
979
|
+
} else if (option === "--workload") {
|
|
980
|
+
parsed.workload = value;
|
|
981
|
+
} else {
|
|
982
|
+
parsed.container = value;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return parsed;
|
|
986
|
+
}
|
|
938
987
|
async function openInBrowser(url) {
|
|
939
988
|
if (process.platform === "darwin") {
|
|
940
989
|
(0, import_node_child_process.spawn)("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
@@ -1096,6 +1145,145 @@ Error: ${result.error}` : "";
|
|
|
1096
1145
|
return `Workspace sync ${result.outcome} on ${result.workerLabel}; HEAD ${head}; ${result.diffSizeBytes} bytes.${discarded}${error}
|
|
1097
1146
|
`;
|
|
1098
1147
|
}
|
|
1148
|
+
function summarizeK8sHistory(history) {
|
|
1149
|
+
if (history.envelope.length === 0) {
|
|
1150
|
+
return {
|
|
1151
|
+
cpuMinNanoCores: null,
|
|
1152
|
+
cpuMaxNanoCores: null,
|
|
1153
|
+
memoryMinBytes: null,
|
|
1154
|
+
memoryMaxBytes: null,
|
|
1155
|
+
sampleCount: 0
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
cpuMinNanoCores: Math.min(...history.envelope.map((point) => point.cpuMinNanoCores)),
|
|
1160
|
+
cpuMaxNanoCores: Math.max(...history.envelope.map((point) => point.cpuMaxNanoCores)),
|
|
1161
|
+
memoryMinBytes: Math.min(...history.envelope.map((point) => point.memoryMinBytes)),
|
|
1162
|
+
memoryMaxBytes: Math.max(...history.envelope.map((point) => point.memoryMaxBytes)),
|
|
1163
|
+
sampleCount: history.envelope.reduce((total, point) => total + point.sampleCount, 0)
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
1167
|
+
const results = new Array(items.length);
|
|
1168
|
+
let nextIndex = 0;
|
|
1169
|
+
await Promise.all(
|
|
1170
|
+
Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
1171
|
+
while (nextIndex < items.length) {
|
|
1172
|
+
const index = nextIndex++;
|
|
1173
|
+
const item = items[index];
|
|
1174
|
+
if (item !== void 0) results[index] = await fn(item);
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
);
|
|
1178
|
+
return results;
|
|
1179
|
+
}
|
|
1180
|
+
async function collectK8sUsage(client, options) {
|
|
1181
|
+
const resources = await client.k8s.resources();
|
|
1182
|
+
const candidates = resources.workloads.flatMap((workload) => workload.containers.map((container) => ({ workload, container }))).filter(({ workload, container }) => {
|
|
1183
|
+
const workloadMatches = !options.workload || options.workload === workload.name || options.workload === `${workload.namespace}/${workload.name}` || options.workload === workload.uid;
|
|
1184
|
+
return (!options.namespace || options.namespace === workload.namespace) && workloadMatches && (!options.container || options.container === container.name);
|
|
1185
|
+
}).sort(
|
|
1186
|
+
(left, right) => left.workload.namespace.localeCompare(right.workload.namespace) || left.workload.name.localeCompare(right.workload.name) || left.container.name.localeCompare(right.container.name)
|
|
1187
|
+
);
|
|
1188
|
+
if (candidates.length === 0 && (options.namespace || options.workload || options.container)) {
|
|
1189
|
+
throw new Error("No managed-vCluster workload containers matched the requested filters");
|
|
1190
|
+
}
|
|
1191
|
+
const entries = await mapWithConcurrency(candidates, 8, async ({ workload, container }) => {
|
|
1192
|
+
const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name });
|
|
1193
|
+
return {
|
|
1194
|
+
workload: {
|
|
1195
|
+
uid: workload.uid,
|
|
1196
|
+
kind: workload.kind,
|
|
1197
|
+
namespace: workload.namespace,
|
|
1198
|
+
name: workload.name
|
|
1199
|
+
},
|
|
1200
|
+
container: {
|
|
1201
|
+
name: container.name,
|
|
1202
|
+
currentUsage: container.currentUsage,
|
|
1203
|
+
pods: container.pods
|
|
1204
|
+
},
|
|
1205
|
+
summary: summarizeK8sHistory(history),
|
|
1206
|
+
history
|
|
1207
|
+
};
|
|
1208
|
+
});
|
|
1209
|
+
return {
|
|
1210
|
+
window: options.window,
|
|
1211
|
+
collectedAt: resources.collectedAt,
|
|
1212
|
+
metricsFreshAt: resources.metricsFreshAt,
|
|
1213
|
+
metricsStale: resources.metricsStale,
|
|
1214
|
+
filters: {
|
|
1215
|
+
namespace: options.namespace,
|
|
1216
|
+
workload: options.workload,
|
|
1217
|
+
container: options.container
|
|
1218
|
+
},
|
|
1219
|
+
entries
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function formatDecimal(value) {
|
|
1223
|
+
if (value >= 100) return value.toFixed(0);
|
|
1224
|
+
if (value >= 10) return value.toFixed(1).replace(/\.0$/, "");
|
|
1225
|
+
return value.toFixed(2).replace(/\.0+$/, "").replace(/(\.\d)0$/, "$1");
|
|
1226
|
+
}
|
|
1227
|
+
function formatK8sCpu(value) {
|
|
1228
|
+
if (value === null) return "-";
|
|
1229
|
+
const milliCores = value / 1e6;
|
|
1230
|
+
return milliCores >= 1e3 ? `${formatDecimal(milliCores / 1e3)} cores` : `${formatDecimal(milliCores)}m`;
|
|
1231
|
+
}
|
|
1232
|
+
function formatK8sMemory(value) {
|
|
1233
|
+
if (value === null) return "-";
|
|
1234
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
1235
|
+
let amount = value;
|
|
1236
|
+
let unitIndex = 0;
|
|
1237
|
+
while (amount >= 1024 && unitIndex < units.length - 1) {
|
|
1238
|
+
amount /= 1024;
|
|
1239
|
+
unitIndex += 1;
|
|
1240
|
+
}
|
|
1241
|
+
return `${formatDecimal(amount)} ${units[unitIndex]}`;
|
|
1242
|
+
}
|
|
1243
|
+
function formatK8sCoverage(start, end) {
|
|
1244
|
+
if (!start || !end) return "none";
|
|
1245
|
+
const durationMs = Date.parse(end) - Date.parse(start);
|
|
1246
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) return "unknown";
|
|
1247
|
+
const totalMinutes = Math.floor(durationMs / 6e4);
|
|
1248
|
+
return `${Math.floor(totalMinutes / 60)}h ${String(totalMinutes % 60).padStart(2, "0")}m`;
|
|
1249
|
+
}
|
|
1250
|
+
function renderK8sUsageReport(report) {
|
|
1251
|
+
const freshness = report.metricsFreshAt ?? "unavailable";
|
|
1252
|
+
const lines = [`Metrics: ${report.metricsStale ? "stale" : "current"} (fresh at ${freshness})`, "Window: 24h"];
|
|
1253
|
+
if (report.entries.length === 0) return `${lines.join("\n")}
|
|
1254
|
+
No managed-vCluster workload containers found.
|
|
1255
|
+
`;
|
|
1256
|
+
const headers = [
|
|
1257
|
+
"NAMESPACE",
|
|
1258
|
+
"WORKLOAD",
|
|
1259
|
+
"CONTAINER",
|
|
1260
|
+
"PODS",
|
|
1261
|
+
"CPU NOW",
|
|
1262
|
+
"CPU 24H MIN/MAX",
|
|
1263
|
+
"MEM NOW",
|
|
1264
|
+
"MEM 24H MIN/MAX",
|
|
1265
|
+
"COVERAGE",
|
|
1266
|
+
"STATUS"
|
|
1267
|
+
];
|
|
1268
|
+
const rows = report.entries.map((entry) => [
|
|
1269
|
+
entry.workload.namespace,
|
|
1270
|
+
`${entry.workload.kind}/${entry.workload.name}`,
|
|
1271
|
+
entry.container.name,
|
|
1272
|
+
String(entry.container.pods.length),
|
|
1273
|
+
formatK8sCpu(entry.container.currentUsage.cpuNanoCores),
|
|
1274
|
+
`${formatK8sCpu(entry.summary.cpuMinNanoCores)} / ${formatK8sCpu(entry.summary.cpuMaxNanoCores)}`,
|
|
1275
|
+
formatK8sMemory(entry.container.currentUsage.memoryBytes),
|
|
1276
|
+
`${formatK8sMemory(entry.summary.memoryMinBytes)} / ${formatK8sMemory(entry.summary.memoryMaxBytes)}`,
|
|
1277
|
+
formatK8sCoverage(entry.history.coverageStart, entry.history.coverageEnd),
|
|
1278
|
+
entry.history.stale ? "stale" : entry.history.incomplete ? "partial" : "complete"
|
|
1279
|
+
]);
|
|
1280
|
+
const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
1281
|
+
const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
|
|
1282
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
1283
|
+
lines.push(formatRow(headers), separator, ...rows.map(formatRow));
|
|
1284
|
+
return `${lines.join("\n")}
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1099
1287
|
function renderSessionDescription(session) {
|
|
1100
1288
|
return [`Session: ${session.id}`, `Project: ${session.projectPath}`, `Branch: ${session.branchName}`].join("\n") + "\n";
|
|
1101
1289
|
}
|
|
@@ -1474,6 +1662,11 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
1474
1662
|
write(result, renderWorkspaceSync(result));
|
|
1475
1663
|
return;
|
|
1476
1664
|
}
|
|
1665
|
+
if (first === "k8s" && second === "usage") {
|
|
1666
|
+
const report = await collectK8sUsage(client, parseK8sUsageArgs(args.slice(2)));
|
|
1667
|
+
write(report, renderK8sUsageReport(report));
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1477
1670
|
if (first === "ps" && second === "list") {
|
|
1478
1671
|
const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
|
|
1479
1672
|
write(processes, renderProcessList(processes));
|
|
@@ -1722,6 +1915,12 @@ function resolveCommandExecution(options, rest) {
|
|
|
1722
1915
|
text: PS_HELP_TEXT
|
|
1723
1916
|
};
|
|
1724
1917
|
}
|
|
1918
|
+
if (command === "k8s" && commandArgs.length === 0) {
|
|
1919
|
+
return {
|
|
1920
|
+
kind: "cli-help",
|
|
1921
|
+
text: K8S_HELP_TEXT
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1725
1924
|
if (trailingHelp) {
|
|
1726
1925
|
const cliOnlyHelp = findCliOnlyCommandHelp(normalizedRest);
|
|
1727
1926
|
if (cliOnlyHelp) {
|
|
@@ -1850,6 +2049,15 @@ function resolveCommandExecution(options, rest) {
|
|
|
1850
2049
|
}
|
|
1851
2050
|
throw new Error(`Unknown ps command: ${subcommand ?? ""}`.trim());
|
|
1852
2051
|
}
|
|
2052
|
+
if (command === "k8s") {
|
|
2053
|
+
const subcommand = commandArgs[0];
|
|
2054
|
+
if (subcommand !== "usage") throw new Error(`Unknown k8s command: ${subcommand ?? ""}`.trim());
|
|
2055
|
+
if (options.project || options.branch || options.session) {
|
|
2056
|
+
throw new Error("Project, branch, and session scopes are not supported for `k8s usage`");
|
|
2057
|
+
}
|
|
2058
|
+
parseK8sUsageArgs(commandArgs.slice(1));
|
|
2059
|
+
return { kind: "plugin", pluginArgs: ["k8s", "usage", ...commandArgs.slice(1)] };
|
|
2060
|
+
}
|
|
1853
2061
|
if (command === "get") {
|
|
1854
2062
|
const target = commandArgs[0];
|
|
1855
2063
|
if (target === "projects") {
|
|
@@ -2278,6 +2486,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2278
2486
|
}
|
|
2279
2487
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2280
2488
|
0 && (module.exports = {
|
|
2489
|
+
collectK8sUsage,
|
|
2490
|
+
formatK8sCpu,
|
|
2491
|
+
formatK8sMemory,
|
|
2281
2492
|
formatProcessAge,
|
|
2282
2493
|
getCliHelpText,
|
|
2283
2494
|
getR5dctlVersion,
|
|
@@ -2288,6 +2499,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2288
2499
|
parseEnvFlags,
|
|
2289
2500
|
parseEnvRequestResponseArgs,
|
|
2290
2501
|
parseGlobalArgs,
|
|
2502
|
+
parseK8sUsageArgs,
|
|
2291
2503
|
parsePromptArgs,
|
|
2292
2504
|
parsePsHistoryArgs,
|
|
2293
2505
|
parsePsListArgs,
|
|
@@ -2298,6 +2510,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2298
2510
|
renderConversationOverviewResponse,
|
|
2299
2511
|
renderConversationResponse,
|
|
2300
2512
|
renderConversationWorkResponse,
|
|
2513
|
+
renderK8sUsageReport,
|
|
2301
2514
|
renderProcessHistory,
|
|
2302
2515
|
renderProcessInspection,
|
|
2303
2516
|
renderProcessList,
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/cli.mjs
CHANGED
|
@@ -69,6 +69,13 @@ const PS_HELP_TEXT = [
|
|
|
69
69
|
"Use -p/--project, -b/--branch, or -s/--session to filter list and history.",
|
|
70
70
|
""
|
|
71
71
|
].join("\n");
|
|
72
|
+
const K8S_HELP_TEXT = [
|
|
73
|
+
"Usage:",
|
|
74
|
+
" r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
|
|
75
|
+
"",
|
|
76
|
+
"Show current and 24-hour CPU and memory usage for managed-vCluster workload containers.",
|
|
77
|
+
""
|
|
78
|
+
].join("\n");
|
|
72
79
|
const SHARED_HELP_ENTRIES = [
|
|
73
80
|
{ section: "auth", usage: "auth status", description: "Show the current authenticated user and credential source." },
|
|
74
81
|
{ section: "auth", usage: "auth logout", description: "Revoke the current credential and clear saved auth." },
|
|
@@ -164,6 +171,11 @@ const SHARED_HELP_ENTRIES = [
|
|
|
164
171
|
usage: "-s <session-id> answer-env-request [-e KEY=value] [--context <text>]",
|
|
165
172
|
description: "Answer a pending environment variable request with values, context, or both."
|
|
166
173
|
},
|
|
174
|
+
{
|
|
175
|
+
section: "kubernetes",
|
|
176
|
+
usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
|
|
177
|
+
description: "Show current and 24-hour CPU and memory usage for managed-vCluster workload containers."
|
|
178
|
+
},
|
|
167
179
|
{
|
|
168
180
|
section: "processes",
|
|
169
181
|
usage: "ps list [--worker <label>]",
|
|
@@ -202,6 +214,7 @@ const HELP_SECTION_ORDER = [
|
|
|
202
214
|
"branches",
|
|
203
215
|
"envs",
|
|
204
216
|
"sessions",
|
|
217
|
+
"kubernetes",
|
|
205
218
|
"processes",
|
|
206
219
|
"shell",
|
|
207
220
|
"agents",
|
|
@@ -214,6 +227,7 @@ const HELP_SECTION_TITLES = {
|
|
|
214
227
|
branches: "Branches",
|
|
215
228
|
envs: "Environment variables",
|
|
216
229
|
sessions: "Sessions",
|
|
230
|
+
kubernetes: "Kubernetes",
|
|
217
231
|
processes: "Processes",
|
|
218
232
|
shell: "Shell",
|
|
219
233
|
agents: "Agents",
|
|
@@ -878,6 +892,36 @@ function parseRecentSessionArgs(args) {
|
|
|
878
892
|
if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new Error("--limit must be an integer between 1 and 200");
|
|
879
893
|
return { limit };
|
|
880
894
|
}
|
|
895
|
+
function parseK8sUsageArgs(args) {
|
|
896
|
+
const parsed = { window: "24h" };
|
|
897
|
+
const seen = /* @__PURE__ */ new Set();
|
|
898
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
899
|
+
const arg = args[index];
|
|
900
|
+
if (!arg) continue;
|
|
901
|
+
const option = ["--namespace", "--workload", "--container", "--window"].find(
|
|
902
|
+
(candidate) => arg === candidate || arg.startsWith(`${candidate}=`)
|
|
903
|
+
);
|
|
904
|
+
if (!option) throw new Error(`Unknown k8s usage argument: ${arg}`);
|
|
905
|
+
if (seen.has(option)) throw new Error(`Use ${option} only once`);
|
|
906
|
+
seen.add(option);
|
|
907
|
+
const inlineValue = parseLongOptionWithEquals(arg, option);
|
|
908
|
+
const candidateValue = inlineValue ?? args[index + 1];
|
|
909
|
+
if (!candidateValue || candidateValue.startsWith("--")) throw new Error(`Missing value for ${option}`);
|
|
910
|
+
const value = candidateValue;
|
|
911
|
+
if (inlineValue === void 0) index += 1;
|
|
912
|
+
if (option === "--window") {
|
|
913
|
+
if (value !== "24h") throw new Error("--window currently supports only 24h");
|
|
914
|
+
parsed.window = value;
|
|
915
|
+
} else if (option === "--namespace") {
|
|
916
|
+
parsed.namespace = value;
|
|
917
|
+
} else if (option === "--workload") {
|
|
918
|
+
parsed.workload = value;
|
|
919
|
+
} else {
|
|
920
|
+
parsed.container = value;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return parsed;
|
|
924
|
+
}
|
|
881
925
|
async function openInBrowser(url) {
|
|
882
926
|
if (process.platform === "darwin") {
|
|
883
927
|
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
@@ -1039,6 +1083,145 @@ Error: ${result.error}` : "";
|
|
|
1039
1083
|
return `Workspace sync ${result.outcome} on ${result.workerLabel}; HEAD ${head}; ${result.diffSizeBytes} bytes.${discarded}${error}
|
|
1040
1084
|
`;
|
|
1041
1085
|
}
|
|
1086
|
+
function summarizeK8sHistory(history) {
|
|
1087
|
+
if (history.envelope.length === 0) {
|
|
1088
|
+
return {
|
|
1089
|
+
cpuMinNanoCores: null,
|
|
1090
|
+
cpuMaxNanoCores: null,
|
|
1091
|
+
memoryMinBytes: null,
|
|
1092
|
+
memoryMaxBytes: null,
|
|
1093
|
+
sampleCount: 0
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
cpuMinNanoCores: Math.min(...history.envelope.map((point) => point.cpuMinNanoCores)),
|
|
1098
|
+
cpuMaxNanoCores: Math.max(...history.envelope.map((point) => point.cpuMaxNanoCores)),
|
|
1099
|
+
memoryMinBytes: Math.min(...history.envelope.map((point) => point.memoryMinBytes)),
|
|
1100
|
+
memoryMaxBytes: Math.max(...history.envelope.map((point) => point.memoryMaxBytes)),
|
|
1101
|
+
sampleCount: history.envelope.reduce((total, point) => total + point.sampleCount, 0)
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
1105
|
+
const results = new Array(items.length);
|
|
1106
|
+
let nextIndex = 0;
|
|
1107
|
+
await Promise.all(
|
|
1108
|
+
Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
1109
|
+
while (nextIndex < items.length) {
|
|
1110
|
+
const index = nextIndex++;
|
|
1111
|
+
const item = items[index];
|
|
1112
|
+
if (item !== void 0) results[index] = await fn(item);
|
|
1113
|
+
}
|
|
1114
|
+
})
|
|
1115
|
+
);
|
|
1116
|
+
return results;
|
|
1117
|
+
}
|
|
1118
|
+
async function collectK8sUsage(client, options) {
|
|
1119
|
+
const resources = await client.k8s.resources();
|
|
1120
|
+
const candidates = resources.workloads.flatMap((workload) => workload.containers.map((container) => ({ workload, container }))).filter(({ workload, container }) => {
|
|
1121
|
+
const workloadMatches = !options.workload || options.workload === workload.name || options.workload === `${workload.namespace}/${workload.name}` || options.workload === workload.uid;
|
|
1122
|
+
return (!options.namespace || options.namespace === workload.namespace) && workloadMatches && (!options.container || options.container === container.name);
|
|
1123
|
+
}).sort(
|
|
1124
|
+
(left, right) => left.workload.namespace.localeCompare(right.workload.namespace) || left.workload.name.localeCompare(right.workload.name) || left.container.name.localeCompare(right.container.name)
|
|
1125
|
+
);
|
|
1126
|
+
if (candidates.length === 0 && (options.namespace || options.workload || options.container)) {
|
|
1127
|
+
throw new Error("No managed-vCluster workload containers matched the requested filters");
|
|
1128
|
+
}
|
|
1129
|
+
const entries = await mapWithConcurrency(candidates, 8, async ({ workload, container }) => {
|
|
1130
|
+
const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name });
|
|
1131
|
+
return {
|
|
1132
|
+
workload: {
|
|
1133
|
+
uid: workload.uid,
|
|
1134
|
+
kind: workload.kind,
|
|
1135
|
+
namespace: workload.namespace,
|
|
1136
|
+
name: workload.name
|
|
1137
|
+
},
|
|
1138
|
+
container: {
|
|
1139
|
+
name: container.name,
|
|
1140
|
+
currentUsage: container.currentUsage,
|
|
1141
|
+
pods: container.pods
|
|
1142
|
+
},
|
|
1143
|
+
summary: summarizeK8sHistory(history),
|
|
1144
|
+
history
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
1147
|
+
return {
|
|
1148
|
+
window: options.window,
|
|
1149
|
+
collectedAt: resources.collectedAt,
|
|
1150
|
+
metricsFreshAt: resources.metricsFreshAt,
|
|
1151
|
+
metricsStale: resources.metricsStale,
|
|
1152
|
+
filters: {
|
|
1153
|
+
namespace: options.namespace,
|
|
1154
|
+
workload: options.workload,
|
|
1155
|
+
container: options.container
|
|
1156
|
+
},
|
|
1157
|
+
entries
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function formatDecimal(value) {
|
|
1161
|
+
if (value >= 100) return value.toFixed(0);
|
|
1162
|
+
if (value >= 10) return value.toFixed(1).replace(/\.0$/, "");
|
|
1163
|
+
return value.toFixed(2).replace(/\.0+$/, "").replace(/(\.\d)0$/, "$1");
|
|
1164
|
+
}
|
|
1165
|
+
function formatK8sCpu(value) {
|
|
1166
|
+
if (value === null) return "-";
|
|
1167
|
+
const milliCores = value / 1e6;
|
|
1168
|
+
return milliCores >= 1e3 ? `${formatDecimal(milliCores / 1e3)} cores` : `${formatDecimal(milliCores)}m`;
|
|
1169
|
+
}
|
|
1170
|
+
function formatK8sMemory(value) {
|
|
1171
|
+
if (value === null) return "-";
|
|
1172
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
1173
|
+
let amount = value;
|
|
1174
|
+
let unitIndex = 0;
|
|
1175
|
+
while (amount >= 1024 && unitIndex < units.length - 1) {
|
|
1176
|
+
amount /= 1024;
|
|
1177
|
+
unitIndex += 1;
|
|
1178
|
+
}
|
|
1179
|
+
return `${formatDecimal(amount)} ${units[unitIndex]}`;
|
|
1180
|
+
}
|
|
1181
|
+
function formatK8sCoverage(start, end) {
|
|
1182
|
+
if (!start || !end) return "none";
|
|
1183
|
+
const durationMs = Date.parse(end) - Date.parse(start);
|
|
1184
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) return "unknown";
|
|
1185
|
+
const totalMinutes = Math.floor(durationMs / 6e4);
|
|
1186
|
+
return `${Math.floor(totalMinutes / 60)}h ${String(totalMinutes % 60).padStart(2, "0")}m`;
|
|
1187
|
+
}
|
|
1188
|
+
function renderK8sUsageReport(report) {
|
|
1189
|
+
const freshness = report.metricsFreshAt ?? "unavailable";
|
|
1190
|
+
const lines = [`Metrics: ${report.metricsStale ? "stale" : "current"} (fresh at ${freshness})`, "Window: 24h"];
|
|
1191
|
+
if (report.entries.length === 0) return `${lines.join("\n")}
|
|
1192
|
+
No managed-vCluster workload containers found.
|
|
1193
|
+
`;
|
|
1194
|
+
const headers = [
|
|
1195
|
+
"NAMESPACE",
|
|
1196
|
+
"WORKLOAD",
|
|
1197
|
+
"CONTAINER",
|
|
1198
|
+
"PODS",
|
|
1199
|
+
"CPU NOW",
|
|
1200
|
+
"CPU 24H MIN/MAX",
|
|
1201
|
+
"MEM NOW",
|
|
1202
|
+
"MEM 24H MIN/MAX",
|
|
1203
|
+
"COVERAGE",
|
|
1204
|
+
"STATUS"
|
|
1205
|
+
];
|
|
1206
|
+
const rows = report.entries.map((entry) => [
|
|
1207
|
+
entry.workload.namespace,
|
|
1208
|
+
`${entry.workload.kind}/${entry.workload.name}`,
|
|
1209
|
+
entry.container.name,
|
|
1210
|
+
String(entry.container.pods.length),
|
|
1211
|
+
formatK8sCpu(entry.container.currentUsage.cpuNanoCores),
|
|
1212
|
+
`${formatK8sCpu(entry.summary.cpuMinNanoCores)} / ${formatK8sCpu(entry.summary.cpuMaxNanoCores)}`,
|
|
1213
|
+
formatK8sMemory(entry.container.currentUsage.memoryBytes),
|
|
1214
|
+
`${formatK8sMemory(entry.summary.memoryMinBytes)} / ${formatK8sMemory(entry.summary.memoryMaxBytes)}`,
|
|
1215
|
+
formatK8sCoverage(entry.history.coverageStart, entry.history.coverageEnd),
|
|
1216
|
+
entry.history.stale ? "stale" : entry.history.incomplete ? "partial" : "complete"
|
|
1217
|
+
]);
|
|
1218
|
+
const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
1219
|
+
const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
|
|
1220
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
1221
|
+
lines.push(formatRow(headers), separator, ...rows.map(formatRow));
|
|
1222
|
+
return `${lines.join("\n")}
|
|
1223
|
+
`;
|
|
1224
|
+
}
|
|
1042
1225
|
function renderSessionDescription(session) {
|
|
1043
1226
|
return [`Session: ${session.id}`, `Project: ${session.projectPath}`, `Branch: ${session.branchName}`].join("\n") + "\n";
|
|
1044
1227
|
}
|
|
@@ -1417,6 +1600,11 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
1417
1600
|
write(result, renderWorkspaceSync(result));
|
|
1418
1601
|
return;
|
|
1419
1602
|
}
|
|
1603
|
+
if (first === "k8s" && second === "usage") {
|
|
1604
|
+
const report = await collectK8sUsage(client, parseK8sUsageArgs(args.slice(2)));
|
|
1605
|
+
write(report, renderK8sUsageReport(report));
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1420
1608
|
if (first === "ps" && second === "list") {
|
|
1421
1609
|
const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
|
|
1422
1610
|
write(processes, renderProcessList(processes));
|
|
@@ -1665,6 +1853,12 @@ function resolveCommandExecution(options, rest) {
|
|
|
1665
1853
|
text: PS_HELP_TEXT
|
|
1666
1854
|
};
|
|
1667
1855
|
}
|
|
1856
|
+
if (command === "k8s" && commandArgs.length === 0) {
|
|
1857
|
+
return {
|
|
1858
|
+
kind: "cli-help",
|
|
1859
|
+
text: K8S_HELP_TEXT
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1668
1862
|
if (trailingHelp) {
|
|
1669
1863
|
const cliOnlyHelp = findCliOnlyCommandHelp(normalizedRest);
|
|
1670
1864
|
if (cliOnlyHelp) {
|
|
@@ -1793,6 +1987,15 @@ function resolveCommandExecution(options, rest) {
|
|
|
1793
1987
|
}
|
|
1794
1988
|
throw new Error(`Unknown ps command: ${subcommand ?? ""}`.trim());
|
|
1795
1989
|
}
|
|
1990
|
+
if (command === "k8s") {
|
|
1991
|
+
const subcommand = commandArgs[0];
|
|
1992
|
+
if (subcommand !== "usage") throw new Error(`Unknown k8s command: ${subcommand ?? ""}`.trim());
|
|
1993
|
+
if (options.project || options.branch || options.session) {
|
|
1994
|
+
throw new Error("Project, branch, and session scopes are not supported for `k8s usage`");
|
|
1995
|
+
}
|
|
1996
|
+
parseK8sUsageArgs(commandArgs.slice(1));
|
|
1997
|
+
return { kind: "plugin", pluginArgs: ["k8s", "usage", ...commandArgs.slice(1)] };
|
|
1998
|
+
}
|
|
1796
1999
|
if (command === "get") {
|
|
1797
2000
|
const target = commandArgs[0];
|
|
1798
2001
|
if (target === "projects") {
|
|
@@ -2220,6 +2423,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2220
2423
|
}
|
|
2221
2424
|
}
|
|
2222
2425
|
export {
|
|
2426
|
+
collectK8sUsage,
|
|
2427
|
+
formatK8sCpu,
|
|
2428
|
+
formatK8sMemory,
|
|
2223
2429
|
formatProcessAge,
|
|
2224
2430
|
getCliHelpText,
|
|
2225
2431
|
getR5dctlVersion,
|
|
@@ -2230,6 +2436,7 @@ export {
|
|
|
2230
2436
|
parseEnvFlags,
|
|
2231
2437
|
parseEnvRequestResponseArgs,
|
|
2232
2438
|
parseGlobalArgs,
|
|
2439
|
+
parseK8sUsageArgs,
|
|
2233
2440
|
parsePromptArgs,
|
|
2234
2441
|
parsePsHistoryArgs,
|
|
2235
2442
|
parsePsListArgs,
|
|
@@ -2240,6 +2447,7 @@ export {
|
|
|
2240
2447
|
renderConversationOverviewResponse,
|
|
2241
2448
|
renderConversationResponse,
|
|
2242
2449
|
renderConversationWorkResponse,
|
|
2450
|
+
renderK8sUsageReport,
|
|
2243
2451
|
renderProcessHistory,
|
|
2244
2452
|
renderProcessInspection,
|
|
2245
2453
|
renderProcessList,
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessRun, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
1
|
+
import { R5dctlClient, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessRun, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
2
2
|
export type GlobalOptions = {
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
json: boolean;
|
|
@@ -65,8 +65,46 @@ export declare function parseShellArgs(args: string[]): {
|
|
|
65
65
|
};
|
|
66
66
|
export declare function parsePsListArgs(args: string[]): R5dctlProcessListInput;
|
|
67
67
|
export declare function parsePsHistoryArgs(args: string[]): R5dctlProcessListInput;
|
|
68
|
+
export type K8sUsageOptions = {
|
|
69
|
+
namespace?: string;
|
|
70
|
+
workload?: string;
|
|
71
|
+
container?: string;
|
|
72
|
+
window: "24h";
|
|
73
|
+
};
|
|
74
|
+
export declare function parseK8sUsageArgs(args: string[]): K8sUsageOptions;
|
|
68
75
|
export declare function renderWorkspaceStatus(status: R5dctlWorkspaceStatus): string;
|
|
69
76
|
export declare function renderWorkspaceSync(result: R5dctlWorkspaceSyncResult): string;
|
|
77
|
+
export type K8sUsageSummary = {
|
|
78
|
+
cpuMinNanoCores: number | null;
|
|
79
|
+
cpuMaxNanoCores: number | null;
|
|
80
|
+
memoryMinBytes: number | null;
|
|
81
|
+
memoryMaxBytes: number | null;
|
|
82
|
+
sampleCount: number;
|
|
83
|
+
};
|
|
84
|
+
export type K8sUsageReport = {
|
|
85
|
+
window: "24h";
|
|
86
|
+
collectedAt: string;
|
|
87
|
+
metricsFreshAt: string | null;
|
|
88
|
+
metricsStale: boolean;
|
|
89
|
+
filters: Omit<K8sUsageOptions, "window">;
|
|
90
|
+
entries: Array<{
|
|
91
|
+
workload: Pick<R5dctlK8sWorkload, "uid" | "kind" | "namespace" | "name">;
|
|
92
|
+
container: {
|
|
93
|
+
name: string;
|
|
94
|
+
currentUsage: R5dctlK8sUsage;
|
|
95
|
+
pods: Array<R5dctlK8sUsage & {
|
|
96
|
+
podUid: string;
|
|
97
|
+
podName: string;
|
|
98
|
+
}>;
|
|
99
|
+
};
|
|
100
|
+
summary: K8sUsageSummary;
|
|
101
|
+
history: R5dctlK8sResourceHistory;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
export declare function collectK8sUsage(client: Pick<R5dctlClient, "k8s">, options: K8sUsageOptions): Promise<K8sUsageReport>;
|
|
105
|
+
export declare function formatK8sCpu(value: number | null): string;
|
|
106
|
+
export declare function formatK8sMemory(value: number | null): string;
|
|
107
|
+
export declare function renderK8sUsageReport(report: K8sUsageReport): string;
|
|
70
108
|
export declare function formatProcessAge(startedAt: string, nowMs?: number): string;
|
|
71
109
|
export declare function renderProcessList(processes: R5dctlProcessRun[], nowMs?: number): string;
|
|
72
110
|
export declare function renderProcessHistory(processes: R5dctlProcessRun[], nowMs?: number): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5dctl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/cjs/cli.cjs",
|
|
6
6
|
"module": "./dist/mjs/cli.mjs",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"r5dctl": "dist/cjs/main.cjs"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ricsam/r5d-api": "^0.0.
|
|
29
|
+
"@ricsam/r5d-api": "^0.0.50",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"ws": "^8.18.3"
|
|
32
32
|
},
|