@ricsam/r5dctl 0.0.50 → 0.0.53

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
@@ -14,9 +14,13 @@ npm install -g @ricsam/r5dctl
14
14
  r5dctl auth login
15
15
  ```
16
16
 
17
- This starts a device-style flow and opens:
17
+ This starts a device-authorization flow, prints an eight-character code and QR,
18
+ and opens `https://r5d.dev/auth`. Approve the matching device details in the
19
+ browser before the credential is saved.
18
20
 
19
- - `https://r5d.dev/r5dctl/login/<requestId>`
21
+ Use `--no-open` when no host browser is available, `--no-qr` to print only the
22
+ short code, `--device-name <name>` to label the authorized device, and
23
+ `--worker-label <label>` to record the worker it is intended to run.
20
24
 
21
25
  Credentials are stored in:
22
26
 
@@ -50,6 +54,8 @@ r5dctl delete project <namespace/name|id>
50
54
  r5dctl -p <project> get branches
51
55
  r5dctl -p <project> describe branch <branch>
52
56
  r5dctl -p <project> get envs
57
+ r5dctl -p <project> get envs --show-values
58
+ r5dctl -p <project> get env API_KEY
53
59
  r5dctl -p <project> set envs --from-env-file ~/.env --only-missing
54
60
  r5dctl -p <project> set envs -e API_KEY=value --description "API credential"
55
61
  r5dctl -p <project> get sessions --branch <branch>
@@ -82,16 +88,24 @@ r5dctl ps stop <run-id>
82
88
 
83
89
  r5dctl k8s usage
84
90
  r5dctl k8s usage --namespace default --workload api --container server
85
- r5dctl k8s usage --workload default/api --window 24h --json
91
+ r5dctl k8s usage --window 7d
92
+ r5dctl k8s usage --workload default/api --window 7d --json
86
93
  ```
87
94
 
88
95
  `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
96
 
90
97
  `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
98
 
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`.
99
+ `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
100
 
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.
101
+ `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:
102
+
103
+ ```bash
104
+ export API_KEY="$(r5dctl -p <project> get env API_KEY)"
105
+ r5dctl -p <project> get env PRIVATE_KEY | openssl pkey -check -noout
106
+ ```
107
+
108
+ 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
109
 
96
110
  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
111
 
package/dist/cjs/cli.cjs CHANGED
@@ -28,18 +28,24 @@ 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
+ advanceTransientDevicePollBackoff: () => advanceTransientDevicePollBackoff,
31
32
  collectK8sUsage: () => collectK8sUsage,
32
33
  formatK8sCpu: () => formatK8sCpu,
33
34
  formatK8sMemory: () => formatK8sMemory,
34
35
  formatProcessAge: () => formatProcessAge,
35
36
  getCliHelpText: () => getCliHelpText,
37
+ getK8sUsageState: () => getK8sUsageState,
38
+ getProjectEnvValue: () => getProjectEnvValue,
36
39
  getR5dctlVersion: () => getR5dctlVersion,
40
+ getTransientDevicePollDelay: () => getTransientDevicePollDelay,
41
+ handleAuthLogin: () => handleAuthLogin,
37
42
  main: () => main,
38
43
  parseAnswerFlags: () => parseAnswerFlags,
39
44
  parseConversationRenderArgs: () => parseConversationRenderArgs,
40
45
  parseConversationWorkDetailArgs: () => parseConversationWorkDetailArgs,
41
46
  parseEnvFlags: () => parseEnvFlags,
42
47
  parseEnvRequestResponseArgs: () => parseEnvRequestResponseArgs,
48
+ parseGetEnvsArgs: () => parseGetEnvsArgs,
43
49
  parseGlobalArgs: () => parseGlobalArgs,
44
50
  parseK8sUsageArgs: () => parseK8sUsageArgs,
45
51
  parsePromptArgs: () => parsePromptArgs,
