@ricsam/r5dctl 0.0.50 → 0.0.51

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
@@ -50,6 +50,8 @@ r5dctl delete project <namespace/name|id>
50
50
  r5dctl -p <project> get branches
51
51
  r5dctl -p <project> describe branch <branch>
52
52
  r5dctl -p <project> get envs
53
+ r5dctl -p <project> get envs --show-values
54
+ r5dctl -p <project> get env API_KEY
53
55
  r5dctl -p <project> set envs --from-env-file ~/.env --only-missing
54
56
  r5dctl -p <project> set envs -e API_KEY=value --description "API credential"
55
57
  r5dctl -p <project> get sessions --branch <branch>
@@ -82,16 +84,24 @@ r5dctl ps stop <run-id>
82
84
 
83
85
  r5dctl k8s usage
84
86
  r5dctl k8s usage --namespace default --workload api --container server
85
- r5dctl k8s usage --workload default/api --window 24h --json
87
+ r5dctl k8s usage --window 7d
88
+ r5dctl k8s usage --workload default/api --window 7d --json
86
89
  ```
87
90
 
88
91
  `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.
89
92
 
90
93
  `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.
91
94
 
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`.
95
+ `k8s usage` shows current and rolling CPU and memory usage for every workload container in your managed vCluster. The default window is `24h`; pass `--window 7d` for seven days. Filter with `--namespace`, `--workload`, or `--container`; a workload may be identified by name, `namespace/name`, or Kubernetes UID. Human-readable output shows current totals across sampled replicas and the selected window's maximum. Global `--json` includes min/max summaries, the complete five-minute history envelope, per-pod series, coverage, and freshness metadata.
93
96
 
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.
97
+ `get envs` reports names and whether values are set without revealing the values by default. Add `--show-values` to reveal every value; multiline and control characters are escaped for readable output. Without the global `--json` flag, `get env <name>` writes only that value to stdout, with no added newline or labels, so it works directly with command substitution and pipelines:
98
+
99
+ ```bash
100
+ export API_KEY="$(r5dctl -p <project> get env API_KEY)"
101
+ r5dctl -p <project> get env PRIVATE_KEY | openssl pkey -check -noout
102
+ ```
103
+
104
+ An undeclared or unset variable makes `get env` fail with a nonzero exit status. An explicitly configured empty value succeeds and writes zero bytes. 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.
95
105
 
96
106
  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.
97
107
 
