@ricsam/r5dctl 0.0.48 → 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 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
  }
@@ -1126,14 +1314,14 @@ function renderProcessList(processes, nowMs = Date.now()) {
1126
1314
  if (processes.length === 0) {
1127
1315
  return "No active processes.\n";
1128
1316
  }
1129
- const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "PROJECT", "BRANCH", "SESSION", "COMMAND"];
1317
+ const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "TARGET", "SCOPE", "SESSION", "COMMAND"];
1130
1318
  const rows = processes.map((process2) => [
1131
1319
  process2.runId,
1132
1320
  process2.status,
1133
1321
  formatProcessAge(process2.startedAt, nowMs),
1134
1322
  process2.workerLabel,
1135
1323
  process2.projectPath,
1136
- process2.branchName,
1324
+ process2.target.type === "project" ? process2.target.branchName : process2.target.rootProfile,
1137
1325
  process2.sessionId,
1138
1326
  process2.command
1139
1327
  ]);
@@ -1152,11 +1340,11 @@ function renderProcessInspection(process2) {
1152
1340
  `Status: ${process2.status}`,
1153
1341
  `Mode: ${process2.mode}`,
1154
1342
  `Worker: ${process2.workerLabel} (${process2.platform ?? "unknown"}/${process2.arch ?? "unknown"})`,
1155
- `Project: ${process2.projectPath} ${process2.branchName}`,
1343
+ `Target: ${process2.projectPath} ${process2.target.type === "project" ? process2.target.branchName : process2.target.rootProfile}`,
1156
1344
  `Session: ${process2.sessionId}`,
1157
1345
  `Command: ${process2.command}`,
1158
1346
  `Argv: ${JSON.stringify(process2.argv)}`,
1159
- `CWD: ${process2.cwd ?? "(project root)"}`,
1347
+ `CWD: ${process2.cwd ?? "(execution root)"}`,
1160
1348
  `PID/process group: ${process2.pid ?? "(unknown)"}/${process2.processGroupId ?? "(unknown)"}`,
1161
1349
  `Started: ${process2.startedAt}`,
1162
1350
  `Updated: ${process2.updatedAt}`,
@@ -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,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
4
4
  "type": "commonjs"
5
5
  }
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
  }
@@ -1069,14 +1252,14 @@ function renderProcessList(processes, nowMs = Date.now()) {
1069
1252
  if (processes.length === 0) {
1070
1253
  return "No active processes.\n";
1071
1254
  }
1072
- const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "PROJECT", "BRANCH", "SESSION", "COMMAND"];
1255
+ const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "TARGET", "SCOPE", "SESSION", "COMMAND"];
1073
1256
  const rows = processes.map((process2) => [
1074
1257
  process2.runId,
1075
1258
  process2.status,
1076
1259
  formatProcessAge(process2.startedAt, nowMs),
1077
1260
  process2.workerLabel,
1078
1261
  process2.projectPath,
1079
- process2.branchName,
1262
+ process2.target.type === "project" ? process2.target.branchName : process2.target.rootProfile,
1080
1263
  process2.sessionId,
1081
1264
  process2.command
1082
1265
  ]);
@@ -1095,11 +1278,11 @@ function renderProcessInspection(process2) {
1095
1278
  `Status: ${process2.status}`,
1096
1279
  `Mode: ${process2.mode}`,
1097
1280
  `Worker: ${process2.workerLabel} (${process2.platform ?? "unknown"}/${process2.arch ?? "unknown"})`,
1098
- `Project: ${process2.projectPath} ${process2.branchName}`,
1281
+ `Target: ${process2.projectPath} ${process2.target.type === "project" ? process2.target.branchName : process2.target.rootProfile}`,
1099
1282
  `Session: ${process2.sessionId}`,
1100
1283
  `Command: ${process2.command}`,
1101
1284
  `Argv: ${JSON.stringify(process2.argv)}`,
1102
- `CWD: ${process2.cwd ?? "(project root)"}`,
1285
+ `CWD: ${process2.cwd ?? "(execution root)"}`,
1103
1286
  `PID/process group: ${process2.pid ?? "(unknown)"}/${process2.processGroupId ?? "(unknown)"}`,
1104
1287
  `Started: ${process2.startedAt}`,
1105
1288
  `Updated: ${process2.updatedAt}`,
@@ -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,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
4
4
  "type": "module"
5
5
  }
@@ -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.48",
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.48",
29
+ "@ricsam/r5d-api": "^0.0.50",
30
30
  "dotenv": "^17",
31
31
  "ws": "^8.18.3"
32
32
  },