@lunaroute/cli 0.2.4 → 0.2.6
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 +3 -1
- package/dist/index.js +241 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ Requires Node.js 20+.
|
|
|
18
18
|
lunaroute setup pi # or: opencode | claude-code | copilot-cli | generic
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
One command from a fresh machine: `setup` asks before touching anything — for `pi` it first offers to install the LunaRoute Pi extension
|
|
21
|
+
One command from a fresh machine: `setup` asks before touching anything — for `pi` it first offers to install the LunaRoute Pi extension (recommended — models then auto-sync, and you sign in with `/login lunaroute` inside pi), then offers to start pi for you. `opencode` works the same way with its extension (`/connect` to sign in). Decline an extension — or pick a harness whose config needs a key — and `setup` offers to run `lunaroute login` right there (it opens your browser) before writing. The fallback config finishes with an `export LUNAROUTE_API_KEY=…` line — run it in your shell, then start your agent and it routes through LunaRoute.
|
|
22
|
+
|
|
23
|
+
Already have a key from the dashboard? `lunaroute setup pi --key lr_… --yes` is the whole onboarding: it checks the key against the catalog, installs the Pi extension, and stores the key in `~/.pi/agent/auth.json`, so pi starts signed in with models syncing from `/v1/models`. No models.json edit and no shell export. A stale `providers.lunaroute` block from an older CLI is removed in the same run (backup kept). `lunaroute setup opencode --key lr_… --yes` works the same way: it installs the OpenCode extension and stores the key in `~/.local/share/opencode/auth.json`, so opencode starts signed in — no `/connect` needed.
|
|
22
24
|
|
|
23
25
|
Prefer a script? `setup pi --extension` / `--models` pick a flow, `--yes` accepts all prompts, and `--print` previews without writing.
|
|
24
26
|
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5
5
|
|
|
6
6
|
// package.json
|
|
7
|
-
var version = "0.2.
|
|
7
|
+
var version = "0.2.6";
|
|
8
8
|
|
|
9
9
|
// src/login.ts
|
|
10
10
|
import { createServer } from "http";
|
|
@@ -359,13 +359,26 @@ function logout(profile) {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// src/catalog.ts
|
|
362
|
+
var KeyRejectedError = class extends Error {
|
|
363
|
+
};
|
|
364
|
+
var NON_CHAT_CAPABILITIES = [
|
|
365
|
+
"image_generation",
|
|
366
|
+
"embeddings",
|
|
367
|
+
"rerank"
|
|
368
|
+
];
|
|
369
|
+
function isChatModel(m) {
|
|
370
|
+
for (const c of NON_CHAT_CAPABILITIES) {
|
|
371
|
+
if (m.capabilities?.[c] === true) return false;
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
362
375
|
async function fetchModels(routingUrl, key) {
|
|
363
376
|
const url = `${routingUrl}/v1/models`;
|
|
364
377
|
const headers = key ? { Authorization: `Bearer ${key}` } : {};
|
|
365
378
|
const res = await fetch(url, { method: "GET", headers });
|
|
366
379
|
if (!res.ok) {
|
|
367
380
|
if (res.status === 401) {
|
|
368
|
-
throw new
|
|
381
|
+
throw new KeyRejectedError(
|
|
369
382
|
key ? `the routing service rejected that key (HTTP 401) \u2014 check that you copied the whole lr_ key` : `the routing service requires a key for /v1/models (HTTP 401) \u2014 run "lunaroute login" or pass --key`
|
|
370
383
|
);
|
|
371
384
|
}
|
|
@@ -379,9 +392,9 @@ async function fetchModels(routingUrl, key) {
|
|
|
379
392
|
max_output_tokens: m.max_output_tokens,
|
|
380
393
|
capabilities: m.capabilities,
|
|
381
394
|
client_compat: m.client_compat
|
|
382
|
-
})).filter((m) => m.id.length > 0);
|
|
395
|
+
})).filter((m) => m.id.length > 0 && isChatModel(m));
|
|
383
396
|
if (models.length === 0) {
|
|
384
|
-
throw new Error(`no models available from ${url}`);
|
|
397
|
+
throw new Error(`no chat models available from ${url}`);
|
|
385
398
|
}
|
|
386
399
|
return models.sort((a, b) => a.id.localeCompare(b.id));
|
|
387
400
|
}
|
|
@@ -578,12 +591,19 @@ function configHome() {
|
|
|
578
591
|
function opencodeConfigPath() {
|
|
579
592
|
return join2(configHome(), "opencode", "opencode.json");
|
|
580
593
|
}
|
|
594
|
+
function opencodeAuthPath() {
|
|
595
|
+
const dataHome = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
|
|
596
|
+
return join2(dataHome, "opencode", "auth.json");
|
|
597
|
+
}
|
|
581
598
|
function openclawConfigPath() {
|
|
582
599
|
return join2(homedir2(), ".openclaw", "openclaw.json");
|
|
583
600
|
}
|
|
584
601
|
function piModelsPath() {
|
|
585
602
|
return join2(homedir2(), ".pi", "agent", "models.json");
|
|
586
603
|
}
|
|
604
|
+
function piAuthPath() {
|
|
605
|
+
return join2(homedir2(), ".pi", "agent", "auth.json");
|
|
606
|
+
}
|
|
587
607
|
var LUNAROUTE_SKILL_NAME = "lunaroute-memory";
|
|
588
608
|
function claudeUserConfigPath() {
|
|
589
609
|
return join2(homedir2(), ".claude.json");
|
|
@@ -692,17 +712,38 @@ function providerPlan(ctx) {
|
|
|
692
712
|
]
|
|
693
713
|
};
|
|
694
714
|
}
|
|
715
|
+
function buildKeyPlan(key) {
|
|
716
|
+
return {
|
|
717
|
+
fileWrites: [
|
|
718
|
+
{
|
|
719
|
+
kind: "json",
|
|
720
|
+
path: opencodeAuthPath(),
|
|
721
|
+
merge: (existing) => {
|
|
722
|
+
const obj = existing ?? {};
|
|
723
|
+
delete obj["lunaroute/"];
|
|
724
|
+
return { ...obj, lunaroute: { type: "api", key } };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
],
|
|
728
|
+
exports: [],
|
|
729
|
+
notes: [
|
|
730
|
+
"opencode: key stored in ~/.local/share/opencode/auth.json \u2014 start opencode (or restart it), then /models to pick a lunaroute/* model."
|
|
731
|
+
]
|
|
732
|
+
};
|
|
733
|
+
}
|
|
695
734
|
|
|
696
735
|
// src/setup/adapters/pi.ts
|
|
736
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
697
737
|
function buildPiModelEntry(m) {
|
|
738
|
+
if (!m.context_window_tokens || !m.max_output_tokens) return null;
|
|
698
739
|
const reasoning = m.capabilities?.reasoning === true;
|
|
699
740
|
const entry = {
|
|
700
741
|
id: m.id,
|
|
701
742
|
name: m.display_name ?? m.id,
|
|
702
743
|
reasoning,
|
|
703
744
|
input: m.capabilities?.vision ? ["text", "image"] : ["text"],
|
|
704
|
-
contextWindow: m.context_window_tokens
|
|
705
|
-
maxTokens: m.max_output_tokens
|
|
745
|
+
contextWindow: m.context_window_tokens,
|
|
746
|
+
maxTokens: m.max_output_tokens
|
|
706
747
|
};
|
|
707
748
|
if (!reasoning) return entry;
|
|
708
749
|
const pi = m.client_compat?.pi;
|
|
@@ -718,13 +759,32 @@ function buildPiModelEntry(m) {
|
|
|
718
759
|
return entry;
|
|
719
760
|
}
|
|
720
761
|
function buildPlan2(ctx) {
|
|
762
|
+
const skipped = [];
|
|
763
|
+
const models = [];
|
|
764
|
+
for (const m of ctx.models) {
|
|
765
|
+
const entry = buildPiModelEntry(m);
|
|
766
|
+
if (entry) models.push(entry);
|
|
767
|
+
else skipped.push(m.id);
|
|
768
|
+
}
|
|
769
|
+
if (models.length === 0) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`no LunaRoute model has a context window in the catalog${skipped.length ? ` (skipped: ${skipped.join(", ")})` : ""}`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
721
774
|
const block = {
|
|
722
775
|
baseUrl: `${ctx.routingUrl}/v1`,
|
|
723
776
|
api: "openai-completions",
|
|
724
777
|
// Real key; rides the native Authorization: Bearer header.
|
|
725
778
|
apiKey: `$${ctx.keyEnvVar}`,
|
|
726
|
-
models
|
|
779
|
+
models
|
|
727
780
|
};
|
|
781
|
+
const notes = [
|
|
782
|
+
"\npi: open /model to pick a LunaRoute model (models.json hot-reloads).",
|
|
783
|
+
"Tip: the lunaroute-pi-extension auto-registers models from /v1/models so you can skip this file \u2014 `pi install npm:@lunaroute/pi-extension` then `/login lunaroute`."
|
|
784
|
+
];
|
|
785
|
+
if (skipped.length > 0) {
|
|
786
|
+
notes.push(`Skipped (no context window in the catalog): ${skipped.join(", ")}`);
|
|
787
|
+
}
|
|
728
788
|
return {
|
|
729
789
|
fileWrites: [
|
|
730
790
|
{
|
|
@@ -739,12 +799,60 @@ function buildPlan2(ctx) {
|
|
|
739
799
|
}
|
|
740
800
|
],
|
|
741
801
|
exports: [{ name: ctx.keyEnvVar, value: "__KEY__" }],
|
|
802
|
+
notes
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function stalePiProviderExists() {
|
|
806
|
+
try {
|
|
807
|
+
const obj = JSON.parse(readFileSync3(piModelsPath(), "utf8"));
|
|
808
|
+
return Boolean(obj.providers?.lunaroute);
|
|
809
|
+
} catch {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function buildStaleProviderCleanupPlan() {
|
|
814
|
+
return {
|
|
815
|
+
fileWrites: [
|
|
816
|
+
{
|
|
817
|
+
kind: "json",
|
|
818
|
+
path: piModelsPath(),
|
|
819
|
+
merge: (existing) => {
|
|
820
|
+
const obj = existing ?? {};
|
|
821
|
+
const providers = { ...obj.providers };
|
|
822
|
+
delete providers.lunaroute;
|
|
823
|
+
const next = { ...obj };
|
|
824
|
+
if (Object.keys(providers).length > 0) next.providers = providers;
|
|
825
|
+
else delete next.providers;
|
|
826
|
+
return next;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
],
|
|
830
|
+
exports: [],
|
|
742
831
|
notes: [
|
|
743
|
-
"
|
|
744
|
-
"Tip: the lunaroute-pi-extension auto-registers models from /v1/models so you can skip this file \u2014 `pi install npm:@lunaroute/pi-extension` then `/login lunaroute`."
|
|
832
|
+
"pi: removed the stale providers.lunaroute block from ~/.pi/agent/models.json (backup kept) \u2014 the extension syncs models live."
|
|
745
833
|
]
|
|
746
834
|
};
|
|
747
835
|
}
|
|
836
|
+
var KEY_EXPIRES_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
837
|
+
function buildKeyPlan2(key) {
|
|
838
|
+
return {
|
|
839
|
+
fileWrites: [
|
|
840
|
+
{
|
|
841
|
+
kind: "json",
|
|
842
|
+
path: piAuthPath(),
|
|
843
|
+
merge: (existing) => {
|
|
844
|
+
const obj = existing ?? {};
|
|
845
|
+
return {
|
|
846
|
+
...obj,
|
|
847
|
+
lunaroute: { type: "oauth", access: key, refresh: "", expires: Date.now() + KEY_EXPIRES_MS }
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
],
|
|
852
|
+
exports: [],
|
|
853
|
+
notes: ["pi: key stored in ~/.pi/agent/auth.json \u2014 start pi, then /model to pick a lunaroute/* model."]
|
|
854
|
+
};
|
|
855
|
+
}
|
|
748
856
|
|
|
749
857
|
// src/setup/adapters/claudeCode.ts
|
|
750
858
|
function buildPlan3(ctx) {
|
|
@@ -911,7 +1019,8 @@ var ADAPTERS = {
|
|
|
911
1019
|
"copilot-cli": buildPlan4,
|
|
912
1020
|
generic: buildPlan5
|
|
913
1021
|
};
|
|
914
|
-
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"
|
|
1022
|
+
var PI_INSTALL_PACKAGES = ["npm:@lunaroute/pi-extension"];
|
|
1023
|
+
var PI_INSTALL_HINT = "Install pi first: npm install -g --ignore-scripts @earendil-works/pi-coding-agent (https://pi.dev)";
|
|
915
1024
|
async function runSetup(harness, opts, deps = {
|
|
916
1025
|
confirm,
|
|
917
1026
|
spawn: makeRealSpawn(),
|
|
@@ -921,6 +1030,10 @@ async function runSetup(harness, opts, deps = {
|
|
|
921
1030
|
console.error("Choose one of --extension or --models.");
|
|
922
1031
|
return 1;
|
|
923
1032
|
}
|
|
1033
|
+
if (opts.key && (opts.extension || opts.models)) {
|
|
1034
|
+
console.error("--key runs the full pi or opencode flow; drop --extension/--models.");
|
|
1035
|
+
return 1;
|
|
1036
|
+
}
|
|
924
1037
|
const adapter = ADAPTERS[harness];
|
|
925
1038
|
if (!adapter && harness !== "hermes") {
|
|
926
1039
|
console.error(
|
|
@@ -930,22 +1043,11 @@ async function runSetup(harness, opts, deps = {
|
|
|
930
1043
|
}
|
|
931
1044
|
let stored = loadProfile(opts.profile);
|
|
932
1045
|
const settings = resolveSettings(opts.profile);
|
|
933
|
-
if (opts.key) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return 1;
|
|
939
|
-
}
|
|
940
|
-
saveProfile(opts.profile, {
|
|
941
|
-
api_url: settings.api_url,
|
|
942
|
-
routing_url: opts.routingUrl || settings.routing_url,
|
|
943
|
-
front_url: settings.front_url,
|
|
944
|
-
org_id: stored?.org_id ?? "",
|
|
945
|
-
user_email: stored?.user_email ?? "",
|
|
946
|
-
routing_key: opts.key
|
|
947
|
-
});
|
|
948
|
-
stored = loadProfile(opts.profile);
|
|
1046
|
+
if (opts.key && !LR_KEY_RE.test(opts.key)) {
|
|
1047
|
+
console.error(
|
|
1048
|
+
"Invalid key: a LunaRoute routing key starts with lr_ (copy the whole key from the dashboard)."
|
|
1049
|
+
);
|
|
1050
|
+
return 1;
|
|
949
1051
|
}
|
|
950
1052
|
const routingUrlRaw = opts.routingUrl || stored?.routing_url || settings.routing_url;
|
|
951
1053
|
if (!routingUrlRaw) {
|
|
@@ -959,6 +1061,29 @@ async function runSetup(harness, opts, deps = {
|
|
|
959
1061
|
console.error(err instanceof Error ? err.message : err);
|
|
960
1062
|
return 1;
|
|
961
1063
|
}
|
|
1064
|
+
if (opts.key) {
|
|
1065
|
+
try {
|
|
1066
|
+
await fetchModels(routingUrl, opts.key);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
if (err instanceof KeyRejectedError) {
|
|
1069
|
+
console.error(err.message);
|
|
1070
|
+
return 1;
|
|
1071
|
+
}
|
|
1072
|
+
console.warn(`Note: ${err instanceof Error ? err.message : err}`);
|
|
1073
|
+
console.warn(
|
|
1074
|
+
"Continuing without a synced catalog. If your organization has no chat models enabled yet, contact hello@lunaroute.com \u2014 models appear on the next refresh once they are on."
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
saveProfile(opts.profile, {
|
|
1078
|
+
api_url: settings.api_url,
|
|
1079
|
+
routing_url: opts.routingUrl || settings.routing_url,
|
|
1080
|
+
front_url: settings.front_url,
|
|
1081
|
+
org_id: stored?.org_id ?? "",
|
|
1082
|
+
user_email: stored?.user_email ?? "",
|
|
1083
|
+
routing_key: opts.key
|
|
1084
|
+
});
|
|
1085
|
+
stored = loadProfile(opts.profile);
|
|
1086
|
+
}
|
|
962
1087
|
if (harness === "pi" && !opts.print) {
|
|
963
1088
|
return runPiSetup(opts, stored, routingUrl, deps);
|
|
964
1089
|
}
|
|
@@ -987,7 +1112,7 @@ async function runSetup(harness, opts, deps = {
|
|
|
987
1112
|
};
|
|
988
1113
|
let plan;
|
|
989
1114
|
try {
|
|
990
|
-
plan = adapter(ctx);
|
|
1115
|
+
plan = harness === "pi" && opts.key ? buildKeyPlan2("<your-api-key>") : adapter(ctx);
|
|
991
1116
|
} catch (err) {
|
|
992
1117
|
console.error(err instanceof Error ? err.message : err);
|
|
993
1118
|
return 1;
|
|
@@ -1043,38 +1168,36 @@ async function runPiSetup(opts, stored, routingUrl, deps) {
|
|
|
1043
1168
|
if (opts.key) {
|
|
1044
1169
|
const creds2 = await requireCreds(opts, deps, stored);
|
|
1045
1170
|
if (!creds2) return 1;
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
console.warn(
|
|
1056
|
-
"Writing the provider block with no models. Your organization has no models enabled yet \u2014 contact hello@lunaroute.com to get them turned on, then re-run this command to sync them."
|
|
1171
|
+
const install = await deps.confirm(
|
|
1172
|
+
"Install the LunaRoute Pi extension via 'pi install'?",
|
|
1173
|
+
{ yes: opts.yes }
|
|
1174
|
+
);
|
|
1175
|
+
if (!install) {
|
|
1176
|
+
return applyPiModels(
|
|
1177
|
+
creds2,
|
|
1178
|
+
routingUrl,
|
|
1179
|
+
await deps.confirm("Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?", { yes: opts.yes })
|
|
1057
1180
|
);
|
|
1058
1181
|
}
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
keyEnvVar: "LUNAROUTE_API_KEY"
|
|
1064
|
-
});
|
|
1065
|
-
const write = await deps.confirm(
|
|
1066
|
-
"Merge LunaRoute provider into ~/.pi/agent/models.json (backup kept, other providers preserved)?",
|
|
1182
|
+
const code = await installPiPackages(deps);
|
|
1183
|
+
if (code !== 0) return code;
|
|
1184
|
+
const store = await deps.confirm(
|
|
1185
|
+
"Store the key in ~/.pi/agent/auth.json (replaces any existing LunaRoute login; other credentials preserved)?",
|
|
1067
1186
|
{ yes: opts.yes }
|
|
1068
1187
|
);
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1188
|
+
if (!store) {
|
|
1189
|
+
console.log("\nSkipped. Inside pi, run /login lunaroute to sign in.");
|
|
1190
|
+
return 0;
|
|
1072
1191
|
}
|
|
1073
|
-
|
|
1192
|
+
const plan = combinePlans(buildKeyPlan2(creds2.routing_key), stalePiProviderExists() ? buildStaleProviderCleanupPlan() : null);
|
|
1193
|
+
const wcode = await applyAndReport(plan, creds2.routing_key, true);
|
|
1194
|
+
if (wcode !== 0) return wcode;
|
|
1195
|
+
console.log("\nDone. Start pi \u2014 it is signed in. Pick a model with /model.");
|
|
1196
|
+
return 0;
|
|
1074
1197
|
}
|
|
1075
1198
|
if (opts.extension) {
|
|
1076
1199
|
const ok = await deps.confirm(
|
|
1077
|
-
"Install the LunaRoute Pi extension
|
|
1200
|
+
"Install the LunaRoute Pi extension via 'pi install'?",
|
|
1078
1201
|
{ yes: opts.yes }
|
|
1079
1202
|
);
|
|
1080
1203
|
if (!ok) {
|
|
@@ -1094,7 +1217,7 @@ async function runPiSetup(opts, stored, routingUrl, deps) {
|
|
|
1094
1217
|
);
|
|
1095
1218
|
}
|
|
1096
1219
|
if (await deps.confirm(
|
|
1097
|
-
"Install the LunaRoute Pi extension
|
|
1220
|
+
"Install the LunaRoute Pi extension via 'pi install' (recommended \u2014 auto-registers models)?",
|
|
1098
1221
|
{ yes: opts.yes }
|
|
1099
1222
|
)) {
|
|
1100
1223
|
return installPiExtension(deps, opts);
|
|
@@ -1191,11 +1314,48 @@ async function runHermesConfigSets(spawn, routingUrl, key) {
|
|
|
1191
1314
|
async function runOpencodeSetup(stored, settings, routingUrl, opts, deps) {
|
|
1192
1315
|
try {
|
|
1193
1316
|
if (opts.print) {
|
|
1317
|
+
const plan = combinePlans(
|
|
1318
|
+
buildPlan(
|
|
1319
|
+
{ routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
|
|
1320
|
+
{ extension: true }
|
|
1321
|
+
),
|
|
1322
|
+
opts.key ? buildKeyPlan("<your-api-key>") : null
|
|
1323
|
+
);
|
|
1324
|
+
return applyAndReport(plan, stored?.routing_key ?? "", false);
|
|
1325
|
+
}
|
|
1326
|
+
if (opts.key) {
|
|
1327
|
+
const creds2 = await requireCreds(opts, deps, stored);
|
|
1328
|
+
if (!creds2) return 1;
|
|
1329
|
+
const install = await deps.confirm("Install the LunaRoute OpenCode extension?", {
|
|
1330
|
+
yes: opts.yes
|
|
1331
|
+
});
|
|
1332
|
+
if (!install) {
|
|
1333
|
+
return applyOpencodeProvider(
|
|
1334
|
+
creds2,
|
|
1335
|
+
routingUrl,
|
|
1336
|
+
await deps.confirm("Merge LunaRoute provider into opencode.json (backup kept, other providers preserved)?", {
|
|
1337
|
+
yes: opts.yes
|
|
1338
|
+
})
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1194
1341
|
const plan = buildPlan(
|
|
1195
1342
|
{ routingUrl, orgId: settings.org_id, models: [], keyEnvVar: "LUNAROUTE_API_KEY" },
|
|
1196
1343
|
{ extension: true }
|
|
1197
1344
|
);
|
|
1198
|
-
|
|
1345
|
+
const code = await applyAndReport(plan, creds2.routing_key, true);
|
|
1346
|
+
if (code !== 0) return code;
|
|
1347
|
+
const store = await deps.confirm(
|
|
1348
|
+
"Store the key in ~/.local/share/opencode/auth.json (replaces any existing LunaRoute login; other credentials preserved)?",
|
|
1349
|
+
{ yes: opts.yes }
|
|
1350
|
+
);
|
|
1351
|
+
if (!store) {
|
|
1352
|
+
console.log("\nSkipped. Inside opencode, run /connect to sign in.");
|
|
1353
|
+
return 0;
|
|
1354
|
+
}
|
|
1355
|
+
const wcode = await applyAndReport(buildKeyPlan(creds2.routing_key), creds2.routing_key, true);
|
|
1356
|
+
if (wcode !== 0) return wcode;
|
|
1357
|
+
console.log("\nDone. Start opencode \u2014 it is signed in. Pick a model with /models.");
|
|
1358
|
+
return 0;
|
|
1199
1359
|
}
|
|
1200
1360
|
if (await deps.confirm(
|
|
1201
1361
|
"Install the LunaRoute OpenCode extension (recommended \u2014 /connect login, models auto-sync)?",
|
|
@@ -1249,6 +1409,14 @@ async function applyOpencodeProvider(creds, routingUrl, write) {
|
|
|
1249
1409
|
});
|
|
1250
1410
|
return applyAndReport(plan, creds.routing_key, write);
|
|
1251
1411
|
}
|
|
1412
|
+
function combinePlans(a, b) {
|
|
1413
|
+
if (!b) return a;
|
|
1414
|
+
return {
|
|
1415
|
+
fileWrites: [...a.fileWrites, ...b.fileWrites],
|
|
1416
|
+
exports: [...a.exports, ...b.exports],
|
|
1417
|
+
notes: [...a.notes, ...b.notes]
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1252
1420
|
async function applyAndReport(plan, key, write) {
|
|
1253
1421
|
try {
|
|
1254
1422
|
const summary = await applyPlan(plan, { print: !write, key });
|
|
@@ -1280,16 +1448,12 @@ async function launchOpencode(spawn) {
|
|
|
1280
1448
|
});
|
|
1281
1449
|
});
|
|
1282
1450
|
}
|
|
1283
|
-
async function
|
|
1451
|
+
async function installPiPackages(deps) {
|
|
1284
1452
|
for (const pkg of PI_INSTALL_PACKAGES) {
|
|
1285
1453
|
const code = await new Promise((resolve) => {
|
|
1286
1454
|
const child = deps.spawn("pi", ["install", pkg]);
|
|
1287
1455
|
child.on("error", (err) => {
|
|
1288
|
-
const { message, exitCode } = describeSpawnError(
|
|
1289
|
-
"pi",
|
|
1290
|
-
"Install pi first: https://pi.dev",
|
|
1291
|
-
err
|
|
1292
|
-
);
|
|
1456
|
+
const { message, exitCode } = describeSpawnError("pi", PI_INSTALL_HINT, err);
|
|
1293
1457
|
console.error(message);
|
|
1294
1458
|
resolve(exitCode);
|
|
1295
1459
|
});
|
|
@@ -1300,11 +1464,26 @@ async function installPiExtension(deps, opts) {
|
|
|
1300
1464
|
if (code !== 0) {
|
|
1301
1465
|
const done = PI_INSTALL_PACKAGES.slice(0, PI_INSTALL_PACKAGES.indexOf(pkg));
|
|
1302
1466
|
console.error(
|
|
1303
|
-
`'pi install ${pkg}' failed (exit ${code}). Installed so far: ${done.length ? done.join(", ") : "nothing"}. Re-run
|
|
1467
|
+
`'pi install ${pkg}' failed (exit ${code}). Installed so far: ${done.length ? done.join(", ") : "nothing"}. Re-run the same command to retry.`
|
|
1304
1468
|
);
|
|
1305
1469
|
return code || 1;
|
|
1306
1470
|
}
|
|
1307
1471
|
}
|
|
1472
|
+
return 0;
|
|
1473
|
+
}
|
|
1474
|
+
async function installPiExtension(deps, opts) {
|
|
1475
|
+
const code = await installPiPackages(deps);
|
|
1476
|
+
if (code !== 0) return code;
|
|
1477
|
+
if (stalePiProviderExists()) {
|
|
1478
|
+
const remove = await deps.confirm(
|
|
1479
|
+
"Remove the stale LunaRoute block from ~/.pi/agent/models.json (backup kept, other providers preserved)?",
|
|
1480
|
+
{ yes: opts.yes }
|
|
1481
|
+
);
|
|
1482
|
+
if (remove) {
|
|
1483
|
+
const ccode = await applyAndReport(buildStaleProviderCleanupPlan(), "", true);
|
|
1484
|
+
if (ccode !== 0) return ccode;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1308
1487
|
if (await deps.confirm("Start pi now? (then run /login lunaroute to sign in)", { yes: opts.yes })) {
|
|
1309
1488
|
return launchPi(deps.spawn);
|
|
1310
1489
|
}
|
|
@@ -1317,11 +1496,7 @@ async function launchPi(spawn) {
|
|
|
1317
1496
|
return new Promise((resolve) => {
|
|
1318
1497
|
const child = spawn("pi", []);
|
|
1319
1498
|
child.on("error", (err) => {
|
|
1320
|
-
const { message, exitCode } = describeSpawnError(
|
|
1321
|
-
"pi",
|
|
1322
|
-
"Install pi first: https://pi.dev",
|
|
1323
|
-
err
|
|
1324
|
-
);
|
|
1499
|
+
const { message, exitCode } = describeSpawnError("pi", PI_INSTALL_HINT, err);
|
|
1325
1500
|
console.error(message);
|
|
1326
1501
|
resolve(exitCode);
|
|
1327
1502
|
});
|
|
@@ -1991,7 +2166,7 @@ program.command("whoami").description("Show the signed-in user and organization.
|
|
|
1991
2166
|
program.command("logout").description("Remove stored credentials for the active profile.").action(() => {
|
|
1992
2167
|
logout(program.opts().profile);
|
|
1993
2168
|
});
|
|
1994
|
-
program.command("setup <harness>").description("Configure a coding harness (opencode | pi | hermes | claude-code | copilot-cli | openclaw | generic).").option("--print", "print the config instead of writing files", false).option("--routing-url <url>", "override the routing base URL").option("--yes", "accept all confirmation prompts without a TTY (for scripts)", false).option("--extension", "pi only: jump straight to installing the Pi extension
|
|
2169
|
+
program.command("setup <harness>").description("Configure a coding harness (opencode | pi | hermes | claude-code | copilot-cli | openclaw | generic).").option("--print", "print the config instead of writing files", false).option("--routing-url <url>", "override the routing base URL").option("--yes", "accept all confirmation prompts without a TTY (for scripts)", false).option("--extension", "pi only: jump straight to installing the Pi extension").option("--models", "pi only: jump straight to writing the models.json provider block").option("--key <key>", "a LunaRoute routing key (lr_\u2026), e.g. from the dashboard's onboarding page \u2014 stores it and skips the browser login").action(async (harness, opts) => {
|
|
1995
2170
|
const code = await runSetup(harness, {
|
|
1996
2171
|
profile: program.opts().profile,
|
|
1997
2172
|
print: opts.print,
|