package/dist/cjs/cli.cjs CHANGED
@@ -33,6 +33,8 @@ __export(cli_exports, {
33
33
  formatK8sMemory: () => formatK8sMemory,
34
34
  formatProcessAge: () => formatProcessAge,
35
35
  getCliHelpText: () => getCliHelpText,
36
+ getK8sUsageState: () => getK8sUsageState,
37
+ getProjectEnvValue: () => getProjectEnvValue,
36
38
  getR5dctlVersion: () => getR5dctlVersion,
37
39
  main: () => main,
38
40
  parseAnswerFlags: () => parseAnswerFlags,
@@ -40,6 +42,7 @@ __export(cli_exports, {
40
42
  parseConversationWorkDetailArgs: () => parseConversationWorkDetailArgs,
41
43
  parseEnvFlags: () => parseEnvFlags,
42
44
  parseEnvRequestResponseArgs: () => parseEnvRequestResponseArgs,
45
+ parseGetEnvsArgs: () => parseGetEnvsArgs,
43
46
  parseGlobalArgs: () => parseGlobalArgs,
44
47
  parseK8sUsageArgs: () => parseK8sUsageArgs,
45
48
  parsePromptArgs: () => parsePromptArgs,
@@ -52,6 +55,7 @@ __export(cli_exports, {
52
55
  renderConversationOverviewResponse: () => renderConversationOverviewResponse,
53
56
  renderConversationResponse: () => renderConversationResponse,
54
57
  renderConversationWorkResponse: () => renderConversationWorkResponse,
58
+ renderEnvValues: () => renderEnvValues,
55
59
  renderK8sUsageReport: () => renderK8sUsageReport,
56
60
  renderProcessHistory: () => renderProcessHistory,
57
61
  renderProcessInspection: () => renderProcessInspection,
@@ -133,9 +137,9 @@ const PS_HELP_TEXT = [
133
137
  ].join("\n");
134
138
  const K8S_HELP_TEXT = [
135
139
  "Usage:",
136
- " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
140
+ " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
137
141
  "",
138
- "Show current and 24-hour CPU and memory usage for managed-vCluster workload containers.",
142
+ "Show current and rolling CPU and memory usage for managed-vCluster workload containers.",
139
143
  ""
140
144
  ].join("\n");
141
145
  const SHARED_HELP_ENTRIES = [
@@ -167,8 +171,13 @@ const SHARED_HELP_ENTRIES = [
167
171
  { section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
168
172
  {
169
173
  section: "envs",
170
- usage: "-p <project> get envs",
171
- description: "List declared envs and whether each has a value, without revealing values."
174
+ usage: "-p <project> get envs [--show-values]",
175
+ description: "List declared envs; use --show-values to reveal their values."
176
+ },
177
+ {
178
+ section: "envs",
179
+ usage: "-p <project> get env <name>",
180
+ description: "Write one env value exactly to stdout for command substitution or piping."
172
181
  },
173
182
  {
174
183
  section: "envs",
@@ -235,8 +244,8 @@ const SHARED_HELP_ENTRIES = [
235
244
  },
236
245
  {
237
246
  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."
247
+ usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
248
+ description: "Show current and rolling CPU and memory usage for managed-vCluster workload containers."
240
249
  },
241
250
  {
242
251
  section: "processes",
@@ -972,7 +981,7 @@ function parseK8sUsageArgs(args) {
972
981
  const value = candidateValue;
973
982
  if (inlineValue === void 0) index += 1;
974
983
  if (option === "--window") {
975
- if (value !== "24h") throw new Error("--window currently supports only 24h");
984
+ if (value !== "24h" && value !== "7d") throw new Error("--window must be 24h or 7d");
976
985
  parsed.window = value;
977
986
  } else if (option === "--namespace") {
978
987
  parsed.namespace = value;
@@ -1189,19 +1198,22 @@ async function collectK8sUsage(client, options) {
1189
1198
  throw new Error("No managed-vCluster workload containers matched the requested filters");
1190
1199
  }
1191
1200
  const entries = await mapWithConcurrency(candidates, 8, async ({ workload, container }) => {
1192
- const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name });
1201
+ const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name, window: options.window });
1193
1202
  return {
1194
1203
  workload: {
1195
1204
  uid: workload.uid,
1196
1205
  kind: workload.kind,
1197
1206
  namespace: workload.namespace,
1198
- name: workload.name
1207
+ name: workload.name,
1208
+ replicas: workload.replicas,
1209
+ readyReplicas: workload.readyReplicas
1199
1210
  },
1200
1211
  container: {
1201
1212
  name: container.name,
1202
1213
  currentUsage: container.currentUsage,
1203
1214
  pods: container.pods
1204
1215
  },
1216
+ sampledPodCount: container.pods.filter((pod) => pod.cpuNanoCores !== null || pod.memoryBytes !== null).length,
1205
1217
  summary: summarizeK8sHistory(history),
1206
1218
  history
1207
1219
  };
@@ -1227,60 +1239,83 @@ function formatDecimal(value) {
1227
1239
  function formatK8sCpu(value) {
1228
1240
  if (value === null) return "-";
1229
1241
  const milliCores = value / 1e6;
1230
- return milliCores >= 1e3 ? `${formatDecimal(milliCores / 1e3)} cores` : `${formatDecimal(milliCores)}m`;
1242
+ return `${formatDecimal(milliCores)}m`;
1231
1243
  }
1232
1244
  function formatK8sMemory(value) {
1233
1245
  if (value === null) return "-";
1234
- const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1246
+ const units = ["B", "Ki", "Mi", "Gi", "Ti"];
1235
1247
  let amount = value;
1236
1248
  let unitIndex = 0;
1237
1249
  while (amount >= 1024 && unitIndex < units.length - 1) {
1238
1250
  amount /= 1024;
1239
1251
  unitIndex += 1;
1240
1252
  }
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`;
1253
+ return `${formatDecimal(amount)}${units[unitIndex]}`;
1254
+ }
1255
+ function getK8sUsageState(entry) {
1256
+ const hasHistory = entry.history.envelope.length > 0;
1257
+ const terminalPods = entry.container.pods.filter((pod) => pod.phase === "Succeeded" || pod.phase === "Failed").length;
1258
+ const inactive = entry.sampledPodCount === 0 && (entry.workload.replicas === 0 || entry.container.pods.length > 0 && terminalPods === entry.container.pods.length || entry.workload.kind === "CronJob" && entry.container.pods.length === 0);
1259
+ if (inactive) return "inactive";
1260
+ if (entry.sampledPodCount === 0) return hasHistory ? entry.history.stale ? "stale" : "no current" : "no data";
1261
+ if (!hasHistory) return "warming";
1262
+ if (entry.history.stale) return "stale";
1263
+ if (entry.history.incomplete) return "partial";
1264
+ return "healthy";
1265
+ }
1266
+ function abbreviatedK8sKind(kind) {
1267
+ switch (kind) {
1268
+ case "Deployment":
1269
+ return "deploy";
1270
+ case "StatefulSet":
1271
+ return "sts";
1272
+ case "DaemonSet":
1273
+ return "ds";
1274
+ case "CronJob":
1275
+ return "cron";
1276
+ case "ReplicaSet":
1277
+ return "rs";
1278
+ case "Job":
1279
+ return "job";
1280
+ case "Pod":
1281
+ return "pod";
1282
+ }
1249
1283
  }
1250
1284
  function renderK8sUsageReport(report) {
1251
1285
  const freshness = report.metricsFreshAt ?? "unavailable";
1252
- const lines = [`Metrics: ${report.metricsStale ? "stale" : "current"} (fresh at ${freshness})`, "Window: 24h"];
1286
+ const lines = [
1287
+ `Window: ${report.window} Collector: ${report.metricsStale ? "stale" : "current"} Fresh: ${freshness}`,
1288
+ "PODS counts pods with current metrics."
1289
+ ];
1253
1290
  if (report.entries.length === 0) return `${lines.join("\n")}
1254
1291
  No managed-vCluster workload containers found.
1255
1292
  `;
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));
1293
+ const headers = ["WORKLOAD", "CONTAINER", "PODS", "CPU NOW", "CPU MAX", "MEM NOW", "MEM MAX", "STATE"];
1294
+ const grouped = /* @__PURE__ */ new Map();
1295
+ for (const entry of report.entries) {
1296
+ const entries = grouped.get(entry.workload.namespace) ?? [];
1297
+ entries.push(entry);
1298
+ grouped.set(entry.workload.namespace, entries);
1299
+ }
1300
+ for (const [namespace, entries] of grouped) {
1301
+ const rows = entries.map((entry) => {
1302
+ const state = getK8sUsageState(entry);
1303
+ return [
1304
+ `${abbreviatedK8sKind(entry.workload.kind)}/${entry.workload.name}`,
1305
+ entry.container.name,
1306
+ String(entry.sampledPodCount),
1307
+ formatK8sCpu(entry.container.currentUsage.cpuNanoCores),
1308
+ formatK8sCpu(entry.summary.cpuMaxNanoCores),
1309
+ formatK8sMemory(entry.container.currentUsage.memoryBytes),
1310
+ formatK8sMemory(entry.summary.memoryMaxBytes),
1311
+ state === "healthy" ? "-" : state
1312
+ ];
1313
+ });
1314
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
1315
+ const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
1316
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
1317
+ lines.push("", `Namespace: ${namespace}`, formatRow(headers), separator, ...rows.map(formatRow));
1318
+ }
1284
1319
  return `${lines.join("\n")}
1285
1320
  `;
1286
1321
  }
@@ -1375,6 +1410,17 @@ function summarizeEnvData(data) {
1375
1410
  ])
1376
1411
  );
1377
1412
  }
1413
+ function parseGetEnvsArgs(args) {
1414
+ let showValues = false;
1415
+ for (const arg of args) {
1416
+ if (arg === "--show-values") {
1417
+ showValues = true;
1418
+ continue;
1419
+ }
1420
+ throw new Error(arg.startsWith("-") ? `Unknown get envs flag: ${arg}` : `Unexpected get envs value: ${arg}`);
1421
+ }
1422
+ return { showValues };
1423
+ }
1378
1424
  function renderEnvSummary(data) {
1379
1425
  const lines = [];
1380
1426
  for (const [name, env] of Object.entries(data)) {
@@ -1383,6 +1429,24 @@ function renderEnvSummary(data) {
1383
1429
  return lines.length > 0 ? `${lines.join("\n")}
1384
1430
  ` : "No environment variables declared.\n";
1385
1431
  }
1432
+ function renderEnvValues(data) {
1433
+ const lines = Object.entries(data).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => `${name} ${env.value === null ? "<missing>" : JSON.stringify(env.value)} ${env.description}`);
1434
+ return lines.length > 0 ? `${lines.join("\n")}
1435
+ ` : "No environment variables declared.\n";
1436
+ }
1437
+ function getProjectEnvValue(data, name) {
1438
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1439
+ throw new Error(`Invalid environment variable name: ${name}`);
1440
+ }
1441
+ if (!Object.prototype.hasOwnProperty.call(data, name)) {
1442
+ throw new Error(`Environment variable ${name} is not declared for this project`);
1443
+ }
1444
+ const value = data[name]?.value;
1445
+ if (value === null || value === void 0) {
1446
+ throw new Error(`Environment variable ${name} does not have a value`);
1447
+ }
1448
+ return value;
1449
+ }
1386
1450
  function renderEnvSetResult(result) {
1387
1451
  const lines = [`Updated ${result.updated.length} environment variable${result.updated.length === 1 ? "" : "s"} for ${result.project}.`];
1388
1452
  if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
@@ -1738,11 +1802,25 @@ Sessions: ${result.sessions.length}
1738
1802
  return;
1739
1803
  }
1740
1804
  if (first === "get" && second === "envs") {
1741
- if (args.length !== 3) throw new Error(`Unexpected get envs argument: ${args[3] ?? ""}`.trim());
1742
1805
  const projectRef = requireValue(args[2], "Missing project reference");
1806
+ const options = parseGetEnvsArgs(args.slice(3));
1743
1807
  const project = await client.projects.describe(projectRef);
1744
- const envs = summarizeEnvData(await client.projects.env.get(project.id));
1745
- write({ project: project.path, envs }, renderEnvSummary(envs));
1808
+ const envs = await client.projects.env.get(project.id);
1809
+ if (options.showValues) {
1810
+ write({ project: project.path, envs }, renderEnvValues(envs));
1811
+ } else {
1812
+ const summary = summarizeEnvData(envs);
1813
+ write({ project: project.path, envs: summary }, renderEnvSummary(summary));
1814
+ }
1815
+ return;
1816
+ }
1817
+ if (first === "get" && second === "env") {
1818
+ if (args.length > 4) throw new Error(`Unexpected get env argument: ${args[4]}`);
1819
+ const projectRef = requireValue(args[2], "Missing project reference");
1820
+ const name = requireValue(args[3], "Missing environment variable name");
1821
+ const project = await client.projects.describe(projectRef);
1822
+ const value = getProjectEnvValue(await client.projects.env.get(project.id), name);
1823
+ write({ project: project.path, name, value }, value);
1746
1824
  return;
1747
1825
  }
1748
1826
  if (first === "set" && second === "envs") {
@@ -2095,9 +2173,29 @@ function resolveCommandExecution(options, rest) {
2095
2173
  if (options.branch) {
2096
2174
  throw new Error("--branch/-b is not supported for project environment variables");
2097
2175
  }
2176
+ parseGetEnvsArgs(commandArgs.slice(1));
2177
+ return {
2178
+ kind: "plugin",
2179
+ pluginArgs: ["get", "envs", options.project, ...commandArgs.slice(1)]
2180
+ };
2181
+ }
2182
+ if (target === "env") {
2183
+ if (!options.project) {
2184
+ throw new Error("--project/-p is required for `get env`");
2185
+ }
2186
+ if (options.branch) {
2187
+ throw new Error("--branch/-b is not supported for project environment variables");
2188
+ }
2189
+ const name = requireValue(commandArgs[1], "Environment variable name is required for `get env`");
2190
+ if (commandArgs.length > 2) {
2191
+ throw new Error(`Unexpected get env argument: ${commandArgs[2]}`);
2192
+ }
2193
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2194
+ throw new Error(`Invalid environment variable name: ${name}`);
2195
+ }
2098
2196
  return {
2099
2197
  kind: "plugin",
2100
- pluginArgs: ["get", "envs", options.project]
2198
+ pluginArgs: ["get", "env", options.project, name]
2101
2199
  };
2102
2200
  }
2103
2201
  throw new Error("Unknown get command");
@@ -2491,6 +2589,8 @@ async function main(argv = process.argv.slice(2)) {
2491
2589
  formatK8sMemory,
2492
2590
  formatProcessAge,
2493
2591
  getCliHelpText,
2592
+ getK8sUsageState,
2593
+ getProjectEnvValue,
2494
2594
  getR5dctlVersion,
2495
2595
  main,
2496
2596
  parseAnswerFlags,
@@ -2498,6 +2598,7 @@ async function main(argv = process.argv.slice(2)) {
2498
2598
  parseConversationWorkDetailArgs,
2499
2599
  parseEnvFlags,
2500
2600
  parseEnvRequestResponseArgs,
2601
+ parseGetEnvsArgs,
2501
2602
  parseGlobalArgs,
2502
2603
  parseK8sUsageArgs,
2503
2604
  parsePromptArgs,
@@ -2510,6 +2611,7 @@ async function main(argv = process.argv.slice(2)) {
2510
2611
  renderConversationOverviewResponse,
2511
2612
  renderConversationResponse,
2512
2613
  renderConversationWorkResponse,
2614
+ renderEnvValues,
2513
2615
  renderK8sUsageReport,
2514
2616
  renderProcessHistory,
2515
2617
  renderProcessInspection,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -71,9 +71,9 @@ const PS_HELP_TEXT = [
71
71
  ].join("\n");
72
72
  const K8S_HELP_TEXT = [
73
73
  "Usage:",
74
- " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
74
+ " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
75
75
  "",
76
- "Show current and 24-hour CPU and memory usage for managed-vCluster workload containers.",
76
+ "Show current and rolling CPU and memory usage for managed-vCluster workload containers.",
77
77
  ""
78
78
  ].join("\n");
79
79
  const SHARED_HELP_ENTRIES = [
@@ -105,8 +105,13 @@ const SHARED_HELP_ENTRIES = [
105
105
  { section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
106
106
  {
107
107
  section: "envs",
108
- usage: "-p <project> get envs",
109
- description: "List declared envs and whether each has a value, without revealing values."
108
+ usage: "-p <project> get envs [--show-values]",
109
+ description: "List declared envs; use --show-values to reveal their values."
110
+ },
111
+ {
112
+ section: "envs",
113
+ usage: "-p <project> get env <name>",
114
+ description: "Write one env value exactly to stdout for command substitution or piping."
110
115
  },
111
116
  {
112
117
  section: "envs",
@@ -173,8 +178,8 @@ const SHARED_HELP_ENTRIES = [
173
178
  },
174
179
  {
175
180
  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."
181
+ usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
182
+ description: "Show current and rolling CPU and memory usage for managed-vCluster workload containers."
178
183
  },
179
184
  {
180
185
  section: "processes",
@@ -910,7 +915,7 @@ function parseK8sUsageArgs(args) {
910
915
  const value = candidateValue;
911
916
  if (inlineValue === void 0) index += 1;
912
917
  if (option === "--window") {
913
- if (value !== "24h") throw new Error("--window currently supports only 24h");
918
+ if (value !== "24h" && value !== "7d") throw new Error("--window must be 24h or 7d");
914
919
  parsed.window = value;
915
920
  } else if (option === "--namespace") {
916
921
  parsed.namespace = value;
@@ -1127,19 +1132,22 @@ async function collectK8sUsage(client, options) {
1127
1132
  throw new Error("No managed-vCluster workload containers matched the requested filters");
1128
1133
  }
1129
1134
  const entries = await mapWithConcurrency(candidates, 8, async ({ workload, container }) => {
1130
- const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name });
1135
+ const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name, window: options.window });
1131
1136
  return {
1132
1137
  workload: {
1133
1138
  uid: workload.uid,
1134
1139
  kind: workload.kind,
1135
1140
  namespace: workload.namespace,
1136
- name: workload.name
1141
+ name: workload.name,
1142
+ replicas: workload.replicas,
1143
+ readyReplicas: workload.readyReplicas
1137
1144
  },
1138
1145
  container: {
1139
1146
  name: container.name,
1140
1147
  currentUsage: container.currentUsage,
1141
1148
  pods: container.pods
1142
1149
  },
1150
+ sampledPodCount: container.pods.filter((pod) => pod.cpuNanoCores !== null || pod.memoryBytes !== null).length,
1143
1151
  summary: summarizeK8sHistory(history),
1144
1152
  history
1145
1153
  };
@@ -1165,60 +1173,83 @@ function formatDecimal(value) {
1165
1173
  function formatK8sCpu(value) {
1166
1174
  if (value === null) return "-";
1167
1175
  const milliCores = value / 1e6;
1168
- return milliCores >= 1e3 ? `${formatDecimal(milliCores / 1e3)} cores` : `${formatDecimal(milliCores)}m`;
1176
+ return `${formatDecimal(milliCores)}m`;
1169
1177
  }
1170
1178
  function formatK8sMemory(value) {
1171
1179
  if (value === null) return "-";
1172
- const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1180
+ const units = ["B", "Ki", "Mi", "Gi", "Ti"];
1173
1181
  let amount = value;
1174
1182
  let unitIndex = 0;
1175
1183
  while (amount >= 1024 && unitIndex < units.length - 1) {
1176
1184
  amount /= 1024;
1177
1185
  unitIndex += 1;
1178
1186
  }
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
+ return `${formatDecimal(amount)}${units[unitIndex]}`;
1188
+ }
1189
+ function getK8sUsageState(entry) {
1190
+ const hasHistory = entry.history.envelope.length > 0;
1191
+ const terminalPods = entry.container.pods.filter((pod) => pod.phase === "Succeeded" || pod.phase === "Failed").length;
1192
+ const inactive = entry.sampledPodCount === 0 && (entry.workload.replicas === 0 || entry.container.pods.length > 0 && terminalPods === entry.container.pods.length || entry.workload.kind === "CronJob" && entry.container.pods.length === 0);
1193
+ if (inactive) return "inactive";
1194
+ if (entry.sampledPodCount === 0) return hasHistory ? entry.history.stale ? "stale" : "no current" : "no data";
1195
+ if (!hasHistory) return "warming";
1196
+ if (entry.history.stale) return "stale";
1197
+ if (entry.history.incomplete) return "partial";
1198
+ return "healthy";
1199
+ }
1200
+ function abbreviatedK8sKind(kind) {
1201
+ switch (kind) {
1202
+ case "Deployment":
1203
+ return "deploy";
1204
+ case "StatefulSet":
1205
+ return "sts";
1206
+ case "DaemonSet":
1207
+ return "ds";
1208
+ case "CronJob":
1209
+ return "cron";
1210
+ case "ReplicaSet":
1211
+ return "rs";
1212
+ case "Job":
1213
+ return "job";
1214
+ case "Pod":
1215
+ return "pod";
1216
+ }
1187
1217
  }
1188
1218
  function renderK8sUsageReport(report) {
1189
1219
  const freshness = report.metricsFreshAt ?? "unavailable";
1190
- const lines = [`Metrics: ${report.metricsStale ? "stale" : "current"} (fresh at ${freshness})`, "Window: 24h"];
1220
+ const lines = [
1221
+ `Window: ${report.window} Collector: ${report.metricsStale ? "stale" : "current"} Fresh: ${freshness}`,
1222
+ "PODS counts pods with current metrics."
1223
+ ];
1191
1224
  if (report.entries.length === 0) return `${lines.join("\n")}
1192
1225
  No managed-vCluster workload containers found.
1193
1226
  `;
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));
1227
+ const headers = ["WORKLOAD", "CONTAINER", "PODS", "CPU NOW", "CPU MAX", "MEM NOW", "MEM MAX", "STATE"];
1228
+ const grouped = /* @__PURE__ */ new Map();
1229
+ for (const entry of report.entries) {
1230
+ const entries = grouped.get(entry.workload.namespace) ?? [];
1231
+ entries.push(entry);
1232
+ grouped.set(entry.workload.namespace, entries);
1233
+ }
1234
+ for (const [namespace, entries] of grouped) {
1235
+ const rows = entries.map((entry) => {
1236
+ const state = getK8sUsageState(entry);
1237
+ return [
1238
+ `${abbreviatedK8sKind(entry.workload.kind)}/${entry.workload.name}`,
1239
+ entry.container.name,
1240
+ String(entry.sampledPodCount),
1241
+ formatK8sCpu(entry.container.currentUsage.cpuNanoCores),
1242
+ formatK8sCpu(entry.summary.cpuMaxNanoCores),
1243
+ formatK8sMemory(entry.container.currentUsage.memoryBytes),
1244
+ formatK8sMemory(entry.summary.memoryMaxBytes),
1245
+ state === "healthy" ? "-" : state
1246
+ ];
1247
+ });
1248
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
1249
+ const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
1250
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
1251
+ lines.push("", `Namespace: ${namespace}`, formatRow(headers), separator, ...rows.map(formatRow));
1252
+ }
1222
1253
  return `${lines.join("\n")}
1223
1254
  `;
1224
1255
  }
@@ -1313,6 +1344,17 @@ function summarizeEnvData(data) {
1313
1344
  ])
1314
1345
  );
1315
1346
  }
1347
+ function parseGetEnvsArgs(args) {
1348
+ let showValues = false;
1349
+ for (const arg of args) {
1350
+ if (arg === "--show-values") {
1351
+ showValues = true;
1352
+ continue;
1353
+ }
1354
+ throw new Error(arg.startsWith("-") ? `Unknown get envs flag: ${arg}` : `Unexpected get envs value: ${arg}`);
1355
+ }
1356
+ return { showValues };
1357
+ }
1316
1358
  function renderEnvSummary(data) {
1317
1359
  const lines = [];
1318
1360
  for (const [name, env] of Object.entries(data)) {
@@ -1321,6 +1363,24 @@ function renderEnvSummary(data) {
1321
1363
  return lines.length > 0 ? `${lines.join("\n")}
1322
1364
  ` : "No environment variables declared.\n";
1323
1365
  }
1366
+ function renderEnvValues(data) {
1367
+ const lines = Object.entries(data).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => `${name} ${env.value === null ? "<missing>" : JSON.stringify(env.value)} ${env.description}`);
1368
+ return lines.length > 0 ? `${lines.join("\n")}
1369
+ ` : "No environment variables declared.\n";
1370
+ }
1371
+ function getProjectEnvValue(data, name) {
1372
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1373
+ throw new Error(`Invalid environment variable name: ${name}`);
1374
+ }
1375
+ if (!Object.prototype.hasOwnProperty.call(data, name)) {
1376
+ throw new Error(`Environment variable ${name} is not declared for this project`);
1377
+ }
1378
+ const value = data[name]?.value;
1379
+ if (value === null || value === void 0) {
1380
+ throw new Error(`Environment variable ${name} does not have a value`);
1381
+ }
1382
+ return value;
1383
+ }
1324
1384
  function renderEnvSetResult(result) {
1325
1385
  const lines = [`Updated ${result.updated.length} environment variable${result.updated.length === 1 ? "" : "s"} for ${result.project}.`];
1326
1386
  if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
@@ -1676,11 +1736,25 @@ Sessions: ${result.sessions.length}
1676
1736
  return;
1677
1737
  }
1678
1738
  if (first === "get" && second === "envs") {
1679
- if (args.length !== 3) throw new Error(`Unexpected get envs argument: ${args[3] ?? ""}`.trim());
1680
1739
  const projectRef = requireValue(args[2], "Missing project reference");
1740
+ const options = parseGetEnvsArgs(args.slice(3));
1681
1741
  const project = await client.projects.describe(projectRef);
1682
- const envs = summarizeEnvData(await client.projects.env.get(project.id));
1683
- write({ project: project.path, envs }, renderEnvSummary(envs));
1742
+ const envs = await client.projects.env.get(project.id);
1743
+ if (options.showValues) {
1744
+ write({ project: project.path, envs }, renderEnvValues(envs));
1745
+ } else {
1746
+ const summary = summarizeEnvData(envs);
1747
+ write({ project: project.path, envs: summary }, renderEnvSummary(summary));
1748
+ }
1749
+ return;
1750
+ }
1751
+ if (first === "get" && second === "env") {
1752
+ if (args.length > 4) throw new Error(`Unexpected get env argument: ${args[4]}`);
1753
+ const projectRef = requireValue(args[2], "Missing project reference");
1754
+ const name = requireValue(args[3], "Missing environment variable name");
1755
+ const project = await client.projects.describe(projectRef);
1756
+ const value = getProjectEnvValue(await client.projects.env.get(project.id), name);
1757
+ write({ project: project.path, name, value }, value);
1684
1758
  return;
1685
1759
  }
1686
1760
  if (first === "set" && second === "envs") {
@@ -2033,9 +2107,29 @@ function resolveCommandExecution(options, rest) {
2033
2107
  if (options.branch) {
2034
2108
  throw new Error("--branch/-b is not supported for project environment variables");
2035
2109
  }
2110
+ parseGetEnvsArgs(commandArgs.slice(1));
2111
+ return {
2112
+ kind: "plugin",
2113
+ pluginArgs: ["get", "envs", options.project, ...commandArgs.slice(1)]
2114
+ };
2115
+ }
2116
+ if (target === "env") {
2117
+ if (!options.project) {
2118
+ throw new Error("--project/-p is required for `get env`");
2119
+ }
2120
+ if (options.branch) {
2121
+ throw new Error("--branch/-b is not supported for project environment variables");
2122
+ }
2123
+ const name = requireValue(commandArgs[1], "Environment variable name is required for `get env`");
2124
+ if (commandArgs.length > 2) {
2125
+ throw new Error(`Unexpected get env argument: ${commandArgs[2]}`);
2126
+ }
2127
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2128
+ throw new Error(`Invalid environment variable name: ${name}`);
2129
+ }
2036
2130
  return {
2037
2131
  kind: "plugin",
2038
- pluginArgs: ["get", "envs", options.project]
2132
+ pluginArgs: ["get", "env", options.project, name]
2039
2133
  };
2040
2134
  }
2041
2135
  throw new Error("Unknown get command");
@@ -2428,6 +2522,8 @@ export {
2428
2522
  formatK8sMemory,
2429
2523
  formatProcessAge,
2430
2524
  getCliHelpText,
2525
+ getK8sUsageState,
2526
+ getProjectEnvValue,
2431
2527
  getR5dctlVersion,
2432
2528
  main,
2433
2529
  parseAnswerFlags,
@@ -2435,6 +2531,7 @@ export {
2435
2531
  parseConversationWorkDetailArgs,
2436
2532
  parseEnvFlags,
2437
2533
  parseEnvRequestResponseArgs,
2534
+ parseGetEnvsArgs,
2438
2535
  parseGlobalArgs,
2439
2536
  parseK8sUsageArgs,
2440
2537
  parsePromptArgs,
@@ -2447,6 +2544,7 @@ export {
2447
2544
  renderConversationOverviewResponse,
2448
2545
  renderConversationResponse,
2449
2546
  renderConversationWorkResponse,
2547
+ renderEnvValues,
2450
2548
  renderK8sUsageReport,
2451
2549
  renderProcessHistory,
2452
2550
  renderProcessInspection,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "type": "module"
5
5
  }
@@ -69,7 +69,7 @@ export type K8sUsageOptions = {
69
69
  namespace?: string;
70
70
  workload?: string;
71
71
  container?: string;
72
- window: "24h";
72
+ window: "24h" | "7d";
73
73
  };
74
74
  export declare function parseK8sUsageArgs(args: string[]): K8sUsageOptions;
75
75
  export declare function renderWorkspaceStatus(status: R5dctlWorkspaceStatus): string;
@@ -82,21 +82,23 @@ export type K8sUsageSummary = {
82
82
  sampleCount: number;
83
83
  };
84
84
  export type K8sUsageReport = {
85
- window: "24h";
85
+ window: "24h" | "7d";
86
86
  collectedAt: string;
87
87
  metricsFreshAt: string | null;
88
88
  metricsStale: boolean;
89
89
  filters: Omit<K8sUsageOptions, "window">;
90
90
  entries: Array<{
91
- workload: Pick<R5dctlK8sWorkload, "uid" | "kind" | "namespace" | "name">;
91
+ workload: Pick<R5dctlK8sWorkload, "uid" | "kind" | "namespace" | "name" | "replicas" | "readyReplicas">;
92
92
  container: {
93
93
  name: string;
94
94
  currentUsage: R5dctlK8sUsage;
95
95
  pods: Array<R5dctlK8sUsage & {
96
96
  podUid: string;
97
97
  podName: string;
98
+ phase: string | null;
98
99
  }>;
99
100
  };
101
+ sampledPodCount: number;
100
102
  summary: K8sUsageSummary;
101
103
  history: R5dctlK8sResourceHistory;
102
104
  }>;
@@ -104,6 +106,8 @@ export type K8sUsageReport = {
104
106
  export declare function collectK8sUsage(client: Pick<R5dctlClient, "k8s">, options: K8sUsageOptions): Promise<K8sUsageReport>;
105
107
  export declare function formatK8sCpu(value: number | null): string;
106
108
  export declare function formatK8sMemory(value: number | null): string;
109
+ export type K8sUsageState = "healthy" | "partial" | "stale" | "inactive" | "warming" | "no data" | "no current";
110
+ export declare function getK8sUsageState(entry: K8sUsageReport["entries"][number]): K8sUsageState;
107
111
  export declare function renderK8sUsageReport(report: K8sUsageReport): string;
108
112
  export declare function formatProcessAge(startedAt: string, nowMs?: number): string;
109
113
  export declare function renderProcessList(processes: R5dctlProcessRun[], nowMs?: number): string;
@@ -114,6 +118,11 @@ export type EnvSummaryData = Record<string, {
114
118
  hasValue: boolean;
115
119
  }>;
116
120
  export declare function summarizeEnvData(data: R5dctlEnvData): EnvSummaryData;
121
+ export declare function parseGetEnvsArgs(args: string[]): {
122
+ showValues: boolean;
123
+ };
124
+ export declare function renderEnvValues(data: R5dctlEnvData): string;
125
+ export declare function getProjectEnvValue(data: R5dctlEnvData, name: string): string;
117
126
  export declare function parseConversationRenderArgs(args: string[]): ConversationRenderOptions;
118
127
  export declare function renderConversationResponse(read: R5dctlConversationResponse): string;
119
128
  export declare function parseConversationWorkDetailArgs(args: string[]): R5dctlConversationWorkDetail;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
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.50",
29
+ "@ricsam/r5d-api": "^0.0.51",
30
30
  "dotenv": "^17",
31
31
  "ws": "^8.18.3"
32
32
  },