@ricsam/r5dctl 0.0.20 → 0.0.22
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 +5 -0
- package/dist/cjs/cli.cjs +282 -4
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +278 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +18 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -49,6 +49,9 @@ r5dctl delete project <namespace/name|id>
|
|
|
49
49
|
|
|
50
50
|
r5dctl -p <project> get branches
|
|
51
51
|
r5dctl -p <project> describe branch <branch>
|
|
52
|
+
r5dctl -p <project> -b <branch> get envs
|
|
53
|
+
r5dctl -p <project> -b <branch> set envs --from-env-file ~/.env --only-missing
|
|
54
|
+
r5dctl -p <project> -b <branch> set envs -e backend/API_KEY=value --description "API credential"
|
|
52
55
|
r5dctl -p <project> get sessions --branch <branch>
|
|
53
56
|
r5dctl -p <project> create session -b <branch> --name "Spec session"
|
|
54
57
|
r5dctl describe session <session-id>
|
|
@@ -66,6 +69,8 @@ r5dctl -s <session-id> apply-required-envs -e backend/KEY=value -e frontend/KEY=
|
|
|
66
69
|
|
|
67
70
|
```
|
|
68
71
|
|
|
72
|
+
`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. Dotenv imports target backend envs by default; use `--target frontend` when needed. Empty dotenv values are skipped unless `--include-empty` is passed, and `--only-missing` preserves values that are already set.
|
|
73
|
+
|
|
69
74
|
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.
|
|
70
75
|
|
|
71
76
|
When available, conversation rendering uses the newest complete persisted model-request snapshot on the active conversation branch, so its mode, tier, model, thinking mode, tools, and messages reflect the request that actually ran. Legacy or empty sessions without snapshots fall back to reconstruction from the latest conversation context.
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -36,9 +36,12 @@ __export(cli_exports, {
|
|
|
36
36
|
parseEnvFlags: () => parseEnvFlags,
|
|
37
37
|
parseGlobalArgs: () => parseGlobalArgs,
|
|
38
38
|
parsePromptArgs: () => parsePromptArgs,
|
|
39
|
+
parseSetEnvArgs: () => parseSetEnvArgs,
|
|
40
|
+
readDotenvFile: () => readDotenvFile,
|
|
39
41
|
renderConversationResponse: () => renderConversationResponse,
|
|
40
42
|
resolveCommandExecution: () => resolveCommandExecution,
|
|
41
|
-
runR5dctlCli: () => runR5dctlCli
|
|
43
|
+
runR5dctlCli: () => runR5dctlCli,
|
|
44
|
+
summarizeEnvData: () => summarizeEnvData
|
|
42
45
|
});
|
|
43
46
|
module.exports = __toCommonJS(cli_exports);
|
|
44
47
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
@@ -46,6 +49,7 @@ var import_node_os = __toESM(require("node:os"), 1);
|
|
|
46
49
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
47
50
|
var import_node_child_process = require("node:child_process");
|
|
48
51
|
var import_promises = require("node:timers/promises");
|
|
52
|
+
var import_dotenv = require("dotenv");
|
|
49
53
|
var import_r5d_api = require("@ricsam/r5d-api");
|
|
50
54
|
const CHAT_MODES = /* @__PURE__ */ new Set([
|
|
51
55
|
"ask",
|
|
@@ -103,6 +107,21 @@ const SHARED_HELP_ENTRIES = [
|
|
|
103
107
|
{ section: "projects", usage: "delete project <namespace/name|id>", description: "Delete a project." },
|
|
104
108
|
{ section: "branches", usage: "-p <project> get branches", description: "List branches for a project." },
|
|
105
109
|
{ section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
|
|
110
|
+
{
|
|
111
|
+
section: "envs",
|
|
112
|
+
usage: "-p <project> -b <branch> get envs",
|
|
113
|
+
description: "List declared envs and whether each has a value, without revealing values."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
section: "envs",
|
|
117
|
+
usage: "-p <project> -b <branch> set envs --from-env-file <path> [--target <backend|frontend>] [--only-missing]",
|
|
118
|
+
description: "Securely import branch envs from a dotenv file without putting values in command arguments."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
section: "envs",
|
|
122
|
+
usage: "-p <project> -b <branch> set envs -e backend/KEY=value [--description <text>] [--only-missing]",
|
|
123
|
+
description: "Set one or more branch envs directly."
|
|
124
|
+
},
|
|
106
125
|
{
|
|
107
126
|
section: "sessions",
|
|
108
127
|
usage: "-p <project> get sessions [--branch <branch>]",
|
|
@@ -151,11 +170,12 @@ const SHARED_HELP_ENTRIES = [
|
|
|
151
170
|
{ section: "merges", usage: "-p <project> continue-merge <target-branch>", description: "Commit a resolved merge." },
|
|
152
171
|
{ section: "merges", usage: "-p <project> abort-merge <target-branch>", description: "Abort an in-progress merge." }
|
|
153
172
|
];
|
|
154
|
-
const HELP_SECTION_ORDER = ["auth", "projects", "branches", "sessions", "agents", "merges"];
|
|
173
|
+
const HELP_SECTION_ORDER = ["auth", "projects", "branches", "envs", "sessions", "agents", "merges"];
|
|
155
174
|
const HELP_SECTION_TITLES = {
|
|
156
175
|
auth: "Auth",
|
|
157
176
|
projects: "Projects",
|
|
158
177
|
branches: "Branches",
|
|
178
|
+
envs: "Environment variables",
|
|
159
179
|
sessions: "Sessions",
|
|
160
180
|
agents: "Agents",
|
|
161
181
|
merges: "Merges"
|
|
@@ -501,10 +521,117 @@ function validateEnvAssignment(value) {
|
|
|
501
521
|
}
|
|
502
522
|
const target = assignment.slice(0, slashIndex);
|
|
503
523
|
const key = assignment.slice(slashIndex + 1);
|
|
504
|
-
if (target !== "backend" && target !== "frontend" || key
|
|
524
|
+
if (target !== "backend" && target !== "frontend" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
505
525
|
throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
|
|
506
526
|
}
|
|
507
527
|
}
|
|
528
|
+
function parseEnvAssignment(value) {
|
|
529
|
+
validateEnvAssignment(value);
|
|
530
|
+
const equalsIndex = value.indexOf("=");
|
|
531
|
+
const slashIndex = value.indexOf("/");
|
|
532
|
+
return {
|
|
533
|
+
target: value.slice(0, slashIndex),
|
|
534
|
+
name: value.slice(slashIndex + 1, equalsIndex),
|
|
535
|
+
value: value.slice(equalsIndex + 1)
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function parseSetEnvArgs(args) {
|
|
539
|
+
const parsed = {
|
|
540
|
+
assignments: [],
|
|
541
|
+
target: "backend",
|
|
542
|
+
onlyMissing: false,
|
|
543
|
+
includeEmpty: false
|
|
544
|
+
};
|
|
545
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
546
|
+
const arg = args[index];
|
|
547
|
+
if (!arg) continue;
|
|
548
|
+
if (arg === "-e" || arg === "--env") {
|
|
549
|
+
const value = requireValue(args[index + 1], `Missing value for ${arg}`);
|
|
550
|
+
validateEnvAssignment(value);
|
|
551
|
+
parsed.assignments.push(value);
|
|
552
|
+
index += 1;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const inlineEnv = parseLongOptionWithEquals(arg, "--env");
|
|
556
|
+
if (inlineEnv !== void 0) {
|
|
557
|
+
validateEnvAssignment(inlineEnv);
|
|
558
|
+
parsed.assignments.push(inlineEnv);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (arg === "--from-env-file") {
|
|
562
|
+
parsed.fromEnvFile = requireValue(args[index + 1], "Missing value for --from-env-file");
|
|
563
|
+
index += 1;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const inlineFile = parseLongOptionWithEquals(arg, "--from-env-file");
|
|
567
|
+
if (inlineFile !== void 0) {
|
|
568
|
+
parsed.fromEnvFile = requireValue(inlineFile, "Missing value for --from-env-file");
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (arg === "--target") {
|
|
572
|
+
const target = requireValue(args[index + 1], "Missing value for --target");
|
|
573
|
+
if (target !== "backend" && target !== "frontend") {
|
|
574
|
+
throw new Error("Invalid --target. Expected backend or frontend");
|
|
575
|
+
}
|
|
576
|
+
parsed.target = target;
|
|
577
|
+
index += 1;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const inlineTarget = parseLongOptionWithEquals(arg, "--target");
|
|
581
|
+
if (inlineTarget !== void 0) {
|
|
582
|
+
if (inlineTarget !== "backend" && inlineTarget !== "frontend") {
|
|
583
|
+
throw new Error("Invalid --target. Expected backend or frontend");
|
|
584
|
+
}
|
|
585
|
+
parsed.target = inlineTarget;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (arg === "--description") {
|
|
589
|
+
parsed.description = requireValue(args[index + 1], "Missing value for --description");
|
|
590
|
+
index += 1;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const inlineDescription = parseLongOptionWithEquals(arg, "--description");
|
|
594
|
+
if (inlineDescription !== void 0) {
|
|
595
|
+
parsed.description = requireValue(inlineDescription, "Missing value for --description");
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (arg === "--optional") {
|
|
599
|
+
if (parsed.optional === false) throw new Error("Use only one of --optional or --required");
|
|
600
|
+
parsed.optional = true;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (arg === "--required") {
|
|
604
|
+
if (parsed.optional === true) throw new Error("Use only one of --optional or --required");
|
|
605
|
+
parsed.optional = false;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (arg === "--only-missing") {
|
|
609
|
+
parsed.onlyMissing = true;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (arg === "--include-empty") {
|
|
613
|
+
parsed.includeEmpty = true;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
throw new Error(arg.startsWith("-") ? `Unknown set envs flag: ${arg}` : `Unexpected set envs value: ${arg}`);
|
|
617
|
+
}
|
|
618
|
+
if (parsed.assignments.length > 0 && parsed.fromEnvFile) {
|
|
619
|
+
throw new Error("Use either -e/--env or --from-env-file, not both");
|
|
620
|
+
}
|
|
621
|
+
if (parsed.assignments.length === 0 && !parsed.fromEnvFile) {
|
|
622
|
+
throw new Error("Provide at least one -e/--env assignment or --from-env-file <path>");
|
|
623
|
+
}
|
|
624
|
+
return parsed;
|
|
625
|
+
}
|
|
626
|
+
function resolveLocalPath(filePath) {
|
|
627
|
+
if (filePath === "~") return import_node_os.default.homedir();
|
|
628
|
+
if (filePath.startsWith("~/")) return import_node_path.default.join(import_node_os.default.homedir(), filePath.slice(2));
|
|
629
|
+
return import_node_path.default.resolve(filePath);
|
|
630
|
+
}
|
|
631
|
+
function readDotenvFile(filePath) {
|
|
632
|
+
const resolvedPath = resolveLocalPath(filePath);
|
|
633
|
+
return (0, import_dotenv.parse)(import_node_fs.default.readFileSync(resolvedPath));
|
|
634
|
+
}
|
|
508
635
|
function parseEnvFlags(args) {
|
|
509
636
|
const envs = [];
|
|
510
637
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -739,6 +866,108 @@ function renderSessionList(sessions) {
|
|
|
739
866
|
function renderSessionDescription(session) {
|
|
740
867
|
return [`Session: ${session.id}`, `Project: ${session.projectPath}`, `Branch: ${session.branchName}`].join("\n") + "\n";
|
|
741
868
|
}
|
|
869
|
+
function summarizeEnvData(data) {
|
|
870
|
+
const summary = {};
|
|
871
|
+
for (const target of ["backend", "frontend"]) {
|
|
872
|
+
const entries = data[target];
|
|
873
|
+
if (!entries) continue;
|
|
874
|
+
summary[target] = Object.fromEntries(
|
|
875
|
+
Object.entries(entries).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => [
|
|
876
|
+
name,
|
|
877
|
+
{
|
|
878
|
+
optional: env.optional ?? false,
|
|
879
|
+
description: env.description,
|
|
880
|
+
hasValue: env.value !== null
|
|
881
|
+
}
|
|
882
|
+
])
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return summary;
|
|
886
|
+
}
|
|
887
|
+
function renderEnvSummary(data) {
|
|
888
|
+
const lines = [];
|
|
889
|
+
for (const target of ["backend", "frontend"]) {
|
|
890
|
+
const entries = Object.entries(data[target] ?? {});
|
|
891
|
+
if (entries.length === 0) continue;
|
|
892
|
+
if (lines.length > 0) lines.push("");
|
|
893
|
+
lines.push(`${target}:`);
|
|
894
|
+
for (const [name, env] of entries) {
|
|
895
|
+
lines.push(` ${name} ${env.hasValue ? "set" : "missing"} ${env.optional ? "optional" : "required"} ${env.description}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return lines.length > 0 ? `${lines.join("\n")}
|
|
899
|
+
` : "No environment variables declared.\n";
|
|
900
|
+
}
|
|
901
|
+
function renderEnvSetResult(result) {
|
|
902
|
+
const lines = [
|
|
903
|
+
`Updated ${result.updated.length} environment variable${result.updated.length === 1 ? "" : "s"} for ${result.project}/${result.branch}.`
|
|
904
|
+
];
|
|
905
|
+
if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
|
|
906
|
+
if (result.skippedEmpty.length > 0) lines.push(`Skipped ${result.skippedEmpty.length} empty value(s).`);
|
|
907
|
+
if (result.updated.length > 0) {
|
|
908
|
+
lines.push(...result.updated.map(({ target, name }) => ` ${target}/${name}`));
|
|
909
|
+
}
|
|
910
|
+
return `${lines.join("\n")}
|
|
911
|
+
`;
|
|
912
|
+
}
|
|
913
|
+
async function setBranchEnvs(client, projectRef, branchName, args) {
|
|
914
|
+
const options = parseSetEnvArgs(args);
|
|
915
|
+
const project = await client.projects.describe(projectRef);
|
|
916
|
+
const current = await client.projects.env.get(project.id, branchName);
|
|
917
|
+
const assignments = options.fromEnvFile ? Object.entries(readDotenvFile(options.fromEnvFile)).map(([name, value]) => {
|
|
918
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
919
|
+
throw new Error(`Invalid environment variable name in dotenv file: ${name}`);
|
|
920
|
+
}
|
|
921
|
+
return { target: options.target, name, value };
|
|
922
|
+
}) : options.assignments.map(parseEnvAssignment);
|
|
923
|
+
const uniqueAssignments = /* @__PURE__ */ new Map();
|
|
924
|
+
for (const assignment of assignments) uniqueAssignments.set(`${assignment.target}/${assignment.name}`, assignment);
|
|
925
|
+
const update = {};
|
|
926
|
+
const result = {
|
|
927
|
+
success: true,
|
|
928
|
+
project: project.path,
|
|
929
|
+
branch: branchName,
|
|
930
|
+
updated: [],
|
|
931
|
+
skippedExisting: [],
|
|
932
|
+
skippedEmpty: []
|
|
933
|
+
};
|
|
934
|
+
const missingDescriptions = [];
|
|
935
|
+
const importedDescription = options.fromEnvFile ? `Imported from ${import_node_path.default.basename(options.fromEnvFile)}.` : void 0;
|
|
936
|
+
for (const assignment of uniqueAssignments.values()) {
|
|
937
|
+
const identity = { target: assignment.target, name: assignment.name };
|
|
938
|
+
if (!options.includeEmpty && assignment.value.length === 0) {
|
|
939
|
+
result.skippedEmpty.push(identity);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const existing = current[assignment.target]?.[assignment.name];
|
|
943
|
+
if (options.onlyMissing && existing?.value !== null && existing?.value !== void 0) {
|
|
944
|
+
result.skippedExisting.push(identity);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const description = options.description ?? existing?.description ?? importedDescription;
|
|
948
|
+
if (!description) {
|
|
949
|
+
missingDescriptions.push(identity);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const targetUpdate = update[assignment.target] ??= {};
|
|
953
|
+
const optional = options.optional ?? existing?.optional;
|
|
954
|
+
targetUpdate[assignment.name] = {
|
|
955
|
+
description,
|
|
956
|
+
value: assignment.value,
|
|
957
|
+
...optional === void 0 ? {} : { optional }
|
|
958
|
+
};
|
|
959
|
+
result.updated.push(identity);
|
|
960
|
+
}
|
|
961
|
+
if (missingDescriptions.length > 0) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`--description is required when declaring new envs with -e: ${missingDescriptions.map(({ target, name }) => `${target}/${name}`).join(", ")}`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (result.updated.length > 0) {
|
|
967
|
+
await client.projects.env.update(project.id, branchName, update);
|
|
968
|
+
}
|
|
969
|
+
return result;
|
|
970
|
+
}
|
|
742
971
|
function renderAgentStart(agent) {
|
|
743
972
|
return [
|
|
744
973
|
`Agent: ${agent.sessionId}`,
|
|
@@ -940,6 +1169,24 @@ Sessions: ${result.sessions.length}
|
|
|
940
1169
|
`);
|
|
941
1170
|
return;
|
|
942
1171
|
}
|
|
1172
|
+
if (first === "get" && second === "envs") {
|
|
1173
|
+
const projectRef = requireValue(args[2], "Missing project reference");
|
|
1174
|
+
const branchName = requireValue(args[3], "Missing branch name");
|
|
1175
|
+
const project = await client.projects.describe(projectRef);
|
|
1176
|
+
const envs = summarizeEnvData(await client.projects.env.get(project.id, branchName));
|
|
1177
|
+
write({ project: project.path, branch: branchName, envs }, renderEnvSummary(envs));
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (first === "set" && second === "envs") {
|
|
1181
|
+
const result = await setBranchEnvs(
|
|
1182
|
+
client,
|
|
1183
|
+
requireValue(args[2], "Missing project reference"),
|
|
1184
|
+
requireValue(args[3], "Missing branch name"),
|
|
1185
|
+
args.slice(4)
|
|
1186
|
+
);
|
|
1187
|
+
write(result, renderEnvSetResult(result));
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
943
1190
|
if (first === "projects" && second === "sessions" && third === "list" || first === "get" && second === "sessions") {
|
|
944
1191
|
const offset = first === "get" ? 2 : 3;
|
|
945
1192
|
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
@@ -1177,8 +1424,36 @@ function resolveCommandExecution(options, rest) {
|
|
|
1177
1424
|
pluginArgs
|
|
1178
1425
|
};
|
|
1179
1426
|
}
|
|
1427
|
+
if (target === "envs") {
|
|
1428
|
+
if (!options.project) {
|
|
1429
|
+
throw new Error("--project/-p is required for `get envs`");
|
|
1430
|
+
}
|
|
1431
|
+
if (!options.branch) {
|
|
1432
|
+
throw new Error("--branch/-b is required for `get envs`");
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
kind: "plugin",
|
|
1436
|
+
pluginArgs: ["get", "envs", options.project, options.branch]
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1180
1439
|
throw new Error("Unknown get command");
|
|
1181
1440
|
}
|
|
1441
|
+
if (command === "set") {
|
|
1442
|
+
const target = commandArgs[0];
|
|
1443
|
+
if (target !== "envs") {
|
|
1444
|
+
throw new Error("Unknown set command");
|
|
1445
|
+
}
|
|
1446
|
+
if (!options.project) {
|
|
1447
|
+
throw new Error("--project/-p is required for `set envs`");
|
|
1448
|
+
}
|
|
1449
|
+
if (!options.branch) {
|
|
1450
|
+
throw new Error("--branch/-b is required for `set envs`");
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
kind: "plugin",
|
|
1454
|
+
pluginArgs: ["set", "envs", options.project, options.branch, ...commandArgs.slice(1)]
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1182
1457
|
if (command === "describe") {
|
|
1183
1458
|
const target = commandArgs[0];
|
|
1184
1459
|
if (target === "project") {
|
|
@@ -1487,7 +1762,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1487
1762
|
parseEnvFlags,
|
|
1488
1763
|
parseGlobalArgs,
|
|
1489
1764
|
parsePromptArgs,
|
|
1765
|
+
parseSetEnvArgs,
|
|
1766
|
+
readDotenvFile,
|
|
1490
1767
|
renderConversationResponse,
|
|
1491
1768
|
resolveCommandExecution,
|
|
1492
|
-
runR5dctlCli
|
|
1769
|
+
runR5dctlCli,
|
|
1770
|
+
summarizeEnvData
|
|
1493
1771
|
});
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
import { parse as parseDotenv } from "dotenv";
|
|
6
7
|
import {
|
|
7
8
|
R5dctlApiError,
|
|
8
9
|
R5dctlClient
|
|
@@ -63,6 +64,21 @@ const SHARED_HELP_ENTRIES = [
|
|
|
63
64
|
{ section: "projects", usage: "delete project <namespace/name|id>", description: "Delete a project." },
|
|
64
65
|
{ section: "branches", usage: "-p <project> get branches", description: "List branches for a project." },
|
|
65
66
|
{ section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
|
|
67
|
+
{
|
|
68
|
+
section: "envs",
|
|
69
|
+
usage: "-p <project> -b <branch> get envs",
|
|
70
|
+
description: "List declared envs and whether each has a value, without revealing values."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
section: "envs",
|
|
74
|
+
usage: "-p <project> -b <branch> set envs --from-env-file <path> [--target <backend|frontend>] [--only-missing]",
|
|
75
|
+
description: "Securely import branch envs from a dotenv file without putting values in command arguments."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
section: "envs",
|
|
79
|
+
usage: "-p <project> -b <branch> set envs -e backend/KEY=value [--description <text>] [--only-missing]",
|
|
80
|
+
description: "Set one or more branch envs directly."
|
|
81
|
+
},
|
|
66
82
|
{
|
|
67
83
|
section: "sessions",
|
|
68
84
|
usage: "-p <project> get sessions [--branch <branch>]",
|
|
@@ -111,11 +127,12 @@ const SHARED_HELP_ENTRIES = [
|
|
|
111
127
|
{ section: "merges", usage: "-p <project> continue-merge <target-branch>", description: "Commit a resolved merge." },
|
|
112
128
|
{ section: "merges", usage: "-p <project> abort-merge <target-branch>", description: "Abort an in-progress merge." }
|
|
113
129
|
];
|
|
114
|
-
const HELP_SECTION_ORDER = ["auth", "projects", "branches", "sessions", "agents", "merges"];
|
|
130
|
+
const HELP_SECTION_ORDER = ["auth", "projects", "branches", "envs", "sessions", "agents", "merges"];
|
|
115
131
|
const HELP_SECTION_TITLES = {
|
|
116
132
|
auth: "Auth",
|
|
117
133
|
projects: "Projects",
|
|
118
134
|
branches: "Branches",
|
|
135
|
+
envs: "Environment variables",
|
|
119
136
|
sessions: "Sessions",
|
|
120
137
|
agents: "Agents",
|
|
121
138
|
merges: "Merges"
|
|
@@ -461,10 +478,117 @@ function validateEnvAssignment(value) {
|
|
|
461
478
|
}
|
|
462
479
|
const target = assignment.slice(0, slashIndex);
|
|
463
480
|
const key = assignment.slice(slashIndex + 1);
|
|
464
|
-
if (target !== "backend" && target !== "frontend" || key
|
|
481
|
+
if (target !== "backend" && target !== "frontend" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
465
482
|
throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
|
|
466
483
|
}
|
|
467
484
|
}
|
|
485
|
+
function parseEnvAssignment(value) {
|
|
486
|
+
validateEnvAssignment(value);
|
|
487
|
+
const equalsIndex = value.indexOf("=");
|
|
488
|
+
const slashIndex = value.indexOf("/");
|
|
489
|
+
return {
|
|
490
|
+
target: value.slice(0, slashIndex),
|
|
491
|
+
name: value.slice(slashIndex + 1, equalsIndex),
|
|
492
|
+
value: value.slice(equalsIndex + 1)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function parseSetEnvArgs(args) {
|
|
496
|
+
const parsed = {
|
|
497
|
+
assignments: [],
|
|
498
|
+
target: "backend",
|
|
499
|
+
onlyMissing: false,
|
|
500
|
+
includeEmpty: false
|
|
501
|
+
};
|
|
502
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
503
|
+
const arg = args[index];
|
|
504
|
+
if (!arg) continue;
|
|
505
|
+
if (arg === "-e" || arg === "--env") {
|
|
506
|
+
const value = requireValue(args[index + 1], `Missing value for ${arg}`);
|
|
507
|
+
validateEnvAssignment(value);
|
|
508
|
+
parsed.assignments.push(value);
|
|
509
|
+
index += 1;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const inlineEnv = parseLongOptionWithEquals(arg, "--env");
|
|
513
|
+
if (inlineEnv !== void 0) {
|
|
514
|
+
validateEnvAssignment(inlineEnv);
|
|
515
|
+
parsed.assignments.push(inlineEnv);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (arg === "--from-env-file") {
|
|
519
|
+
parsed.fromEnvFile = requireValue(args[index + 1], "Missing value for --from-env-file");
|
|
520
|
+
index += 1;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const inlineFile = parseLongOptionWithEquals(arg, "--from-env-file");
|
|
524
|
+
if (inlineFile !== void 0) {
|
|
525
|
+
parsed.fromEnvFile = requireValue(inlineFile, "Missing value for --from-env-file");
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (arg === "--target") {
|
|
529
|
+
const target = requireValue(args[index + 1], "Missing value for --target");
|
|
530
|
+
if (target !== "backend" && target !== "frontend") {
|
|
531
|
+
throw new Error("Invalid --target. Expected backend or frontend");
|
|
532
|
+
}
|
|
533
|
+
parsed.target = target;
|
|
534
|
+
index += 1;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const inlineTarget = parseLongOptionWithEquals(arg, "--target");
|
|
538
|
+
if (inlineTarget !== void 0) {
|
|
539
|
+
if (inlineTarget !== "backend" && inlineTarget !== "frontend") {
|
|
540
|
+
throw new Error("Invalid --target. Expected backend or frontend");
|
|
541
|
+
}
|
|
542
|
+
parsed.target = inlineTarget;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (arg === "--description") {
|
|
546
|
+
parsed.description = requireValue(args[index + 1], "Missing value for --description");
|
|
547
|
+
index += 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const inlineDescription = parseLongOptionWithEquals(arg, "--description");
|
|
551
|
+
if (inlineDescription !== void 0) {
|
|
552
|
+
parsed.description = requireValue(inlineDescription, "Missing value for --description");
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (arg === "--optional") {
|
|
556
|
+
if (parsed.optional === false) throw new Error("Use only one of --optional or --required");
|
|
557
|
+
parsed.optional = true;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (arg === "--required") {
|
|
561
|
+
if (parsed.optional === true) throw new Error("Use only one of --optional or --required");
|
|
562
|
+
parsed.optional = false;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (arg === "--only-missing") {
|
|
566
|
+
parsed.onlyMissing = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (arg === "--include-empty") {
|
|
570
|
+
parsed.includeEmpty = true;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
throw new Error(arg.startsWith("-") ? `Unknown set envs flag: ${arg}` : `Unexpected set envs value: ${arg}`);
|
|
574
|
+
}
|
|
575
|
+
if (parsed.assignments.length > 0 && parsed.fromEnvFile) {
|
|
576
|
+
throw new Error("Use either -e/--env or --from-env-file, not both");
|
|
577
|
+
}
|
|
578
|
+
if (parsed.assignments.length === 0 && !parsed.fromEnvFile) {
|
|
579
|
+
throw new Error("Provide at least one -e/--env assignment or --from-env-file <path>");
|
|
580
|
+
}
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
function resolveLocalPath(filePath) {
|
|
584
|
+
if (filePath === "~") return os.homedir();
|
|
585
|
+
if (filePath.startsWith("~/")) return path.join(os.homedir(), filePath.slice(2));
|
|
586
|
+
return path.resolve(filePath);
|
|
587
|
+
}
|
|
588
|
+
function readDotenvFile(filePath) {
|
|
589
|
+
const resolvedPath = resolveLocalPath(filePath);
|
|
590
|
+
return parseDotenv(fs.readFileSync(resolvedPath));
|
|
591
|
+
}
|
|
468
592
|
function parseEnvFlags(args) {
|
|
469
593
|
const envs = [];
|
|
470
594
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -699,6 +823,108 @@ function renderSessionList(sessions) {
|
|
|
699
823
|
function renderSessionDescription(session) {
|
|
700
824
|
return [`Session: ${session.id}`, `Project: ${session.projectPath}`, `Branch: ${session.branchName}`].join("\n") + "\n";
|
|
701
825
|
}
|
|
826
|
+
function summarizeEnvData(data) {
|
|
827
|
+
const summary = {};
|
|
828
|
+
for (const target of ["backend", "frontend"]) {
|
|
829
|
+
const entries = data[target];
|
|
830
|
+
if (!entries) continue;
|
|
831
|
+
summary[target] = Object.fromEntries(
|
|
832
|
+
Object.entries(entries).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => [
|
|
833
|
+
name,
|
|
834
|
+
{
|
|
835
|
+
optional: env.optional ?? false,
|
|
836
|
+
description: env.description,
|
|
837
|
+
hasValue: env.value !== null
|
|
838
|
+
}
|
|
839
|
+
])
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return summary;
|
|
843
|
+
}
|
|
844
|
+
function renderEnvSummary(data) {
|
|
845
|
+
const lines = [];
|
|
846
|
+
for (const target of ["backend", "frontend"]) {
|
|
847
|
+
const entries = Object.entries(data[target] ?? {});
|
|
848
|
+
if (entries.length === 0) continue;
|
|
849
|
+
if (lines.length > 0) lines.push("");
|
|
850
|
+
lines.push(`${target}:`);
|
|
851
|
+
for (const [name, env] of entries) {
|
|
852
|
+
lines.push(` ${name} ${env.hasValue ? "set" : "missing"} ${env.optional ? "optional" : "required"} ${env.description}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return lines.length > 0 ? `${lines.join("\n")}
|
|
856
|
+
` : "No environment variables declared.\n";
|
|
857
|
+
}
|
|
858
|
+
function renderEnvSetResult(result) {
|
|
859
|
+
const lines = [
|
|
860
|
+
`Updated ${result.updated.length} environment variable${result.updated.length === 1 ? "" : "s"} for ${result.project}/${result.branch}.`
|
|
861
|
+
];
|
|
862
|
+
if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
|
|
863
|
+
if (result.skippedEmpty.length > 0) lines.push(`Skipped ${result.skippedEmpty.length} empty value(s).`);
|
|
864
|
+
if (result.updated.length > 0) {
|
|
865
|
+
lines.push(...result.updated.map(({ target, name }) => ` ${target}/${name}`));
|
|
866
|
+
}
|
|
867
|
+
return `${lines.join("\n")}
|
|
868
|
+
`;
|
|
869
|
+
}
|
|
870
|
+
async function setBranchEnvs(client, projectRef, branchName, args) {
|
|
871
|
+
const options = parseSetEnvArgs(args);
|
|
872
|
+
const project = await client.projects.describe(projectRef);
|
|
873
|
+
const current = await client.projects.env.get(project.id, branchName);
|
|
874
|
+
const assignments = options.fromEnvFile ? Object.entries(readDotenvFile(options.fromEnvFile)).map(([name, value]) => {
|
|
875
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
876
|
+
throw new Error(`Invalid environment variable name in dotenv file: ${name}`);
|
|
877
|
+
}
|
|
878
|
+
return { target: options.target, name, value };
|
|
879
|
+
}) : options.assignments.map(parseEnvAssignment);
|
|
880
|
+
const uniqueAssignments = /* @__PURE__ */ new Map();
|
|
881
|
+
for (const assignment of assignments) uniqueAssignments.set(`${assignment.target}/${assignment.name}`, assignment);
|
|
882
|
+
const update = {};
|
|
883
|
+
const result = {
|
|
884
|
+
success: true,
|
|
885
|
+
project: project.path,
|
|
886
|
+
branch: branchName,
|
|
887
|
+
updated: [],
|
|
888
|
+
skippedExisting: [],
|
|
889
|
+
skippedEmpty: []
|
|
890
|
+
};
|
|
891
|
+
const missingDescriptions = [];
|
|
892
|
+
const importedDescription = options.fromEnvFile ? `Imported from ${path.basename(options.fromEnvFile)}.` : void 0;
|
|
893
|
+
for (const assignment of uniqueAssignments.values()) {
|
|
894
|
+
const identity = { target: assignment.target, name: assignment.name };
|
|
895
|
+
if (!options.includeEmpty && assignment.value.length === 0) {
|
|
896
|
+
result.skippedEmpty.push(identity);
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const existing = current[assignment.target]?.[assignment.name];
|
|
900
|
+
if (options.onlyMissing && existing?.value !== null && existing?.value !== void 0) {
|
|
901
|
+
result.skippedExisting.push(identity);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const description = options.description ?? existing?.description ?? importedDescription;
|
|
905
|
+
if (!description) {
|
|
906
|
+
missingDescriptions.push(identity);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const targetUpdate = update[assignment.target] ??= {};
|
|
910
|
+
const optional = options.optional ?? existing?.optional;
|
|
911
|
+
targetUpdate[assignment.name] = {
|
|
912
|
+
description,
|
|
913
|
+
value: assignment.value,
|
|
914
|
+
...optional === void 0 ? {} : { optional }
|
|
915
|
+
};
|
|
916
|
+
result.updated.push(identity);
|
|
917
|
+
}
|
|
918
|
+
if (missingDescriptions.length > 0) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`--description is required when declaring new envs with -e: ${missingDescriptions.map(({ target, name }) => `${target}/${name}`).join(", ")}`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (result.updated.length > 0) {
|
|
924
|
+
await client.projects.env.update(project.id, branchName, update);
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
702
928
|
function renderAgentStart(agent) {
|
|
703
929
|
return [
|
|
704
930
|
`Agent: ${agent.sessionId}`,
|
|
@@ -900,6 +1126,24 @@ Sessions: ${result.sessions.length}
|
|
|
900
1126
|
`);
|
|
901
1127
|
return;
|
|
902
1128
|
}
|
|
1129
|
+
if (first === "get" && second === "envs") {
|
|
1130
|
+
const projectRef = requireValue(args[2], "Missing project reference");
|
|
1131
|
+
const branchName = requireValue(args[3], "Missing branch name");
|
|
1132
|
+
const project = await client.projects.describe(projectRef);
|
|
1133
|
+
const envs = summarizeEnvData(await client.projects.env.get(project.id, branchName));
|
|
1134
|
+
write({ project: project.path, branch: branchName, envs }, renderEnvSummary(envs));
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (first === "set" && second === "envs") {
|
|
1138
|
+
const result = await setBranchEnvs(
|
|
1139
|
+
client,
|
|
1140
|
+
requireValue(args[2], "Missing project reference"),
|
|
1141
|
+
requireValue(args[3], "Missing branch name"),
|
|
1142
|
+
args.slice(4)
|
|
1143
|
+
);
|
|
1144
|
+
write(result, renderEnvSetResult(result));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
903
1147
|
if (first === "projects" && second === "sessions" && third === "list" || first === "get" && second === "sessions") {
|
|
904
1148
|
const offset = first === "get" ? 2 : 3;
|
|
905
1149
|
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
@@ -1137,8 +1381,36 @@ function resolveCommandExecution(options, rest) {
|
|
|
1137
1381
|
pluginArgs
|
|
1138
1382
|
};
|
|
1139
1383
|
}
|
|
1384
|
+
if (target === "envs") {
|
|
1385
|
+
if (!options.project) {
|
|
1386
|
+
throw new Error("--project/-p is required for `get envs`");
|
|
1387
|
+
}
|
|
1388
|
+
if (!options.branch) {
|
|
1389
|
+
throw new Error("--branch/-b is required for `get envs`");
|
|
1390
|
+
}
|
|
1391
|
+
return {
|
|
1392
|
+
kind: "plugin",
|
|
1393
|
+
pluginArgs: ["get", "envs", options.project, options.branch]
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1140
1396
|
throw new Error("Unknown get command");
|
|
1141
1397
|
}
|
|
1398
|
+
if (command === "set") {
|
|
1399
|
+
const target = commandArgs[0];
|
|
1400
|
+
if (target !== "envs") {
|
|
1401
|
+
throw new Error("Unknown set command");
|
|
1402
|
+
}
|
|
1403
|
+
if (!options.project) {
|
|
1404
|
+
throw new Error("--project/-p is required for `set envs`");
|
|
1405
|
+
}
|
|
1406
|
+
if (!options.branch) {
|
|
1407
|
+
throw new Error("--branch/-b is required for `set envs`");
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
kind: "plugin",
|
|
1411
|
+
pluginArgs: ["set", "envs", options.project, options.branch, ...commandArgs.slice(1)]
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1142
1414
|
if (command === "describe") {
|
|
1143
1415
|
const target = commandArgs[0];
|
|
1144
1416
|
if (target === "project") {
|
|
@@ -1446,7 +1718,10 @@ export {
|
|
|
1446
1718
|
parseEnvFlags,
|
|
1447
1719
|
parseGlobalArgs,
|
|
1448
1720
|
parsePromptArgs,
|
|
1721
|
+
parseSetEnvArgs,
|
|
1722
|
+
readDotenvFile,
|
|
1449
1723
|
renderConversationResponse,
|
|
1450
1724
|
resolveCommandExecution,
|
|
1451
|
-
runR5dctlCli
|
|
1725
|
+
runR5dctlCli,
|
|
1726
|
+
summarizeEnvData
|
|
1452
1727
|
};
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
1
|
+
import { type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlEnvData, type R5dctlEnvTarget, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
2
2
|
export type GlobalOptions = {
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
json: boolean;
|
|
@@ -38,12 +38,29 @@ export declare function parseGlobalArgs(argv: string[]): {
|
|
|
38
38
|
rest: string[];
|
|
39
39
|
};
|
|
40
40
|
export declare function parseAnswerFlags(args: string[]): string[];
|
|
41
|
+
export type SetEnvOptions = {
|
|
42
|
+
assignments: string[];
|
|
43
|
+
fromEnvFile?: string;
|
|
44
|
+
target: R5dctlEnvTarget;
|
|
45
|
+
description?: string;
|
|
46
|
+
optional?: boolean;
|
|
47
|
+
onlyMissing: boolean;
|
|
48
|
+
includeEmpty: boolean;
|
|
49
|
+
};
|
|
50
|
+
export declare function parseSetEnvArgs(args: string[]): SetEnvOptions;
|
|
51
|
+
export declare function readDotenvFile(filePath: string): Record<string, string>;
|
|
41
52
|
export declare function parseEnvFlags(args: string[]): string[];
|
|
42
53
|
export declare function parsePromptArgs(args: string[]): {
|
|
43
54
|
mode: ChatMode;
|
|
44
55
|
model: ModelTier;
|
|
45
56
|
message: string;
|
|
46
57
|
};
|
|
58
|
+
export type EnvSummaryData = Partial<Record<R5dctlEnvTarget, Record<string, {
|
|
59
|
+
optional: boolean;
|
|
60
|
+
description: string;
|
|
61
|
+
hasValue: boolean;
|
|
62
|
+
}>>>;
|
|
63
|
+
export declare function summarizeEnvData(data: R5dctlEnvData): EnvSummaryData;
|
|
47
64
|
export declare function parseConversationRenderArgs(args: string[]): ConversationRenderOptions;
|
|
48
65
|
export declare function renderConversationResponse(read: R5dctlConversationResponse): string;
|
|
49
66
|
export declare function resolveCommandExecution(options: GlobalOptions, rest: string[]): CommandExecutionPlan;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5dctl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/cjs/cli.cjs",
|
|
6
6
|
"module": "./dist/mjs/cli.mjs",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"r5dctl": "dist/cjs/main.cjs"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ricsam/r5d-api": "^0.0.
|
|
29
|
+
"@ricsam/r5d-api": "^0.0.22",
|
|
30
|
+
"dotenv": "^17"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"dist",
|