@srouter/cli 0.1.3-rc.2 → 0.1.3-rc.4
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/LICENSE +21 -0
- package/bin/srouter.js +3 -37
- package/dist/chunk-BJ7B4R26.js +42 -0
- package/dist/chunk-BJ7B4R26.js.map +1 -0
- package/dist/esm-5KJBUOHT.js +5321 -0
- package/dist/esm-5KJBUOHT.js.map +1 -0
- package/dist/index.js +1501 -490
- package/dist/index.js.map +1 -1
- package/package.json +43 -32
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "./chunk-BJ7B4R26.js";
|
|
2
|
+
|
|
1
3
|
// src/index.ts
|
|
2
4
|
import { Command } from "commander";
|
|
3
5
|
|
|
@@ -32,7 +34,7 @@ var GOROUTER_BASE_URL = "https://gorouter.app/v1";
|
|
|
32
34
|
var GOROUTER_PROVIDER = {
|
|
33
35
|
id: "gorouter",
|
|
34
36
|
name: "GoRouter",
|
|
35
|
-
category: "
|
|
37
|
+
category: "free_tier",
|
|
36
38
|
protocol: "openai",
|
|
37
39
|
base_url: GOROUTER_BASE_URL,
|
|
38
40
|
web_url: "https://gorouter.app/sign-up?aff=cJJn",
|
|
@@ -46,7 +48,7 @@ var BLUESMINDS_BASE_URL = "https://api.bluesminds.com/v1";
|
|
|
46
48
|
var BLUESMINDS_PROVIDER = {
|
|
47
49
|
id: "bluesminds",
|
|
48
50
|
name: "BluesMinds",
|
|
49
|
-
category: "
|
|
51
|
+
category: "free_tier",
|
|
50
52
|
protocol: "openai",
|
|
51
53
|
base_url: BLUESMINDS_BASE_URL,
|
|
52
54
|
web_url: "https://api.bluesminds.com/sign-up?aff=nCAw",
|
|
@@ -60,7 +62,7 @@ var SEEKAI_BASE_URL = "https://seekai.cc/v1";
|
|
|
60
62
|
var SEEKAI_PROVIDER = {
|
|
61
63
|
id: "seekai",
|
|
62
64
|
name: "SeekAI",
|
|
63
|
-
category: "
|
|
65
|
+
category: "free_tier",
|
|
64
66
|
protocol: "openai",
|
|
65
67
|
base_url: SEEKAI_BASE_URL,
|
|
66
68
|
web_url: "https://seekai.cc/sign-up?aff=UU0C",
|
|
@@ -74,7 +76,7 @@ var TABITOKEN_BASE_URL = "https://tabitoken.com/v1";
|
|
|
74
76
|
var TABITOKEN_PROVIDER = {
|
|
75
77
|
id: "tabitoken",
|
|
76
78
|
name: "TabiToken",
|
|
77
|
-
category: "
|
|
79
|
+
category: "free_tier",
|
|
78
80
|
protocol: "openai",
|
|
79
81
|
base_url: TABITOKEN_BASE_URL,
|
|
80
82
|
web_url: "https://tabitoken.com/sign-up?aff=h5iN",
|
|
@@ -140,6 +142,9 @@ var ANTHROPIC_PROVIDER = {
|
|
|
140
142
|
// ../../packages/constants/dist/providers/antigravity.js
|
|
141
143
|
var ANTIGRAVITY_IDE_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
|
|
142
144
|
var ANTIGRAVITY_MODELS = [
|
|
145
|
+
{ id: "gemini-3.8-flash-high", name: "Gemini 3.8 Flash (High)" },
|
|
146
|
+
{ id: "gemini-3.8-flash-medium", name: "Gemini 3.8 Flash (Medium)" },
|
|
147
|
+
{ id: "gemini-3.8-flash-low", name: "Gemini 3.8 Flash (Low)" },
|
|
143
148
|
{ id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)" },
|
|
144
149
|
{ id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)" },
|
|
145
150
|
{ id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)" },
|
|
@@ -290,7 +295,6 @@ var QODER_PROVIDER = {
|
|
|
290
295
|
// ../../packages/constants/dist/providers/opencode.js
|
|
291
296
|
var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
|
|
292
297
|
var OPENCODE_ZEN_MODELS = [
|
|
293
|
-
{ id: "x-preview-f-free", name: "Ox Alpha Free (Unlimited)" },
|
|
294
298
|
{ id: "big-pickle", name: "Big Pickle (Free)" },
|
|
295
299
|
{ id: "laguna-s-2.1-free", name: "Poolside Laguna S 2.1 (Free)" },
|
|
296
300
|
{ id: "nemotron-3.5-lightning-free", name: "Nemotron 3.5 Lightning (Free)" },
|
|
@@ -312,6 +316,21 @@ var OPENCODE_ZEN_PROVIDER = {
|
|
|
312
316
|
status_message: "Free Tier Ready (Unlimited)"
|
|
313
317
|
};
|
|
314
318
|
|
|
319
|
+
// ../../packages/constants/dist/providers/bai.js
|
|
320
|
+
var BAI_BASE_URL = "https://api.b.ai/v1";
|
|
321
|
+
var BAI_PROVIDER = {
|
|
322
|
+
id: "bai",
|
|
323
|
+
name: "B.AI",
|
|
324
|
+
category: "free_tier",
|
|
325
|
+
protocol: "openai",
|
|
326
|
+
alias: "bai",
|
|
327
|
+
base_url: BAI_BASE_URL,
|
|
328
|
+
web_url: "https://b.ai",
|
|
329
|
+
requires_api_key: true,
|
|
330
|
+
supports_custom_url: true,
|
|
331
|
+
status_message: "B.AI API key missing"
|
|
332
|
+
};
|
|
333
|
+
|
|
315
334
|
// ../../packages/constants/dist/providers/catalog.js
|
|
316
335
|
var KNOWN_PROVIDERS = [
|
|
317
336
|
KIRO_PROVIDER,
|
|
@@ -328,13 +347,19 @@ var KNOWN_PROVIDERS = [
|
|
|
328
347
|
QODER_PROVIDER,
|
|
329
348
|
CODEBUDDY_PROVIDER,
|
|
330
349
|
CODEBUDDY_CN_PROVIDER,
|
|
331
|
-
OPENCODE_ZEN_PROVIDER
|
|
350
|
+
OPENCODE_ZEN_PROVIDER,
|
|
351
|
+
BAI_PROVIDER
|
|
332
352
|
];
|
|
333
353
|
var KNOWN_PROVIDER_MAP = Object.freeze(Object.fromEntries(KNOWN_PROVIDERS.map((Provider) => [Provider.id, Provider])));
|
|
334
354
|
var KNOWN_PROVIDER_IDS_DESC = Object.freeze(Object.keys(KNOWN_PROVIDER_MAP).sort((A, B) => B.length - A.length));
|
|
335
355
|
var LEGACY_ALIAS_MAP = Object.freeze({
|
|
336
356
|
claude: "claude",
|
|
337
|
-
cbai: "codebuddy"
|
|
357
|
+
cbai: "codebuddy",
|
|
358
|
+
// OpenCode Zen's executor registers with base id "opencode_zen"; its
|
|
359
|
+
// listModels splits on "_" and produces "opencode/<model>" ids, while
|
|
360
|
+
// alias-prefixed connections produce "zen/<model>". Map both so log
|
|
361
|
+
// attribution and routing resolve to the registered base id.
|
|
362
|
+
opencode: "opencode_zen"
|
|
338
363
|
});
|
|
339
364
|
|
|
340
365
|
// ../../packages/constants/dist/seed.js
|
|
@@ -342,8 +367,8 @@ var DEFAULT_PROVIDERS = Object.freeze(KNOWN_PROVIDERS.map(({ alias: _alias, ...s
|
|
|
342
367
|
var DEFAULT_PROVIDER_MAP = Object.freeze(Object.fromEntries(DEFAULT_PROVIDERS.map((seed) => [seed.id, seed])));
|
|
343
368
|
|
|
344
369
|
// ../../packages/constants/dist/version.js
|
|
345
|
-
var GLOBAL_VERSION = "0.1.
|
|
346
|
-
var CLI_VERSION =
|
|
370
|
+
var GLOBAL_VERSION = "0.1.4";
|
|
371
|
+
var CLI_VERSION = "0.1.3-rc.4";
|
|
347
372
|
var SROUTER_VERSION_TAG = `v${GLOBAL_VERSION}`;
|
|
348
373
|
|
|
349
374
|
// src/commands/setup.ts
|
|
@@ -475,10 +500,10 @@ import { exec } from "node:child_process";
|
|
|
475
500
|
import { promisify } from "node:util";
|
|
476
501
|
var execAsync = promisify(exec);
|
|
477
502
|
function getPlatform() {
|
|
478
|
-
const
|
|
479
|
-
if (
|
|
480
|
-
if (
|
|
481
|
-
if (
|
|
503
|
+
const p3 = process.platform;
|
|
504
|
+
if (p3 === "win32") return "windows";
|
|
505
|
+
if (p3 === "darwin") return "macos";
|
|
506
|
+
if (p3 === "linux") return "linux";
|
|
482
507
|
return "unknown";
|
|
483
508
|
}
|
|
484
509
|
function isWindows() {
|
|
@@ -488,10 +513,10 @@ function isMacOS() {
|
|
|
488
513
|
return process.platform === "darwin";
|
|
489
514
|
}
|
|
490
515
|
function getOsDisplayName() {
|
|
491
|
-
const
|
|
516
|
+
const p3 = getPlatform();
|
|
492
517
|
const arch = os2.arch();
|
|
493
518
|
const rel = os2.release();
|
|
494
|
-
switch (
|
|
519
|
+
switch (p3) {
|
|
495
520
|
case "macos":
|
|
496
521
|
return `macOS (${arch === "arm64" ? "Apple Silicon" : arch}) [Darwin ${rel}]`;
|
|
497
522
|
case "windows":
|
|
@@ -561,9 +586,9 @@ function getClaudeConfigPath() {
|
|
|
561
586
|
candidatePaths.push(path2.join(home, ".claude", "config.json"));
|
|
562
587
|
candidatePaths.push(path2.join(home, ".config", "claude", "config.json"));
|
|
563
588
|
}
|
|
564
|
-
for (const
|
|
565
|
-
if (fs2.existsSync(
|
|
566
|
-
return
|
|
589
|
+
for (const p3 of candidatePaths) {
|
|
590
|
+
if (fs2.existsSync(p3)) {
|
|
591
|
+
return p3;
|
|
567
592
|
}
|
|
568
593
|
}
|
|
569
594
|
if (fs2.existsSync(path2.join(home, ".claude"))) {
|
|
@@ -605,9 +630,9 @@ function getOpenCodeConfigPath() {
|
|
|
605
630
|
}
|
|
606
631
|
candidatePaths.push(path2.join(home, ".opencode.jsonc"));
|
|
607
632
|
candidatePaths.push(path2.join(home, ".opencode.json"));
|
|
608
|
-
for (const
|
|
609
|
-
if (fs2.existsSync(
|
|
610
|
-
return
|
|
633
|
+
for (const p3 of candidatePaths) {
|
|
634
|
+
if (fs2.existsSync(p3)) {
|
|
635
|
+
return p3;
|
|
611
636
|
}
|
|
612
637
|
}
|
|
613
638
|
if (isWindows()) {
|
|
@@ -721,6 +746,7 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
|
|
|
721
746
|
data.env.ANTHROPIC_DEFAULT_OPUS_MODEL = context.opusModel || defaultModel;
|
|
722
747
|
data.env.ANTHROPIC_DEFAULT_SONNET_MODEL = context.sonnetModel || defaultModel;
|
|
723
748
|
data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = context.haikuModel || defaultModel;
|
|
749
|
+
data.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT = "1";
|
|
724
750
|
if (data.env.ANTHROPIC_DEFAULT_FABLE_MODEL) {
|
|
725
751
|
delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
|
|
726
752
|
}
|
|
@@ -783,6 +809,7 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
|
|
|
783
809
|
delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
784
810
|
delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
785
811
|
delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
|
|
812
|
+
delete data.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT;
|
|
786
813
|
}
|
|
787
814
|
await fs3.writeFile(configPath, JSON.stringify(data, null, 2), "utf-8");
|
|
788
815
|
return true;
|
|
@@ -802,15 +829,16 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
|
|
|
802
829
|
ANTHROPIC_MODEL: defaultModel,
|
|
803
830
|
ANTHROPIC_DEFAULT_OPUS_MODEL: context.opusModel || defaultModel,
|
|
804
831
|
ANTHROPIC_DEFAULT_SONNET_MODEL: context.sonnetModel || defaultModel,
|
|
805
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel
|
|
832
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel,
|
|
833
|
+
CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT: "1"
|
|
806
834
|
};
|
|
807
835
|
return env;
|
|
808
836
|
}
|
|
809
837
|
};
|
|
810
838
|
|
|
811
839
|
// src/adapters/opencode.ts
|
|
812
|
-
import
|
|
813
|
-
import
|
|
840
|
+
import fs5 from "node:fs/promises";
|
|
841
|
+
import path5 from "node:path";
|
|
814
842
|
|
|
815
843
|
// src/lib/srouterClient.ts
|
|
816
844
|
function normalizeBaseUrl(baseUrl) {
|
|
@@ -881,7 +909,361 @@ async function fetchAvailableModels(baseUrl, apiKey) {
|
|
|
881
909
|
}
|
|
882
910
|
}
|
|
883
911
|
|
|
912
|
+
// ../../packages/pricing/dist/parser.js
|
|
913
|
+
import fs4 from "node:fs";
|
|
914
|
+
import path4 from "node:path";
|
|
915
|
+
import { fileURLToPath } from "node:url";
|
|
916
|
+
function stripJsonComments(jsonc) {
|
|
917
|
+
let insideString = false;
|
|
918
|
+
let isEscaped = false;
|
|
919
|
+
let output = "";
|
|
920
|
+
let i = 0;
|
|
921
|
+
while (i < jsonc.length) {
|
|
922
|
+
const char = jsonc[i];
|
|
923
|
+
const nextChar = jsonc[i + 1];
|
|
924
|
+
if (insideString) {
|
|
925
|
+
output += char;
|
|
926
|
+
if (isEscaped) {
|
|
927
|
+
isEscaped = false;
|
|
928
|
+
} else if (char === "\\") {
|
|
929
|
+
isEscaped = true;
|
|
930
|
+
} else if (char === '"') {
|
|
931
|
+
insideString = false;
|
|
932
|
+
}
|
|
933
|
+
i++;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (char === '"') {
|
|
937
|
+
insideString = true;
|
|
938
|
+
output += char;
|
|
939
|
+
i++;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (char === "/" && nextChar === "/") {
|
|
943
|
+
i += 2;
|
|
944
|
+
while (i < jsonc.length && jsonc[i] !== "\n" && jsonc[i] !== "\r") {
|
|
945
|
+
i++;
|
|
946
|
+
}
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (char === "/" && nextChar === "*") {
|
|
950
|
+
i += 2;
|
|
951
|
+
while (i < jsonc.length && !(jsonc[i] === "*" && jsonc[i + 1] === "/")) {
|
|
952
|
+
i++;
|
|
953
|
+
}
|
|
954
|
+
i += 2;
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
output += char;
|
|
958
|
+
i++;
|
|
959
|
+
}
|
|
960
|
+
return output.replace(/,(\s*[}\]])/g, "$1");
|
|
961
|
+
}
|
|
962
|
+
function parseJsonc(jsonc) {
|
|
963
|
+
const cleanJson = stripJsonComments(jsonc);
|
|
964
|
+
return JSON.parse(cleanJson);
|
|
965
|
+
}
|
|
966
|
+
function flattenModelPrices(models) {
|
|
967
|
+
const flat = {};
|
|
968
|
+
for (const [key, val] of Object.entries(models || {})) {
|
|
969
|
+
if (Array.isArray(val)) {
|
|
970
|
+
for (const item of val) {
|
|
971
|
+
if (item && item.id) {
|
|
972
|
+
flat[item.id] = item;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
} else if (val && typeof val === "object") {
|
|
976
|
+
if ("input" in val && "output" in val) {
|
|
977
|
+
flat[key] = val;
|
|
978
|
+
} else {
|
|
979
|
+
for (const [subKey, subVal] of Object.entries(val)) {
|
|
980
|
+
flat[subKey] = subVal;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return flat;
|
|
986
|
+
}
|
|
987
|
+
function resolvePricingDataPath(customPath) {
|
|
988
|
+
if (customPath)
|
|
989
|
+
return customPath;
|
|
990
|
+
const currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
991
|
+
const candidates = [
|
|
992
|
+
path4.resolve(currentDir, "../data/pricing.jsonc"),
|
|
993
|
+
path4.resolve(currentDir, "../data/pricing.json"),
|
|
994
|
+
path4.resolve(currentDir, "../../data/pricing.jsonc"),
|
|
995
|
+
path4.resolve(currentDir, "../../data/pricing.json"),
|
|
996
|
+
path4.resolve(process.cwd(), "packages/pricing/data/pricing.jsonc"),
|
|
997
|
+
path4.resolve(process.cwd(), "packages/pricing/data/pricing.json"),
|
|
998
|
+
path4.resolve(process.cwd(), "data/pricing.jsonc"),
|
|
999
|
+
path4.resolve(process.cwd(), "data/pricing.json")
|
|
1000
|
+
];
|
|
1001
|
+
for (const candidate of candidates) {
|
|
1002
|
+
if (fs4.existsSync(candidate)) {
|
|
1003
|
+
return candidate;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return candidates[0];
|
|
1007
|
+
}
|
|
1008
|
+
function modelsDevToModelPrice(model) {
|
|
1009
|
+
if (!model.cost)
|
|
1010
|
+
return void 0;
|
|
1011
|
+
const input = model.cost.input ?? 0;
|
|
1012
|
+
const output = model.cost.output ?? 0;
|
|
1013
|
+
return {
|
|
1014
|
+
id: model.id,
|
|
1015
|
+
name: model.name,
|
|
1016
|
+
input,
|
|
1017
|
+
output,
|
|
1018
|
+
cached: model.cost.cache_read,
|
|
1019
|
+
reasoning: model.cost.reasoning ?? output,
|
|
1020
|
+
cache_creation: model.cost.cache_write ?? input
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function loadPricingFromModelsDev(customPath) {
|
|
1024
|
+
const modelsData = loadModelsDevData(customPath);
|
|
1025
|
+
const models = {};
|
|
1026
|
+
const providerModels = {};
|
|
1027
|
+
const aliases = {};
|
|
1028
|
+
for (const [key, item] of Object.entries(modelsData)) {
|
|
1029
|
+
const price = modelsDevToModelPrice(item);
|
|
1030
|
+
if (!price)
|
|
1031
|
+
continue;
|
|
1032
|
+
models[key] = price;
|
|
1033
|
+
if (item.id && item.id !== key) {
|
|
1034
|
+
models[item.id] = price;
|
|
1035
|
+
}
|
|
1036
|
+
if (key.includes("/")) {
|
|
1037
|
+
const modelName = key.split("/").slice(1).join("/");
|
|
1038
|
+
if (!aliases[modelName]) {
|
|
1039
|
+
aliases[modelName] = key;
|
|
1040
|
+
}
|
|
1041
|
+
if (!models[modelName]) {
|
|
1042
|
+
models[modelName] = price;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const provider = key.includes("/") ? key.split("/")[0] : "other";
|
|
1046
|
+
if (!providerModels[provider]) {
|
|
1047
|
+
providerModels[provider] = [];
|
|
1048
|
+
}
|
|
1049
|
+
providerModels[provider].push(price);
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
version: "1.0.0",
|
|
1053
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
1054
|
+
defaults: {
|
|
1055
|
+
input: 2,
|
|
1056
|
+
output: 8,
|
|
1057
|
+
cached: 1,
|
|
1058
|
+
reasoning: 12,
|
|
1059
|
+
cache_creation: 2
|
|
1060
|
+
},
|
|
1061
|
+
models,
|
|
1062
|
+
providerModels,
|
|
1063
|
+
aliases
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function loadPricingData(customPath) {
|
|
1067
|
+
if (customPath && (customPath.endsWith("models.jsonc") || customPath.endsWith("models.json"))) {
|
|
1068
|
+
return loadPricingFromModelsDev(customPath);
|
|
1069
|
+
}
|
|
1070
|
+
const filePath = resolvePricingDataPath(customPath);
|
|
1071
|
+
if (!fs4.existsSync(filePath)) {
|
|
1072
|
+
const modelsDevPath2 = resolveModelsDevDataPath();
|
|
1073
|
+
if (fs4.existsSync(modelsDevPath2)) {
|
|
1074
|
+
return loadPricingFromModelsDev(modelsDevPath2);
|
|
1075
|
+
}
|
|
1076
|
+
throw new Error(`Pricing dataset file not found at: ${filePath}`);
|
|
1077
|
+
}
|
|
1078
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1079
|
+
const raw = parseJsonc(content);
|
|
1080
|
+
const flatModels = flattenModelPrices(raw.models);
|
|
1081
|
+
const isGroupedArray = raw.models && Object.values(raw.models).length > 0 && Array.isArray(Object.values(raw.models)[0]);
|
|
1082
|
+
const dataset = {
|
|
1083
|
+
version: raw.version,
|
|
1084
|
+
updatedAt: raw.updatedAt,
|
|
1085
|
+
defaults: raw.defaults,
|
|
1086
|
+
models: { ...flatModels },
|
|
1087
|
+
providerModels: isGroupedArray ? { ...raw.models } : void 0,
|
|
1088
|
+
aliases: { ...raw.aliases || {} }
|
|
1089
|
+
};
|
|
1090
|
+
const modelsDevPath = resolveModelsDevDataPath();
|
|
1091
|
+
if (fs4.existsSync(modelsDevPath)) {
|
|
1092
|
+
try {
|
|
1093
|
+
const modelsDevDataset = loadPricingFromModelsDev(modelsDevPath);
|
|
1094
|
+
for (const [k, v] of Object.entries(modelsDevDataset.models)) {
|
|
1095
|
+
if (!dataset.models[k]) {
|
|
1096
|
+
dataset.models[k] = v;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
for (const [k, v] of Object.entries(modelsDevDataset.aliases)) {
|
|
1100
|
+
if (!dataset.aliases[k] && !dataset.models[k]) {
|
|
1101
|
+
dataset.aliases[k] = v;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return dataset;
|
|
1108
|
+
}
|
|
1109
|
+
function resolveModelsDevDataPath(customPath) {
|
|
1110
|
+
if (customPath) {
|
|
1111
|
+
if (path4.isAbsolute(customPath)) {
|
|
1112
|
+
return customPath;
|
|
1113
|
+
}
|
|
1114
|
+
return path4.resolve(process.cwd(), customPath);
|
|
1115
|
+
}
|
|
1116
|
+
const currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1117
|
+
const candidates = [
|
|
1118
|
+
path4.resolve(currentDir, "../models.jsonc"),
|
|
1119
|
+
path4.resolve(currentDir, "../models.json"),
|
|
1120
|
+
path4.resolve(currentDir, "../../models.jsonc"),
|
|
1121
|
+
path4.resolve(currentDir, "../../models.json"),
|
|
1122
|
+
path4.resolve(process.cwd(), "packages/pricing/models.jsonc"),
|
|
1123
|
+
path4.resolve(process.cwd(), "packages/pricing/models.json"),
|
|
1124
|
+
path4.resolve(process.cwd(), "models.jsonc"),
|
|
1125
|
+
path4.resolve(process.cwd(), "models.json")
|
|
1126
|
+
];
|
|
1127
|
+
for (const candidate of candidates) {
|
|
1128
|
+
if (fs4.existsSync(candidate)) {
|
|
1129
|
+
return candidate;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return candidates[0];
|
|
1133
|
+
}
|
|
1134
|
+
function loadModelsDevData(customPath) {
|
|
1135
|
+
const filePath = resolveModelsDevDataPath(customPath);
|
|
1136
|
+
if (!fs4.existsSync(filePath)) {
|
|
1137
|
+
return {};
|
|
1138
|
+
}
|
|
1139
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1140
|
+
return parseJsonc(content);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// ../../packages/pricing/dist/matcher.js
|
|
1144
|
+
function normalizeModelName(rawModel, aliases) {
|
|
1145
|
+
if (!rawModel)
|
|
1146
|
+
return "";
|
|
1147
|
+
let clean = rawModel.trim();
|
|
1148
|
+
if (clean.includes(":") && !clean.includes("://")) {
|
|
1149
|
+
const colonIndex = clean.lastIndexOf(":");
|
|
1150
|
+
clean = clean.substring(0, colonIndex).trim();
|
|
1151
|
+
}
|
|
1152
|
+
if (clean.includes("/")) {
|
|
1153
|
+
clean = clean.split("/").pop()?.trim() ?? clean;
|
|
1154
|
+
}
|
|
1155
|
+
if (aliases) {
|
|
1156
|
+
const lower = clean.toLowerCase();
|
|
1157
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
1158
|
+
if (alias.toLowerCase() === lower) {
|
|
1159
|
+
return target;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return clean;
|
|
1164
|
+
}
|
|
1165
|
+
function findCanonicalModelKey(rawModel, models, aliases) {
|
|
1166
|
+
if (!rawModel)
|
|
1167
|
+
return void 0;
|
|
1168
|
+
const normalized = normalizeModelName(rawModel, aliases);
|
|
1169
|
+
if (models[normalized]) {
|
|
1170
|
+
return normalized;
|
|
1171
|
+
}
|
|
1172
|
+
if (models[rawModel]) {
|
|
1173
|
+
return rawModel;
|
|
1174
|
+
}
|
|
1175
|
+
const normalizedLower = normalized.toLowerCase();
|
|
1176
|
+
for (const key of Object.keys(models)) {
|
|
1177
|
+
if (key.toLowerCase() === normalizedLower) {
|
|
1178
|
+
return key;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const baseClean = (rawModel.includes("/") ? rawModel.split("/").pop() ?? rawModel : rawModel).split(":")[0].trim().toLowerCase();
|
|
1182
|
+
for (const key of Object.keys(models)) {
|
|
1183
|
+
if (key.toLowerCase() === baseClean) {
|
|
1184
|
+
return key;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// ../../packages/pricing/dist/pricing.js
|
|
1191
|
+
var EMBEDDED_DEFAULT_PRICING = {
|
|
1192
|
+
input: 2,
|
|
1193
|
+
output: 8,
|
|
1194
|
+
cached: 1,
|
|
1195
|
+
reasoning: 12,
|
|
1196
|
+
cache_creation: 2
|
|
1197
|
+
};
|
|
1198
|
+
var loadedDataset;
|
|
1199
|
+
try {
|
|
1200
|
+
loadedDataset = loadPricingData();
|
|
1201
|
+
} catch {
|
|
1202
|
+
loadedDataset = {
|
|
1203
|
+
defaults: EMBEDDED_DEFAULT_PRICING,
|
|
1204
|
+
models: {},
|
|
1205
|
+
aliases: {}
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
var DEFAULT_PRICING = loadedDataset.defaults || EMBEDDED_DEFAULT_PRICING;
|
|
1209
|
+
var MODEL_PRICING = loadedDataset.models;
|
|
1210
|
+
var PROVIDER_MODELS = loadedDataset.providerModels;
|
|
1211
|
+
var MODEL_ALIASES = loadedDataset.aliases;
|
|
1212
|
+
var cachedModelsDevData;
|
|
1213
|
+
function getModelMetadata(modelId) {
|
|
1214
|
+
if (!modelId)
|
|
1215
|
+
return void 0;
|
|
1216
|
+
if (!cachedModelsDevData) {
|
|
1217
|
+
try {
|
|
1218
|
+
cachedModelsDevData = loadModelsDevData();
|
|
1219
|
+
} catch {
|
|
1220
|
+
cachedModelsDevData = {};
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const data = cachedModelsDevData;
|
|
1224
|
+
if (data[modelId]) {
|
|
1225
|
+
return data[modelId];
|
|
1226
|
+
}
|
|
1227
|
+
const canonical = findCanonicalModelKey(modelId, MODEL_PRICING, MODEL_ALIASES) || normalizeModelName(modelId, MODEL_ALIASES);
|
|
1228
|
+
if (canonical && data[canonical]) {
|
|
1229
|
+
return data[canonical];
|
|
1230
|
+
}
|
|
1231
|
+
const targetSuffix = `/${canonical}`;
|
|
1232
|
+
for (const [key, item] of Object.entries(data)) {
|
|
1233
|
+
if (key === canonical || key.endsWith(targetSuffix) || item.id === canonical || item.id.endsWith(targetSuffix)) {
|
|
1234
|
+
return item;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return void 0;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
884
1240
|
// src/adapters/opencode.ts
|
|
1241
|
+
function createOpenCodeModelConfig(id, name) {
|
|
1242
|
+
const displayName = name || formatModelDisplayName(id);
|
|
1243
|
+
const config = {
|
|
1244
|
+
id,
|
|
1245
|
+
name: displayName
|
|
1246
|
+
};
|
|
1247
|
+
const meta = getModelMetadata(id);
|
|
1248
|
+
if (meta?.modalities?.input?.includes("image") || meta?.attachment) {
|
|
1249
|
+
config.attachment = true;
|
|
1250
|
+
config.modalities = {
|
|
1251
|
+
input: meta.modalities?.input || ["text", "image"],
|
|
1252
|
+
output: meta.modalities?.output || ["text"]
|
|
1253
|
+
};
|
|
1254
|
+
} else {
|
|
1255
|
+
const lower = id.toLowerCase();
|
|
1256
|
+
const isKnownVision = lower.includes("gemini") || lower.includes("vision") || lower.includes("gpt-4o") || lower.includes("claude-3") || lower.includes("claude-sonnet") || lower.includes("claude-opus") || lower.includes("pixtral");
|
|
1257
|
+
if (isKnownVision) {
|
|
1258
|
+
config.attachment = true;
|
|
1259
|
+
config.modalities = {
|
|
1260
|
+
input: ["text", "image"],
|
|
1261
|
+
output: ["text"]
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return config;
|
|
1266
|
+
}
|
|
885
1267
|
var DEFAULT_SROUTER_MODELS = [
|
|
886
1268
|
{ id: "anthropic/claude-3-7-sonnet", name: "Claude 3.7 Sonnet (Anthropic)" },
|
|
887
1269
|
{ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet" },
|
|
@@ -963,7 +1345,7 @@ function formatProviderLabel(provider) {
|
|
|
963
1345
|
function humanizeName(str2) {
|
|
964
1346
|
return str2.replace(/[-_]/g, " ").replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
|
965
1347
|
}
|
|
966
|
-
function
|
|
1348
|
+
function parseJsonc2(content) {
|
|
967
1349
|
try {
|
|
968
1350
|
const clean = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
|
|
969
1351
|
return JSON.parse(clean);
|
|
@@ -999,8 +1381,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
999
1381
|
const configPath = this.getConfigPath();
|
|
1000
1382
|
const installed = await this.isInstalled();
|
|
1001
1383
|
try {
|
|
1002
|
-
const raw = await
|
|
1003
|
-
const parsed =
|
|
1384
|
+
const raw = await fs5.readFile(configPath, "utf-8");
|
|
1385
|
+
const parsed = parseJsonc2(raw);
|
|
1004
1386
|
const baseUrl = parsed.provider?.srouter?.options?.baseURL || parsed.openai_base_url || parsed.api_base || parsed.baseUrl || parsed.providers?.srouter?.baseUrl || void 0;
|
|
1005
1387
|
const model = parsed.model || parsed.default_model || void 0;
|
|
1006
1388
|
const linked = Boolean(
|
|
@@ -1030,8 +1412,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
1030
1412
|
const backupPath = context.dryRun ? void 0 : await this.store.createBackup(this.id, configPath);
|
|
1031
1413
|
let data = {};
|
|
1032
1414
|
try {
|
|
1033
|
-
const raw = await
|
|
1034
|
-
data =
|
|
1415
|
+
const raw = await fs5.readFile(configPath, "utf-8");
|
|
1416
|
+
data = parseJsonc2(raw);
|
|
1035
1417
|
} catch {
|
|
1036
1418
|
data = {};
|
|
1037
1419
|
}
|
|
@@ -1046,14 +1428,18 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
1046
1428
|
const existingModels = existingSrouter.models || {};
|
|
1047
1429
|
const modelsMap = {};
|
|
1048
1430
|
for (const m of DEFAULT_SROUTER_MODELS) {
|
|
1049
|
-
modelsMap[m.id] =
|
|
1050
|
-
id: m.id,
|
|
1051
|
-
name: m.name
|
|
1052
|
-
};
|
|
1431
|
+
modelsMap[m.id] = createOpenCodeModelConfig(m.id, m.name);
|
|
1053
1432
|
}
|
|
1054
1433
|
for (const [key, val] of Object.entries(existingModels)) {
|
|
1055
1434
|
if (val && typeof val === "object") {
|
|
1056
|
-
|
|
1435
|
+
const existingVal = val;
|
|
1436
|
+
const fresh = createOpenCodeModelConfig(key, existingVal.name);
|
|
1437
|
+
modelsMap[key] = {
|
|
1438
|
+
...fresh,
|
|
1439
|
+
...existingVal,
|
|
1440
|
+
...fresh.attachment !== void 0 && { attachment: fresh.attachment },
|
|
1441
|
+
...fresh.modalities !== void 0 && { modalities: fresh.modalities }
|
|
1442
|
+
};
|
|
1057
1443
|
}
|
|
1058
1444
|
}
|
|
1059
1445
|
try {
|
|
@@ -1061,19 +1447,16 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
1061
1447
|
for (const rawId of liveModels) {
|
|
1062
1448
|
if (!rawId) continue;
|
|
1063
1449
|
const cleanId = rawId.startsWith("srouter/") ? rawId.replace(/^srouter\//, "") : rawId;
|
|
1064
|
-
modelsMap[cleanId] =
|
|
1065
|
-
id: cleanId,
|
|
1066
|
-
name: formatModelDisplayName(cleanId)
|
|
1067
|
-
};
|
|
1450
|
+
modelsMap[cleanId] = createOpenCodeModelConfig(cleanId);
|
|
1068
1451
|
}
|
|
1069
1452
|
} catch {
|
|
1070
1453
|
}
|
|
1071
1454
|
const rawModel = context.model || "claude-3-7-sonnet";
|
|
1072
1455
|
const cleanModelId = rawModel.startsWith("srouter/") ? rawModel.replace(/^srouter\//, "") : rawModel;
|
|
1073
|
-
modelsMap[cleanModelId] =
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1456
|
+
modelsMap[cleanModelId] = createOpenCodeModelConfig(
|
|
1457
|
+
cleanModelId,
|
|
1458
|
+
modelsMap[cleanModelId]?.name
|
|
1459
|
+
);
|
|
1077
1460
|
data.provider.srouter = {
|
|
1078
1461
|
name: "SRouter",
|
|
1079
1462
|
npm: "@ai-sdk/openai-compatible",
|
|
@@ -1085,8 +1468,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
1085
1468
|
};
|
|
1086
1469
|
data.model = `srouter/${cleanModelId}`;
|
|
1087
1470
|
if (!context.dryRun) {
|
|
1088
|
-
await
|
|
1089
|
-
await
|
|
1471
|
+
await fs5.mkdir(path5.dirname(configPath), { recursive: true });
|
|
1472
|
+
await fs5.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
|
|
1090
1473
|
}
|
|
1091
1474
|
return {
|
|
1092
1475
|
backupPath,
|
|
@@ -1100,8 +1483,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
1100
1483
|
}
|
|
1101
1484
|
const configPath = this.getConfigPath();
|
|
1102
1485
|
try {
|
|
1103
|
-
const raw = await
|
|
1104
|
-
const data =
|
|
1486
|
+
const raw = await fs5.readFile(configPath, "utf-8");
|
|
1487
|
+
const data = parseJsonc2(raw);
|
|
1105
1488
|
if (data.provider?.srouter) {
|
|
1106
1489
|
delete data.provider.srouter;
|
|
1107
1490
|
}
|
|
@@ -1113,7 +1496,7 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
|
1113
1496
|
delete data.openai_api_key;
|
|
1114
1497
|
delete data.api_key;
|
|
1115
1498
|
delete data.providers;
|
|
1116
|
-
await
|
|
1499
|
+
await fs5.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
|
|
1117
1500
|
return true;
|
|
1118
1501
|
} catch {
|
|
1119
1502
|
return false;
|
|
@@ -1566,124 +1949,988 @@ ${pc.yellow("No tools selected for configuration.")}`,
|
|
|
1566
1949
|
);
|
|
1567
1950
|
}
|
|
1568
1951
|
|
|
1569
|
-
// src/commands/
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1952
|
+
// src/commands/init.ts
|
|
1953
|
+
import fs7 from "node:fs";
|
|
1954
|
+
import os6 from "node:os";
|
|
1955
|
+
import path8 from "node:path";
|
|
1956
|
+
import { spawn } from "node:child_process";
|
|
1957
|
+
import * as p from "@clack/prompts";
|
|
1958
|
+
|
|
1959
|
+
// ../../packages/db/dist/db.js
|
|
1960
|
+
import path7 from "node:path";
|
|
1961
|
+
import os5 from "node:os";
|
|
1962
|
+
|
|
1963
|
+
// ../../packages/db/dist/sqlite.js
|
|
1964
|
+
import path6 from "node:path";
|
|
1965
|
+
import fs6 from "node:fs";
|
|
1966
|
+
import os4 from "node:os";
|
|
1967
|
+
import { DatabaseSync } from "node:sqlite";
|
|
1968
|
+
var SROUTER_DIR = path6.join(os4.homedir(), ".srouter");
|
|
1969
|
+
var DEFAULT_DB_PATH = path6.join(SROUTER_DIR, "srouter.db");
|
|
1970
|
+
var LEGACY_DB_LOCATIONS = [
|
|
1971
|
+
path6.resolve(process.cwd(), "apps/api/srouter.db"),
|
|
1972
|
+
path6.resolve(process.cwd(), "srouter.db")
|
|
1973
|
+
];
|
|
1974
|
+
function getDatabasePath() {
|
|
1975
|
+
if (process.env.DATABASE_PATH)
|
|
1976
|
+
return process.env.DATABASE_PATH;
|
|
1977
|
+
for (const legacyPath of LEGACY_DB_LOCATIONS) {
|
|
1978
|
+
if (fs6.existsSync(legacyPath))
|
|
1979
|
+
return legacyPath;
|
|
1580
1980
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
);
|
|
1603
|
-
console.log(` ${pc.gray("Target Config:")} ${pc.white(result.modifiedPath)}`);
|
|
1604
|
-
console.log(` ${pc.gray("Proxy URL:")} ${pc.white(baseUrl)}`);
|
|
1605
|
-
if (model) {
|
|
1606
|
-
console.log(` ${pc.gray("Model:")} ${pc.white(model)}`);
|
|
1607
|
-
}
|
|
1608
|
-
if (opusModel) {
|
|
1609
|
-
console.log(` ${pc.gray("Opus Model:")} ${pc.white(opusModel)}`);
|
|
1610
|
-
}
|
|
1611
|
-
if (sonnetModel) {
|
|
1612
|
-
console.log(` ${pc.gray("Sonnet Model:")} ${pc.white(sonnetModel)}`);
|
|
1613
|
-
}
|
|
1614
|
-
if (haikuModel) {
|
|
1615
|
-
console.log(` ${pc.gray("Haiku Model:")} ${pc.white(haikuModel)}`);
|
|
1616
|
-
}
|
|
1617
|
-
if (result.backupPath) {
|
|
1618
|
-
console.log(` ${pc.gray("Backup Saved:")} ${pc.white(result.backupPath)}`);
|
|
1981
|
+
return DEFAULT_DB_PATH;
|
|
1982
|
+
}
|
|
1983
|
+
var dbPath = getDatabasePath();
|
|
1984
|
+
var dbDir = path6.dirname(dbPath);
|
|
1985
|
+
if (!fs6.existsSync(dbDir)) {
|
|
1986
|
+
fs6.mkdirSync(dbDir, { recursive: true });
|
|
1987
|
+
}
|
|
1988
|
+
var sqliteDb = new DatabaseSync(dbPath);
|
|
1989
|
+
sqliteDb.exec("PRAGMA busy_timeout = 5000;");
|
|
1990
|
+
function execWithRetry(sql, attempts = 5) {
|
|
1991
|
+
for (let i = 0; i < attempts; i++) {
|
|
1992
|
+
try {
|
|
1993
|
+
sqliteDb.exec(sql);
|
|
1994
|
+
return;
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
const code = error.code;
|
|
1997
|
+
if (code === "SQLITE_BUSY" && i < attempts - 1) {
|
|
1998
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
throw error;
|
|
1619
2002
|
}
|
|
1620
|
-
} catch (err) {
|
|
1621
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1622
|
-
console.error(formatError(`Failed to link ${adapter.name}: ${msg}`));
|
|
1623
|
-
process.exitCode = 1;
|
|
1624
2003
|
}
|
|
1625
2004
|
}
|
|
2005
|
+
execWithRetry("PRAGMA journal_mode = WAL;");
|
|
2006
|
+
execWithRetry("PRAGMA foreign_keys = ON;");
|
|
2007
|
+
execWithRetry("PRAGMA synchronous = NORMAL;");
|
|
2008
|
+
execWithRetry("PRAGMA temp_store = MEMORY;");
|
|
2009
|
+
execWithRetry("PRAGMA cache_size = -20000;");
|
|
1626
2010
|
|
|
1627
|
-
//
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
`Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
1634
|
-
)
|
|
1635
|
-
);
|
|
1636
|
-
process.exitCode = 1;
|
|
1637
|
-
return;
|
|
2011
|
+
// ../../packages/db/dist/client.js
|
|
2012
|
+
import {} from "node:sqlite";
|
|
2013
|
+
var SqliteClient = class {
|
|
2014
|
+
db;
|
|
2015
|
+
constructor(db2) {
|
|
2016
|
+
this.db = db2;
|
|
1638
2017
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
console.log(
|
|
1643
|
-
formatSuccess(
|
|
1644
|
-
`Restored original configuration for ${pc.bold(pc.cyan(adapter.name))}!`
|
|
1645
|
-
)
|
|
1646
|
-
);
|
|
1647
|
-
} else {
|
|
1648
|
-
console.log(
|
|
1649
|
-
formatInfo(
|
|
1650
|
-
`No active SRouter configuration or backup found for ${pc.bold(adapter.name)}.`
|
|
1651
|
-
)
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
} catch (err) {
|
|
1655
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1656
|
-
console.error(formatError(`Failed to unlink ${adapter.name}: ${msg}`));
|
|
1657
|
-
process.exitCode = 1;
|
|
2018
|
+
/** Normalize undefined → null (node:sqlite previously tolerated undefined; pg needs null). */
|
|
2019
|
+
normalizeParams(params) {
|
|
2020
|
+
return params.map((p3) => p3 === void 0 ? null : p3);
|
|
1658
2021
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
// src/commands/sync.ts
|
|
1662
|
-
async function syncCommand(toolId, options = {}) {
|
|
1663
|
-
const savedConfig = await defaultStore.loadConfig();
|
|
1664
|
-
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1665
|
-
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
1666
|
-
const health = await checkServerHealth(baseUrl, apiKey);
|
|
1667
|
-
if (!health.healthy) {
|
|
1668
|
-
console.error(
|
|
1669
|
-
formatError(
|
|
1670
|
-
`Cannot sync: SRouter Gateway is unreachable at ${pc.bold(baseUrl)} (${health.error || "offline"}).`
|
|
1671
|
-
)
|
|
1672
|
-
);
|
|
1673
|
-
process.exitCode = 1;
|
|
1674
|
-
return;
|
|
2022
|
+
all(sql, ...params) {
|
|
2023
|
+
return Promise.resolve(this.db.prepare(sql).all(...this.normalizeParams(params)));
|
|
1675
2024
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
console.warn(
|
|
1679
|
-
formatWarning(`SRouter Gateway responded at ${baseUrl}, but returned 0 models.`)
|
|
1680
|
-
);
|
|
2025
|
+
get(sql, ...params) {
|
|
2026
|
+
return Promise.resolve(this.db.prepare(sql).get(...this.normalizeParams(params)));
|
|
1681
2027
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2028
|
+
run(sql, ...params) {
|
|
2029
|
+
const result = this.db.prepare(sql).run(...this.normalizeParams(params));
|
|
2030
|
+
return Promise.resolve(result);
|
|
2031
|
+
}
|
|
2032
|
+
exec(sql) {
|
|
2033
|
+
this.db.exec(sql);
|
|
2034
|
+
return Promise.resolve();
|
|
2035
|
+
}
|
|
2036
|
+
async tableColumns(table) {
|
|
2037
|
+
const rows = await this.all(`PRAGMA table_info("${table}")`);
|
|
2038
|
+
return rows.map((r) => r.name);
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
var pgPool = null;
|
|
2042
|
+
async function getPool() {
|
|
2043
|
+
if (pgPool)
|
|
2044
|
+
return pgPool;
|
|
2045
|
+
const { Pool } = await import("./esm-5KJBUOHT.js");
|
|
2046
|
+
pgPool = new Pool({
|
|
2047
|
+
connectionString: process.env.DATABASE_URL,
|
|
2048
|
+
max: 10,
|
|
2049
|
+
idleTimeoutMillis: 3e4,
|
|
2050
|
+
connectionTimeoutMillis: 1e4,
|
|
2051
|
+
// Heroku Postgres requires TLS; self-signed cert → rejectUnauthorized: false
|
|
2052
|
+
ssl: { rejectUnauthorized: false }
|
|
2053
|
+
});
|
|
2054
|
+
return pgPool;
|
|
2055
|
+
}
|
|
2056
|
+
function toPg(sql) {
|
|
2057
|
+
let i = 0;
|
|
2058
|
+
return sql.replace(/\?/g, () => `$${++i}`);
|
|
2059
|
+
}
|
|
2060
|
+
function splitStatements(sql) {
|
|
2061
|
+
const statements = [];
|
|
2062
|
+
let current = "";
|
|
2063
|
+
let inSingle = false;
|
|
2064
|
+
let inDouble = false;
|
|
2065
|
+
let inDollar = false;
|
|
2066
|
+
for (let i = 0; i < sql.length; i++) {
|
|
2067
|
+
const ch = sql[i];
|
|
2068
|
+
const next = sql[i + 1];
|
|
2069
|
+
if (!inDollar && !inSingle && !inDouble && ch === "'") {
|
|
2070
|
+
inSingle = true;
|
|
2071
|
+
} else if (inSingle && ch === "'" && next !== "'") {
|
|
2072
|
+
inSingle = false;
|
|
2073
|
+
} else if (!inDollar && !inSingle && !inDouble && ch === '"') {
|
|
2074
|
+
inDouble = true;
|
|
2075
|
+
} else if (inDouble && ch === '"' && next !== '"') {
|
|
2076
|
+
inDouble = false;
|
|
2077
|
+
} else if (!inSingle && !inDouble && ch === "$" && /^[a-zA-Z_0-9]*\$/.test(sql.slice(i, i + 40))) {
|
|
2078
|
+
inDollar = true;
|
|
2079
|
+
} else if (inDollar && ch === "$" && next !== "$") {
|
|
2080
|
+
inDollar = false;
|
|
2081
|
+
} else if (!inSingle && !inDouble && !inDollar && ch === ";") {
|
|
2082
|
+
if (current.trim())
|
|
2083
|
+
statements.push(current.trim());
|
|
2084
|
+
current = "";
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
current += ch;
|
|
2088
|
+
}
|
|
2089
|
+
if (current.trim())
|
|
2090
|
+
statements.push(current.trim());
|
|
2091
|
+
return statements;
|
|
2092
|
+
}
|
|
2093
|
+
var PgClient = class {
|
|
2094
|
+
/** Normalize undefined → null (pg rejects undefined params). */
|
|
2095
|
+
normalizeParams(params) {
|
|
2096
|
+
return params.map((p3) => p3 === void 0 ? null : p3);
|
|
2097
|
+
}
|
|
2098
|
+
async all(sql, ...params) {
|
|
2099
|
+
const pool = await getPool();
|
|
2100
|
+
const result = await pool.query(toPg(sql), this.normalizeParams(params));
|
|
2101
|
+
return result.rows;
|
|
2102
|
+
}
|
|
2103
|
+
async get(sql, ...params) {
|
|
2104
|
+
const pool = await getPool();
|
|
2105
|
+
const result = await pool.query(toPg(sql), this.normalizeParams(params));
|
|
2106
|
+
return result.rows[0] ?? null;
|
|
2107
|
+
}
|
|
2108
|
+
async run(sql, ...params) {
|
|
2109
|
+
const pool = await getPool();
|
|
2110
|
+
const result = await pool.query(toPg(sql), this.normalizeParams(params));
|
|
2111
|
+
return { changes: result.rowCount ?? 0 };
|
|
2112
|
+
}
|
|
2113
|
+
async exec(sql) {
|
|
2114
|
+
const trimmed = sql.trim().toUpperCase();
|
|
2115
|
+
if (trimmed.startsWith("PRAGMA"))
|
|
2116
|
+
return;
|
|
2117
|
+
const pool = await getPool();
|
|
2118
|
+
const statements = splitStatements(sql);
|
|
2119
|
+
for (const stmt of statements) {
|
|
2120
|
+
await pool.query(stmt);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
async tableColumns(table) {
|
|
2124
|
+
const rows = await this.all("SELECT column_name AS name FROM information_schema.columns WHERE table_name = $1", table);
|
|
2125
|
+
return rows.map((r) => r.name);
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
var _client = null;
|
|
2129
|
+
function getDbClient() {
|
|
2130
|
+
if (_client)
|
|
2131
|
+
return _client;
|
|
2132
|
+
if (process.env.DATABASE_URL) {
|
|
2133
|
+
_client = new PgClient();
|
|
2134
|
+
} else {
|
|
2135
|
+
_client = new SqliteClient(sqliteDb);
|
|
2136
|
+
}
|
|
2137
|
+
return _client;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// ../../packages/db/dist/db.js
|
|
2141
|
+
var SROUTER_DIR2 = path7.join(os5.homedir(), ".srouter");
|
|
2142
|
+
var DEFAULT_DB_PATH2 = path7.join(SROUTER_DIR2, "srouter.db");
|
|
2143
|
+
var LEGACY_DB_LOCATIONS2 = [
|
|
2144
|
+
path7.resolve(process.cwd(), "apps/api/srouter.db"),
|
|
2145
|
+
path7.resolve(process.cwd(), "srouter.db")
|
|
2146
|
+
];
|
|
2147
|
+
var CompatStatement = class {
|
|
2148
|
+
client;
|
|
2149
|
+
sql;
|
|
2150
|
+
constructor(client, sql) {
|
|
2151
|
+
this.client = client;
|
|
2152
|
+
this.sql = sql;
|
|
2153
|
+
}
|
|
2154
|
+
all(...params) {
|
|
2155
|
+
return this.client.all(this.sql, ...params);
|
|
2156
|
+
}
|
|
2157
|
+
get(...params) {
|
|
2158
|
+
return this.client.get(this.sql, ...params);
|
|
2159
|
+
}
|
|
2160
|
+
run(...params) {
|
|
2161
|
+
return this.client.run(this.sql, ...params);
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
var CompatDb = class {
|
|
2165
|
+
client;
|
|
2166
|
+
constructor(client) {
|
|
2167
|
+
this.client = client;
|
|
2168
|
+
}
|
|
2169
|
+
prepare(sql) {
|
|
2170
|
+
return new CompatStatement(this.client, sql);
|
|
2171
|
+
}
|
|
2172
|
+
exec(sql) {
|
|
2173
|
+
return this.client.exec(sql);
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
function makeCompatDb() {
|
|
2177
|
+
return new CompatDb(getDbClient());
|
|
2178
|
+
}
|
|
2179
|
+
var db = makeCompatDb();
|
|
2180
|
+
sqliteDb.exec("PRAGMA busy_timeout = 5000;");
|
|
2181
|
+
function execWithRetry2(sql, attempts = 5) {
|
|
2182
|
+
for (let i = 0; i < attempts; i++) {
|
|
2183
|
+
try {
|
|
2184
|
+
sqliteDb.exec(sql);
|
|
2185
|
+
return;
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
const code = error.code;
|
|
2188
|
+
if (code === "SQLITE_BUSY" && i < attempts - 1) {
|
|
2189
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
throw error;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
execWithRetry2("PRAGMA journal_mode = WAL;");
|
|
2197
|
+
execWithRetry2("PRAGMA foreign_keys = ON;");
|
|
2198
|
+
execWithRetry2("PRAGMA synchronous = NORMAL;");
|
|
2199
|
+
execWithRetry2("PRAGMA temp_store = MEMORY;");
|
|
2200
|
+
execWithRetry2("PRAGMA cache_size = -20000;");
|
|
2201
|
+
function isPostgres() {
|
|
2202
|
+
return Boolean(process.env.DATABASE_URL);
|
|
2203
|
+
}
|
|
2204
|
+
function tableSql(table, pg) {
|
|
2205
|
+
const cols = table.columns.map((c) => `${c.name} ${pg && c.definition.includes("INTEGER") ? "BIGINT" : c.definition}`).join(",\n ");
|
|
2206
|
+
return `CREATE TABLE IF NOT EXISTS ${table.name} (
|
|
2207
|
+
${cols}
|
|
2208
|
+
);`;
|
|
2209
|
+
}
|
|
2210
|
+
var TABLES = [
|
|
2211
|
+
{
|
|
2212
|
+
name: "providers",
|
|
2213
|
+
columns: [
|
|
2214
|
+
{ name: "id", definition: "TEXT PRIMARY KEY" },
|
|
2215
|
+
{ name: "provider_id", definition: "TEXT NOT NULL" },
|
|
2216
|
+
{ name: "name", definition: "TEXT NOT NULL" },
|
|
2217
|
+
{ name: "alias", definition: "TEXT" },
|
|
2218
|
+
{ name: "category", definition: "TEXT NOT NULL" },
|
|
2219
|
+
{ name: "protocol", definition: "TEXT NOT NULL" },
|
|
2220
|
+
{ name: "base_url", definition: "TEXT" },
|
|
2221
|
+
{ name: "api_key", definition: "TEXT" },
|
|
2222
|
+
{ name: "access_token", definition: "TEXT" },
|
|
2223
|
+
{ name: "refresh_token", definition: "TEXT" },
|
|
2224
|
+
{ name: "account_id", definition: "TEXT" },
|
|
2225
|
+
{ name: "organization_id", definition: "TEXT" },
|
|
2226
|
+
{ name: "provider_specific_data", definition: "TEXT" },
|
|
2227
|
+
{ name: "custom_headers", definition: "TEXT" },
|
|
2228
|
+
{ name: "token_expires_at", definition: "INTEGER" },
|
|
2229
|
+
{ name: "last_refreshed_at", definition: "INTEGER" },
|
|
2230
|
+
{ name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
|
|
2231
|
+
{ name: "created_at", definition: "INTEGER NOT NULL" }
|
|
2232
|
+
]
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
name: "api_keys",
|
|
2236
|
+
columns: [
|
|
2237
|
+
{ name: "id", definition: "TEXT PRIMARY KEY" },
|
|
2238
|
+
{ name: "key", definition: "TEXT UNIQUE NOT NULL" },
|
|
2239
|
+
{ name: "name", definition: "TEXT NOT NULL" },
|
|
2240
|
+
{ name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
|
|
2241
|
+
{ name: "rate_limit", definition: "INTEGER DEFAULT 0" },
|
|
2242
|
+
{ name: "quota_limit", definition: "INTEGER DEFAULT 0" },
|
|
2243
|
+
{ name: "usage_tokens", definition: "INTEGER DEFAULT 0" },
|
|
2244
|
+
{ name: "credit_limit", definition: "REAL DEFAULT 0" },
|
|
2245
|
+
{ name: "usage_cost", definition: "REAL DEFAULT 0" },
|
|
2246
|
+
{ name: "allowed_models", definition: "TEXT" },
|
|
2247
|
+
{ name: "created_at", definition: "INTEGER NOT NULL" }
|
|
2248
|
+
]
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
name: "request_logs",
|
|
2252
|
+
columns: [
|
|
2253
|
+
{ name: "id", definition: "TEXT PRIMARY KEY" },
|
|
2254
|
+
{ name: "api_key_id", definition: "TEXT" },
|
|
2255
|
+
{ name: "provider_id", definition: "TEXT NOT NULL" },
|
|
2256
|
+
{ name: "model", definition: "TEXT NOT NULL" },
|
|
2257
|
+
{ name: "prompt_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2258
|
+
{ name: "completion_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2259
|
+
{ name: "total_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2260
|
+
{ name: "status_code", definition: "INTEGER NOT NULL" },
|
|
2261
|
+
{ name: "latency_ms", definition: "INTEGER NOT NULL" },
|
|
2262
|
+
{ name: "cached_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2263
|
+
{ name: "cache_creation_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2264
|
+
{ name: "reasoning_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2265
|
+
{ name: "estimated_cost", definition: "REAL NOT NULL DEFAULT 0" },
|
|
2266
|
+
{ name: "fallback_occurred", definition: "INTEGER NOT NULL DEFAULT 0" },
|
|
2267
|
+
{ name: "fallback_path", definition: "TEXT" },
|
|
2268
|
+
{ name: "fallback_reason", definition: "TEXT" },
|
|
2269
|
+
{ name: "resolved_model", definition: "TEXT" },
|
|
2270
|
+
{ name: "created_at", definition: "INTEGER NOT NULL" }
|
|
2271
|
+
]
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
name: "oauth_sessions",
|
|
2275
|
+
columns: [
|
|
2276
|
+
{ name: "state", definition: "TEXT PRIMARY KEY" },
|
|
2277
|
+
{ name: "code_verifier", definition: "TEXT NOT NULL" },
|
|
2278
|
+
{ name: "client_id", definition: "TEXT NOT NULL" },
|
|
2279
|
+
{ name: "redirect_uri", definition: "TEXT NOT NULL" },
|
|
2280
|
+
{ name: "created_at", definition: "INTEGER NOT NULL" }
|
|
2281
|
+
]
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
name: "fallback_rules",
|
|
2285
|
+
columns: [
|
|
2286
|
+
{ name: "id", definition: "TEXT PRIMARY KEY" },
|
|
2287
|
+
{ name: "source_model", definition: "TEXT NOT NULL" },
|
|
2288
|
+
{ name: "target_model", definition: "TEXT NOT NULL" },
|
|
2289
|
+
{ name: "priority", definition: "INTEGER NOT NULL DEFAULT 1" },
|
|
2290
|
+
{ name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
|
|
2291
|
+
{ name: "trigger_on_status", definition: "TEXT" },
|
|
2292
|
+
{ name: "max_retries", definition: "INTEGER DEFAULT 1" },
|
|
2293
|
+
{ name: "created_at", definition: "INTEGER NOT NULL" }
|
|
2294
|
+
]
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
name: "system_settings",
|
|
2298
|
+
columns: [
|
|
2299
|
+
{ name: "key", definition: "TEXT PRIMARY KEY" },
|
|
2300
|
+
{ name: "value", definition: "TEXT NOT NULL" }
|
|
2301
|
+
]
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
name: "custom_models",
|
|
2305
|
+
columns: [
|
|
2306
|
+
{ name: "provider_id", definition: "TEXT NOT NULL" },
|
|
2307
|
+
{ name: "model_id", definition: "TEXT NOT NULL" },
|
|
2308
|
+
{ name: "created_at", definition: "INTEGER NOT NULL" },
|
|
2309
|
+
{ name: "PRIMARY KEY (provider_id, model_id)", definition: "" }
|
|
2310
|
+
]
|
|
2311
|
+
}
|
|
2312
|
+
];
|
|
2313
|
+
var INDEXES = [
|
|
2314
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at DESC);" },
|
|
2315
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_provider_created ON request_logs(provider_id, created_at DESC);" },
|
|
2316
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_provider_model ON request_logs(provider_id, model);" },
|
|
2317
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_model ON request_logs(model);" },
|
|
2318
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_fallback_rules_priority ON fallback_rules(priority ASC, created_at ASC);" },
|
|
2319
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_providers_provider_id ON providers(provider_id);" },
|
|
2320
|
+
{ sql: "CREATE INDEX IF NOT EXISTS idx_custom_models_provider ON custom_models(provider_id, created_at ASC);" }
|
|
2321
|
+
];
|
|
2322
|
+
var ADMIN_TABLES = (pg) => {
|
|
2323
|
+
const integer = pg ? "BIGINT" : "INTEGER";
|
|
2324
|
+
return `
|
|
2325
|
+
CREATE TABLE IF NOT EXISTS admin_account (
|
|
2326
|
+
id ${integer} PRIMARY KEY CHECK (id = 1),
|
|
2327
|
+
password_hash TEXT NOT NULL,
|
|
2328
|
+
created_at ${integer} NOT NULL,
|
|
2329
|
+
updated_at ${integer} NOT NULL
|
|
2330
|
+
);
|
|
2331
|
+
|
|
2332
|
+
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
2333
|
+
token_hash TEXT PRIMARY KEY,
|
|
2334
|
+
created_at ${integer} NOT NULL,
|
|
2335
|
+
expires_at ${integer} NOT NULL
|
|
2336
|
+
);
|
|
2337
|
+
`;
|
|
2338
|
+
};
|
|
2339
|
+
async function ensureColumns(table, columns) {
|
|
2340
|
+
const client = getDbClient();
|
|
2341
|
+
const existing = new Set(await client.tableColumns(table));
|
|
2342
|
+
for (const col of columns) {
|
|
2343
|
+
if (existing.has(col.name))
|
|
2344
|
+
continue;
|
|
2345
|
+
try {
|
|
2346
|
+
await client.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
|
|
2347
|
+
existing.add(col.name);
|
|
2348
|
+
} catch (error) {
|
|
2349
|
+
const message = error.message || "";
|
|
2350
|
+
if (!message.includes("duplicate column")) {
|
|
2351
|
+
throw error;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function initDatabase() {
|
|
2357
|
+
if (isPostgres()) {
|
|
2358
|
+
return initPostgresSchema();
|
|
2359
|
+
}
|
|
2360
|
+
initSqliteSchemaSync();
|
|
2361
|
+
}
|
|
2362
|
+
function initSqliteSchemaSync() {
|
|
2363
|
+
const raw = sqliteDb;
|
|
2364
|
+
for (const table of TABLES) {
|
|
2365
|
+
raw.exec(tableSql(table, false));
|
|
2366
|
+
}
|
|
2367
|
+
for (const index of INDEXES) {
|
|
2368
|
+
raw.exec(index.sql);
|
|
2369
|
+
}
|
|
2370
|
+
raw.exec(ADMIN_TABLES(false));
|
|
2371
|
+
const ensureSync = (table, columns) => {
|
|
2372
|
+
const existing = new Set(raw.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name));
|
|
2373
|
+
for (const col of columns) {
|
|
2374
|
+
if (existing.has(col.name))
|
|
2375
|
+
continue;
|
|
2376
|
+
try {
|
|
2377
|
+
raw.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
|
|
2378
|
+
existing.add(col.name);
|
|
2379
|
+
} catch (error) {
|
|
2380
|
+
const message = error.message || "";
|
|
2381
|
+
if (!message.includes("duplicate column")) {
|
|
2382
|
+
throw error;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
ensureSync("providers", [
|
|
2388
|
+
{ name: "alias", definition: "alias TEXT" },
|
|
2389
|
+
{ name: "refresh_token", definition: "refresh_token TEXT" },
|
|
2390
|
+
{ name: "account_id", definition: "account_id TEXT" },
|
|
2391
|
+
{ name: "provider_specific_data", definition: "provider_specific_data TEXT" },
|
|
2392
|
+
{ name: "token_expires_at", definition: "token_expires_at INTEGER" },
|
|
2393
|
+
{ name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
|
|
2394
|
+
{ name: "organization_id", definition: "organization_id TEXT" }
|
|
2395
|
+
]);
|
|
2396
|
+
ensureSync("api_keys", [
|
|
2397
|
+
{ name: "allowed_models", definition: "allowed_models TEXT" },
|
|
2398
|
+
{ name: "credit_limit", definition: "credit_limit REAL DEFAULT 0" },
|
|
2399
|
+
{ name: "usage_cost", definition: "usage_cost REAL DEFAULT 0" }
|
|
2400
|
+
]);
|
|
2401
|
+
ensureSync("request_logs", [
|
|
2402
|
+
{ name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2403
|
+
{ name: "cache_creation_tokens", definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2404
|
+
{ name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2405
|
+
{ name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
|
|
2406
|
+
{ name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
|
|
2407
|
+
{ name: "fallback_path", definition: "fallback_path TEXT" },
|
|
2408
|
+
{ name: "fallback_reason", definition: "fallback_reason TEXT" },
|
|
2409
|
+
{ name: "resolved_model", definition: "resolved_model TEXT" }
|
|
2410
|
+
]);
|
|
2411
|
+
}
|
|
2412
|
+
async function initPostgresSchema() {
|
|
2413
|
+
const client = getDbClient();
|
|
2414
|
+
for (const table of TABLES) {
|
|
2415
|
+
await client.exec(tableSql(table, true));
|
|
2416
|
+
}
|
|
2417
|
+
for (const index of INDEXES) {
|
|
2418
|
+
await client.exec(index.sql);
|
|
2419
|
+
}
|
|
2420
|
+
await client.exec(ADMIN_TABLES(true));
|
|
2421
|
+
await ensureColumns("providers", [
|
|
2422
|
+
{ name: "alias", definition: "alias TEXT" },
|
|
2423
|
+
{ name: "refresh_token", definition: "refresh_token TEXT" },
|
|
2424
|
+
{ name: "account_id", definition: "account_id TEXT" },
|
|
2425
|
+
{ name: "provider_specific_data", definition: "provider_specific_data TEXT" },
|
|
2426
|
+
{ name: "token_expires_at", definition: "token_expires_at INTEGER" },
|
|
2427
|
+
{ name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
|
|
2428
|
+
{ name: "organization_id", definition: "organization_id TEXT" }
|
|
2429
|
+
]);
|
|
2430
|
+
await ensureColumns("api_keys", [
|
|
2431
|
+
{ name: "allowed_models", definition: "allowed_models TEXT" },
|
|
2432
|
+
{ name: "credit_limit", definition: "credit_limit REAL DEFAULT 0" },
|
|
2433
|
+
{ name: "usage_cost", definition: "usage_cost REAL DEFAULT 0" }
|
|
2434
|
+
]);
|
|
2435
|
+
await ensureColumns("request_logs", [
|
|
2436
|
+
{ name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2437
|
+
{ name: "cache_creation_tokens", definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2438
|
+
{ name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2439
|
+
{ name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
|
|
2440
|
+
{ name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
|
|
2441
|
+
{ name: "fallback_path", definition: "fallback_path TEXT" },
|
|
2442
|
+
{ name: "fallback_reason", definition: "fallback_reason TEXT" },
|
|
2443
|
+
{ name: "resolved_model", definition: "resolved_model TEXT" }
|
|
2444
|
+
]);
|
|
2445
|
+
}
|
|
2446
|
+
if (!isPostgres()) {
|
|
2447
|
+
initDatabase();
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// ../../packages/db/dist/row-utils.js
|
|
2451
|
+
function str(value, fallback = "") {
|
|
2452
|
+
return value === null || value === void 0 ? fallback : String(value);
|
|
2453
|
+
}
|
|
2454
|
+
function num(value, fallback = 0) {
|
|
2455
|
+
const n = Number(value);
|
|
2456
|
+
return Number.isFinite(n) ? n : fallback;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// ../../packages/db/dist/adminAuth.js
|
|
2460
|
+
var AdminAuthStore = class {
|
|
2461
|
+
initialized = false;
|
|
2462
|
+
client;
|
|
2463
|
+
/** Accept a DbClient for isolation (tests use :memory: SqliteClient); defaults to global. */
|
|
2464
|
+
constructor(client) {
|
|
2465
|
+
this.client = client ?? getDbClient();
|
|
2466
|
+
}
|
|
2467
|
+
async ensureTables() {
|
|
2468
|
+
if (this.initialized)
|
|
2469
|
+
return;
|
|
2470
|
+
const pg = Boolean(process.env.DATABASE_URL);
|
|
2471
|
+
const integer = pg ? "BIGINT" : "INTEGER";
|
|
2472
|
+
await this.client.exec(`
|
|
2473
|
+
CREATE TABLE IF NOT EXISTS admin_account (
|
|
2474
|
+
id ${integer} PRIMARY KEY CHECK (id = 1),
|
|
2475
|
+
password_hash TEXT NOT NULL,
|
|
2476
|
+
created_at ${integer} NOT NULL,
|
|
2477
|
+
updated_at ${integer} NOT NULL
|
|
2478
|
+
);
|
|
2479
|
+
|
|
2480
|
+
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
2481
|
+
token_hash TEXT PRIMARY KEY,
|
|
2482
|
+
created_at ${integer} NOT NULL,
|
|
2483
|
+
expires_at ${integer} NOT NULL
|
|
2484
|
+
);
|
|
2485
|
+
`);
|
|
2486
|
+
this.initialized = true;
|
|
2487
|
+
}
|
|
2488
|
+
async hasAdminAccount() {
|
|
2489
|
+
await this.ensureTables();
|
|
2490
|
+
const Row = await this.client.get("SELECT 1 AS present FROM admin_account WHERE id = 1");
|
|
2491
|
+
return Boolean(Row);
|
|
2492
|
+
}
|
|
2493
|
+
async createAdminAccount(passwordHash, now = Date.now()) {
|
|
2494
|
+
await this.ensureTables();
|
|
2495
|
+
const Result = await this.client.run(`INSERT INTO admin_account (id, password_hash, created_at, updated_at)
|
|
2496
|
+
VALUES (1, ?, ?, ?)
|
|
2497
|
+
ON CONFLICT(id) DO NOTHING`, passwordHash, now, now);
|
|
2498
|
+
return num(Result.changes) > 0;
|
|
2499
|
+
}
|
|
2500
|
+
async getPasswordHash() {
|
|
2501
|
+
await this.ensureTables();
|
|
2502
|
+
const Row = await this.client.get("SELECT password_hash FROM admin_account WHERE id = 1");
|
|
2503
|
+
return Row?.password_hash ? str(Row.password_hash) : null;
|
|
2504
|
+
}
|
|
2505
|
+
async updatePasswordHash(passwordHash, now = Date.now()) {
|
|
2506
|
+
await this.ensureTables();
|
|
2507
|
+
const Result = await this.client.run(`UPDATE admin_account
|
|
2508
|
+
SET password_hash = ?, updated_at = ?
|
|
2509
|
+
WHERE id = 1`, passwordHash, now);
|
|
2510
|
+
return num(Result.changes) > 0;
|
|
2511
|
+
}
|
|
2512
|
+
async createSession(tokenHash, createdAt, expiresAt) {
|
|
2513
|
+
await this.ensureTables();
|
|
2514
|
+
await this.client.run(`INSERT INTO admin_sessions (token_hash, created_at, expires_at)
|
|
2515
|
+
VALUES (?, ?, ?)`, tokenHash, createdAt, expiresAt);
|
|
2516
|
+
}
|
|
2517
|
+
async getSession(tokenHash, now = Date.now()) {
|
|
2518
|
+
await this.ensureTables();
|
|
2519
|
+
await this.client.run("DELETE FROM admin_sessions WHERE expires_at <= ?", now);
|
|
2520
|
+
const Row = await this.client.get(`SELECT token_hash, created_at, expires_at
|
|
2521
|
+
FROM admin_sessions
|
|
2522
|
+
WHERE token_hash = ? AND expires_at > ?`, tokenHash, now);
|
|
2523
|
+
if (!Row)
|
|
2524
|
+
return null;
|
|
2525
|
+
return {
|
|
2526
|
+
tokenHash: str(Row.token_hash),
|
|
2527
|
+
createdAt: num(Row.created_at),
|
|
2528
|
+
expiresAt: num(Row.expires_at)
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
async deleteSession(tokenHash) {
|
|
2532
|
+
await this.ensureTables();
|
|
2533
|
+
const Result = await this.client.run("DELETE FROM admin_sessions WHERE token_hash = ?", tokenHash);
|
|
2534
|
+
return num(Result.changes) > 0;
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
var adminAuthStore = new AdminAuthStore();
|
|
2538
|
+
|
|
2539
|
+
// src/commands/init.ts
|
|
2540
|
+
var DEFAULT_PORT = "3000";
|
|
2541
|
+
var REPO_URL = "https://github.com/seaavey/SRouter.git";
|
|
2542
|
+
var DOCKER_IMAGE = "ghcr.io/seaavey/srouter:latest";
|
|
2543
|
+
function runProcess(command, args, options) {
|
|
2544
|
+
return new Promise((resolve) => {
|
|
2545
|
+
const child = spawn(command, args, {
|
|
2546
|
+
cwd: options?.cwd,
|
|
2547
|
+
stdio: options?.stdio ?? "inherit"
|
|
2548
|
+
});
|
|
2549
|
+
let stdout = "";
|
|
2550
|
+
let stderr = "";
|
|
2551
|
+
if (options?.stdio === "pipe") {
|
|
2552
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
2553
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
2554
|
+
}
|
|
2555
|
+
child.on("close", (code) => {
|
|
2556
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
2557
|
+
});
|
|
2558
|
+
child.on("error", (err) => {
|
|
2559
|
+
resolve({ code: 1, stdout, stderr: err.message });
|
|
2560
|
+
});
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
async function initDocker(port, detached) {
|
|
2564
|
+
const s = p.spinner();
|
|
2565
|
+
s.start("Checking Docker environment");
|
|
2566
|
+
const hasDocker = await isExecutableInPath("docker");
|
|
2567
|
+
if (!hasDocker) {
|
|
2568
|
+
s.stop(formatError("Docker is not installed or not available in PATH."));
|
|
2569
|
+
const fallbackChoice = await p.select({
|
|
2570
|
+
message: "Docker was not found. What would you like to do?",
|
|
2571
|
+
options: [
|
|
2572
|
+
{
|
|
2573
|
+
value: "source",
|
|
2574
|
+
label: "Run from Source Code instead (Node.js & pnpm)",
|
|
2575
|
+
hint: "Clones repo and builds locally without Docker"
|
|
2576
|
+
},
|
|
2577
|
+
{
|
|
2578
|
+
value: "guide",
|
|
2579
|
+
label: "View Docker installation instructions",
|
|
2580
|
+
hint: "Get download link for your OS"
|
|
2581
|
+
},
|
|
2582
|
+
{
|
|
2583
|
+
value: "exit",
|
|
2584
|
+
label: "Exit",
|
|
2585
|
+
hint: "Cancel initialization"
|
|
2586
|
+
}
|
|
2587
|
+
]
|
|
2588
|
+
});
|
|
2589
|
+
if (p.isCancel(fallbackChoice) || fallbackChoice === "exit") {
|
|
2590
|
+
p.outro("Initialization cancelled. Install Docker from https://docs.docker.com/get-docker/ and try again.");
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
if (fallbackChoice === "guide") {
|
|
2594
|
+
p.log.message(
|
|
2595
|
+
[
|
|
2596
|
+
"\u{1F4E6} " + pc.bold("How to install Docker:"),
|
|
2597
|
+
`\u2022 Windows / macOS: ${pc.cyan("https://www.docker.com/products/docker-desktop/")}`,
|
|
2598
|
+
`\u2022 Linux (Ubuntu/Debian): ${pc.yellow("curl -fsSL https://get.docker.com | sh")}`,
|
|
2599
|
+
`\u2022 Linux (Arch): ${pc.yellow("sudo pacman -S docker && sudo systemctl enable --now docker")}`
|
|
2600
|
+
].join("\n")
|
|
2601
|
+
);
|
|
2602
|
+
p.outro("Run 'srouter init' again after installing Docker.");
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (fallbackChoice === "source") {
|
|
2606
|
+
const defaultDir = path8.join(os6.homedir(), "srouter");
|
|
2607
|
+
await initSource(defaultDir, port);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
s.message("Verifying Docker daemon status");
|
|
2612
|
+
const checkDaemon = await runProcess("docker", ["info"], { stdio: "pipe" });
|
|
2613
|
+
if (checkDaemon.code !== 0) {
|
|
2614
|
+
s.stop(formatError("Docker daemon is not running."));
|
|
2615
|
+
p.log.info("Please start the Docker service/daemon and try again.");
|
|
2616
|
+
process.exitCode = 1;
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
const dataDir = path8.join(SROUTER_DIR2, "data");
|
|
2620
|
+
fs7.mkdirSync(dataDir, { recursive: true, mode: 448 });
|
|
2621
|
+
s.message(`Pulling latest SRouter Docker image (${DOCKER_IMAGE})`);
|
|
2622
|
+
const pullRes = await runProcess("docker", ["pull", DOCKER_IMAGE], { stdio: "inherit" });
|
|
2623
|
+
if (pullRes.code !== 0) {
|
|
2624
|
+
s.stop(formatWarning("Failed to pull image from registry, checking local image cache"));
|
|
2625
|
+
}
|
|
2626
|
+
await runProcess("docker", ["rm", "-f", "srouter"], { stdio: "pipe" });
|
|
2627
|
+
s.message(`Starting SRouter Gateway container on port ${port}`);
|
|
2628
|
+
const dockerArgs = [
|
|
2629
|
+
"run",
|
|
2630
|
+
detached ? "-d" : "-it",
|
|
2631
|
+
"--name",
|
|
2632
|
+
"srouter",
|
|
2633
|
+
"--restart",
|
|
2634
|
+
"unless-stopped",
|
|
2635
|
+
"-p",
|
|
2636
|
+
`${port}:3000`,
|
|
2637
|
+
"-p",
|
|
2638
|
+
"1455:1455",
|
|
2639
|
+
"-v",
|
|
2640
|
+
`${dataDir}:/app/data`,
|
|
2641
|
+
"-e",
|
|
2642
|
+
`PORT=${port}`,
|
|
2643
|
+
"-e",
|
|
2644
|
+
"OAUTH_PORT=1455",
|
|
2645
|
+
"-e",
|
|
2646
|
+
"DATABASE_PATH=/app/data/srouter.db",
|
|
2647
|
+
DOCKER_IMAGE
|
|
2648
|
+
];
|
|
2649
|
+
if (detached) {
|
|
2650
|
+
const runRes = await runProcess("docker", dockerArgs, { stdio: "pipe" });
|
|
2651
|
+
if (runRes.code !== 0) {
|
|
2652
|
+
s.stop(formatError("Failed to start SRouter Docker container."));
|
|
2653
|
+
p.log.error(runRes.stderr);
|
|
2654
|
+
process.exitCode = 1;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
s.stop(formatSuccess("SRouter Docker container started successfully!"));
|
|
2658
|
+
p.log.message(
|
|
2659
|
+
[
|
|
2660
|
+
`\u{1F680} Gateway URL: ${pc.bold(pc.cyan(`http://localhost:${port}`))}`,
|
|
2661
|
+
`\u{1F4CA} Web Dashboard: ${pc.bold(pc.cyan(`http://localhost:${port}`))}`,
|
|
2662
|
+
`\u{1F4BE} Persistent Data: ${pc.dim(dataDir)}`,
|
|
2663
|
+
"",
|
|
2664
|
+
`To inspect logs: ${pc.yellow("docker logs -f srouter")}`,
|
|
2665
|
+
`To stop: ${pc.yellow("docker stop srouter")}`
|
|
2666
|
+
].join("\n")
|
|
2667
|
+
);
|
|
2668
|
+
p.outro("SRouter is ready to use!");
|
|
2669
|
+
} else {
|
|
2670
|
+
s.stop("Launching container in foreground mode...");
|
|
2671
|
+
await runProcess("docker", dockerArgs, { stdio: "inherit" });
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
async function initSource(targetDir, port) {
|
|
2675
|
+
const s = p.spinner();
|
|
2676
|
+
s.start("Checking prerequisites (git, node, pnpm)");
|
|
2677
|
+
const hasGit = await isExecutableInPath("git");
|
|
2678
|
+
const hasPnpm = await isExecutableInPath("pnpm");
|
|
2679
|
+
if (!hasGit) {
|
|
2680
|
+
s.stop(formatError("Git is required to clone SRouter repository."));
|
|
2681
|
+
process.exitCode = 1;
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const resolvedDir = path8.resolve(targetDir);
|
|
2685
|
+
if (!fs7.existsSync(path8.join(resolvedDir, "package.json"))) {
|
|
2686
|
+
s.message(`Cloning SRouter into ${pc.bold(resolvedDir)}`);
|
|
2687
|
+
fs7.mkdirSync(resolvedDir, { recursive: true });
|
|
2688
|
+
const cloneRes = await runProcess("git", ["clone", REPO_URL, resolvedDir], {
|
|
2689
|
+
stdio: "inherit"
|
|
2690
|
+
});
|
|
2691
|
+
if (cloneRes.code !== 0) {
|
|
2692
|
+
s.stop(formatError("Failed to clone SRouter repository."));
|
|
2693
|
+
process.exitCode = 1;
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
} else {
|
|
2697
|
+
p.log.info(`Using existing SRouter repository at ${pc.bold(resolvedDir)}`);
|
|
2698
|
+
}
|
|
2699
|
+
s.message("Installing dependencies with pnpm");
|
|
2700
|
+
const pnpmCmd = hasPnpm ? "pnpm" : "npx";
|
|
2701
|
+
const installArgs = hasPnpm ? ["install"] : ["pnpm", "install"];
|
|
2702
|
+
const installRes = await runProcess(pnpmCmd, installArgs, {
|
|
2703
|
+
cwd: resolvedDir,
|
|
2704
|
+
stdio: "inherit"
|
|
2705
|
+
});
|
|
2706
|
+
if (installRes.code !== 0) {
|
|
2707
|
+
s.stop(formatError("Dependency installation failed."));
|
|
2708
|
+
process.exitCode = 1;
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
s.message("Building SRouter packages, API, and Dashboard");
|
|
2712
|
+
const buildArgs = hasPnpm ? ["run", "build"] : ["pnpm", "run", "build"];
|
|
2713
|
+
const buildRes = await runProcess(pnpmCmd, buildArgs, {
|
|
2714
|
+
cwd: resolvedDir,
|
|
2715
|
+
stdio: "inherit"
|
|
2716
|
+
});
|
|
2717
|
+
if (buildRes.code !== 0) {
|
|
2718
|
+
s.stop(formatError("Build step failed."));
|
|
2719
|
+
process.exitCode = 1;
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
s.stop(formatSuccess("SRouter built successfully!"));
|
|
2723
|
+
p.log.message(
|
|
2724
|
+
[
|
|
2725
|
+
`\u{1F4C1} Project Directory: ${pc.bold(resolvedDir)}`,
|
|
2726
|
+
`\u{1F680} Start Dev Server: ${pc.yellow(`cd ${resolvedDir} && pnpm dev`)}`,
|
|
2727
|
+
`\u26A1 Start Production: ${pc.yellow(`PORT=${port} cd ${resolvedDir}/apps/api && pnpm start`)}`
|
|
2728
|
+
].join("\n")
|
|
2729
|
+
);
|
|
2730
|
+
const startNow = await p.confirm({
|
|
2731
|
+
message: "Start SRouter API & Web server now?",
|
|
2732
|
+
initialValue: true
|
|
2733
|
+
});
|
|
2734
|
+
if (startNow === true) {
|
|
2735
|
+
p.outro(`Starting SRouter Gateway on port ${port}...`);
|
|
2736
|
+
process.env.PORT = port;
|
|
2737
|
+
const startArgs = hasPnpm ? ["--filter", "api", "start"] : ["pnpm", "--filter", "api", "start"];
|
|
2738
|
+
await runProcess(pnpmCmd, startArgs, {
|
|
2739
|
+
cwd: resolvedDir,
|
|
2740
|
+
stdio: "inherit"
|
|
2741
|
+
});
|
|
2742
|
+
} else {
|
|
2743
|
+
p.outro("Setup complete! You can start SRouter anytime.");
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
async function initCommand(options) {
|
|
2747
|
+
p.intro(pc.bold(pc.cyan("\u26A1 SRouter Gateway Initialization")));
|
|
2748
|
+
let mode = options.mode;
|
|
2749
|
+
if (!mode) {
|
|
2750
|
+
const choice = await p.select({
|
|
2751
|
+
message: "How would you like to run SRouter?",
|
|
2752
|
+
options: [
|
|
2753
|
+
{
|
|
2754
|
+
value: "docker",
|
|
2755
|
+
label: "Docker (Recommended)",
|
|
2756
|
+
hint: "Zero-config container with isolated environment & persistent database"
|
|
2757
|
+
},
|
|
2758
|
+
{
|
|
2759
|
+
value: "source",
|
|
2760
|
+
label: "Source Code (Node.js & pnpm)",
|
|
2761
|
+
hint: "Clone repository, build from source, and run natively"
|
|
2762
|
+
}
|
|
2763
|
+
]
|
|
2764
|
+
});
|
|
2765
|
+
if (p.isCancel(choice)) {
|
|
2766
|
+
p.outro("Initialization cancelled.");
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
mode = choice;
|
|
2770
|
+
}
|
|
2771
|
+
let port = options.port;
|
|
2772
|
+
if (!port && !options.yes) {
|
|
2773
|
+
const inputPort = await p.text({
|
|
2774
|
+
message: "Gateway Port:",
|
|
2775
|
+
defaultValue: DEFAULT_PORT,
|
|
2776
|
+
placeholder: DEFAULT_PORT,
|
|
2777
|
+
validate: (val) => {
|
|
2778
|
+
const n = Number(val);
|
|
2779
|
+
if (Number.isNaN(n) || n <= 0 || n > 65535) {
|
|
2780
|
+
return "Please enter a valid port number (1-65535).";
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
if (p.isCancel(inputPort)) {
|
|
2785
|
+
p.outro("Initialization cancelled.");
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
port = inputPort || DEFAULT_PORT;
|
|
2789
|
+
} else {
|
|
2790
|
+
port = port || DEFAULT_PORT;
|
|
2791
|
+
}
|
|
2792
|
+
if (mode === "docker") {
|
|
2793
|
+
const detached = options.detached ?? true;
|
|
2794
|
+
await initDocker(port, detached);
|
|
2795
|
+
} else {
|
|
2796
|
+
const defaultDir = path8.join(os6.homedir(), "srouter");
|
|
2797
|
+
let targetDir = options.dir;
|
|
2798
|
+
if (!targetDir && !options.yes) {
|
|
2799
|
+
const inputDir = await p.text({
|
|
2800
|
+
message: "Target directory for SRouter source code:",
|
|
2801
|
+
defaultValue: defaultDir,
|
|
2802
|
+
placeholder: defaultDir
|
|
2803
|
+
});
|
|
2804
|
+
if (p.isCancel(inputDir)) {
|
|
2805
|
+
p.outro("Initialization cancelled.");
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
targetDir = inputDir || defaultDir;
|
|
2809
|
+
} else {
|
|
2810
|
+
targetDir = targetDir || defaultDir;
|
|
2811
|
+
}
|
|
2812
|
+
await initSource(targetDir, port);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// src/commands/link.ts
|
|
2817
|
+
async function linkCommand(toolId, options) {
|
|
2818
|
+
const adapter = getAdapter(toolId);
|
|
2819
|
+
if (!adapter) {
|
|
2820
|
+
console.error(
|
|
2821
|
+
formatError(
|
|
2822
|
+
`Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
2823
|
+
)
|
|
2824
|
+
);
|
|
2825
|
+
process.exitCode = 1;
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
2829
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
2830
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
2831
|
+
const model = options.model || savedConfig.defaultModel;
|
|
2832
|
+
const opusModel = options.opusModel || savedConfig.defaultOpusModel;
|
|
2833
|
+
const sonnetModel = options.sonnetModel || savedConfig.defaultSonnetModel;
|
|
2834
|
+
const haikuModel = options.haikuModel || savedConfig.defaultHaikuModel;
|
|
2835
|
+
try {
|
|
2836
|
+
const result = await adapter.link({
|
|
2837
|
+
baseUrl,
|
|
2838
|
+
apiKey,
|
|
2839
|
+
model,
|
|
2840
|
+
opusModel,
|
|
2841
|
+
sonnetModel,
|
|
2842
|
+
haikuModel,
|
|
2843
|
+
dryRun: options.dryRun
|
|
2844
|
+
});
|
|
2845
|
+
console.log(
|
|
2846
|
+
formatSuccess(
|
|
2847
|
+
`Successfully configured ${pc.bold(pc.cyan(adapter.name))} with SRouter proxy!`
|
|
2848
|
+
)
|
|
2849
|
+
);
|
|
2850
|
+
console.log(` ${pc.gray("Target Config:")} ${pc.white(result.modifiedPath)}`);
|
|
2851
|
+
console.log(` ${pc.gray("Proxy URL:")} ${pc.white(baseUrl)}`);
|
|
2852
|
+
if (model) {
|
|
2853
|
+
console.log(` ${pc.gray("Model:")} ${pc.white(model)}`);
|
|
2854
|
+
}
|
|
2855
|
+
if (opusModel) {
|
|
2856
|
+
console.log(` ${pc.gray("Opus Model:")} ${pc.white(opusModel)}`);
|
|
2857
|
+
}
|
|
2858
|
+
if (sonnetModel) {
|
|
2859
|
+
console.log(` ${pc.gray("Sonnet Model:")} ${pc.white(sonnetModel)}`);
|
|
2860
|
+
}
|
|
2861
|
+
if (haikuModel) {
|
|
2862
|
+
console.log(` ${pc.gray("Haiku Model:")} ${pc.white(haikuModel)}`);
|
|
2863
|
+
}
|
|
2864
|
+
if (result.backupPath) {
|
|
2865
|
+
console.log(` ${pc.gray("Backup Saved:")} ${pc.white(result.backupPath)}`);
|
|
2866
|
+
}
|
|
2867
|
+
} catch (err) {
|
|
2868
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2869
|
+
console.error(formatError(`Failed to link ${adapter.name}: ${msg}`));
|
|
2870
|
+
process.exitCode = 1;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// src/commands/unlink.ts
|
|
2875
|
+
async function unlinkCommand(toolId) {
|
|
2876
|
+
const adapter = getAdapter(toolId);
|
|
2877
|
+
if (!adapter) {
|
|
2878
|
+
console.error(
|
|
2879
|
+
formatError(
|
|
2880
|
+
`Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
2881
|
+
)
|
|
2882
|
+
);
|
|
2883
|
+
process.exitCode = 1;
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
const restored = await adapter.unlink();
|
|
2888
|
+
if (restored) {
|
|
2889
|
+
console.log(
|
|
2890
|
+
formatSuccess(
|
|
2891
|
+
`Restored original configuration for ${pc.bold(pc.cyan(adapter.name))}!`
|
|
2892
|
+
)
|
|
2893
|
+
);
|
|
2894
|
+
} else {
|
|
2895
|
+
console.log(
|
|
2896
|
+
formatInfo(
|
|
2897
|
+
`No active SRouter configuration or backup found for ${pc.bold(adapter.name)}.`
|
|
2898
|
+
)
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
} catch (err) {
|
|
2902
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2903
|
+
console.error(formatError(`Failed to unlink ${adapter.name}: ${msg}`));
|
|
2904
|
+
process.exitCode = 1;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/commands/sync.ts
|
|
2909
|
+
async function syncCommand(toolId, options = {}) {
|
|
2910
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
2911
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
2912
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
2913
|
+
const health = await checkServerHealth(baseUrl, apiKey);
|
|
2914
|
+
if (!health.healthy) {
|
|
2915
|
+
console.error(
|
|
2916
|
+
formatError(
|
|
2917
|
+
`Cannot sync: SRouter Gateway is unreachable at ${pc.bold(baseUrl)} (${health.error || "offline"}).`
|
|
2918
|
+
)
|
|
2919
|
+
);
|
|
2920
|
+
process.exitCode = 1;
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const availableModels = await fetchAvailableModels(baseUrl, apiKey);
|
|
2924
|
+
if (availableModels.length === 0) {
|
|
2925
|
+
console.warn(
|
|
2926
|
+
formatWarning(`SRouter Gateway responded at ${baseUrl}, but returned 0 models.`)
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
const adaptersToSync = toolId ? [getAdapter(toolId)].filter((a) => Boolean(a)) : getAllAdapters();
|
|
2930
|
+
if (toolId && adaptersToSync.length === 0) {
|
|
2931
|
+
console.error(formatError(`Tool '${pc.bold(toolId)}' not supported.`));
|
|
2932
|
+
process.exitCode = 1;
|
|
2933
|
+
return;
|
|
1687
2934
|
}
|
|
1688
2935
|
for (const adapter of adaptersToSync) {
|
|
1689
2936
|
if (!adapter) continue;
|
|
@@ -1848,7 +3095,7 @@ async function envCommand(toolId, options = {}) {
|
|
|
1848
3095
|
}
|
|
1849
3096
|
|
|
1850
3097
|
// src/commands/run.ts
|
|
1851
|
-
import { spawn } from "node:child_process";
|
|
3098
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1852
3099
|
async function runCommand(toolId, toolArgs, options = {}) {
|
|
1853
3100
|
const adapter = getAdapter(toolId);
|
|
1854
3101
|
if (!adapter) {
|
|
@@ -1876,7 +3123,7 @@ async function runCommand(toolId, toolArgs, options = {}) {
|
|
|
1876
3123
|
haikuModel
|
|
1877
3124
|
});
|
|
1878
3125
|
const binaryName = toolId === "claude" ? "claude" : "opencode";
|
|
1879
|
-
const child =
|
|
3126
|
+
const child = spawn2(binaryName, toolArgs, {
|
|
1880
3127
|
stdio: "inherit",
|
|
1881
3128
|
env: {
|
|
1882
3129
|
...process.env,
|
|
@@ -1903,254 +3150,11 @@ async function runCommand(toolId, toolArgs, options = {}) {
|
|
|
1903
3150
|
}
|
|
1904
3151
|
|
|
1905
3152
|
// src/commands/migrate.ts
|
|
1906
|
-
import
|
|
1907
|
-
import
|
|
1908
|
-
import
|
|
1909
|
-
import * as
|
|
3153
|
+
import fs8 from "node:fs";
|
|
3154
|
+
import os7 from "node:os";
|
|
3155
|
+
import path9 from "node:path";
|
|
3156
|
+
import * as p2 from "@clack/prompts";
|
|
1910
3157
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1911
|
-
|
|
1912
|
-
// ../../packages/db/dist/db.js
|
|
1913
|
-
import { DatabaseSync } from "node:sqlite";
|
|
1914
|
-
import path5 from "node:path";
|
|
1915
|
-
import fs5 from "node:fs";
|
|
1916
|
-
import os4 from "node:os";
|
|
1917
|
-
var SROUTER_DIR = path5.join(os4.homedir(), ".srouter");
|
|
1918
|
-
var DEFAULT_DB_PATH = path5.join(SROUTER_DIR, "srouter.db");
|
|
1919
|
-
var LEGACY_DB_LOCATIONS = [
|
|
1920
|
-
path5.resolve(process.cwd(), "apps/api/srouter.db"),
|
|
1921
|
-
path5.resolve(process.cwd(), "srouter.db")
|
|
1922
|
-
];
|
|
1923
|
-
function getDatabasePath() {
|
|
1924
|
-
if (process.env.DATABASE_PATH)
|
|
1925
|
-
return process.env.DATABASE_PATH;
|
|
1926
|
-
for (const legacyPath of LEGACY_DB_LOCATIONS) {
|
|
1927
|
-
if (fs5.existsSync(legacyPath))
|
|
1928
|
-
return legacyPath;
|
|
1929
|
-
}
|
|
1930
|
-
return DEFAULT_DB_PATH;
|
|
1931
|
-
}
|
|
1932
|
-
var dbPath = getDatabasePath();
|
|
1933
|
-
var dbDir = path5.dirname(dbPath);
|
|
1934
|
-
if (!fs5.existsSync(dbDir)) {
|
|
1935
|
-
fs5.mkdirSync(dbDir, { recursive: true });
|
|
1936
|
-
}
|
|
1937
|
-
var db = new DatabaseSync(dbPath);
|
|
1938
|
-
db.exec("PRAGMA busy_timeout = 5000;");
|
|
1939
|
-
function execWithRetry(sql, attempts = 5) {
|
|
1940
|
-
for (let i = 0; i < attempts; i++) {
|
|
1941
|
-
try {
|
|
1942
|
-
db.exec(sql);
|
|
1943
|
-
return;
|
|
1944
|
-
} catch (error) {
|
|
1945
|
-
const code = error.code;
|
|
1946
|
-
if (code === "SQLITE_BUSY" && i < attempts - 1) {
|
|
1947
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
|
|
1948
|
-
continue;
|
|
1949
|
-
}
|
|
1950
|
-
throw error;
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
execWithRetry("PRAGMA journal_mode = WAL;");
|
|
1955
|
-
execWithRetry("PRAGMA foreign_keys = ON;");
|
|
1956
|
-
function ensureColumns(table, columns) {
|
|
1957
|
-
const existing = new Set(db.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name));
|
|
1958
|
-
for (const col of columns) {
|
|
1959
|
-
if (existing.has(col.name))
|
|
1960
|
-
continue;
|
|
1961
|
-
try {
|
|
1962
|
-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
|
|
1963
|
-
existing.add(col.name);
|
|
1964
|
-
} catch (error) {
|
|
1965
|
-
const message = error.message || "";
|
|
1966
|
-
if (!message.includes("duplicate column name")) {
|
|
1967
|
-
throw error;
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
function initDatabase() {
|
|
1973
|
-
db.exec(`
|
|
1974
|
-
CREATE TABLE IF NOT EXISTS providers (
|
|
1975
|
-
id TEXT PRIMARY KEY,
|
|
1976
|
-
provider_id TEXT NOT NULL,
|
|
1977
|
-
name TEXT NOT NULL,
|
|
1978
|
-
category TEXT NOT NULL,
|
|
1979
|
-
protocol TEXT NOT NULL,
|
|
1980
|
-
base_url TEXT,
|
|
1981
|
-
api_key TEXT,
|
|
1982
|
-
access_token TEXT,
|
|
1983
|
-
refresh_token TEXT,
|
|
1984
|
-
custom_headers TEXT,
|
|
1985
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1986
|
-
created_at INTEGER NOT NULL
|
|
1987
|
-
);
|
|
1988
|
-
`);
|
|
1989
|
-
ensureColumns("providers", [
|
|
1990
|
-
{ name: "refresh_token", definition: "refresh_token TEXT" },
|
|
1991
|
-
// Multi-account OAuth binding (e.g. Codex ChatGPT-Account-ID)
|
|
1992
|
-
{ name: "account_id", definition: "account_id TEXT" },
|
|
1993
|
-
// Provider-specific metadata (for example Kiro auth method/region/profile ARN)
|
|
1994
|
-
{ name: "provider_specific_data", definition: "provider_specific_data TEXT" },
|
|
1995
|
-
// Token expiry tracking
|
|
1996
|
-
{ name: "token_expires_at", definition: "token_expires_at INTEGER" },
|
|
1997
|
-
{ name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
|
|
1998
|
-
// Claude OAuth organization binding
|
|
1999
|
-
{ name: "organization_id", definition: "organization_id TEXT" }
|
|
2000
|
-
]);
|
|
2001
|
-
db.exec(`
|
|
2002
|
-
CREATE TABLE IF NOT EXISTS api_keys (
|
|
2003
|
-
id TEXT PRIMARY KEY,
|
|
2004
|
-
key TEXT UNIQUE NOT NULL,
|
|
2005
|
-
name TEXT NOT NULL,
|
|
2006
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2007
|
-
rate_limit INTEGER DEFAULT 0,
|
|
2008
|
-
quota_limit INTEGER DEFAULT 0,
|
|
2009
|
-
usage_tokens INTEGER DEFAULT 0,
|
|
2010
|
-
created_at INTEGER NOT NULL
|
|
2011
|
-
);
|
|
2012
|
-
`);
|
|
2013
|
-
db.exec(`
|
|
2014
|
-
CREATE TABLE IF NOT EXISTS request_logs (
|
|
2015
|
-
id TEXT PRIMARY KEY,
|
|
2016
|
-
api_key_id TEXT,
|
|
2017
|
-
provider_id TEXT NOT NULL,
|
|
2018
|
-
model TEXT NOT NULL,
|
|
2019
|
-
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2020
|
-
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2021
|
-
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2022
|
-
status_code INTEGER NOT NULL,
|
|
2023
|
-
latency_ms INTEGER NOT NULL,
|
|
2024
|
-
created_at INTEGER NOT NULL
|
|
2025
|
-
);
|
|
2026
|
-
`);
|
|
2027
|
-
db.exec(`
|
|
2028
|
-
CREATE TABLE IF NOT EXISTS oauth_sessions (
|
|
2029
|
-
state TEXT PRIMARY KEY,
|
|
2030
|
-
code_verifier TEXT NOT NULL,
|
|
2031
|
-
client_id TEXT NOT NULL,
|
|
2032
|
-
redirect_uri TEXT NOT NULL,
|
|
2033
|
-
created_at INTEGER NOT NULL
|
|
2034
|
-
);
|
|
2035
|
-
`);
|
|
2036
|
-
ensureColumns("request_logs", [
|
|
2037
|
-
{ name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2038
|
-
{
|
|
2039
|
-
name: "cache_creation_tokens",
|
|
2040
|
-
definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
|
|
2041
|
-
},
|
|
2042
|
-
{ name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2043
|
-
{ name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
|
|
2044
|
-
{ name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
|
|
2045
|
-
{ name: "fallback_path", definition: "fallback_path TEXT" },
|
|
2046
|
-
{ name: "fallback_reason", definition: "fallback_reason TEXT" },
|
|
2047
|
-
{ name: "resolved_model", definition: "resolved_model TEXT" }
|
|
2048
|
-
]);
|
|
2049
|
-
db.exec(`
|
|
2050
|
-
CREATE TABLE IF NOT EXISTS fallback_rules (
|
|
2051
|
-
id TEXT PRIMARY KEY,
|
|
2052
|
-
source_model TEXT NOT NULL,
|
|
2053
|
-
target_model TEXT NOT NULL,
|
|
2054
|
-
priority INTEGER NOT NULL DEFAULT 1,
|
|
2055
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2056
|
-
trigger_on_status TEXT,
|
|
2057
|
-
max_retries INTEGER DEFAULT 1,
|
|
2058
|
-
created_at INTEGER NOT NULL
|
|
2059
|
-
);
|
|
2060
|
-
`);
|
|
2061
|
-
db.exec(`
|
|
2062
|
-
CREATE TABLE IF NOT EXISTS system_settings (
|
|
2063
|
-
key TEXT PRIMARY KEY,
|
|
2064
|
-
value TEXT NOT NULL
|
|
2065
|
-
);
|
|
2066
|
-
`);
|
|
2067
|
-
db.exec(`
|
|
2068
|
-
CREATE TABLE IF NOT EXISTS custom_models (
|
|
2069
|
-
provider_id TEXT NOT NULL,
|
|
2070
|
-
model_id TEXT NOT NULL,
|
|
2071
|
-
created_at INTEGER NOT NULL,
|
|
2072
|
-
PRIMARY KEY (provider_id, model_id)
|
|
2073
|
-
);
|
|
2074
|
-
`);
|
|
2075
|
-
}
|
|
2076
|
-
initDatabase();
|
|
2077
|
-
|
|
2078
|
-
// ../../packages/db/dist/row-utils.js
|
|
2079
|
-
function str(value, fallback = "") {
|
|
2080
|
-
return value === null || value === void 0 ? fallback : String(value);
|
|
2081
|
-
}
|
|
2082
|
-
function num(value, fallback = 0) {
|
|
2083
|
-
const n = Number(value);
|
|
2084
|
-
return Number.isFinite(n) ? n : fallback;
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
// ../../packages/db/dist/adminAuth.js
|
|
2088
|
-
var AdminAuthStore = class {
|
|
2089
|
-
database;
|
|
2090
|
-
constructor(database = db) {
|
|
2091
|
-
this.database = database;
|
|
2092
|
-
this.database.exec(`
|
|
2093
|
-
CREATE TABLE IF NOT EXISTS admin_account (
|
|
2094
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
2095
|
-
password_hash TEXT NOT NULL,
|
|
2096
|
-
created_at INTEGER NOT NULL,
|
|
2097
|
-
updated_at INTEGER NOT NULL
|
|
2098
|
-
);
|
|
2099
|
-
|
|
2100
|
-
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
2101
|
-
token_hash TEXT PRIMARY KEY,
|
|
2102
|
-
created_at INTEGER NOT NULL,
|
|
2103
|
-
expires_at INTEGER NOT NULL
|
|
2104
|
-
);
|
|
2105
|
-
`);
|
|
2106
|
-
}
|
|
2107
|
-
hasAdminAccount() {
|
|
2108
|
-
const Row = this.database.prepare("SELECT 1 AS present FROM admin_account WHERE id = 1").get();
|
|
2109
|
-
return Boolean(Row);
|
|
2110
|
-
}
|
|
2111
|
-
createAdminAccount(passwordHash, now = Date.now()) {
|
|
2112
|
-
const Result = this.database.prepare(`INSERT OR IGNORE INTO admin_account (id, password_hash, created_at, updated_at)
|
|
2113
|
-
VALUES (1, ?, ?, ?)`).run(passwordHash, now, now);
|
|
2114
|
-
return num(Result.changes) > 0;
|
|
2115
|
-
}
|
|
2116
|
-
getPasswordHash() {
|
|
2117
|
-
const Row = this.database.prepare("SELECT password_hash FROM admin_account WHERE id = 1").get();
|
|
2118
|
-
return Row?.password_hash ? str(Row.password_hash) : null;
|
|
2119
|
-
}
|
|
2120
|
-
updatePasswordHash(passwordHash, now = Date.now()) {
|
|
2121
|
-
const Result = this.database.prepare(`UPDATE admin_account
|
|
2122
|
-
SET password_hash = ?, updated_at = ?
|
|
2123
|
-
WHERE id = 1`).run(passwordHash, now);
|
|
2124
|
-
return num(Result.changes) > 0;
|
|
2125
|
-
}
|
|
2126
|
-
createSession(tokenHash, createdAt, expiresAt) {
|
|
2127
|
-
this.database.prepare(`INSERT INTO admin_sessions (token_hash, created_at, expires_at)
|
|
2128
|
-
VALUES (?, ?, ?)`).run(tokenHash, createdAt, expiresAt);
|
|
2129
|
-
}
|
|
2130
|
-
getSession(tokenHash, now = Date.now()) {
|
|
2131
|
-
this.database.prepare("DELETE FROM admin_sessions WHERE expires_at <= ?").run(now);
|
|
2132
|
-
const Row = this.database.prepare(`SELECT token_hash, created_at, expires_at
|
|
2133
|
-
FROM admin_sessions
|
|
2134
|
-
WHERE token_hash = ? AND expires_at > ?`).get(tokenHash, now);
|
|
2135
|
-
if (!Row)
|
|
2136
|
-
return null;
|
|
2137
|
-
return {
|
|
2138
|
-
tokenHash: str(Row.token_hash),
|
|
2139
|
-
createdAt: num(Row.created_at),
|
|
2140
|
-
expiresAt: num(Row.expires_at)
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
deleteSession(tokenHash) {
|
|
2144
|
-
const Result = this.database.prepare("DELETE FROM admin_sessions WHERE token_hash = ?").run(tokenHash);
|
|
2145
|
-
return num(Result.changes) > 0;
|
|
2146
|
-
}
|
|
2147
|
-
};
|
|
2148
|
-
var adminAuthStore = new AdminAuthStore();
|
|
2149
|
-
|
|
2150
|
-
// ../../packages/db/dist/quota.js
|
|
2151
|
-
var CN_REFILL_GAP_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
2152
|
-
|
|
2153
|
-
// src/commands/migrate.ts
|
|
2154
3158
|
function parseDateToTimestamp(val) {
|
|
2155
3159
|
if (typeof val === "number" && Number.isFinite(val)) return val;
|
|
2156
3160
|
if (typeof val === "string") {
|
|
@@ -2279,58 +3283,58 @@ function importNineRouterJson(data, targetDb, action) {
|
|
|
2279
3283
|
}
|
|
2280
3284
|
return { inserted, skipped, tablesCount };
|
|
2281
3285
|
}
|
|
2282
|
-
var TargetDbPath =
|
|
2283
|
-
var BackupDir =
|
|
3286
|
+
var TargetDbPath = DEFAULT_DB_PATH2;
|
|
3287
|
+
var BackupDir = path9.join(SROUTER_DIR2, "backups");
|
|
2284
3288
|
var NineRouterDbLocations = [
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
3289
|
+
path9.join(os7.homedir(), ".9router", "srouter.db"),
|
|
3290
|
+
path9.join(os7.homedir(), ".9router", "9router.db"),
|
|
3291
|
+
path9.join(os7.homedir(), "9router", "srouter.db"),
|
|
3292
|
+
path9.join(os7.homedir(), "9router", "data.db"),
|
|
3293
|
+
path9.join(os7.homedir(), "9router", "db", "srouter.db"),
|
|
3294
|
+
path9.join(os7.homedir(), ".config", "9router", "srouter.db"),
|
|
3295
|
+
path9.join(os7.homedir(), ".config", "9router", "9router.db"),
|
|
2292
3296
|
"/root/9router/srouter.db",
|
|
2293
3297
|
"/root/project/9router/db/srouter.db",
|
|
2294
|
-
...
|
|
3298
|
+
...LEGACY_DB_LOCATIONS2
|
|
2295
3299
|
];
|
|
2296
3300
|
function fileKb(filePath) {
|
|
2297
|
-
return `${(
|
|
3301
|
+
return `${(fs8.statSync(filePath).size / 1024).toFixed(2)} KB`;
|
|
2298
3302
|
}
|
|
2299
3303
|
function ensureDirs() {
|
|
2300
|
-
|
|
2301
|
-
|
|
3304
|
+
fs8.mkdirSync(SROUTER_DIR2, { recursive: true, mode: 448 });
|
|
3305
|
+
fs8.mkdirSync(BackupDir, { recursive: true, mode: 493 });
|
|
2302
3306
|
}
|
|
2303
3307
|
function backupDb(source, label) {
|
|
2304
3308
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2305
|
-
const backupPath =
|
|
2306
|
-
|
|
2307
|
-
|
|
3309
|
+
const backupPath = path9.join(BackupDir, `${label}-backup-${timestamp}.db`);
|
|
3310
|
+
fs8.copyFileSync(source, backupPath);
|
|
3311
|
+
p2.log.step(`Backed up to ${pc.dim(backupPath)}`);
|
|
2308
3312
|
return backupPath;
|
|
2309
3313
|
}
|
|
2310
3314
|
function getDownloadDirs() {
|
|
2311
|
-
const home =
|
|
3315
|
+
const home = os7.homedir();
|
|
2312
3316
|
const dirs = [
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
3317
|
+
path9.join(home, "Downloads"),
|
|
3318
|
+
path9.join(home, "download"),
|
|
3319
|
+
path9.join(home, "downloads"),
|
|
3320
|
+
path9.join(home, "Desktop")
|
|
2317
3321
|
];
|
|
2318
3322
|
if (process.platform === "win32") {
|
|
2319
3323
|
if (process.env.USERPROFILE) {
|
|
2320
|
-
dirs.push(
|
|
2321
|
-
dirs.push(
|
|
3324
|
+
dirs.push(path9.join(process.env.USERPROFILE, "Downloads"));
|
|
3325
|
+
dirs.push(path9.join(process.env.USERPROFILE, "Desktop"));
|
|
2322
3326
|
}
|
|
2323
3327
|
}
|
|
2324
|
-
return Array.from(new Set(dirs.filter((d) =>
|
|
3328
|
+
return Array.from(new Set(dirs.filter((d) => fs8.existsSync(d))));
|
|
2325
3329
|
}
|
|
2326
3330
|
function scanFor9RouterFiles() {
|
|
2327
3331
|
const results = [];
|
|
2328
3332
|
const seen = /* @__PURE__ */ new Set();
|
|
2329
3333
|
const addFile = (filePath, label) => {
|
|
2330
|
-
const resolved =
|
|
2331
|
-
if (!seen.has(resolved) &&
|
|
3334
|
+
const resolved = path9.resolve(filePath);
|
|
3335
|
+
if (!seen.has(resolved) && fs8.existsSync(resolved)) {
|
|
2332
3336
|
try {
|
|
2333
|
-
const stat =
|
|
3337
|
+
const stat = fs8.statSync(resolved);
|
|
2334
3338
|
if (stat.isFile()) {
|
|
2335
3339
|
seen.add(resolved);
|
|
2336
3340
|
results.push({ path: resolved, label, mtime: stat.mtimeMs });
|
|
@@ -2344,12 +3348,12 @@ function scanFor9RouterFiles() {
|
|
|
2344
3348
|
}
|
|
2345
3349
|
for (const downloadDir of getDownloadDirs()) {
|
|
2346
3350
|
try {
|
|
2347
|
-
const files =
|
|
3351
|
+
const files = fs8.readdirSync(downloadDir);
|
|
2348
3352
|
for (const file of files) {
|
|
2349
3353
|
const lower = file.toLowerCase();
|
|
2350
|
-
const fullPath =
|
|
3354
|
+
const fullPath = path9.join(downloadDir, file);
|
|
2351
3355
|
if (lower.includes("9router") || lower.includes("srouter-backup") || lower.endsWith(".db") || lower.endsWith(".json") && lower.includes("backup")) {
|
|
2352
|
-
addFile(fullPath,
|
|
3356
|
+
addFile(fullPath, path9.basename(downloadDir));
|
|
2353
3357
|
}
|
|
2354
3358
|
}
|
|
2355
3359
|
} catch {
|
|
@@ -2359,88 +3363,88 @@ function scanFor9RouterFiles() {
|
|
|
2359
3363
|
}
|
|
2360
3364
|
function findDatabase(candidates, label) {
|
|
2361
3365
|
for (const candidate of candidates) {
|
|
2362
|
-
const fullPath =
|
|
2363
|
-
if (
|
|
2364
|
-
|
|
3366
|
+
const fullPath = path9.resolve(candidate);
|
|
3367
|
+
if (fs8.existsSync(fullPath)) {
|
|
3368
|
+
p2.log.info(`Found ${label} database: ${pc.bold(fullPath)} (${fileKb(fullPath)})`);
|
|
2365
3369
|
return fullPath;
|
|
2366
3370
|
}
|
|
2367
3371
|
}
|
|
2368
3372
|
return null;
|
|
2369
3373
|
}
|
|
2370
3374
|
async function migrateDb(options) {
|
|
2371
|
-
|
|
2372
|
-
if (
|
|
2373
|
-
|
|
2374
|
-
|
|
3375
|
+
p2.intro("SRouter Database Migration");
|
|
3376
|
+
if (fs8.existsSync(TargetDbPath)) {
|
|
3377
|
+
p2.log.warn(`Database already exists at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
|
|
3378
|
+
p2.outro("No migration needed \u2014 already using the new location.");
|
|
2375
3379
|
return;
|
|
2376
3380
|
}
|
|
2377
|
-
const source = options.source &&
|
|
3381
|
+
const source = options.source && fs8.existsSync(options.source) ? path9.resolve(options.source) : findDatabase(LEGACY_DB_LOCATIONS2, "legacy");
|
|
2378
3382
|
if (!source) {
|
|
2379
|
-
|
|
2380
|
-
|
|
3383
|
+
p2.log.error("No existing database found.");
|
|
3384
|
+
p2.outro(
|
|
2381
3385
|
`Start SRouter and a fresh database will be created at ${TargetDbPath}, or pass --source /path/to/srouter.db.`
|
|
2382
3386
|
);
|
|
2383
3387
|
return;
|
|
2384
3388
|
}
|
|
2385
3389
|
ensureDirs();
|
|
2386
|
-
const proceed = options.yes || await
|
|
3390
|
+
const proceed = options.yes || await p2.confirm({ message: `Migrate database from ${source}?` }) === true;
|
|
2387
3391
|
if (!proceed) {
|
|
2388
|
-
|
|
3392
|
+
p2.outro("Migration cancelled.");
|
|
2389
3393
|
return;
|
|
2390
3394
|
}
|
|
2391
3395
|
try {
|
|
2392
3396
|
backupDb(source, "srouter");
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
3397
|
+
fs8.copyFileSync(source, TargetDbPath);
|
|
3398
|
+
fs8.chmodSync(TargetDbPath, 384);
|
|
3399
|
+
p2.log.success(`Database moved to ${TargetDbPath}`);
|
|
3400
|
+
p2.outro("Restart SRouter to use the migrated database.");
|
|
2397
3401
|
} catch (error) {
|
|
2398
|
-
|
|
3402
|
+
p2.log.error(formatError(`Migration failed: ${error.message}`));
|
|
2399
3403
|
process.exitCode = 1;
|
|
2400
3404
|
}
|
|
2401
3405
|
}
|
|
2402
3406
|
async function migrateNineRouter(options) {
|
|
2403
|
-
|
|
2404
|
-
let source = options.source &&
|
|
3407
|
+
p2.intro("9Router \u2192 SRouter Database Migration");
|
|
3408
|
+
let source = options.source && fs8.existsSync(options.source) ? path9.resolve(options.source) : null;
|
|
2405
3409
|
if (!source) {
|
|
2406
3410
|
const foundFiles = scanFor9RouterFiles();
|
|
2407
3411
|
if (foundFiles.length === 1) {
|
|
2408
3412
|
source = foundFiles[0].path;
|
|
2409
|
-
|
|
3413
|
+
p2.log.info(`Found 9Router file: ${pc.bold(source)} (${fileKb(source)}) [${foundFiles[0].label}]`);
|
|
2410
3414
|
} else if (foundFiles.length > 1) {
|
|
2411
3415
|
if (options.yes) {
|
|
2412
3416
|
source = foundFiles[0].path;
|
|
2413
|
-
|
|
3417
|
+
p2.log.info(`Auto-selected latest 9Router file: ${pc.bold(source)} (${fileKb(source)})`);
|
|
2414
3418
|
} else {
|
|
2415
|
-
const choice = await
|
|
3419
|
+
const choice = await p2.select({
|
|
2416
3420
|
message: "Multiple 9Router database/backup files found. Select one to migrate:",
|
|
2417
3421
|
options: [
|
|
2418
3422
|
...foundFiles.map((f) => ({
|
|
2419
3423
|
value: f.path,
|
|
2420
|
-
label: `${
|
|
3424
|
+
label: `${path9.basename(f.path)} (${fileKb(f.path)})`,
|
|
2421
3425
|
hint: `${f.label} \u2022 ${f.path}`
|
|
2422
3426
|
})),
|
|
2423
3427
|
{ value: "custom", label: "Enter custom path manually..." }
|
|
2424
3428
|
]
|
|
2425
3429
|
});
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
3430
|
+
if (p2.isCancel(choice)) {
|
|
3431
|
+
p2.outro("Migration cancelled.");
|
|
2428
3432
|
return;
|
|
2429
3433
|
}
|
|
2430
3434
|
if (choice === "custom") {
|
|
2431
|
-
const customPath = await
|
|
3435
|
+
const customPath = await p2.text({
|
|
2432
3436
|
message: "Enter path to 9Router .db or .json backup file:",
|
|
2433
3437
|
validate: (val) => {
|
|
2434
|
-
if (!val || !
|
|
3438
|
+
if (!val || !fs8.existsSync(path9.resolve(val))) {
|
|
2435
3439
|
return "File does not exist. Please check the path.";
|
|
2436
3440
|
}
|
|
2437
3441
|
}
|
|
2438
3442
|
});
|
|
2439
|
-
if (
|
|
2440
|
-
|
|
3443
|
+
if (p2.isCancel(customPath) || !customPath) {
|
|
3444
|
+
p2.outro("Migration cancelled.");
|
|
2441
3445
|
return;
|
|
2442
3446
|
}
|
|
2443
|
-
source =
|
|
3447
|
+
source = path9.resolve(customPath);
|
|
2444
3448
|
} else {
|
|
2445
3449
|
source = choice;
|
|
2446
3450
|
}
|
|
@@ -2448,21 +3452,21 @@ async function migrateNineRouter(options) {
|
|
|
2448
3452
|
}
|
|
2449
3453
|
}
|
|
2450
3454
|
if (!source) {
|
|
2451
|
-
|
|
2452
|
-
|
|
3455
|
+
p2.log.error("No 9Router database or backup file found.");
|
|
3456
|
+
p2.outro(
|
|
2453
3457
|
"Pass the location explicitly: srouter migrate 9router --source /path/to/9router-backup.json"
|
|
2454
3458
|
);
|
|
2455
3459
|
process.exitCode = 1;
|
|
2456
3460
|
return;
|
|
2457
3461
|
}
|
|
2458
|
-
const existingTarget =
|
|
3462
|
+
const existingTarget = fs8.existsSync(TargetDbPath);
|
|
2459
3463
|
let action = options.action ?? "copy";
|
|
2460
3464
|
if (existingTarget && !options.action) {
|
|
2461
3465
|
if (options.yes) {
|
|
2462
3466
|
action = "merge";
|
|
2463
3467
|
} else {
|
|
2464
|
-
|
|
2465
|
-
const choice = await
|
|
3468
|
+
p2.log.warn(`Existing SRouter database found at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
|
|
3469
|
+
const choice = await p2.select({
|
|
2466
3470
|
message: "How should the existing SRouter database be handled?",
|
|
2467
3471
|
options: [
|
|
2468
3472
|
{ value: "backup_and_replace", label: "Backup current, replace with 9Router data" },
|
|
@@ -2470,8 +3474,8 @@ async function migrateNineRouter(options) {
|
|
|
2470
3474
|
{ value: "abort", label: "Cancel migration" }
|
|
2471
3475
|
]
|
|
2472
3476
|
});
|
|
2473
|
-
if (choice === "abort" ||
|
|
2474
|
-
|
|
3477
|
+
if (choice === "abort" || p2.isCancel(choice)) {
|
|
3478
|
+
p2.outro("Migration cancelled. Your 9Router installation remains intact.");
|
|
2475
3479
|
return;
|
|
2476
3480
|
}
|
|
2477
3481
|
if (choice === "merge" || choice === "backup_and_replace") {
|
|
@@ -2479,9 +3483,9 @@ async function migrateNineRouter(options) {
|
|
|
2479
3483
|
}
|
|
2480
3484
|
}
|
|
2481
3485
|
}
|
|
2482
|
-
const proceed = options.yes || await
|
|
3486
|
+
const proceed = options.yes || await p2.confirm({ message: "Proceed with migration?" }) === true;
|
|
2483
3487
|
if (!proceed) {
|
|
2484
|
-
|
|
3488
|
+
p2.outro("Migration cancelled. No changes made.");
|
|
2485
3489
|
return;
|
|
2486
3490
|
}
|
|
2487
3491
|
ensureDirs();
|
|
@@ -2490,10 +3494,10 @@ async function migrateNineRouter(options) {
|
|
|
2490
3494
|
if (action === "backup_and_replace") {
|
|
2491
3495
|
targetBackup = backupDb(TargetDbPath, "srouter");
|
|
2492
3496
|
}
|
|
2493
|
-
const s =
|
|
3497
|
+
const s = p2.spinner();
|
|
2494
3498
|
try {
|
|
2495
3499
|
s.start("Preparing target database");
|
|
2496
|
-
initDatabase();
|
|
3500
|
+
await initDatabase();
|
|
2497
3501
|
const targetDb = new DatabaseSync2(TargetDbPath);
|
|
2498
3502
|
targetDb.exec("PRAGMA foreign_keys = OFF;");
|
|
2499
3503
|
let inserted = 0;
|
|
@@ -2502,7 +3506,7 @@ async function migrateNineRouter(options) {
|
|
|
2502
3506
|
const isJson = source.endsWith(".json");
|
|
2503
3507
|
if (isJson) {
|
|
2504
3508
|
s.message("Reading 9Router JSON backup");
|
|
2505
|
-
const raw =
|
|
3509
|
+
const raw = fs8.readFileSync(source, "utf-8");
|
|
2506
3510
|
const parsed = JSON.parse(raw);
|
|
2507
3511
|
const res = importNineRouterJson(parsed, targetDb, action);
|
|
2508
3512
|
inserted = res.inserted;
|
|
@@ -2517,7 +3521,7 @@ async function migrateNineRouter(options) {
|
|
|
2517
3521
|
);
|
|
2518
3522
|
for (const table of sourceTables) {
|
|
2519
3523
|
if (!targetTables.has(table)) {
|
|
2520
|
-
|
|
3524
|
+
p2.log.warn(`Skipping table ${table} (not recognized by SRouter)`);
|
|
2521
3525
|
continue;
|
|
2522
3526
|
}
|
|
2523
3527
|
const rows = sourceDb.prepare(`SELECT * FROM "${table}"`).all();
|
|
@@ -2526,7 +3530,7 @@ async function migrateNineRouter(options) {
|
|
|
2526
3530
|
try {
|
|
2527
3531
|
targetDb.prepare(`DELETE FROM "${table}"`).run();
|
|
2528
3532
|
} catch {
|
|
2529
|
-
|
|
3533
|
+
p2.log.warn(`Could not clear table ${table}, rows will be appended`);
|
|
2530
3534
|
}
|
|
2531
3535
|
}
|
|
2532
3536
|
const sourceColumns = new Set(
|
|
@@ -2553,7 +3557,7 @@ async function migrateNineRouter(options) {
|
|
|
2553
3557
|
targetDb.exec("PRAGMA foreign_keys = ON;");
|
|
2554
3558
|
targetDb.close();
|
|
2555
3559
|
s.stop("Migration complete");
|
|
2556
|
-
|
|
3560
|
+
p2.log.message(
|
|
2557
3561
|
[
|
|
2558
3562
|
`Tables/Categories migrated: ${tablesCount}`,
|
|
2559
3563
|
`Rows/Items inserted: ${inserted}`,
|
|
@@ -2563,14 +3567,14 @@ async function migrateNineRouter(options) {
|
|
|
2563
3567
|
].join("\n")
|
|
2564
3568
|
);
|
|
2565
3569
|
if (targetBackup) {
|
|
2566
|
-
|
|
3570
|
+
p2.log.info(`Old SRouter database backed up to ${targetBackup}`);
|
|
2567
3571
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
3572
|
+
p2.log.success(formatSuccess(`9Router backup saved to ${sourceBackup}`));
|
|
3573
|
+
p2.outro("Your 9Router providers are now available in SRouter.");
|
|
2570
3574
|
} catch (error) {
|
|
2571
3575
|
s.stop(formatError("Migration failed"));
|
|
2572
|
-
|
|
2573
|
-
|
|
3576
|
+
p2.log.error(error.message);
|
|
3577
|
+
p2.log.info(formatInfo(`Restore from backup if needed: cp ${sourceBackup} ${source}`));
|
|
2574
3578
|
process.exitCode = 1;
|
|
2575
3579
|
}
|
|
2576
3580
|
}
|
|
@@ -2581,8 +3585,8 @@ async function migrateCommand(target, options) {
|
|
|
2581
3585
|
case "9router":
|
|
2582
3586
|
return migrateNineRouter(options);
|
|
2583
3587
|
default:
|
|
2584
|
-
|
|
2585
|
-
|
|
3588
|
+
p2.log.error(formatError(`Unknown migration target: ${target}`));
|
|
3589
|
+
p2.log.info(
|
|
2586
3590
|
formatWarning("Available targets: db (legacy location), 9router (9Router import)")
|
|
2587
3591
|
);
|
|
2588
3592
|
process.exitCode = 1;
|
|
@@ -2609,6 +3613,9 @@ function createCli() {
|
|
|
2609
3613
|
).action(async (opts) => {
|
|
2610
3614
|
await setupCommand(opts);
|
|
2611
3615
|
});
|
|
3616
|
+
program.command("init").description("Initialize and run SRouter Gateway (Docker container or Source Code)").option("-m, --mode <mode>", "Run mode (docker or source)").option("-p, --port <port>", "Gateway port (default: 3000)").option("-d, --dir <dir>", "Source code clone directory (default: ~/srouter)").option("-y, --yes", "Accept default values without interactive prompt").option("--detached", "Run docker container in background (default: true)").action(async (opts) => {
|
|
3617
|
+
await initCommand(opts);
|
|
3618
|
+
});
|
|
2612
3619
|
program.command("link <tool>").description("Configure a specific tool to use SRouter proxy (claude, opencode)").option("-u, --url <url>", "SRouter Gateway Base URL (e.g. http://localhost:3000/v1)").option("-k, --key <key>", "SRouter API Key").option("-m, --model <model>", "Model ID").option(
|
|
2613
3620
|
"--opus-model <model>",
|
|
2614
3621
|
"Opus tier model ID for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
|
|
@@ -2661,10 +3668,14 @@ function createCli() {
|
|
|
2661
3668
|
});
|
|
2662
3669
|
return program;
|
|
2663
3670
|
}
|
|
2664
|
-
|
|
2665
|
-
const
|
|
2666
|
-
|
|
3671
|
+
function runCli() {
|
|
3672
|
+
const isEntrypoint = process.argv[1]?.endsWith("srouter.js") || process.argv[1]?.endsWith("index.js") || process.argv[1]?.endsWith("index.ts") || process.argv[1]?.includes("/.bin/srouter") || process.argv[1]?.includes("/bin/srouter");
|
|
3673
|
+
if (isEntrypoint) {
|
|
3674
|
+
const program = createCli();
|
|
3675
|
+
program.parse(process.argv);
|
|
3676
|
+
}
|
|
2667
3677
|
}
|
|
3678
|
+
runCli();
|
|
2668
3679
|
export {
|
|
2669
3680
|
createCli
|
|
2670
3681
|
};
|