routstrd 0.1.0
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/.claude/settings.local.json +7 -0
- package/README.md +191 -0
- package/bun.lock +376 -0
- package/dist/index.js +27019 -0
- package/package.json +34 -0
- package/routstr-cost-logging.md +71 -0
- package/src/TUI refactor.md +113 -0
- package/src/cli-shared.ts +204 -0
- package/src/cli.ts +650 -0
- package/src/daemon/args.ts +19 -0
- package/src/daemon/config-store.ts +36 -0
- package/src/daemon/http/index.ts +608 -0
- package/src/daemon/index.ts +151 -0
- package/src/daemon/models.ts +49 -0
- package/src/daemon/sse.ts +98 -0
- package/src/daemon/types.ts +25 -0
- package/src/daemon/wallet/index.ts +207 -0
- package/src/daemon.ts +1 -0
- package/src/index.ts +4 -0
- package/src/integrations/index.ts +67 -0
- package/src/integrations/openclaw.ts +177 -0
- package/src/integrations/opencode.ts +120 -0
- package/src/integrations/pi.ts +116 -0
- package/src/start-daemon.ts +90 -0
- package/src/tui/usage/app.ts +247 -0
- package/src/tui/usage/constants.ts +42 -0
- package/src/tui/usage/data.ts +228 -0
- package/src/tui/usage/index.ts +1 -0
- package/src/tui/usage/render.ts +539 -0
- package/src/tui/usage/state.ts +100 -0
- package/src/tui/usage/terminal.ts +39 -0
- package/src/tui/usage/types.ts +65 -0
- package/src/utils/config.ts +22 -0
- package/src/utils/logger.ts +54 -0
- package/test_box.ts +15 -0
- package/test_curl.sh +11 -0
- package/test_split_box.ts +17 -0
- package/test_split_box2.ts +23 -0
- package/tsconfig.json +20 -0
- package/v1-messages-format-report.md +223 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import type { RoutstrdConfig } from "../utils/config";
|
|
6
|
+
import { logger } from "../utils/logger";
|
|
7
|
+
import type { SdkStore } from "@routstr/sdk";
|
|
8
|
+
|
|
9
|
+
const OPENCLAW_CONFIG_PATH = join(process.env.HOME || "", ".openclaw/openclaw.json");
|
|
10
|
+
const OPENCLAW_PROVIDER_ID = "routstr";
|
|
11
|
+
const OPENCLAW_API_BASE = "http://localhost:8008/v1";
|
|
12
|
+
const OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
|
|
13
|
+
const OPENCLAW_DEFAULT_FALLBACK_MODEL = "routstr/kimi-k2.5";
|
|
14
|
+
const OPENCLAW_CLIENT_ID = "openclaw";
|
|
15
|
+
const OPENCLAW_NAME = "OpenClaw";
|
|
16
|
+
|
|
17
|
+
type RoutstrModel = {
|
|
18
|
+
id: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type OpenClawModelEntry = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
reasoning: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type OpenClawConfig = {
|
|
29
|
+
models?: {
|
|
30
|
+
providers?: Record<string, {
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
api?: string;
|
|
34
|
+
models?: OpenClawModelEntry[];
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
agents?: {
|
|
38
|
+
defaults?: {
|
|
39
|
+
model?: {
|
|
40
|
+
primary?: string;
|
|
41
|
+
fallbacks?: string[];
|
|
42
|
+
};
|
|
43
|
+
models?: Record<string, {
|
|
44
|
+
alias?: string;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function toAlias(modelId: string): string {
|
|
51
|
+
if (modelId === "claude-sonnet-4.5") return "sonnet-4.5";
|
|
52
|
+
if (modelId === "claude-opus-4.5") return "opus-4.5";
|
|
53
|
+
if (modelId === "gemini-3-pro-preview") return "gemini-3-pro";
|
|
54
|
+
if (modelId === "gemini-3-flash-preview") return "gemini-3-flash";
|
|
55
|
+
if (modelId === "kimi-k2-thinking") return "kimi-k2";
|
|
56
|
+
if (modelId === "deepseek-v3.2-speciale") return "deepseek-special";
|
|
57
|
+
if (modelId === "grok-code-fast-1") return "grok-code";
|
|
58
|
+
return modelId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generateApiKey(): string {
|
|
62
|
+
const bytes = randomBytes(24);
|
|
63
|
+
return `sk-${bytes.toString("hex")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function installOpenClawIntegration(
|
|
67
|
+
config: RoutstrdConfig,
|
|
68
|
+
store: SdkStore,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
logger.log("\nInstalling routstr models in openclaw.json...");
|
|
71
|
+
|
|
72
|
+
const port = config.port || 8008;
|
|
73
|
+
|
|
74
|
+
// Get or create clientId entry for OpenClaw
|
|
75
|
+
const state = store.getState();
|
|
76
|
+
const existingClient = (state.clientIds || []).find(
|
|
77
|
+
(c: { clientId: string }) => c.clientId === OPENCLAW_CLIENT_ID,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let apiKey: string;
|
|
81
|
+
if (existingClient) {
|
|
82
|
+
apiKey = existingClient.apiKey;
|
|
83
|
+
logger.log(`Using existing API key for ${OPENCLAW_NAME}`);
|
|
84
|
+
} else {
|
|
85
|
+
apiKey = generateApiKey();
|
|
86
|
+
// Add new clientId entry using proper store action
|
|
87
|
+
store.getState().setClientIds((prev) => [
|
|
88
|
+
...(prev || []),
|
|
89
|
+
{
|
|
90
|
+
clientId: OPENCLAW_CLIENT_ID,
|
|
91
|
+
name: OPENCLAW_NAME,
|
|
92
|
+
apiKey,
|
|
93
|
+
createdAt: Date.now(),
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
logger.log(`Created new API key for ${OPENCLAW_NAME}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let openclawConfig: OpenClawConfig = {};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
103
|
+
const content = await readFile(OPENCLAW_CONFIG_PATH, "utf-8");
|
|
104
|
+
openclawConfig = JSON.parse(content) as OpenClawConfig;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
openclawConfig = {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!openclawConfig.models) {
|
|
111
|
+
openclawConfig.models = {};
|
|
112
|
+
}
|
|
113
|
+
if (!openclawConfig.models.providers) {
|
|
114
|
+
openclawConfig.models.providers = {};
|
|
115
|
+
}
|
|
116
|
+
if (!openclawConfig.agents) {
|
|
117
|
+
openclawConfig.agents = {};
|
|
118
|
+
}
|
|
119
|
+
if (!openclawConfig.agents.defaults) {
|
|
120
|
+
openclawConfig.agents.defaults = {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
mkdirSync(dirname(OPENCLAW_CONFIG_PATH), { recursive: true });
|
|
125
|
+
|
|
126
|
+
const response = await fetch(`http://localhost:${port}/models`);
|
|
127
|
+
const data = await response.json() as { output?: { models: RoutstrModel[] } };
|
|
128
|
+
const models = data.output?.models || [];
|
|
129
|
+
|
|
130
|
+
if (models.length === 0) {
|
|
131
|
+
logger.log("No models found from routstr daemon.");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const providerModels: OpenClawModelEntry[] = models.map((model) => ({
|
|
136
|
+
id: model.id,
|
|
137
|
+
name: model.name || model.id,
|
|
138
|
+
reasoning: true,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
|
|
142
|
+
baseUrl: OPENCLAW_API_BASE,
|
|
143
|
+
apiKey,
|
|
144
|
+
api: "openai-completions",
|
|
145
|
+
models: providerModels,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const availableModelIds = new Set(providerModels.map((model) => model.id));
|
|
149
|
+
const primaryId = availableModelIds.has("gpt-5.3-codex") ? "gpt-5.3-codex" : providerModels[0]?.id;
|
|
150
|
+
const fallbackId = availableModelIds.has("minimax-m2.5")
|
|
151
|
+
? "minimax-m2.5"
|
|
152
|
+
: providerModels.find((model) => model.id !== primaryId)?.id;
|
|
153
|
+
|
|
154
|
+
if (primaryId) {
|
|
155
|
+
openclawConfig.agents.defaults.model = {
|
|
156
|
+
primary: `${OPENCLAW_PROVIDER_ID}/${primaryId}`,
|
|
157
|
+
fallbacks: fallbackId ? [`${OPENCLAW_PROVIDER_ID}/${fallbackId}`] : [],
|
|
158
|
+
};
|
|
159
|
+
} else {
|
|
160
|
+
openclawConfig.agents.defaults.model = {
|
|
161
|
+
primary: OPENCLAW_DEFAULT_PRIMARY_MODEL,
|
|
162
|
+
fallbacks: [OPENCLAW_DEFAULT_FALLBACK_MODEL],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// const aliasMap: Record<string, { alias?: string }> = {};
|
|
167
|
+
// for (const model of providerModels) {
|
|
168
|
+
// aliasMap[`${OPENCLAW_PROVIDER_ID}/${model.id}`] = { alias: toAlias(model.id) };
|
|
169
|
+
// }
|
|
170
|
+
// openclawConfig.agents.defaults.models = aliasMap;
|
|
171
|
+
|
|
172
|
+
await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(openclawConfig, null, 2));
|
|
173
|
+
logger.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logger.error("Failed to install models in openclaw.json:", error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import type { RoutstrdConfig } from "../utils/config";
|
|
6
|
+
import { logger } from "../utils/logger";
|
|
7
|
+
import type { SdkStore } from "@routstr/sdk";
|
|
8
|
+
|
|
9
|
+
const OPENCODE_CONFIG_PATH = join(process.env.HOME || "", ".config/opencode/opencode.json");
|
|
10
|
+
const OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
11
|
+
const OPENCODE_CLIENT_ID = "opencode";
|
|
12
|
+
const OPENCODE_NAME = "OpenCode";
|
|
13
|
+
|
|
14
|
+
type RoutstrModel = {
|
|
15
|
+
id: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function generateApiKey(): string {
|
|
20
|
+
const bytes = randomBytes(24);
|
|
21
|
+
return `sk-${bytes.toString("hex")}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function installOpencodeIntegration(
|
|
25
|
+
config: RoutstrdConfig,
|
|
26
|
+
store: SdkStore,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
logger.log("\nInstalling routstr models in opencode.json...");
|
|
29
|
+
|
|
30
|
+
const port = config.port || 8008;
|
|
31
|
+
|
|
32
|
+
// Get or create clientId entry for OpenCode
|
|
33
|
+
const state = store.getState();
|
|
34
|
+
const existingClient = (state.clientIds || []).find(
|
|
35
|
+
(c: { clientId: string }) => c.clientId === OPENCODE_CLIENT_ID,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
let apiKey: string;
|
|
39
|
+
if (existingClient) {
|
|
40
|
+
apiKey = existingClient.apiKey;
|
|
41
|
+
logger.log(`Using existing API key for ${OPENCODE_NAME}`);
|
|
42
|
+
} else {
|
|
43
|
+
apiKey = generateApiKey();
|
|
44
|
+
// Add new clientId entry using proper store action
|
|
45
|
+
store.getState().setClientIds((prev) => [
|
|
46
|
+
...(prev || []),
|
|
47
|
+
{
|
|
48
|
+
clientId: OPENCODE_CLIENT_ID,
|
|
49
|
+
name: OPENCODE_NAME,
|
|
50
|
+
apiKey,
|
|
51
|
+
createdAt: Date.now(),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
logger.log(`Created new API key for ${OPENCODE_NAME}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let opencodeConfig: {
|
|
58
|
+
provider?: Record<string, {
|
|
59
|
+
npm?: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
options?: {
|
|
62
|
+
baseURL?: string;
|
|
63
|
+
apiKey?: string;
|
|
64
|
+
includeUsage?: boolean;
|
|
65
|
+
};
|
|
66
|
+
models?: Record<string, { name: string }>;
|
|
67
|
+
}>;
|
|
68
|
+
small_model?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (existsSync(OPENCODE_CONFIG_PATH)) {
|
|
73
|
+
const content = await readFile(OPENCODE_CONFIG_PATH, "utf-8");
|
|
74
|
+
opencodeConfig = JSON.parse(content);
|
|
75
|
+
} else {
|
|
76
|
+
opencodeConfig = { provider: {} };
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
opencodeConfig = { provider: {} };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!opencodeConfig.provider) {
|
|
83
|
+
opencodeConfig.provider = {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dirname(OPENCODE_CONFIG_PATH), { recursive: true });
|
|
88
|
+
|
|
89
|
+
const response = await fetch(`http://localhost:${port}/models`);
|
|
90
|
+
const data = await response.json() as { output?: { models: RoutstrModel[] } };
|
|
91
|
+
const models = data.output?.models || [];
|
|
92
|
+
|
|
93
|
+
if (models.length === 0) {
|
|
94
|
+
logger.log("No models found from routstr daemon.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const modelsObj: Record<string, { name: string }> = {};
|
|
99
|
+
for (const model of models) {
|
|
100
|
+
modelsObj[model.id] = { name: model.name || model.id };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
opencodeConfig.provider["routstr"] = {
|
|
104
|
+
npm: "@ai-sdk/openai-compatible",
|
|
105
|
+
name: "routstr",
|
|
106
|
+
options: {
|
|
107
|
+
baseURL: `http://localhost:${port}/`,
|
|
108
|
+
apiKey,
|
|
109
|
+
includeUsage: true,
|
|
110
|
+
},
|
|
111
|
+
models: modelsObj,
|
|
112
|
+
};
|
|
113
|
+
opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
|
|
114
|
+
|
|
115
|
+
await writeFile(OPENCODE_CONFIG_PATH, JSON.stringify(opencodeConfig, null, 2));
|
|
116
|
+
logger.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error("Failed to install models in opencode.json:", error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import type { RoutstrdConfig } from "../utils/config";
|
|
6
|
+
import { logger } from "../utils/logger";
|
|
7
|
+
import type { SdkStore } from "@routstr/sdk";
|
|
8
|
+
|
|
9
|
+
const PI_CONFIG_PATH = join(process.env.HOME || "", ".pi/agent/models.json");
|
|
10
|
+
const PI_CLIENT_ID = "pi-agent";
|
|
11
|
+
const PI_NAME = "Pi Agent";
|
|
12
|
+
|
|
13
|
+
type RoutstrModel = {
|
|
14
|
+
id: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PiModelEntry = {
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type PiProviderConfig = {
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
api?: string;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
models?: PiModelEntry[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type PiConfig = {
|
|
30
|
+
providers?: Record<string, PiProviderConfig>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function generateApiKey(): string {
|
|
34
|
+
const bytes = randomBytes(24);
|
|
35
|
+
return `sk-${bytes.toString("hex")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function installPiIntegration(
|
|
39
|
+
config: RoutstrdConfig,
|
|
40
|
+
store: SdkStore,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
logger.log("\nInstalling routstr models in pi models.json...");
|
|
43
|
+
|
|
44
|
+
const port = config.port || 8008;
|
|
45
|
+
const baseUrl = `http://localhost:${port}/v1`;
|
|
46
|
+
|
|
47
|
+
// Get or create clientId entry for Pi Agent
|
|
48
|
+
const state = store.getState();
|
|
49
|
+
const existingClient = (state.clientIds || []).find(
|
|
50
|
+
(c: { clientId: string }) => c.clientId === PI_CLIENT_ID,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let apiKey: string;
|
|
54
|
+
if (existingClient) {
|
|
55
|
+
apiKey = existingClient.apiKey;
|
|
56
|
+
logger.log(`Using existing API key for ${PI_NAME}`);
|
|
57
|
+
} else {
|
|
58
|
+
apiKey = generateApiKey();
|
|
59
|
+
// Add new clientId entry using proper store action
|
|
60
|
+
store.getState().setClientIds((prev) => [
|
|
61
|
+
...(prev || []),
|
|
62
|
+
{
|
|
63
|
+
clientId: PI_CLIENT_ID,
|
|
64
|
+
name: PI_NAME,
|
|
65
|
+
apiKey,
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
logger.log(`Created new API key for ${PI_NAME}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let piConfig: PiConfig = {};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(PI_CONFIG_PATH)) {
|
|
76
|
+
const content = await readFile(PI_CONFIG_PATH, "utf-8");
|
|
77
|
+
piConfig = JSON.parse(content) as PiConfig;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
piConfig = {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!piConfig.providers) {
|
|
84
|
+
piConfig.providers = {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Ensure directory exists
|
|
89
|
+
mkdirSync(dirname(PI_CONFIG_PATH), { recursive: true });
|
|
90
|
+
|
|
91
|
+
const response = await fetch(`http://localhost:${port}/models`);
|
|
92
|
+
const data = await response.json() as { output?: { models: RoutstrModel[] } };
|
|
93
|
+
const models = data.output?.models || [];
|
|
94
|
+
|
|
95
|
+
if (models.length === 0) {
|
|
96
|
+
logger.log("No models found from routstr daemon.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const providerModels: PiModelEntry[] = models.map((model) => ({
|
|
101
|
+
id: model.id,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
piConfig.providers["routstr"] = {
|
|
105
|
+
baseUrl,
|
|
106
|
+
api: "openai-completions",
|
|
107
|
+
apiKey,
|
|
108
|
+
models: providerModels,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await writeFile(PI_CONFIG_PATH, JSON.stringify(piConfig, null, 2));
|
|
112
|
+
logger.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error("Failed to install models in pi models.json:", error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { LOG_FILE } from "./utils/config";
|
|
2
|
+
import { logger } from "./utils/logger";
|
|
3
|
+
import { existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
|
|
6
|
+
export async function startDaemon(
|
|
7
|
+
options: { port?: string; provider?: string } = {},
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const args: string[] = [];
|
|
10
|
+
const port = options.port || "8008";
|
|
11
|
+
const pollIntervalMs = 250;
|
|
12
|
+
const startupTimeoutMs = 10 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
17
|
+
const existing = await fetch(`http://localhost:${port}/health`, {
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
});
|
|
20
|
+
clearTimeout(timeoutId);
|
|
21
|
+
if (existing.ok) {
|
|
22
|
+
logger.log(`Routstr daemon already running on http://localhost:${port}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Daemon is not running yet; continue with startup.
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.port) {
|
|
30
|
+
args.push("--port", options.port);
|
|
31
|
+
}
|
|
32
|
+
if (options.provider) {
|
|
33
|
+
args.push("--provider", options.provider);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ensure log directory exists
|
|
37
|
+
const logDir = dirname(LOG_FILE);
|
|
38
|
+
if (!existsSync(logDir)) {
|
|
39
|
+
mkdirSync(logDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Use shell redirection to append stdout/stderr to log file
|
|
43
|
+
// Bun.file() overwrites, so we need shell >> for appending
|
|
44
|
+
const daemonScript = `${import.meta.dir}/daemon/index.ts`;
|
|
45
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map(a => `'${a}'`).join(" ")} >> "${LOG_FILE}" 2>&1`;
|
|
46
|
+
|
|
47
|
+
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
48
|
+
stdout: "inherit",
|
|
49
|
+
stderr: "inherit",
|
|
50
|
+
stdin: "ignore",
|
|
51
|
+
detached: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
proc.unref();
|
|
55
|
+
|
|
56
|
+
let exitCode: number | null = null;
|
|
57
|
+
proc.exited.then((code) => {
|
|
58
|
+
exitCode = code;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
|
|
62
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
64
|
+
|
|
65
|
+
if (exitCode !== null) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Daemon process exited early with code ${exitCode}. Check logs at ${LOG_FILE}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
74
|
+
const res = await fetch(`http://localhost:${port}/health`, {
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
if (res.ok) {
|
|
79
|
+
logger.log(`Routstr daemon started (PID: ${proc.pid}).`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Not ready yet
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs at ${LOG_FILE}`,
|
|
89
|
+
);
|
|
90
|
+
}
|