@@ -52,6 +58,8 @@ __export(cli_exports, {
52
58
  renderConversationOverviewResponse: () => renderConversationOverviewResponse,
53
59
  renderConversationResponse: () => renderConversationResponse,
54
60
  renderConversationWorkResponse: () => renderConversationWorkResponse,
61
+ renderDeviceAuthorizationPrompt: () => renderDeviceAuthorizationPrompt,
62
+ renderEnvValues: () => renderEnvValues,
55
63
  renderK8sUsageReport: () => renderK8sUsageReport,
56
64
  renderProcessHistory: () => renderProcessHistory,
57
65
  renderProcessInspection: () => renderProcessInspection,
@@ -60,13 +68,16 @@ __export(cli_exports, {
60
68
  renderWorkspaceSync: () => renderWorkspaceSync,
61
69
  resolveCommandExecution: () => resolveCommandExecution,
62
70
  runR5dctlCli: () => runR5dctlCli,
63
- summarizeEnvData: () => summarizeEnvData
71
+ summarizeEnvData: () => summarizeEnvData,
72
+ writeConfig: () => writeConfig
64
73
  });
65
74
  module.exports = __toCommonJS(cli_exports);
66
75
  var import_node_fs = __toESM(require("node:fs"), 1);
67
76
  var import_node_os = __toESM(require("node:os"), 1);
68
77
  var import_node_path = __toESM(require("node:path"), 1);
69
78
  var import_node_child_process = require("node:child_process");
79
+ var import_node_crypto = require("node:crypto");
80
+ var import_qrcode = __toESM(require("qrcode"), 1);
70
81
  var import_promises = require("node:timers/promises");
71
82
  var import_dotenv = require("dotenv");
72
83
  var import_r5d_api = require("@ricsam/r5d-api");
@@ -101,7 +112,7 @@ const CLI_GLOBAL_OPTION_HELP = [
101
112
  const CLI_ONLY_HELP_ENTRIES = [
102
113
  {
103
114
  section: "auth",
104
- usage: "auth login [--no-open]",
115
+ usage: "auth login [--no-open] [--no-qr] [--device-name <name>] [--worker-label <label>]",
105
116
  description: "Start the browser login flow."
106
117
  },
107
118
  {
@@ -113,7 +124,7 @@ const CLI_ONLY_HELP_ENTRIES = [
113
124
  const CLI_ONLY_COMMAND_HELP = [
114
125
  {
115
126
  path: ["auth", "login"],
116
- usage: "auth login [--no-open]"
127
+ usage: "auth login [--no-open] [--no-qr] [--device-name <name>] [--worker-label <label>]"
117
128
  },
118
129
  {
119
130
  path: ["shell"],
@@ -133,9 +144,9 @@ const PS_HELP_TEXT = [
133
144
  ].join("\n");
134
145
  const K8S_HELP_TEXT = [
135
146
  "Usage:",
136
- " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
147
+ " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
137
148
  "",
138
- "Show current and 24-hour CPU and memory usage for managed-vCluster workload containers.",
149
+ "Show current and rolling CPU and memory usage for managed-vCluster workload containers.",
139
150
  ""
140
151
  ].join("\n");
141
152
  const SHARED_HELP_ENTRIES = [
@@ -167,8 +178,13 @@ const SHARED_HELP_ENTRIES = [
167
178
  { section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
168
179
  {
169
180
  section: "envs",
170
- usage: "-p <project> get envs",
171
- description: "List declared envs and whether each has a value, without revealing values."
181
+ usage: "-p <project> get envs [--show-values]",
182
+ description: "List declared envs; use --show-values to reveal their values."
183
+ },
184
+ {
185
+ section: "envs",
186
+ usage: "-p <project> get env <name>",
187
+ description: "Write one env value exactly to stdout for command substitution or piping."
172
188
  },
173
189
  {
174
190
  section: "envs",
@@ -235,8 +251,8 @@ const SHARED_HELP_ENTRIES = [
235
251
  },
236
252
  {
237
253
  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."
254
+ usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
255
+ description: "Show current and rolling CPU and memory usage for managed-vCluster workload containers."
240
256
  },
241
257
  {
242
258
  section: "processes",
@@ -440,6 +456,21 @@ function parseOptionalFlagValue(args, flag, shortFlag) {
440
456
  function hasBooleanFlag(args, flag) {
441
457
  return args.includes(flag);
442
458
  }
459
+ function assertAuthLoginArgs(args) {
460
+ const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
461
+ const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
462
+ for (let index = 0; index < args.length; index += 1) {
463
+ const arg = args[index];
464
+ if (booleanFlags.has(arg)) continue;
465
+ if (valueFlags.has(arg)) {
466
+ if (!args[index + 1]) throw new Error(`Missing value for ${arg}`);
467
+ index += 1;
468
+ continue;
469
+ }
470
+ if ([...valueFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
471
+ throw new Error(`Unknown auth login flag: ${arg}`);
472
+ }
473
+ }
443
474
  function parseGlobalArgs(argv) {
444
475
  const options = {
445
476
  json: false,
@@ -571,12 +602,27 @@ function serializeConfig(config) {
571
602
  function writeConfig(configPath, config) {
572
603
  const dir = import_node_path.default.dirname(configPath);
573
604
  import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
605
+ import_node_fs.default.chmodSync(dir, 448);
574
606
  const serialized = serializeConfig(config);
575
- import_node_fs.default.writeFileSync(configPath, `${JSON.stringify(serialized, null, 2)}
576
- `, {
577
- mode: 384
578
- });
579
- import_node_fs.default.chmodSync(configPath, 384);
607
+ const tempPath = import_node_path.default.join(dir, `.${import_node_path.default.basename(configPath)}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
608
+ let descriptor;
609
+ try {
610
+ descriptor = import_node_fs.default.openSync(tempPath, "wx", 384);
611
+ import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify(serialized, null, 2)}
612
+ `, "utf8");
613
+ import_node_fs.default.fsyncSync(descriptor);
614
+ import_node_fs.default.closeSync(descriptor);
615
+ descriptor = void 0;
616
+ import_node_fs.default.renameSync(tempPath, configPath);
617
+ import_node_fs.default.chmodSync(configPath, 384);
618
+ } catch (error) {
619
+ if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
620
+ try {
621
+ import_node_fs.default.unlinkSync(tempPath);
622
+ } catch {
623
+ }
624
+ throw error;
625
+ }
580
626
  }
581
627
  function resolveClientOptions(options, config) {
582
628
  return {
@@ -972,7 +1018,7 @@ function parseK8sUsageArgs(args) {
972
1018
  const value = candidateValue;
973
1019
  if (inlineValue === void 0) index += 1;
974
1020
  if (option === "--window") {
975
- if (value !== "24h") throw new Error("--window currently supports only 24h");
1021
+ if (value !== "24h" && value !== "7d") throw new Error("--window must be 24h or 7d");
976
1022
  parsed.window = value;
977
1023
  } else if (option === "--namespace") {
978
1024
  parsed.namespace = value;
@@ -1010,41 +1056,89 @@ function createSavedApiKeyConfig(options, config, apiKey) {
1010
1056
  token: void 0
1011
1057
  };
1012
1058
  }
1059
+ function renderDeviceAuthorizationPrompt(input) {
1060
+ const codeBorder = "\u2500".repeat(input.userCode.length + 4);
1061
+ return `Authorize this device at:
1062
+ ${input.verificationUri}
1063
+
1064
+ \u250C${codeBorder}\u2510
1065
+ \u2502 ${input.userCode} \u2502
1066
+ \u2514${codeBorder}\u2518
1067
+ `;
1068
+ }
1069
+ function getTransientDevicePollDelay(intervalMs, transientBackoffMs) {
1070
+ return Math.min(3e4, Math.max(intervalMs, transientBackoffMs));
1071
+ }
1072
+ function advanceTransientDevicePollBackoff(intervalMs, transientBackoffMs) {
1073
+ return {
1074
+ delayMs: getTransientDevicePollDelay(intervalMs, transientBackoffMs),
1075
+ nextBackoffMs: Math.min(3e4, transientBackoffMs * 2)
1076
+ };
1077
+ }
1013
1078
  async function handleAuthLogin(client, options, commandArgs, config) {
1079
+ assertAuthLoginArgs(commandArgs);
1014
1080
  const noOpen = hasBooleanFlag(commandArgs, "--no-open");
1015
- const start = await client.auth.loginStart({
1016
- baseUrl: options.baseUrl ?? config.baseUrl
1017
- });
1081
+ const noQr = hasBooleanFlag(commandArgs, "--no-qr");
1082
+ const deviceName = parseOptionalFlagValue(commandArgs, "--device-name") ?? `${import_node_os.default.hostname()} r5dctl`;
1083
+ const workerLabel = parseOptionalFlagValue(commandArgs, "--worker-label");
1084
+ const start = await client.auth.deviceStart({ deviceName, workerLabel });
1018
1085
  if (options.json) {
1019
1086
  writeDataOutput(
1020
1087
  true,
1021
1088
  {
1022
1089
  status: "pending",
1023
1090
  requestId: start.requestId,
1024
- loginUrl: start.loginUrl,
1091
+ userCode: start.userCode,
1092
+ verificationUri: start.verificationUri,
1093
+ verificationUriComplete: start.verificationUriComplete,
1025
1094
  expiresAt: start.expiresAt,
1026
1095
  intervalMs: start.intervalMs
1027
1096
  },
1028
1097
  ""
1029
1098
  );
1030
1099
  } else {
1031
- process.stdout.write(`Open this URL to approve login:
1032
- ${start.loginUrl}
1100
+ process.stdout.write(renderDeviceAuthorizationPrompt(start));
1101
+ if (!noQr) {
1102
+ process.stdout.write(`
1103
+ ${await import_qrcode.default.toString(start.verificationUriComplete, { type: "terminal", small: true })}
1033
1104
  `);
1105
+ }
1034
1106
  }
1035
1107
  if (!noOpen) {
1036
1108
  try {
1037
- await openInBrowser(start.loginUrl);
1109
+ await openInBrowser(start.verificationUriComplete);
1038
1110
  } catch {
1039
1111
  }
1040
1112
  }
1041
1113
  const expiresAtMs = new Date(start.expiresAt).getTime();
1042
1114
  let intervalMs = start.intervalMs;
1115
+ let nextDelayMs = intervalMs;
1116
+ let transientBackoffMs = 1e3;
1043
1117
  while (Date.now() < expiresAtMs) {
1044
- await (0, import_promises.setTimeout)(intervalMs);
1045
- const polled = await client.auth.loginPoll(start.requestId, { pollCode: start.pollCode });
1118
+ await (0, import_promises.setTimeout)(Math.min(nextDelayMs, Math.max(0, expiresAtMs - Date.now())));
1119
+ let polled;
1120
+ try {
1121
+ polled = await client.auth.devicePoll(start.requestId, { deviceCode: start.deviceCode });
1122
+ } catch (error) {
1123
+ if (error instanceof import_r5d_api.R5dctlApiError && error.status === 429) {
1124
+ const body = error.body;
1125
+ if (typeof body?.intervalMs === "number") intervalMs = body.intervalMs;
1126
+ nextDelayMs = intervalMs;
1127
+ continue;
1128
+ }
1129
+ const transient = !(error instanceof import_r5d_api.R5dctlApiError) || error.status === 408 || error.status >= 500;
1130
+ if (transient) {
1131
+ const retry = advanceTransientDevicePollBackoff(intervalMs, transientBackoffMs);
1132
+ nextDelayMs = retry.delayMs;
1133
+ transientBackoffMs = retry.nextBackoffMs;
1134
+ continue;
1135
+ }
1136
+ throw error;
1137
+ }
1138
+ transientBackoffMs = 1e3;
1046
1139
  if (polled.status === "pending") {
1047
1140
  intervalMs = polled.intervalMs;
1141
+ nextDelayMs = intervalMs;
1048
1142
  continue;
1049
1143
  }
1050
1144
  if (polled.status === "approved") {
@@ -1069,6 +1163,9 @@ ${start.loginUrl}
1069
1163
  if (polled.status === "expired") {
1070
1164
  throw new Error("Login request expired. Run `r5dctl auth login` again.");
1071
1165
  }
1166
+ if (polled.status === "denied") {
1167
+ throw new Error("Device authorization denied.");
1168
+ }
1072
1169
  if (polled.status === "consumed") {
1073
1170
  throw new Error("Login request was already consumed.");
1074
1171
  }
@@ -1189,19 +1286,22 @@ async function collectK8sUsage(client, options) {
1189
1286
  throw new Error("No managed-vCluster workload containers matched the requested filters");
1190
1287
  }
1191
1288
  const entries = await mapWithConcurrency(candidates, 8, async ({ workload, container }) => {
1192
- const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name });
1289
+ const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name, window: options.window });
1193
1290
  return {
1194
1291
  workload: {
1195
1292
  uid: workload.uid,
1196
1293
  kind: workload.kind,
1197
1294
  namespace: workload.namespace,
1198
- name: workload.name
1295
+ name: workload.name,
1296
+ replicas: workload.replicas,
1297
+ readyReplicas: workload.readyReplicas
1199
1298
  },
1200
1299
  container: {
1201
1300
  name: container.name,
1202
1301
  currentUsage: container.currentUsage,
1203
1302
  pods: container.pods
1204
1303
  },
1304
+ sampledPodCount: container.pods.filter((pod) => pod.cpuNanoCores !== null || pod.memoryBytes !== null).length,
1205
1305
  summary: summarizeK8sHistory(history),
1206
1306
  history
1207
1307
  };
@@ -1227,60 +1327,83 @@ function formatDecimal(value) {
1227
1327
  function formatK8sCpu(value) {
1228
1328
  if (value === null) return "-";
1229
1329
  const milliCores = value / 1e6;
1230
- return milliCores >= 1e3 ? `${formatDecimal(milliCores / 1e3)} cores` : `${formatDecimal(milliCores)}m`;
1330
+ return `${formatDecimal(milliCores)}m`;
1231
1331
  }
1232
1332
  function formatK8sMemory(value) {
1233
1333
  if (value === null) return "-";
1234
- const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1334
+ const units = ["B", "Ki", "Mi", "Gi", "Ti"];
1235
1335
  let amount = value;
1236
1336
  let unitIndex = 0;
1237
1337
  while (amount >= 1024 && unitIndex < units.length - 1) {
1238
1338
  amount /= 1024;
1239
1339
  unitIndex += 1;
1240
1340
  }
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`;
1341
+ return `${formatDecimal(amount)}${units[unitIndex]}`;
1342
+ }
1343
+ function getK8sUsageState(entry) {
1344
+ const hasHistory = entry.history.envelope.length > 0;
1345
+ const terminalPods = entry.container.pods.filter((pod) => pod.phase === "Succeeded" || pod.phase === "Failed").length;
1346
+ 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);
1347
+ if (inactive) return "inactive";
1348
+ if (entry.sampledPodCount === 0) return hasHistory ? entry.history.stale ? "stale" : "no current" : "no data";
1349
+ if (!hasHistory) return "warming";
1350
+ if (entry.history.stale) return "stale";
1351
+ if (entry.history.incomplete) return "partial";
1352
+ return "healthy";
1353
+ }
1354
+ function abbreviatedK8sKind(kind) {
1355
+ switch (kind) {
1356
+ case "Deployment":
1357
+ return "deploy";
1358
+ case "StatefulSet":
1359
+ return "sts";
1360
+ case "DaemonSet":
1361
+ return "ds";
1362
+ case "CronJob":
1363
+ return "cron";
1364
+ case "ReplicaSet":
1365
+ return "rs";
1366
+ case "Job":
1367
+ return "job";
1368
+ case "Pod":
1369
+ return "pod";
1370
+ }
1249
1371
  }
1250
1372
  function renderK8sUsageReport(report) {
1251
1373
  const freshness = report.metricsFreshAt ?? "unavailable";
1252
- const lines = [`Metrics: ${report.metricsStale ? "stale" : "current"} (fresh at ${freshness})`, "Window: 24h"];
1374
+ const lines = [
1375
+ `Window: ${report.window} Collector: ${report.metricsStale ? "stale" : "current"} Fresh: ${freshness}`,
1376
+ "PODS counts pods with current metrics."
1377
+ ];
1253
1378
  if (report.entries.length === 0) return `${lines.join("\n")}
1254
1379
  No managed-vCluster workload containers found.
1255
1380
  `;
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));
1381
+ const headers = ["WORKLOAD", "CONTAINER", "PODS", "CPU NOW", "CPU MAX", "MEM NOW", "MEM MAX", "STATE"];
1382
+ const grouped = /* @__PURE__ */ new Map();
1383
+ for (const entry of report.entries) {
1384
+ const entries = grouped.get(entry.workload.namespace) ?? [];
1385
+ entries.push(entry);
1386
+ grouped.set(entry.workload.namespace, entries);
1387
+ }
1388
+ for (const [namespace, entries] of grouped) {
1389
+ const rows = entries.map((entry) => {
1390
+ const state = getK8sUsageState(entry);
1391
+ return [
1392
+ `${abbreviatedK8sKind(entry.workload.kind)}/${entry.workload.name}`,
1393
+ entry.container.name,
1394
+ String(entry.sampledPodCount),
1395
+ formatK8sCpu(entry.container.currentUsage.cpuNanoCores),
1396
+ formatK8sCpu(entry.summary.cpuMaxNanoCores),
1397
+ formatK8sMemory(entry.container.currentUsage.memoryBytes),
1398
+ formatK8sMemory(entry.summary.memoryMaxBytes),
1399
+ state === "healthy" ? "-" : state
1400
+ ];
1401
+ });
1402
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
1403
+ const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
1404
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
1405
+ lines.push("", `Namespace: ${namespace}`, formatRow(headers), separator, ...rows.map(formatRow));
1406
+ }
1284
1407
  return `${lines.join("\n")}
1285
1408
  `;
1286
1409
  }
@@ -1375,6 +1498,17 @@ function summarizeEnvData(data) {
1375
1498
  ])
1376
1499
  );
1377
1500
  }
1501
+ function parseGetEnvsArgs(args) {
1502
+ let showValues = false;
1503
+ for (const arg of args) {
1504
+ if (arg === "--show-values") {
1505
+ showValues = true;
1506
+ continue;
1507
+ }
1508
+ throw new Error(arg.startsWith("-") ? `Unknown get envs flag: ${arg}` : `Unexpected get envs value: ${arg}`);
1509
+ }
1510
+ return { showValues };
1511
+ }
1378
1512
  function renderEnvSummary(data) {
1379
1513
  const lines = [];
1380
1514
  for (const [name, env] of Object.entries(data)) {
@@ -1383,6 +1517,24 @@ function renderEnvSummary(data) {
1383
1517
  return lines.length > 0 ? `${lines.join("\n")}
1384
1518
  ` : "No environment variables declared.\n";
1385
1519
  }
1520
+ function renderEnvValues(data) {
1521
+ 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}`);
1522
+ return lines.length > 0 ? `${lines.join("\n")}
1523
+ ` : "No environment variables declared.\n";
1524
+ }
1525
+ function getProjectEnvValue(data, name) {
1526
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1527
+ throw new Error(`Invalid environment variable name: ${name}`);
1528
+ }
1529
+ if (!Object.prototype.hasOwnProperty.call(data, name)) {
1530
+ throw new Error(`Environment variable ${name} is not declared for this project`);
1531
+ }
1532
+ const value = data[name]?.value;
1533
+ if (value === null || value === void 0) {
1534
+ throw new Error(`Environment variable ${name} does not have a value`);
1535
+ }
1536
+ return value;
1537
+ }
1386
1538
  function renderEnvSetResult(result) {
1387
1539
  const lines = [`Updated ${result.updated.length} environment variable${result.updated.length === 1 ? "" : "s"} for ${result.project}.`];
1388
1540
  if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
@@ -1738,11 +1890,25 @@ Sessions: ${result.sessions.length}
1738
1890
  return;
1739
1891
  }
1740
1892
  if (first === "get" && second === "envs") {
1741
- if (args.length !== 3) throw new Error(`Unexpected get envs argument: ${args[3] ?? ""}`.trim());
1742
1893
  const projectRef = requireValue(args[2], "Missing project reference");
1894
+ const options = parseGetEnvsArgs(args.slice(3));
1743
1895
  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));
1896
+ const envs = await client.projects.env.get(project.id);
1897
+ if (options.showValues) {
1898
+ write({ project: project.path, envs }, renderEnvValues(envs));
1899
+ } else {
1900
+ const summary = summarizeEnvData(envs);
1901
+ write({ project: project.path, envs: summary }, renderEnvSummary(summary));
1902
+ }
1903
+ return;
1904
+ }
1905
+ if (first === "get" && second === "env") {
1906
+ if (args.length > 4) throw new Error(`Unexpected get env argument: ${args[4]}`);
1907
+ const projectRef = requireValue(args[2], "Missing project reference");
1908
+ const name = requireValue(args[3], "Missing environment variable name");
1909
+ const project = await client.projects.describe(projectRef);
1910
+ const value = getProjectEnvValue(await client.projects.env.get(project.id), name);
1911
+ write({ project: project.path, name, value }, value);
1746
1912
  return;
1747
1913
  }
1748
1914
  if (first === "set" && second === "envs") {
@@ -2095,9 +2261,29 @@ function resolveCommandExecution(options, rest) {
2095
2261
  if (options.branch) {
2096
2262
  throw new Error("--branch/-b is not supported for project environment variables");
2097
2263
  }
2264
+ parseGetEnvsArgs(commandArgs.slice(1));
2265
+ return {
2266
+ kind: "plugin",
2267
+ pluginArgs: ["get", "envs", options.project, ...commandArgs.slice(1)]
2268
+ };
2269
+ }
2270
+ if (target === "env") {
2271
+ if (!options.project) {
2272
+ throw new Error("--project/-p is required for `get env`");
2273
+ }
2274
+ if (options.branch) {
2275
+ throw new Error("--branch/-b is not supported for project environment variables");
2276
+ }
2277
+ const name = requireValue(commandArgs[1], "Environment variable name is required for `get env`");
2278
+ if (commandArgs.length > 2) {
2279
+ throw new Error(`Unexpected get env argument: ${commandArgs[2]}`);
2280
+ }
2281
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2282
+ throw new Error(`Invalid environment variable name: ${name}`);
2283
+ }
2098
2284
  return {
2099
2285
  kind: "plugin",
2100
- pluginArgs: ["get", "envs", options.project]
2286
+ pluginArgs: ["get", "env", options.project, name]
2101
2287
  };
2102
2288
  }
2103
2289
  throw new Error("Unknown get command");
@@ -2486,18 +2672,24 @@ async function main(argv = process.argv.slice(2)) {
2486
2672
  }
2487
2673
  // Annotate the CommonJS export names for ESM import in node:
2488
2674
  0 && (module.exports = {
2675
+ advanceTransientDevicePollBackoff,
2489
2676
  collectK8sUsage,
2490
2677
  formatK8sCpu,
2491
2678
  formatK8sMemory,
2492
2679
  formatProcessAge,
2493
2680
  getCliHelpText,
2681
+ getK8sUsageState,
2682
+ getProjectEnvValue,
2494
2683
  getR5dctlVersion,
2684
+ getTransientDevicePollDelay,
2685
+ handleAuthLogin,
2495
2686
  main,
2496
2687
  parseAnswerFlags,
2497
2688
  parseConversationRenderArgs,
2498
2689
  parseConversationWorkDetailArgs,
2499
2690
  parseEnvFlags,
2500
2691
  parseEnvRequestResponseArgs,
2692
+ parseGetEnvsArgs,
2501
2693
  parseGlobalArgs,
2502
2694
  parseK8sUsageArgs,
2503
2695
  parsePromptArgs,
@@ -2510,6 +2702,8 @@ async function main(argv = process.argv.slice(2)) {
2510
2702
  renderConversationOverviewResponse,
2511
2703
  renderConversationResponse,
2512
2704
  renderConversationWorkResponse,
2705
+ renderDeviceAuthorizationPrompt,
2706
+ renderEnvValues,
2513
2707
  renderK8sUsageReport,
2514
2708
  renderProcessHistory,
2515
2709
  renderProcessInspection,
@@ -2518,5 +2712,6 @@ async function main(argv = process.argv.slice(2)) {
2518
2712
  renderWorkspaceSync,
2519
2713
  resolveCommandExecution,
2520
2714
  runR5dctlCli,
2521
- summarizeEnvData
2715
+ summarizeEnvData,
2716
+ writeConfig
2522
2717
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.50",
3
+ "version": "0.0.53",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -2,6 +2,8 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
+ import { randomUUID } from "node:crypto";
6
+ import QRCode from "qrcode";
5
7
  import { setTimeout as sleep } from "node:timers/promises";
6
8
  import { parse as parseDotenv } from "dotenv";
7
9
  import {
@@ -39,7 +41,7 @@ const CLI_GLOBAL_OPTION_HELP = [
39
41
  const CLI_ONLY_HELP_ENTRIES = [
40
42
  {
41
43
  section: "auth",
42
- usage: "auth login [--no-open]",
44
+ usage: "auth login [--no-open] [--no-qr] [--device-name <name>] [--worker-label <label>]",
43
45
  description: "Start the browser login flow."
44
46
  },
45
47
  {
@@ -51,7 +53,7 @@ const CLI_ONLY_HELP_ENTRIES = [
51
53
  const CLI_ONLY_COMMAND_HELP = [
52
54
  {
53
55
  path: ["auth", "login"],
54
- usage: "auth login [--no-open]"
56
+ usage: "auth login [--no-open] [--no-qr] [--device-name <name>] [--worker-label <label>]"
55
57
  },
56
58
  {
57
59
  path: ["shell"],
@@ -71,9 +73,9 @@ const PS_HELP_TEXT = [
71
73
  ].join("\n");
72
74
  const K8S_HELP_TEXT = [
73
75
  "Usage:",
74
- " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window=24h]",
76
+ " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
75
77
  "",
76
- "Show current and 24-hour CPU and memory usage for managed-vCluster workload containers.",
78
+ "Show current and rolling CPU and memory usage for managed-vCluster workload containers.",
77
79
  ""
78
80
  ].join("\n");
79
81
  const SHARED_HELP_ENTRIES = [
@@ -105,8 +107,13 @@ const SHARED_HELP_ENTRIES = [
105
107
  { section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
106
108
  {
107
109
  section: "envs",
108
- usage: "-p <project> get envs",
109
- description: "List declared envs and whether each has a value, without revealing values."
110
+ usage: "-p <project> get envs [--show-values]",
111
+ description: "List declared envs; use --show-values to reveal their values."
112
+ },
113
+ {
114
+ section: "envs",
115
+ usage: "-p <project> get env <name>",
116
+ description: "Write one env value exactly to stdout for command substitution or piping."
110
117
  },
111
118
  {
112
119
  section: "envs",
@@ -173,8 +180,8 @@ const SHARED_HELP_ENTRIES = [
173
180
  },
174
181
  {
175
182
  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."
183
+ usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
184
+ description: "Show current and rolling CPU and memory usage for managed-vCluster workload containers."
178
185
  },
179
186
  {
180
187
  section: "processes",
@@ -378,6 +385,21 @@ function parseOptionalFlagValue(args, flag, shortFlag) {
378
385
  function hasBooleanFlag(args, flag) {
379
386
  return args.includes(flag);
380
387
  }
388
+ function assertAuthLoginArgs(args) {
389
+ const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
390
+ const booleanFlags = /* @__PURE__ */ new Set(["--no-open", "--no-qr"]);
391
+ for (let index = 0; index < args.length; index += 1) {
392
+ const arg = args[index];
393
+ if (booleanFlags.has(arg)) continue;
394
+ if (valueFlags.has(arg)) {
395
+ if (!args[index + 1]) throw new Error(`Missing value for ${arg}`);
396
+ index += 1;
397
+ continue;
398
+ }
399
+ if ([...valueFlags].some((flag) => arg.startsWith(`${flag}=`))) continue;
400
+ throw new Error(`Unknown auth login flag: ${arg}`);
401
+ }
402
+ }
381
403
  function parseGlobalArgs(argv) {
382
404
  const options = {
383
405
  json: false,
@@ -509,12 +531,27 @@ function serializeConfig(config) {
509
531
  function writeConfig(configPath, config) {
510
532
  const dir = path.dirname(configPath);
511
533
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
534
+ fs.chmodSync(dir, 448);
512
535
  const serialized = serializeConfig(config);
513
- fs.writeFileSync(configPath, `${JSON.stringify(serialized, null, 2)}
514
- `, {
515
- mode: 384
516
- });
517
- fs.chmodSync(configPath, 384);
536
+ const tempPath = path.join(dir, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
537
+ let descriptor;
538
+ try {
539
+ descriptor = fs.openSync(tempPath, "wx", 384);
540
+ fs.writeFileSync(descriptor, `${JSON.stringify(serialized, null, 2)}
541
+ `, "utf8");
542
+ fs.fsyncSync(descriptor);
543
+ fs.closeSync(descriptor);
544
+ descriptor = void 0;
545
+ fs.renameSync(tempPath, configPath);
546
+ fs.chmodSync(configPath, 384);
547
+ } catch (error) {
548
+ if (descriptor !== void 0) fs.closeSync(descriptor);
549
+ try {
550
+ fs.unlinkSync(tempPath);
551
+ } catch {
552
+ }
553
+ throw error;
554
+ }
518
555
  }
519
556
  function resolveClientOptions(options, config) {
520
557
  return {
@@ -910,7 +947,7 @@ function parseK8sUsageArgs(args) {
910
947
  const value = candidateValue;
911
948
  if (inlineValue === void 0) index += 1;
912
949
  if (option === "--window") {
913
- if (value !== "24h") throw new Error("--window currently supports only 24h");
950
+ if (value !== "24h" && value !== "7d") throw new Error("--window must be 24h or 7d");
914
951
  parsed.window = value;
915
952
  } else if (option === "--namespace") {
916
953
  parsed.namespace = value;
@@ -948,41 +985,89 @@ function createSavedApiKeyConfig(options, config, apiKey) {
948
985
  token: void 0
949
986
  };
950
987
  }
988
+ function renderDeviceAuthorizationPrompt(input) {
989
+ const codeBorder = "\u2500".repeat(input.userCode.length + 4);
990
+ return `Authorize this device at:
991
+ ${input.verificationUri}
992
+
993
+ \u250C${codeBorder}\u2510
994
+ \u2502 ${input.userCode} \u2502
995
+ \u2514${codeBorder}\u2518
996
+ `;
997
+ }
998
+ function getTransientDevicePollDelay(intervalMs, transientBackoffMs) {
999
+ return Math.min(3e4, Math.max(intervalMs, transientBackoffMs));
1000
+ }
1001
+ function advanceTransientDevicePollBackoff(intervalMs, transientBackoffMs) {
1002
+ return {
1003
+ delayMs: getTransientDevicePollDelay(intervalMs, transientBackoffMs),
1004
+ nextBackoffMs: Math.min(3e4, transientBackoffMs * 2)
1005
+ };
1006
+ }
951
1007
  async function handleAuthLogin(client, options, commandArgs, config) {
1008
+ assertAuthLoginArgs(commandArgs);
952
1009
  const noOpen = hasBooleanFlag(commandArgs, "--no-open");
953
- const start = await client.auth.loginStart({
954
- baseUrl: options.baseUrl ?? config.baseUrl
955
- });
1010
+ const noQr = hasBooleanFlag(commandArgs, "--no-qr");
1011
+ const deviceName = parseOptionalFlagValue(commandArgs, "--device-name") ?? `${os.hostname()} r5dctl`;
1012
+ const workerLabel = parseOptionalFlagValue(commandArgs, "--worker-label");
1013
+ const start = await client.auth.deviceStart({ deviceName, workerLabel });
956
1014
  if (options.json) {
957
1015
  writeDataOutput(
958
1016
  true,
959
1017
  {
960
1018
  status: "pending",
961
1019
  requestId: start.requestId,
962
- loginUrl: start.loginUrl,
1020
+ userCode: start.userCode,
1021
+ verificationUri: start.verificationUri,
1022
+ verificationUriComplete: start.verificationUriComplete,
963
1023
  expiresAt: start.expiresAt,
964
1024
  intervalMs: start.intervalMs
965
1025
  },
966
1026
  ""
967
1027
  );
968
1028
  } else {
969
- process.stdout.write(`Open this URL to approve login:
970
- ${start.loginUrl}
1029
+ process.stdout.write(renderDeviceAuthorizationPrompt(start));
1030
+ if (!noQr) {
1031
+ process.stdout.write(`
1032
+ ${await QRCode.toString(start.verificationUriComplete, { type: "terminal", small: true })}
971
1033
  `);
1034
+ }
972
1035
  }
973
1036
  if (!noOpen) {
974
1037
  try {
975
- await openInBrowser(start.loginUrl);
1038
+ await openInBrowser(start.verificationUriComplete);
976
1039
  } catch {
977
1040
  }
978
1041
  }
979
1042
  const expiresAtMs = new Date(start.expiresAt).getTime();
980
1043
  let intervalMs = start.intervalMs;
1044
+ let nextDelayMs = intervalMs;
1045
+ let transientBackoffMs = 1e3;
981
1046
  while (Date.now() < expiresAtMs) {
982
- await sleep(intervalMs);
983
- const polled = await client.auth.loginPoll(start.requestId, { pollCode: start.pollCode });
1047
+ await sleep(Math.min(nextDelayMs, Math.max(0, expiresAtMs - Date.now())));
1048
+ let polled;
1049
+ try {
1050
+ polled = await client.auth.devicePoll(start.requestId, { deviceCode: start.deviceCode });
1051
+ } catch (error) {
1052
+ if (error instanceof R5dctlApiError && error.status === 429) {
1053
+ const body = error.body;
1054
+ if (typeof body?.intervalMs === "number") intervalMs = body.intervalMs;
1055
+ nextDelayMs = intervalMs;
1056
+ continue;
1057
+ }
1058
+ const transient = !(error instanceof R5dctlApiError) || error.status === 408 || error.status >= 500;
1059
+ if (transient) {
1060
+ const retry = advanceTransientDevicePollBackoff(intervalMs, transientBackoffMs);
1061
+ nextDelayMs = retry.delayMs;
1062
+ transientBackoffMs = retry.nextBackoffMs;
1063
+ continue;
1064
+ }
1065
+ throw error;
1066
+ }
1067
+ transientBackoffMs = 1e3;
984
1068
  if (polled.status === "pending") {
985
1069
  intervalMs = polled.intervalMs;
1070
+ nextDelayMs = intervalMs;
986
1071
  continue;
987
1072
  }
988
1073
  if (polled.status === "approved") {
@@ -1007,6 +1092,9 @@ ${start.loginUrl}
1007
1092
  if (polled.status === "expired") {
1008
1093
  throw new Error("Login request expired. Run `r5dctl auth login` again.");
1009
1094
  }
1095
+ if (polled.status === "denied") {
1096
+ throw new Error("Device authorization denied.");
1097
+ }
1010
1098
  if (polled.status === "consumed") {
1011
1099
  throw new Error("Login request was already consumed.");
1012
1100
  }
@@ -1127,19 +1215,22 @@ async function collectK8sUsage(client, options) {
1127
1215
  throw new Error("No managed-vCluster workload containers matched the requested filters");
1128
1216
  }
1129
1217
  const entries = await mapWithConcurrency(candidates, 8, async ({ workload, container }) => {
1130
- const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name });
1218
+ const history = await client.k8s.resourceHistory({ workloadUid: workload.uid, containerName: container.name, window: options.window });
1131
1219
  return {
1132
1220
  workload: {
1133
1221
  uid: workload.uid,
1134
1222
  kind: workload.kind,
1135
1223
  namespace: workload.namespace,
1136
- name: workload.name
1224
+ name: workload.name,
1225
+ replicas: workload.replicas,
1226
+ readyReplicas: workload.readyReplicas
1137
1227
  },
1138
1228
  container: {
1139
1229
  name: container.name,
1140
1230
  currentUsage: container.currentUsage,
1141
1231
  pods: container.pods
1142
1232
  },
1233
+ sampledPodCount: container.pods.filter((pod) => pod.cpuNanoCores !== null || pod.memoryBytes !== null).length,
1143
1234
  summary: summarizeK8sHistory(history),
1144
1235
  history
1145
1236
  };
@@ -1165,60 +1256,83 @@ function formatDecimal(value) {
1165
1256
  function formatK8sCpu(value) {
1166
1257
  if (value === null) return "-";
1167
1258
  const milliCores = value / 1e6;
1168
- return milliCores >= 1e3 ? `${formatDecimal(milliCores / 1e3)} cores` : `${formatDecimal(milliCores)}m`;
1259
+ return `${formatDecimal(milliCores)}m`;
1169
1260
  }
1170
1261
  function formatK8sMemory(value) {
1171
1262
  if (value === null) return "-";
1172
- const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1263
+ const units = ["B", "Ki", "Mi", "Gi", "Ti"];
1173
1264
  let amount = value;
1174
1265
  let unitIndex = 0;
1175
1266
  while (amount >= 1024 && unitIndex < units.length - 1) {
1176
1267
  amount /= 1024;
1177
1268
  unitIndex += 1;
1178
1269
  }
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`;
1270
+ return `${formatDecimal(amount)}${units[unitIndex]}`;
1271
+ }
1272
+ function getK8sUsageState(entry) {
1273
+ const hasHistory = entry.history.envelope.length > 0;
1274
+ const terminalPods = entry.container.pods.filter((pod) => pod.phase === "Succeeded" || pod.phase === "Failed").length;
1275
+ 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);
1276
+ if (inactive) return "inactive";
1277
+ if (entry.sampledPodCount === 0) return hasHistory ? entry.history.stale ? "stale" : "no current" : "no data";
1278
+ if (!hasHistory) return "warming";
1279
+ if (entry.history.stale) return "stale";
1280
+ if (entry.history.incomplete) return "partial";
1281
+ return "healthy";
1282
+ }
1283
+ function abbreviatedK8sKind(kind) {
1284
+ switch (kind) {
1285
+ case "Deployment":
1286
+ return "deploy";
1287
+ case "StatefulSet":
1288
+ return "sts";
1289
+ case "DaemonSet":
1290
+ return "ds";
1291
+ case "CronJob":
1292
+ return "cron";
1293
+ case "ReplicaSet":
1294
+ return "rs";
1295
+ case "Job":
1296
+ return "job";
1297
+ case "Pod":
1298
+ return "pod";
1299
+ }
1187
1300
  }
1188
1301
  function renderK8sUsageReport(report) {
1189
1302
  const freshness = report.metricsFreshAt ?? "unavailable";
1190
- const lines = [`Metrics: ${report.metricsStale ? "stale" : "current"} (fresh at ${freshness})`, "Window: 24h"];
1303
+ const lines = [
1304
+ `Window: ${report.window} Collector: ${report.metricsStale ? "stale" : "current"} Fresh: ${freshness}`,
1305
+ "PODS counts pods with current metrics."
1306
+ ];
1191
1307
  if (report.entries.length === 0) return `${lines.join("\n")}
1192
1308
  No managed-vCluster workload containers found.
1193
1309
  `;
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));
1310
+ const headers = ["WORKLOAD", "CONTAINER", "PODS", "CPU NOW", "CPU MAX", "MEM NOW", "MEM MAX", "STATE"];
1311
+ const grouped = /* @__PURE__ */ new Map();
1312
+ for (const entry of report.entries) {
1313
+ const entries = grouped.get(entry.workload.namespace) ?? [];
1314
+ entries.push(entry);
1315
+ grouped.set(entry.workload.namespace, entries);
1316
+ }
1317
+ for (const [namespace, entries] of grouped) {
1318
+ const rows = entries.map((entry) => {
1319
+ const state = getK8sUsageState(entry);
1320
+ return [
1321
+ `${abbreviatedK8sKind(entry.workload.kind)}/${entry.workload.name}`,
1322
+ entry.container.name,
1323
+ String(entry.sampledPodCount),
1324
+ formatK8sCpu(entry.container.currentUsage.cpuNanoCores),
1325
+ formatK8sCpu(entry.summary.cpuMaxNanoCores),
1326
+ formatK8sMemory(entry.container.currentUsage.memoryBytes),
1327
+ formatK8sMemory(entry.summary.memoryMaxBytes),
1328
+ state === "healthy" ? "-" : state
1329
+ ];
1330
+ });
1331
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
1332
+ const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
1333
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
1334
+ lines.push("", `Namespace: ${namespace}`, formatRow(headers), separator, ...rows.map(formatRow));
1335
+ }
1222
1336
  return `${lines.join("\n")}
1223
1337
  `;
1224
1338
  }
@@ -1313,6 +1427,17 @@ function summarizeEnvData(data) {
1313
1427
  ])
1314
1428
  );
1315
1429
  }
1430
+ function parseGetEnvsArgs(args) {
1431
+ let showValues = false;
1432
+ for (const arg of args) {
1433
+ if (arg === "--show-values") {
1434
+ showValues = true;
1435
+ continue;
1436
+ }
1437
+ throw new Error(arg.startsWith("-") ? `Unknown get envs flag: ${arg}` : `Unexpected get envs value: ${arg}`);
1438
+ }
1439
+ return { showValues };
1440
+ }
1316
1441
  function renderEnvSummary(data) {
1317
1442
  const lines = [];
1318
1443
  for (const [name, env] of Object.entries(data)) {
@@ -1321,6 +1446,24 @@ function renderEnvSummary(data) {
1321
1446
  return lines.length > 0 ? `${lines.join("\n")}
1322
1447
  ` : "No environment variables declared.\n";
1323
1448
  }
1449
+ function renderEnvValues(data) {
1450
+ 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}`);
1451
+ return lines.length > 0 ? `${lines.join("\n")}
1452
+ ` : "No environment variables declared.\n";
1453
+ }
1454
+ function getProjectEnvValue(data, name) {
1455
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1456
+ throw new Error(`Invalid environment variable name: ${name}`);
1457
+ }
1458
+ if (!Object.prototype.hasOwnProperty.call(data, name)) {
1459
+ throw new Error(`Environment variable ${name} is not declared for this project`);
1460
+ }
1461
+ const value = data[name]?.value;
1462
+ if (value === null || value === void 0) {
1463
+ throw new Error(`Environment variable ${name} does not have a value`);
1464
+ }
1465
+ return value;
1466
+ }
1324
1467
  function renderEnvSetResult(result) {
1325
1468
  const lines = [`Updated ${result.updated.length} environment variable${result.updated.length === 1 ? "" : "s"} for ${result.project}.`];
1326
1469
  if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
@@ -1676,11 +1819,25 @@ Sessions: ${result.sessions.length}
1676
1819
  return;
1677
1820
  }
1678
1821
  if (first === "get" && second === "envs") {
1679
- if (args.length !== 3) throw new Error(`Unexpected get envs argument: ${args[3] ?? ""}`.trim());
1680
1822
  const projectRef = requireValue(args[2], "Missing project reference");
1823
+ const options = parseGetEnvsArgs(args.slice(3));
1681
1824
  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));
1825
+ const envs = await client.projects.env.get(project.id);
1826
+ if (options.showValues) {
1827
+ write({ project: project.path, envs }, renderEnvValues(envs));
1828
+ } else {
1829
+ const summary = summarizeEnvData(envs);
1830
+ write({ project: project.path, envs: summary }, renderEnvSummary(summary));
1831
+ }
1832
+ return;
1833
+ }
1834
+ if (first === "get" && second === "env") {
1835
+ if (args.length > 4) throw new Error(`Unexpected get env argument: ${args[4]}`);
1836
+ const projectRef = requireValue(args[2], "Missing project reference");
1837
+ const name = requireValue(args[3], "Missing environment variable name");
1838
+ const project = await client.projects.describe(projectRef);
1839
+ const value = getProjectEnvValue(await client.projects.env.get(project.id), name);
1840
+ write({ project: project.path, name, value }, value);
1684
1841
  return;
1685
1842
  }
1686
1843
  if (first === "set" && second === "envs") {
@@ -2033,9 +2190,29 @@ function resolveCommandExecution(options, rest) {
2033
2190
  if (options.branch) {
2034
2191
  throw new Error("--branch/-b is not supported for project environment variables");
2035
2192
  }
2193
+ parseGetEnvsArgs(commandArgs.slice(1));
2194
+ return {
2195
+ kind: "plugin",
2196
+ pluginArgs: ["get", "envs", options.project, ...commandArgs.slice(1)]
2197
+ };
2198
+ }
2199
+ if (target === "env") {
2200
+ if (!options.project) {
2201
+ throw new Error("--project/-p is required for `get env`");
2202
+ }
2203
+ if (options.branch) {
2204
+ throw new Error("--branch/-b is not supported for project environment variables");
2205
+ }
2206
+ const name = requireValue(commandArgs[1], "Environment variable name is required for `get env`");
2207
+ if (commandArgs.length > 2) {
2208
+ throw new Error(`Unexpected get env argument: ${commandArgs[2]}`);
2209
+ }
2210
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2211
+ throw new Error(`Invalid environment variable name: ${name}`);
2212
+ }
2036
2213
  return {
2037
2214
  kind: "plugin",
2038
- pluginArgs: ["get", "envs", options.project]
2215
+ pluginArgs: ["get", "env", options.project, name]
2039
2216
  };
2040
2217
  }
2041
2218
  throw new Error("Unknown get command");
@@ -2423,18 +2600,24 @@ async function main(argv = process.argv.slice(2)) {
2423
2600
  }
2424
2601
  }
2425
2602
  export {
2603
+ advanceTransientDevicePollBackoff,
2426
2604
  collectK8sUsage,
2427
2605
  formatK8sCpu,
2428
2606
  formatK8sMemory,
2429
2607
  formatProcessAge,
2430
2608
  getCliHelpText,
2609
+ getK8sUsageState,
2610
+ getProjectEnvValue,
2431
2611
  getR5dctlVersion,
2612
+ getTransientDevicePollDelay,
2613
+ handleAuthLogin,
2432
2614
  main,
2433
2615
  parseAnswerFlags,
2434
2616
  parseConversationRenderArgs,
2435
2617
  parseConversationWorkDetailArgs,
2436
2618
  parseEnvFlags,
2437
2619
  parseEnvRequestResponseArgs,
2620
+ parseGetEnvsArgs,
2438
2621
  parseGlobalArgs,
2439
2622
  parseK8sUsageArgs,
2440
2623
  parsePromptArgs,
@@ -2447,6 +2630,8 @@ export {
2447
2630
  renderConversationOverviewResponse,
2448
2631
  renderConversationResponse,
2449
2632
  renderConversationWorkResponse,
2633
+ renderDeviceAuthorizationPrompt,
2634
+ renderEnvValues,
2450
2635
  renderK8sUsageReport,
2451
2636
  renderProcessHistory,
2452
2637
  renderProcessInspection,
@@ -2455,5 +2640,6 @@ export {
2455
2640
  renderWorkspaceSync,
2456
2641
  resolveCommandExecution,
2457
2642
  runR5dctlCli,
2458
- summarizeEnvData
2643
+ summarizeEnvData,
2644
+ writeConfig
2459
2645
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.50",
3
+ "version": "0.0.53",
4
4
  "type": "module"
5
5
  }
@@ -40,6 +40,7 @@ export declare function parseGlobalArgs(argv: string[]): {
40
40
  options: GlobalOptions;
41
41
  rest: string[];
42
42
  };
43
+ export declare function writeConfig(configPath: string, config: R5dctlConfig): void;
43
44
  export declare function parseAnswerFlags(args: string[]): string[];
44
45
  export type SetEnvOptions = {
45
46
  assignments: string[];
@@ -69,9 +70,19 @@ export type K8sUsageOptions = {
69
70
  namespace?: string;
70
71
  workload?: string;
71
72
  container?: string;
72
- window: "24h";
73
+ window: "24h" | "7d";
73
74
  };
74
75
  export declare function parseK8sUsageArgs(args: string[]): K8sUsageOptions;
76
+ export declare function renderDeviceAuthorizationPrompt(input: {
77
+ verificationUri: string;
78
+ userCode: string;
79
+ }): string;
80
+ export declare function getTransientDevicePollDelay(intervalMs: number, transientBackoffMs: number): number;
81
+ export declare function advanceTransientDevicePollBackoff(intervalMs: number, transientBackoffMs: number): {
82
+ delayMs: number;
83
+ nextBackoffMs: number;
84
+ };
85
+ export declare function handleAuthLogin(client: R5dctlClient, options: GlobalOptions, commandArgs: string[], config: R5dctlConfig): Promise<void>;
75
86
  export declare function renderWorkspaceStatus(status: R5dctlWorkspaceStatus): string;
76
87
  export declare function renderWorkspaceSync(result: R5dctlWorkspaceSyncResult): string;
77
88
  export type K8sUsageSummary = {
@@ -82,21 +93,23 @@ export type K8sUsageSummary = {
82
93
  sampleCount: number;
83
94
  };
84
95
  export type K8sUsageReport = {
85
- window: "24h";
96
+ window: "24h" | "7d";
86
97
  collectedAt: string;
87
98
  metricsFreshAt: string | null;
88
99
  metricsStale: boolean;
89
100
  filters: Omit<K8sUsageOptions, "window">;
90
101
  entries: Array<{
91
- workload: Pick<R5dctlK8sWorkload, "uid" | "kind" | "namespace" | "name">;
102
+ workload: Pick<R5dctlK8sWorkload, "uid" | "kind" | "namespace" | "name" | "replicas" | "readyReplicas">;
92
103
  container: {
93
104
  name: string;
94
105
  currentUsage: R5dctlK8sUsage;
95
106
  pods: Array<R5dctlK8sUsage & {
96
107
  podUid: string;
97
108
  podName: string;
109
+ phase: string | null;
98
110
  }>;
99
111
  };
112
+ sampledPodCount: number;
100
113
  summary: K8sUsageSummary;
101
114
  history: R5dctlK8sResourceHistory;
102
115
  }>;
@@ -104,6 +117,8 @@ export type K8sUsageReport = {
104
117
  export declare function collectK8sUsage(client: Pick<R5dctlClient, "k8s">, options: K8sUsageOptions): Promise<K8sUsageReport>;
105
118
  export declare function formatK8sCpu(value: number | null): string;
106
119
  export declare function formatK8sMemory(value: number | null): string;
120
+ export type K8sUsageState = "healthy" | "partial" | "stale" | "inactive" | "warming" | "no data" | "no current";
121
+ export declare function getK8sUsageState(entry: K8sUsageReport["entries"][number]): K8sUsageState;
107
122
  export declare function renderK8sUsageReport(report: K8sUsageReport): string;
108
123
  export declare function formatProcessAge(startedAt: string, nowMs?: number): string;
109
124
  export declare function renderProcessList(processes: R5dctlProcessRun[], nowMs?: number): string;
@@ -114,6 +129,11 @@ export type EnvSummaryData = Record<string, {
114
129
  hasValue: boolean;
115
130
  }>;
116
131
  export declare function summarizeEnvData(data: R5dctlEnvData): EnvSummaryData;
132
+ export declare function parseGetEnvsArgs(args: string[]): {
133
+ showValues: boolean;
134
+ };
135
+ export declare function renderEnvValues(data: R5dctlEnvData): string;
136
+ export declare function getProjectEnvValue(data: R5dctlEnvData, name: string): string;
117
137
  export declare function parseConversationRenderArgs(args: string[]): ConversationRenderOptions;
118
138
  export declare function renderConversationResponse(read: R5dctlConversationResponse): string;
119
139
  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.53",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/cli.cjs",
6
6
  "module": "./dist/mjs/cli.mjs",
@@ -26,8 +26,9 @@
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.53",
30
30
  "dotenv": "^17",
31
+ "qrcode": "^1.5.4",
31
32
  "ws": "^8.18.3"
32
33
  },
33
34
  "files": [