abm1click-openclaw 1.0.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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1788 -0
- package/dist/pages/dashboard.html +991 -0
- package/dist/pages/error.html +164 -0
- package/dist/pages/success.html +143 -0
- package/dist/pages/waiting.html +127 -0
- package/package.json +60 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1788 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import chalk3 from "chalk";
|
|
12
|
+
|
|
13
|
+
// src/constants.ts
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
var DEFAULT_PORT = 20128;
|
|
17
|
+
var DEFAULT_BASE_URL = `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
18
|
+
var DEFAULT_API_BASE_URL = `${DEFAULT_BASE_URL}/v1`;
|
|
19
|
+
var DEFAULT_DASHBOARD_URL = DEFAULT_BASE_URL;
|
|
20
|
+
var STATE_DIR = join(homedir(), ".oneclick-openclaw");
|
|
21
|
+
var STATE_FILE_PATH = join(STATE_DIR, "state.json");
|
|
22
|
+
var LOG_DIR = join(STATE_DIR, "logs");
|
|
23
|
+
var LOG_FILE_PATH = join(LOG_DIR, "install.log");
|
|
24
|
+
var ARTIFACTS_DIR = join(STATE_DIR, "artifacts");
|
|
25
|
+
var TIMEOUTS = {
|
|
26
|
+
NINE_ROUTER_READY: 6e4,
|
|
27
|
+
// 60s for 9router to start
|
|
28
|
+
NINE_ROUTER_POLL_INTERVAL: 2e3,
|
|
29
|
+
// 2s between readiness polls
|
|
30
|
+
AUTH_TIMEOUT: 6e5,
|
|
31
|
+
// 10 min for OAuth
|
|
32
|
+
AUTH_POLL_INTERVAL: 3e3,
|
|
33
|
+
// 3s between auth polls
|
|
34
|
+
SMOKE_9ROUTER: 6e4,
|
|
35
|
+
// 60s for gateway smoke
|
|
36
|
+
SMOKE_OPENCLAW: 9e4,
|
|
37
|
+
// 90s for openclaw smoke
|
|
38
|
+
HEALTH_CHECK: 1e4
|
|
39
|
+
// 10s for health check
|
|
40
|
+
};
|
|
41
|
+
var EXIT_CODES = {
|
|
42
|
+
SUCCESS: 0,
|
|
43
|
+
FATAL_GENERIC: 1,
|
|
44
|
+
UNSUPPORTED_ENV: 2,
|
|
45
|
+
DEPENDENCY_MISSING: 3,
|
|
46
|
+
PORT_CONFLICT: 4,
|
|
47
|
+
OAUTH_FAILED: 5,
|
|
48
|
+
NINE_ROUTER_FAILURE: 6,
|
|
49
|
+
OPENCLAW_FAILURE: 7,
|
|
50
|
+
SMOKE_TEST_FAILURE: 8,
|
|
51
|
+
STATE_CORRUPTION: 9,
|
|
52
|
+
USER_ABORTED: 10
|
|
53
|
+
};
|
|
54
|
+
var INSTALL_STEPS = [
|
|
55
|
+
"PRECHECK",
|
|
56
|
+
"INSTALL_9ROUTER",
|
|
57
|
+
"START_9ROUTER",
|
|
58
|
+
"WAIT_9ROUTER_READY",
|
|
59
|
+
"OPEN_AUTH_PAGE",
|
|
60
|
+
"WAIT_GEMINI_AUTH",
|
|
61
|
+
"ENSURE_9ROUTER_API_KEY",
|
|
62
|
+
"INSTALL_OPENCLAW",
|
|
63
|
+
"LOCATE_OPENCLAW_CONFIG",
|
|
64
|
+
"PATCH_OPENCLAW_PROVIDER",
|
|
65
|
+
"SET_DEFAULT_MODEL",
|
|
66
|
+
"VALIDATE_OPENCLAW_CONFIG",
|
|
67
|
+
"SMOKE_TEST_9ROUTER",
|
|
68
|
+
"SMOKE_TEST_OPENCLAW"
|
|
69
|
+
];
|
|
70
|
+
var STATE_VERSION = 1;
|
|
71
|
+
var PROVIDER_ID = "9router";
|
|
72
|
+
var DEFAULT_MODEL = {
|
|
73
|
+
primary: "gc/gemini-3-flash-preview",
|
|
74
|
+
fallbacks: [
|
|
75
|
+
"if/kimi-k2-thinking",
|
|
76
|
+
"qw/qwen3-coder-plus"
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
var APP = {
|
|
80
|
+
NAME: "oneclick-openclaw",
|
|
81
|
+
VERSION: "1.0.0",
|
|
82
|
+
DESCRIPTION: "OneClick OpenClaw Launcher \u2014 1 command to setup 9router + openclaw"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/state/store.ts
|
|
86
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
87
|
+
|
|
88
|
+
// src/utils/fs.ts
|
|
89
|
+
import {
|
|
90
|
+
writeFileSync,
|
|
91
|
+
readFileSync,
|
|
92
|
+
renameSync,
|
|
93
|
+
copyFileSync,
|
|
94
|
+
mkdirSync,
|
|
95
|
+
existsSync,
|
|
96
|
+
accessSync,
|
|
97
|
+
unlinkSync,
|
|
98
|
+
constants as fsConstants
|
|
99
|
+
} from "fs";
|
|
100
|
+
import { dirname, join as join2 } from "path";
|
|
101
|
+
import { randomUUID } from "crypto";
|
|
102
|
+
function atomicWriteFileSync(filePath, data) {
|
|
103
|
+
const dir = dirname(filePath);
|
|
104
|
+
mkdirSync(dir, { recursive: true });
|
|
105
|
+
const tempPath = join2(dir, `.tmp-${randomUUID().slice(0, 8)}`);
|
|
106
|
+
writeFileSync(tempPath, data, "utf-8");
|
|
107
|
+
try {
|
|
108
|
+
renameSync(tempPath, filePath);
|
|
109
|
+
} catch {
|
|
110
|
+
copyFileSync(tempPath, filePath);
|
|
111
|
+
try {
|
|
112
|
+
unlinkSync(tempPath);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function readFileSafe(filePath) {
|
|
118
|
+
try {
|
|
119
|
+
return readFileSync(filePath, "utf-8");
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function backupFile(filePath) {
|
|
125
|
+
if (!existsSync(filePath)) {
|
|
126
|
+
throw new Error(`Cannot backup: file not found: ${filePath}`);
|
|
127
|
+
}
|
|
128
|
+
const dir = dirname(filePath);
|
|
129
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
130
|
+
const backupPath = join2(dir, `backup-${timestamp2}-${randomUUID().slice(0, 6)}.json`);
|
|
131
|
+
copyFileSync(filePath, backupPath);
|
|
132
|
+
return backupPath;
|
|
133
|
+
}
|
|
134
|
+
function ensureDir(dirPath) {
|
|
135
|
+
mkdirSync(dirPath, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
function hasWriteAccess(dirPath) {
|
|
138
|
+
try {
|
|
139
|
+
mkdirSync(dirPath, { recursive: true });
|
|
140
|
+
accessSync(dirPath, fsConstants.W_OK);
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function fileExists(filePath) {
|
|
147
|
+
return existsSync(filePath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/utils/json.ts
|
|
151
|
+
function safeJsonParse(text) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(text);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function prettyJson(value) {
|
|
159
|
+
return JSON.stringify(value, null, 2);
|
|
160
|
+
}
|
|
161
|
+
function isPlainObject(value) {
|
|
162
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
163
|
+
}
|
|
164
|
+
function deepMerge(target, source) {
|
|
165
|
+
const result = { ...target };
|
|
166
|
+
for (const key of Object.keys(source)) {
|
|
167
|
+
const targetVal = result[key];
|
|
168
|
+
const sourceVal = source[key];
|
|
169
|
+
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
|
|
170
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
171
|
+
} else {
|
|
172
|
+
result[key] = sourceVal;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/utils/env.ts
|
|
179
|
+
import { platform, arch } from "os";
|
|
180
|
+
function detectPlatform() {
|
|
181
|
+
const os = platform();
|
|
182
|
+
const cpuArch = arch();
|
|
183
|
+
let shell = "bash";
|
|
184
|
+
if (os === "win32") {
|
|
185
|
+
shell = process.env.SHELL ?? "powershell";
|
|
186
|
+
} else {
|
|
187
|
+
shell = process.env.SHELL ?? "/bin/bash";
|
|
188
|
+
}
|
|
189
|
+
return { os, arch: cpuArch, shell };
|
|
190
|
+
}
|
|
191
|
+
function isWSL() {
|
|
192
|
+
try {
|
|
193
|
+
const { readFileSync: readFileSync4 } = __require("fs");
|
|
194
|
+
const procVersion = readFileSync4("/proc/version", "utf-8");
|
|
195
|
+
return procVersion.toLowerCase().includes("microsoft");
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function commandExists(cmd) {
|
|
201
|
+
try {
|
|
202
|
+
const { execaCommand } = await import("execa");
|
|
203
|
+
const whichCmd = platform() === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
204
|
+
const result = await execaCommand(whichCmd, { reject: false });
|
|
205
|
+
return result.exitCode === 0;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/utils/logger.ts
|
|
212
|
+
import chalk from "chalk";
|
|
213
|
+
import { mkdirSync as mkdirSync2, appendFileSync } from "fs";
|
|
214
|
+
import { dirname as dirname2 } from "path";
|
|
215
|
+
var SECRET_PATTERNS = [
|
|
216
|
+
/sk_[a-zA-Z0-9_-]{10,}/g,
|
|
217
|
+
/Bearer\s+[a-zA-Z0-9._-]+/gi,
|
|
218
|
+
/api[_-]?key["\s:=]+["']?[a-zA-Z0-9_-]{8,}["']?/gi,
|
|
219
|
+
/token["\s:=]+["']?[a-zA-Z0-9._-]{8,}["']?/gi
|
|
220
|
+
];
|
|
221
|
+
function redact(text) {
|
|
222
|
+
let result = text;
|
|
223
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
224
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
function timestamp() {
|
|
229
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
230
|
+
}
|
|
231
|
+
function ensureLogDir() {
|
|
232
|
+
try {
|
|
233
|
+
mkdirSync2(dirname2(LOG_FILE_PATH), { recursive: true });
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function writeToFile(level, message, installId) {
|
|
238
|
+
try {
|
|
239
|
+
ensureLogDir();
|
|
240
|
+
const idPart = installId ? `[id=${installId.slice(0, 8)}]` : "";
|
|
241
|
+
const line = `${timestamp()} ${level.toUpperCase().padEnd(5)} ${idPart} ${redact(message)}
|
|
242
|
+
`;
|
|
243
|
+
appendFileSync(LOG_FILE_PATH, line, "utf-8");
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
var ICONS = {
|
|
248
|
+
info: chalk.blue("\u2139"),
|
|
249
|
+
warn: chalk.yellow("\u26A0"),
|
|
250
|
+
error: chalk.red("\u2716"),
|
|
251
|
+
debug: chalk.gray("\u2299")
|
|
252
|
+
};
|
|
253
|
+
var Logger = class {
|
|
254
|
+
installId;
|
|
255
|
+
verbose;
|
|
256
|
+
constructor(opts) {
|
|
257
|
+
this.installId = opts?.installId;
|
|
258
|
+
this.verbose = opts?.verbose ?? false;
|
|
259
|
+
}
|
|
260
|
+
setInstallId(id) {
|
|
261
|
+
this.installId = id;
|
|
262
|
+
}
|
|
263
|
+
info(message, meta) {
|
|
264
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
265
|
+
console.log(`${ICONS.info} ${message}`);
|
|
266
|
+
writeToFile("INFO", `${message}${metaStr}`, this.installId);
|
|
267
|
+
}
|
|
268
|
+
warn(message, meta) {
|
|
269
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
270
|
+
console.log(`${ICONS.warn} ${chalk.yellow(message)}`);
|
|
271
|
+
writeToFile("WARN", `${message}${metaStr}`, this.installId);
|
|
272
|
+
}
|
|
273
|
+
error(message, meta) {
|
|
274
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
275
|
+
console.error(`${ICONS.error} ${chalk.red(message)}`);
|
|
276
|
+
writeToFile("ERROR", `${message}${metaStr}`, this.installId);
|
|
277
|
+
}
|
|
278
|
+
debug(message, meta) {
|
|
279
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
280
|
+
if (this.verbose) {
|
|
281
|
+
console.log(`${ICONS.debug} ${chalk.gray(message)}`);
|
|
282
|
+
}
|
|
283
|
+
writeToFile("DEBUG", `${message}${metaStr}`, this.installId);
|
|
284
|
+
}
|
|
285
|
+
/** Log a step transition */
|
|
286
|
+
step(state, message) {
|
|
287
|
+
console.log(`${chalk.cyan("\u2192")} ${chalk.bold(state)} ${message}`);
|
|
288
|
+
writeToFile("INFO", `[state=${state}] ${message}`, this.installId);
|
|
289
|
+
}
|
|
290
|
+
/** Log success with green checkmark */
|
|
291
|
+
success(message) {
|
|
292
|
+
console.log(`${chalk.green("\u2714")} ${chalk.green(message)}`);
|
|
293
|
+
writeToFile("INFO", `[SUCCESS] ${message}`, this.installId);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var logger = new Logger();
|
|
297
|
+
|
|
298
|
+
// src/utils/crypto.ts
|
|
299
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
|
|
300
|
+
import { homedir as homedir2, hostname, userInfo } from "os";
|
|
301
|
+
var ALGORITHM = "aes-256-gcm";
|
|
302
|
+
var IV_LENGTH = 16;
|
|
303
|
+
var AUTH_TAG_LENGTH = 16;
|
|
304
|
+
var SALT_LENGTH = 32;
|
|
305
|
+
var KEY_LENGTH = 32;
|
|
306
|
+
var ENCRYPTED_PREFIX = "enc:v1:";
|
|
307
|
+
function deriveMachineKey(salt) {
|
|
308
|
+
const machineId = [
|
|
309
|
+
hostname(),
|
|
310
|
+
homedir2(),
|
|
311
|
+
userInfo().username,
|
|
312
|
+
"oneclick-openclaw-v1"
|
|
313
|
+
// namespace
|
|
314
|
+
].join("|");
|
|
315
|
+
const seedHash = createHash("sha256").update(machineId).digest();
|
|
316
|
+
return scryptSync(seedHash, salt, KEY_LENGTH);
|
|
317
|
+
}
|
|
318
|
+
function encryptField(plaintext) {
|
|
319
|
+
if (!plaintext || plaintext.startsWith(ENCRYPTED_PREFIX)) {
|
|
320
|
+
return plaintext;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
324
|
+
const key = deriveMachineKey(salt);
|
|
325
|
+
const iv = randomBytes(IV_LENGTH);
|
|
326
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, {
|
|
327
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
328
|
+
});
|
|
329
|
+
let encrypted = cipher.update(plaintext, "utf-8", "hex");
|
|
330
|
+
encrypted += cipher.final("hex");
|
|
331
|
+
const authTag = cipher.getAuthTag();
|
|
332
|
+
return [
|
|
333
|
+
ENCRYPTED_PREFIX,
|
|
334
|
+
salt.toString("hex"),
|
|
335
|
+
iv.toString("hex"),
|
|
336
|
+
authTag.toString("hex"),
|
|
337
|
+
encrypted
|
|
338
|
+
].join("");
|
|
339
|
+
} catch (err) {
|
|
340
|
+
logger.warn(`Encryption failed, storing plaintext: ${err instanceof Error ? err.message : String(err)}`);
|
|
341
|
+
return plaintext;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function decryptField(encryptedValue) {
|
|
345
|
+
if (!encryptedValue || !encryptedValue.startsWith(ENCRYPTED_PREFIX)) {
|
|
346
|
+
return encryptedValue;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const payload = encryptedValue.slice(ENCRYPTED_PREFIX.length);
|
|
350
|
+
const saltHex = payload.slice(0, SALT_LENGTH * 2);
|
|
351
|
+
const rest = payload.slice(SALT_LENGTH * 2);
|
|
352
|
+
const ivHex = rest.slice(0, IV_LENGTH * 2);
|
|
353
|
+
const authTagHex = rest.slice(IV_LENGTH * 2, IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2);
|
|
354
|
+
const ciphertextHex = rest.slice(IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2);
|
|
355
|
+
const salt = Buffer.from(saltHex, "hex");
|
|
356
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
357
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
358
|
+
const key = deriveMachineKey(salt);
|
|
359
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, {
|
|
360
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
361
|
+
});
|
|
362
|
+
decipher.setAuthTag(authTag);
|
|
363
|
+
let decrypted = decipher.update(ciphertextHex, "hex", "utf-8");
|
|
364
|
+
decrypted += decipher.final("utf-8");
|
|
365
|
+
return decrypted;
|
|
366
|
+
} catch (err) {
|
|
367
|
+
logger.warn(`Decryption failed, returning raw value: ${err instanceof Error ? err.message : String(err)}`);
|
|
368
|
+
return encryptedValue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/state/store.ts
|
|
373
|
+
import { dirname as dirname3 } from "path";
|
|
374
|
+
function encryptArtifacts(artifacts) {
|
|
375
|
+
const copy = { ...artifacts };
|
|
376
|
+
if (copy.nineRouterApiKey) {
|
|
377
|
+
copy.nineRouterApiKey = encryptField(copy.nineRouterApiKey);
|
|
378
|
+
}
|
|
379
|
+
return copy;
|
|
380
|
+
}
|
|
381
|
+
function decryptArtifacts(artifacts) {
|
|
382
|
+
const copy = { ...artifacts };
|
|
383
|
+
if (copy.nineRouterApiKey) {
|
|
384
|
+
copy.nineRouterApiKey = decryptField(copy.nineRouterApiKey);
|
|
385
|
+
}
|
|
386
|
+
return copy;
|
|
387
|
+
}
|
|
388
|
+
function loadOrCreateState() {
|
|
389
|
+
const raw = readFileSafe(STATE_FILE_PATH);
|
|
390
|
+
if (raw) {
|
|
391
|
+
const parsed = safeJsonParse(raw);
|
|
392
|
+
if (parsed && parsed.version === STATE_VERSION) {
|
|
393
|
+
parsed.artifacts = decryptArtifacts(parsed.artifacts);
|
|
394
|
+
logger.debug(`State loaded: currentState=${parsed.currentState}`);
|
|
395
|
+
return parsed;
|
|
396
|
+
}
|
|
397
|
+
logger.warn("State file corrupted or wrong version, creating new state");
|
|
398
|
+
}
|
|
399
|
+
const newState = {
|
|
400
|
+
version: STATE_VERSION,
|
|
401
|
+
installationId: randomUUID2(),
|
|
402
|
+
currentState: "PRECHECK",
|
|
403
|
+
completedStates: [],
|
|
404
|
+
stepMeta: {},
|
|
405
|
+
artifacts: {},
|
|
406
|
+
platform: detectPlatform()
|
|
407
|
+
};
|
|
408
|
+
saveState(newState);
|
|
409
|
+
logger.debug(`New state created: id=${newState.installationId}`);
|
|
410
|
+
return newState;
|
|
411
|
+
}
|
|
412
|
+
function saveState(state) {
|
|
413
|
+
ensureDir(dirname3(STATE_FILE_PATH));
|
|
414
|
+
const secureState = {
|
|
415
|
+
...state,
|
|
416
|
+
artifacts: encryptArtifacts(state.artifacts)
|
|
417
|
+
};
|
|
418
|
+
atomicWriteFileSync(STATE_FILE_PATH, prettyJson(secureState));
|
|
419
|
+
notifyStateChange(state);
|
|
420
|
+
}
|
|
421
|
+
var stateListeners = [];
|
|
422
|
+
function onStateChange(listener) {
|
|
423
|
+
stateListeners.push(listener);
|
|
424
|
+
return () => {
|
|
425
|
+
const idx = stateListeners.indexOf(listener);
|
|
426
|
+
if (idx >= 0) stateListeners.splice(idx, 1);
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function notifyStateChange(state) {
|
|
430
|
+
for (const listener of stateListeners) {
|
|
431
|
+
try {
|
|
432
|
+
listener(state);
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function isStepCompleted(state, step) {
|
|
438
|
+
return state.completedStates.includes(step);
|
|
439
|
+
}
|
|
440
|
+
function markStepRunning(state, step) {
|
|
441
|
+
const meta = state.stepMeta[step] ?? {
|
|
442
|
+
status: "pending",
|
|
443
|
+
attemptCount: 0,
|
|
444
|
+
startedAt: null,
|
|
445
|
+
finishedAt: null,
|
|
446
|
+
lastError: null
|
|
447
|
+
};
|
|
448
|
+
meta.status = "running";
|
|
449
|
+
meta.attemptCount += 1;
|
|
450
|
+
meta.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
451
|
+
meta.finishedAt = null;
|
|
452
|
+
const next = {
|
|
453
|
+
...state,
|
|
454
|
+
currentState: step,
|
|
455
|
+
stepMeta: { ...state.stepMeta, [step]: meta }
|
|
456
|
+
};
|
|
457
|
+
saveState(next);
|
|
458
|
+
return next;
|
|
459
|
+
}
|
|
460
|
+
function markStepCompleted(state, step) {
|
|
461
|
+
const meta = state.stepMeta[step] ?? {
|
|
462
|
+
status: "pending",
|
|
463
|
+
attemptCount: 1,
|
|
464
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
465
|
+
finishedAt: null,
|
|
466
|
+
lastError: null
|
|
467
|
+
};
|
|
468
|
+
meta.status = "completed";
|
|
469
|
+
meta.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
470
|
+
const completedStates = state.completedStates.includes(step) ? state.completedStates : [...state.completedStates, step];
|
|
471
|
+
const next = {
|
|
472
|
+
...state,
|
|
473
|
+
completedStates,
|
|
474
|
+
stepMeta: { ...state.stepMeta, [step]: meta }
|
|
475
|
+
};
|
|
476
|
+
saveState(next);
|
|
477
|
+
return next;
|
|
478
|
+
}
|
|
479
|
+
function markStepFailed(state, step, error, recoverable) {
|
|
480
|
+
const meta = state.stepMeta[step] ?? {
|
|
481
|
+
status: "pending",
|
|
482
|
+
attemptCount: 1,
|
|
483
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
484
|
+
finishedAt: null,
|
|
485
|
+
lastError: null
|
|
486
|
+
};
|
|
487
|
+
meta.status = "failed";
|
|
488
|
+
meta.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
489
|
+
meta.lastError = error;
|
|
490
|
+
const next = {
|
|
491
|
+
...state,
|
|
492
|
+
currentState: recoverable ? "FAILED_RECOVERABLE" : "FAILED_FATAL",
|
|
493
|
+
stepMeta: { ...state.stepMeta, [step]: meta }
|
|
494
|
+
};
|
|
495
|
+
saveState(next);
|
|
496
|
+
return next;
|
|
497
|
+
}
|
|
498
|
+
function updateArtifacts(state, artifacts) {
|
|
499
|
+
const next = {
|
|
500
|
+
...state,
|
|
501
|
+
artifacts: { ...state.artifacts, ...artifacts }
|
|
502
|
+
};
|
|
503
|
+
saveState(next);
|
|
504
|
+
return next;
|
|
505
|
+
}
|
|
506
|
+
function resetState() {
|
|
507
|
+
try {
|
|
508
|
+
const { unlinkSync: unlinkSync2 } = __require("fs");
|
|
509
|
+
unlinkSync2(STATE_FILE_PATH);
|
|
510
|
+
logger.info("State file deleted");
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/state/machine.ts
|
|
516
|
+
import chalk2 from "chalk";
|
|
517
|
+
function mapError(err) {
|
|
518
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
519
|
+
const se = err;
|
|
520
|
+
return {
|
|
521
|
+
code: se.code ?? "UNKNOWN",
|
|
522
|
+
message: se.message ?? String(err),
|
|
523
|
+
recoverable: se.recoverable ?? false,
|
|
524
|
+
details: se.details
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
code: "UNKNOWN",
|
|
529
|
+
message: err instanceof Error ? err.message : String(err),
|
|
530
|
+
recoverable: true
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async function runStep(state, stepName, handler) {
|
|
534
|
+
if (isStepCompleted(state, stepName)) {
|
|
535
|
+
logger.debug(`Step ${stepName} already completed, skipping`);
|
|
536
|
+
return state;
|
|
537
|
+
}
|
|
538
|
+
logger.step(stepName, "Starting...");
|
|
539
|
+
let currentState = markStepRunning(state, stepName);
|
|
540
|
+
try {
|
|
541
|
+
currentState = await handler(currentState);
|
|
542
|
+
currentState = markStepCompleted(currentState, stepName);
|
|
543
|
+
logger.success(`${stepName} completed`);
|
|
544
|
+
return currentState;
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const mapped = mapError(err);
|
|
547
|
+
logger.error(`${stepName} failed: ${mapped.message}`);
|
|
548
|
+
currentState = markStepFailed(currentState, stepName, mapped.message, mapped.recoverable);
|
|
549
|
+
throw mapped;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async function runInstall(handlers2) {
|
|
553
|
+
let state = loadOrCreateState();
|
|
554
|
+
logger.setInstallId(state.installationId);
|
|
555
|
+
console.log("");
|
|
556
|
+
console.log(chalk2.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
557
|
+
console.log(chalk2.bold.cyan(" \u2551 OneClick OpenClaw Launcher v1.0.0 \u2551"));
|
|
558
|
+
console.log(chalk2.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
559
|
+
console.log("");
|
|
560
|
+
for (const step of INSTALL_STEPS) {
|
|
561
|
+
const handler = handlers2[step];
|
|
562
|
+
if (!handler) {
|
|
563
|
+
logger.warn(`No handler registered for step: ${step}`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
state = await runStep(state, step, handler);
|
|
567
|
+
}
|
|
568
|
+
state.currentState = "DONE";
|
|
569
|
+
saveState(state);
|
|
570
|
+
console.log("");
|
|
571
|
+
console.log(chalk2.green.bold(" \u2714 Setup complete!"));
|
|
572
|
+
console.log("");
|
|
573
|
+
console.log(` 9Router: ${chalk2.cyan(state.artifacts.nineRouterBaseUrl ?? "http://127.0.0.1:20128")}`);
|
|
574
|
+
console.log(` OpenClaw: ${chalk2.cyan("configured to use 9Router")}`);
|
|
575
|
+
console.log(` Auth: ${chalk2.green("Google connected")}`);
|
|
576
|
+
console.log(` Smoke: ${chalk2.green("PASS")}`);
|
|
577
|
+
console.log("");
|
|
578
|
+
console.log(chalk2.gray(" Useful commands:"));
|
|
579
|
+
console.log(chalk2.gray(" oneclick-openclaw doctor"));
|
|
580
|
+
console.log(chalk2.gray(" oneclick-openclaw logs"));
|
|
581
|
+
console.log("");
|
|
582
|
+
}
|
|
583
|
+
async function runResume(handlers2) {
|
|
584
|
+
let state = loadOrCreateState();
|
|
585
|
+
logger.setInstallId(state.installationId);
|
|
586
|
+
if (state.currentState === "DONE") {
|
|
587
|
+
logger.info("Installation already completed. Nothing to resume.");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (state.currentState === "FAILED_FATAL") {
|
|
591
|
+
logger.warn("Previous run ended with fatal error. Attempting recovery...");
|
|
592
|
+
} else if (state.currentState === "FAILED_RECOVERABLE") {
|
|
593
|
+
logger.info("Previous run paused with recoverable error. Resuming...");
|
|
594
|
+
}
|
|
595
|
+
let resumeFrom = INSTALL_STEPS.length;
|
|
596
|
+
for (let i = 0; i < INSTALL_STEPS.length; i++) {
|
|
597
|
+
if (!state.completedStates.includes(INSTALL_STEPS[i])) {
|
|
598
|
+
resumeFrom = i;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (resumeFrom >= INSTALL_STEPS.length) {
|
|
603
|
+
state.currentState = "DONE";
|
|
604
|
+
saveState(state);
|
|
605
|
+
logger.success("All steps already completed!");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
logger.info(`Resuming from step ${resumeFrom + 1}/${INSTALL_STEPS.length}: ${INSTALL_STEPS[resumeFrom]}`);
|
|
609
|
+
for (let i = resumeFrom; i < INSTALL_STEPS.length; i++) {
|
|
610
|
+
const step = INSTALL_STEPS[i];
|
|
611
|
+
const handler = handlers2[step];
|
|
612
|
+
if (!handler) continue;
|
|
613
|
+
state = await runStep(state, step, handler);
|
|
614
|
+
}
|
|
615
|
+
state.currentState = "DONE";
|
|
616
|
+
saveState(state);
|
|
617
|
+
logger.success("Resume completed!");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/process/ports.ts
|
|
621
|
+
import { createServer } from "net";
|
|
622
|
+
function isPortAvailable(port) {
|
|
623
|
+
return new Promise((resolve) => {
|
|
624
|
+
const server = createServer();
|
|
625
|
+
server.once("error", () => {
|
|
626
|
+
resolve(false);
|
|
627
|
+
});
|
|
628
|
+
server.once("listening", () => {
|
|
629
|
+
server.close(() => resolve(true));
|
|
630
|
+
});
|
|
631
|
+
server.listen(port, "127.0.0.1");
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/steps/precheck.ts
|
|
636
|
+
async function precheck(state) {
|
|
637
|
+
const platform3 = detectPlatform();
|
|
638
|
+
logger.info(`Platform: ${platform3.os} ${platform3.arch} (shell: ${platform3.shell})`);
|
|
639
|
+
const checks = [
|
|
640
|
+
{
|
|
641
|
+
name: "Node.js",
|
|
642
|
+
check: () => commandExists("node"),
|
|
643
|
+
required: true,
|
|
644
|
+
failMessage: "Node.js is required but not found. Install from https://nodejs.org"
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "npm",
|
|
648
|
+
check: () => commandExists("npm"),
|
|
649
|
+
required: true,
|
|
650
|
+
failMessage: "npm is required but not found. It comes with Node.js."
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "curl",
|
|
654
|
+
check: () => commandExists("curl"),
|
|
655
|
+
required: false,
|
|
656
|
+
failMessage: "curl is recommended but not found."
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: `Port ${DEFAULT_PORT}`,
|
|
660
|
+
check: () => isPortAvailable(DEFAULT_PORT),
|
|
661
|
+
required: false,
|
|
662
|
+
// Not required — might have 9router already running
|
|
663
|
+
failMessage: `Port ${DEFAULT_PORT} is in use. 9router may already be running, or another service is using it.`
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "Write access",
|
|
667
|
+
check: async () => hasWriteAccess(STATE_DIR),
|
|
668
|
+
required: true,
|
|
669
|
+
failMessage: `Cannot write to ${STATE_DIR}. Check permissions.`
|
|
670
|
+
}
|
|
671
|
+
];
|
|
672
|
+
const fatal = [];
|
|
673
|
+
for (const item of checks) {
|
|
674
|
+
const ok = await item.check();
|
|
675
|
+
if (ok) {
|
|
676
|
+
logger.info(` \u2714 ${item.name}`);
|
|
677
|
+
} else if (item.required) {
|
|
678
|
+
logger.error(` \u2716 ${item.name}: ${item.failMessage}`);
|
|
679
|
+
fatal.push(item.failMessage);
|
|
680
|
+
} else {
|
|
681
|
+
logger.warn(` \u26A0 ${item.name}: ${item.failMessage}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (fatal.length > 0) {
|
|
685
|
+
throw {
|
|
686
|
+
code: "PRECHECK_FAILED",
|
|
687
|
+
message: `Precheck failed:
|
|
688
|
+
${fatal.join("\n")}`,
|
|
689
|
+
recoverable: false
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return { ...state, platform: platform3 };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/process/spawn.ts
|
|
696
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
697
|
+
async function spawnCommand(command, args = [], opts) {
|
|
698
|
+
const { execa } = await import("execa");
|
|
699
|
+
logger.debug(`Spawning: ${command} ${args.join(" ")}`);
|
|
700
|
+
try {
|
|
701
|
+
const result = await execa(command, args, {
|
|
702
|
+
cwd: opts?.cwd,
|
|
703
|
+
timeout: opts?.timeout,
|
|
704
|
+
reject: false
|
|
705
|
+
});
|
|
706
|
+
const pid = result.pid;
|
|
707
|
+
return {
|
|
708
|
+
pid: pid ?? void 0,
|
|
709
|
+
exitCode: result.exitCode ?? null,
|
|
710
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
711
|
+
stderr: typeof result.stderr === "string" ? result.stderr : ""
|
|
712
|
+
};
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
715
|
+
logger.error(`Spawn failed: ${command} \u2014 ${message}`);
|
|
716
|
+
return { pid: void 0, exitCode: 1, stdout: "", stderr: message };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async function spawnBackground(command, args = [], opts) {
|
|
720
|
+
logger.debug(`Spawning background: ${command} ${args.join(" ")}`);
|
|
721
|
+
try {
|
|
722
|
+
const child = nodeSpawn(command, args, {
|
|
723
|
+
cwd: opts?.cwd,
|
|
724
|
+
detached: true,
|
|
725
|
+
stdio: "ignore",
|
|
726
|
+
shell: true
|
|
727
|
+
});
|
|
728
|
+
child.unref();
|
|
729
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
730
|
+
return { pid: child.pid };
|
|
731
|
+
} catch (err) {
|
|
732
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
733
|
+
logger.error(`Background spawn failed: ${command} \u2014 ${message}`);
|
|
734
|
+
return { pid: void 0 };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/process/health.ts
|
|
739
|
+
async function httpHealthCheck(endpoint, timeoutMs = 1e4) {
|
|
740
|
+
const start = Date.now();
|
|
741
|
+
try {
|
|
742
|
+
const controller = new AbortController();
|
|
743
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
744
|
+
const response = await fetch(endpoint, {
|
|
745
|
+
method: "GET",
|
|
746
|
+
signal: controller.signal
|
|
747
|
+
});
|
|
748
|
+
clearTimeout(timer);
|
|
749
|
+
const latencyMs = Date.now() - start;
|
|
750
|
+
if (response.ok) {
|
|
751
|
+
return { healthy: true, endpoint, latencyMs };
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
healthy: false,
|
|
755
|
+
endpoint,
|
|
756
|
+
latencyMs,
|
|
757
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
758
|
+
};
|
|
759
|
+
} catch (err) {
|
|
760
|
+
const latencyMs = Date.now() - start;
|
|
761
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
762
|
+
logger.debug(`Health check failed: ${endpoint} \u2014 ${message}`);
|
|
763
|
+
return {
|
|
764
|
+
healthy: false,
|
|
765
|
+
endpoint,
|
|
766
|
+
latencyMs,
|
|
767
|
+
error: message
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/utils/retry.ts
|
|
773
|
+
function sleep(ms) {
|
|
774
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
775
|
+
}
|
|
776
|
+
async function pollUntil(condition, opts) {
|
|
777
|
+
const start = Date.now();
|
|
778
|
+
while (Date.now() - start < opts.timeoutMs) {
|
|
779
|
+
if (await condition()) return;
|
|
780
|
+
await sleep(opts.intervalMs);
|
|
781
|
+
}
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Timeout after ${opts.timeoutMs}ms waiting for: ${opts.label ?? "condition"}`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/adapters/nineRouterAdapter.ts
|
|
788
|
+
var NineRouterAdapter = class {
|
|
789
|
+
port;
|
|
790
|
+
constructor(port = DEFAULT_PORT) {
|
|
791
|
+
this.port = port;
|
|
792
|
+
}
|
|
793
|
+
async isInstalled() {
|
|
794
|
+
const hasNpm = await commandExists("9router");
|
|
795
|
+
if (hasNpm) return true;
|
|
796
|
+
const result = await spawnCommand("npx", ["9router", "--version"]);
|
|
797
|
+
return result.exitCode === 0;
|
|
798
|
+
}
|
|
799
|
+
async install() {
|
|
800
|
+
logger.info("Installing 9router via npm...");
|
|
801
|
+
const result = await spawnCommand("npm", ["install", "-g", "9router"], {
|
|
802
|
+
timeout: 12e4
|
|
803
|
+
});
|
|
804
|
+
if (result.exitCode !== 0) {
|
|
805
|
+
throw {
|
|
806
|
+
code: "9ROUTER_INSTALL_FAILED",
|
|
807
|
+
message: `9router installation failed: ${result.stderr}`,
|
|
808
|
+
recoverable: true
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
logger.info("9router installed successfully");
|
|
812
|
+
}
|
|
813
|
+
async isRunning(baseUrl = DEFAULT_BASE_URL) {
|
|
814
|
+
const check = await httpHealthCheck(baseUrl, 5e3);
|
|
815
|
+
return check.healthy;
|
|
816
|
+
}
|
|
817
|
+
async start(opts) {
|
|
818
|
+
const port = opts?.port ?? this.port;
|
|
819
|
+
if (await this.isRunning()) {
|
|
820
|
+
logger.info("9router is already running");
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
logger.info(`Starting 9router on port ${port}...`);
|
|
824
|
+
const result = await spawnBackground("9router", ["--port", String(port)]);
|
|
825
|
+
if (!result.pid) {
|
|
826
|
+
throw {
|
|
827
|
+
code: "9ROUTER_START_FAILED",
|
|
828
|
+
message: "Failed to start 9router process",
|
|
829
|
+
recoverable: true
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
logger.info(`9router started with PID: ${result.pid}`);
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
async waitUntilReady(baseUrl = DEFAULT_BASE_URL, timeoutMs = 6e4) {
|
|
836
|
+
logger.info("Waiting for 9router to be ready...");
|
|
837
|
+
await pollUntil(
|
|
838
|
+
async () => {
|
|
839
|
+
const check = await httpHealthCheck(baseUrl, 5e3);
|
|
840
|
+
return check.healthy;
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
intervalMs: 2e3,
|
|
844
|
+
timeoutMs,
|
|
845
|
+
label: "9router readiness"
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
logger.info("9router is ready");
|
|
849
|
+
}
|
|
850
|
+
getBaseUrl() {
|
|
851
|
+
return DEFAULT_BASE_URL;
|
|
852
|
+
}
|
|
853
|
+
getDashboardUrl() {
|
|
854
|
+
return DEFAULT_DASHBOARD_URL;
|
|
855
|
+
}
|
|
856
|
+
getApiBaseUrl() {
|
|
857
|
+
return DEFAULT_API_BASE_URL;
|
|
858
|
+
}
|
|
859
|
+
async getProviderStatus(providerId) {
|
|
860
|
+
try {
|
|
861
|
+
const response = await fetch(`${DEFAULT_BASE_URL}/api/providers/${providerId}/status`, {
|
|
862
|
+
method: "GET"
|
|
863
|
+
});
|
|
864
|
+
if (response.ok) {
|
|
865
|
+
const data = await response.json();
|
|
866
|
+
if (data.connected) {
|
|
867
|
+
return {
|
|
868
|
+
connected: true,
|
|
869
|
+
providerId,
|
|
870
|
+
accountHint: data.accountHint
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return { connected: false, providerId, reason: "Not connected" };
|
|
875
|
+
} catch {
|
|
876
|
+
return { connected: false, providerId, reason: "API unreachable" };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async ensureApiKey() {
|
|
880
|
+
logger.info("Ensuring 9router API key...");
|
|
881
|
+
try {
|
|
882
|
+
const response = await fetch(`${DEFAULT_BASE_URL}/api/keys`, {
|
|
883
|
+
method: "GET"
|
|
884
|
+
});
|
|
885
|
+
if (response.ok) {
|
|
886
|
+
const data = await response.json();
|
|
887
|
+
if (data.apiKey && typeof data.apiKey === "string") {
|
|
888
|
+
logger.info("Retrieved existing API key");
|
|
889
|
+
return { apiKey: data.apiKey };
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const createResponse = await fetch(`${DEFAULT_BASE_URL}/api/keys`, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: { "Content-Type": "application/json" },
|
|
895
|
+
body: JSON.stringify({ name: "oneclick-openclaw" })
|
|
896
|
+
});
|
|
897
|
+
if (createResponse.ok) {
|
|
898
|
+
const data = await createResponse.json();
|
|
899
|
+
logger.info("Created new API key");
|
|
900
|
+
return { apiKey: data.apiKey };
|
|
901
|
+
}
|
|
902
|
+
const fallbackKey = `sk_local_${Date.now()}`;
|
|
903
|
+
logger.warn("Could not get/create API key from 9router, using fallback");
|
|
904
|
+
return { apiKey: fallbackKey };
|
|
905
|
+
} catch {
|
|
906
|
+
const fallbackKey = `sk_local_${Date.now()}`;
|
|
907
|
+
logger.warn("9router API unreachable for key management, using fallback");
|
|
908
|
+
return { apiKey: fallbackKey };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async runHealthcheck() {
|
|
912
|
+
return httpHealthCheck(DEFAULT_BASE_URL);
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// src/steps/install9router.ts
|
|
917
|
+
var adapter = new NineRouterAdapter();
|
|
918
|
+
async function install9router(state) {
|
|
919
|
+
const installed = await adapter.isInstalled();
|
|
920
|
+
if (installed) {
|
|
921
|
+
logger.info("9router is already installed");
|
|
922
|
+
return state;
|
|
923
|
+
}
|
|
924
|
+
await adapter.install();
|
|
925
|
+
return state;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/steps/start9router.ts
|
|
929
|
+
var adapter2 = new NineRouterAdapter();
|
|
930
|
+
async function start9router(state) {
|
|
931
|
+
if (await adapter2.isRunning()) {
|
|
932
|
+
logger.info("9router is already running");
|
|
933
|
+
return updateArtifacts(state, {
|
|
934
|
+
nineRouterBaseUrl: adapter2.getApiBaseUrl(),
|
|
935
|
+
nineRouterDashboardUrl: adapter2.getDashboardUrl()
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
const result = await adapter2.start();
|
|
939
|
+
return updateArtifacts(state, {
|
|
940
|
+
nineRouterBaseUrl: adapter2.getApiBaseUrl(),
|
|
941
|
+
nineRouterDashboardUrl: adapter2.getDashboardUrl(),
|
|
942
|
+
nineRouterPid: result.pid
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/steps/wait9router.ts
|
|
947
|
+
var adapter3 = new NineRouterAdapter();
|
|
948
|
+
async function wait9router(state) {
|
|
949
|
+
await adapter3.waitUntilReady(
|
|
950
|
+
adapter3.getBaseUrl(),
|
|
951
|
+
TIMEOUTS.NINE_ROUTER_READY
|
|
952
|
+
);
|
|
953
|
+
return state;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/utils/browser.ts
|
|
957
|
+
import { platform as platform2 } from "os";
|
|
958
|
+
async function openBrowser(url) {
|
|
959
|
+
const { execaCommand } = await import("execa");
|
|
960
|
+
const os = platform2();
|
|
961
|
+
let command;
|
|
962
|
+
if (os === "darwin") {
|
|
963
|
+
command = `open "${url}"`;
|
|
964
|
+
} else if (os === "win32" || isWSL()) {
|
|
965
|
+
command = `cmd.exe /c start "" "${url}"`;
|
|
966
|
+
} else if (os === "linux") {
|
|
967
|
+
command = `xdg-open "${url}"`;
|
|
968
|
+
} else {
|
|
969
|
+
logger.warn(`Cannot auto-open browser. Please open manually:
|
|
970
|
+
${url}`);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
await execaCommand(command, { reject: false, shell: true });
|
|
975
|
+
logger.debug(`Browser opened: ${url}`);
|
|
976
|
+
} catch {
|
|
977
|
+
logger.warn(`Failed to open browser. Please open manually:
|
|
978
|
+
${url}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/steps/openAuth.ts
|
|
983
|
+
var adapter4 = new NineRouterAdapter();
|
|
984
|
+
async function openAuth(state) {
|
|
985
|
+
const dashboardUrl = adapter4.getDashboardUrl();
|
|
986
|
+
const authUrl = `${dashboardUrl}/providers/google-gemini/connect`;
|
|
987
|
+
logger.info("Opening browser for Google authentication...");
|
|
988
|
+
logger.info(`Auth URL: ${authUrl}`);
|
|
989
|
+
await openBrowser(authUrl);
|
|
990
|
+
logger.info("Please complete Google sign-in in your browser.");
|
|
991
|
+
logger.info("The installer will automatically detect when you're done.");
|
|
992
|
+
return state;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/steps/waitAuth.ts
|
|
996
|
+
import ora from "ora";
|
|
997
|
+
var adapter5 = new NineRouterAdapter();
|
|
998
|
+
async function waitAuth(state) {
|
|
999
|
+
const spinner = ora("Waiting for Google authentication...").start();
|
|
1000
|
+
try {
|
|
1001
|
+
await pollUntil(
|
|
1002
|
+
async () => {
|
|
1003
|
+
const status = await adapter5.getProviderStatus("google-gemini");
|
|
1004
|
+
if (status.connected) {
|
|
1005
|
+
spinner.succeed(`Google authenticated${status.accountHint ? ` (${status.accountHint})` : ""}`);
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
return false;
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
intervalMs: TIMEOUTS.AUTH_POLL_INTERVAL,
|
|
1012
|
+
timeoutMs: TIMEOUTS.AUTH_TIMEOUT,
|
|
1013
|
+
label: "Google authentication"
|
|
1014
|
+
}
|
|
1015
|
+
);
|
|
1016
|
+
} catch {
|
|
1017
|
+
spinner.fail("Google authentication timed out");
|
|
1018
|
+
throw {
|
|
1019
|
+
code: "AUTH_TIMEOUT",
|
|
1020
|
+
message: "Google authentication was not completed in time (10 min timeout).",
|
|
1021
|
+
recoverable: true
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
return state;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/steps/ensureApiKey.ts
|
|
1028
|
+
var adapter6 = new NineRouterAdapter();
|
|
1029
|
+
async function ensureApiKey(state) {
|
|
1030
|
+
const { apiKey } = await adapter6.ensureApiKey();
|
|
1031
|
+
logger.info("API key secured (redacted in logs)");
|
|
1032
|
+
return updateArtifacts(state, {
|
|
1033
|
+
nineRouterApiKey: apiKey
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/adapters/openClawAdapter.ts
|
|
1038
|
+
import { join as join3 } from "path";
|
|
1039
|
+
import { homedir as homedir3 } from "os";
|
|
1040
|
+
var CONFIG_PATHS = [
|
|
1041
|
+
join3(homedir3(), ".openclaw", "openclaw.json"),
|
|
1042
|
+
join3(homedir3(), ".config", "openclaw", "openclaw.json"),
|
|
1043
|
+
join3(homedir3(), ".openclaw", "config.json")
|
|
1044
|
+
];
|
|
1045
|
+
var OpenClawAdapter = class {
|
|
1046
|
+
configPath = null;
|
|
1047
|
+
async isInstalled() {
|
|
1048
|
+
return commandExists("openclaw");
|
|
1049
|
+
}
|
|
1050
|
+
async install() {
|
|
1051
|
+
logger.info("Installing OpenClaw...");
|
|
1052
|
+
const result = await spawnCommand("npm", ["install", "-g", "openclaw"], {
|
|
1053
|
+
timeout: 12e4
|
|
1054
|
+
});
|
|
1055
|
+
if (result.exitCode !== 0) {
|
|
1056
|
+
throw {
|
|
1057
|
+
code: "OPENCLAW_INSTALL_FAILED",
|
|
1058
|
+
message: `OpenClaw installation failed: ${result.stderr}`,
|
|
1059
|
+
recoverable: true
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
logger.info("OpenClaw installed successfully");
|
|
1063
|
+
}
|
|
1064
|
+
async getConfigPath() {
|
|
1065
|
+
if (this.configPath) return this.configPath;
|
|
1066
|
+
for (const path of CONFIG_PATHS) {
|
|
1067
|
+
if (fileExists(path)) {
|
|
1068
|
+
this.configPath = path;
|
|
1069
|
+
logger.debug(`Found OpenClaw config at: ${path}`);
|
|
1070
|
+
return path;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
this.configPath = CONFIG_PATHS[0];
|
|
1074
|
+
logger.debug(`Using default OpenClaw config path: ${this.configPath}`);
|
|
1075
|
+
return this.configPath;
|
|
1076
|
+
}
|
|
1077
|
+
async readConfig() {
|
|
1078
|
+
const path = await this.getConfigPath();
|
|
1079
|
+
const raw = readFileSafe(path);
|
|
1080
|
+
if (!raw) {
|
|
1081
|
+
logger.debug("No existing OpenClaw config found, using empty config");
|
|
1082
|
+
return {};
|
|
1083
|
+
}
|
|
1084
|
+
const parsed = safeJsonParse(raw);
|
|
1085
|
+
if (!parsed) {
|
|
1086
|
+
logger.warn("OpenClaw config is invalid JSON, using empty config");
|
|
1087
|
+
return {};
|
|
1088
|
+
}
|
|
1089
|
+
return parsed;
|
|
1090
|
+
}
|
|
1091
|
+
async writeConfig(config) {
|
|
1092
|
+
const path = await this.getConfigPath();
|
|
1093
|
+
const { dirname: dirname5 } = await import("path");
|
|
1094
|
+
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
1095
|
+
try {
|
|
1096
|
+
mkdirSync3(dirname5(path), { recursive: true });
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
atomicWriteFileSync(path, prettyJson(config));
|
|
1100
|
+
logger.debug(`Config written to: ${path}`);
|
|
1101
|
+
}
|
|
1102
|
+
async backupConfig() {
|
|
1103
|
+
const path = await this.getConfigPath();
|
|
1104
|
+
if (!fileExists(path)) {
|
|
1105
|
+
logger.debug("No config to backup");
|
|
1106
|
+
return "";
|
|
1107
|
+
}
|
|
1108
|
+
const backupPath = backupFile(path);
|
|
1109
|
+
logger.info(`Config backed up to: ${backupPath}`);
|
|
1110
|
+
return backupPath;
|
|
1111
|
+
}
|
|
1112
|
+
async validateConfig() {
|
|
1113
|
+
const config = await this.readConfig();
|
|
1114
|
+
const errors = [];
|
|
1115
|
+
if (typeof config !== "object" || config === null) {
|
|
1116
|
+
errors.push("Config must be a valid JSON object");
|
|
1117
|
+
}
|
|
1118
|
+
const models = config.models;
|
|
1119
|
+
if (models?.providers) {
|
|
1120
|
+
const providers = models.providers;
|
|
1121
|
+
for (const [id, provider] of Object.entries(providers)) {
|
|
1122
|
+
const p = provider;
|
|
1123
|
+
if (!p.baseUrl) errors.push(`Provider "${id}" missing baseUrl`);
|
|
1124
|
+
if (!p.apiKey) errors.push(`Provider "${id}" missing apiKey`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
1128
|
+
}
|
|
1129
|
+
async runSmokePrompt(prompt) {
|
|
1130
|
+
const start = Date.now();
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await spawnCommand("openclaw", ["--prompt", prompt], {
|
|
1133
|
+
timeout: 9e4
|
|
1134
|
+
});
|
|
1135
|
+
const latencyMs = Date.now() - start;
|
|
1136
|
+
if (result.exitCode === 0 && result.stdout.length > 0) {
|
|
1137
|
+
return {
|
|
1138
|
+
success: true,
|
|
1139
|
+
responseText: result.stdout.slice(0, 500),
|
|
1140
|
+
latencyMs
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
success: false,
|
|
1145
|
+
latencyMs,
|
|
1146
|
+
error: result.stderr || "No output"
|
|
1147
|
+
};
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
return {
|
|
1150
|
+
success: false,
|
|
1151
|
+
latencyMs: Date.now() - start,
|
|
1152
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// src/steps/installOpenClaw.ts
|
|
1159
|
+
var adapter7 = new OpenClawAdapter();
|
|
1160
|
+
async function installOpenClaw(state) {
|
|
1161
|
+
const installed = await adapter7.isInstalled();
|
|
1162
|
+
if (installed) {
|
|
1163
|
+
logger.info("OpenClaw is already installed");
|
|
1164
|
+
return state;
|
|
1165
|
+
}
|
|
1166
|
+
await adapter7.install();
|
|
1167
|
+
return state;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/steps/locateOpenClawConfig.ts
|
|
1171
|
+
var adapter8 = new OpenClawAdapter();
|
|
1172
|
+
async function locateOpenClawConfig(state) {
|
|
1173
|
+
const configPath = await adapter8.getConfigPath();
|
|
1174
|
+
logger.info(`OpenClaw config: ${configPath}`);
|
|
1175
|
+
return updateArtifacts(state, {
|
|
1176
|
+
openClawConfigPath: configPath
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/adapters/configAdapter.ts
|
|
1181
|
+
function patchConfig(input) {
|
|
1182
|
+
const { existingConfig, nineRouterBaseUrl, nineRouterApiKey, providerId, defaultModel } = input;
|
|
1183
|
+
const changedPaths = [];
|
|
1184
|
+
const config = JSON.parse(JSON.stringify(existingConfig));
|
|
1185
|
+
if (!config.models) {
|
|
1186
|
+
config.models = {};
|
|
1187
|
+
changedPaths.push("models");
|
|
1188
|
+
}
|
|
1189
|
+
const models = config.models;
|
|
1190
|
+
if (!models.providers) {
|
|
1191
|
+
models.providers = {};
|
|
1192
|
+
changedPaths.push("models.providers");
|
|
1193
|
+
}
|
|
1194
|
+
const providers = models.providers;
|
|
1195
|
+
const providerBlock = {
|
|
1196
|
+
baseUrl: nineRouterBaseUrl,
|
|
1197
|
+
apiKey: nineRouterApiKey,
|
|
1198
|
+
api: "openai-completions",
|
|
1199
|
+
models: [
|
|
1200
|
+
{
|
|
1201
|
+
id: defaultModel.primary,
|
|
1202
|
+
name: "Gemini Flash Free"
|
|
1203
|
+
},
|
|
1204
|
+
...(defaultModel.fallbacks ?? []).map((fb) => ({
|
|
1205
|
+
id: fb,
|
|
1206
|
+
name: fb.split("/").pop()
|
|
1207
|
+
}))
|
|
1208
|
+
]
|
|
1209
|
+
};
|
|
1210
|
+
if (providers[providerId]) {
|
|
1211
|
+
const existing = providers[providerId];
|
|
1212
|
+
providers[providerId] = deepMerge(existing, providerBlock);
|
|
1213
|
+
changedPaths.push(`models.providers.${providerId} (updated)`);
|
|
1214
|
+
logger.debug(`Updated existing provider: ${providerId}`);
|
|
1215
|
+
} else {
|
|
1216
|
+
providers[providerId] = providerBlock;
|
|
1217
|
+
changedPaths.push(`models.providers.${providerId} (added)`);
|
|
1218
|
+
logger.debug(`Added new provider: ${providerId}`);
|
|
1219
|
+
}
|
|
1220
|
+
if (!models.default) {
|
|
1221
|
+
models.default = `${providerId}/${defaultModel.primary}`;
|
|
1222
|
+
changedPaths.push("models.default");
|
|
1223
|
+
logger.debug(`Set default model: ${models.default}`);
|
|
1224
|
+
} else {
|
|
1225
|
+
logger.debug(`User has custom default model, not overriding: ${models.default}`);
|
|
1226
|
+
}
|
|
1227
|
+
const oneclick = config._oneclick ?? {};
|
|
1228
|
+
oneclick.version = 1;
|
|
1229
|
+
oneclick.managedProviderIds = [providerId];
|
|
1230
|
+
oneclick.lastPatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1231
|
+
config._oneclick = oneclick;
|
|
1232
|
+
changedPaths.push("_oneclick");
|
|
1233
|
+
return { nextConfig: config, changedPaths };
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/steps/patchOpenClaw.ts
|
|
1237
|
+
var adapter9 = new OpenClawAdapter();
|
|
1238
|
+
async function patchOpenClaw(state) {
|
|
1239
|
+
const backupPath = await adapter9.backupConfig();
|
|
1240
|
+
const existingConfig = await adapter9.readConfig();
|
|
1241
|
+
const apiKey = state.artifacts.nineRouterApiKey ?? "sk_local_default";
|
|
1242
|
+
const baseUrl = state.artifacts.nineRouterBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
1243
|
+
const { nextConfig, changedPaths } = patchConfig({
|
|
1244
|
+
existingConfig,
|
|
1245
|
+
nineRouterBaseUrl: baseUrl,
|
|
1246
|
+
nineRouterApiKey: apiKey,
|
|
1247
|
+
providerId: PROVIDER_ID,
|
|
1248
|
+
defaultModel: {
|
|
1249
|
+
primary: DEFAULT_MODEL.primary,
|
|
1250
|
+
fallbacks: [...DEFAULT_MODEL.fallbacks]
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
await adapter9.writeConfig(nextConfig);
|
|
1254
|
+
logger.info(`Config patched. Changed: ${changedPaths.join(", ")}`);
|
|
1255
|
+
return updateArtifacts(state, {
|
|
1256
|
+
configBackupPath: backupPath || void 0
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/steps/validate.ts
|
|
1261
|
+
import { copyFileSync as copyFileSync2 } from "fs";
|
|
1262
|
+
var adapter10 = new OpenClawAdapter();
|
|
1263
|
+
async function validate(state) {
|
|
1264
|
+
const result = await adapter10.validateConfig();
|
|
1265
|
+
if (result.valid) {
|
|
1266
|
+
logger.info("OpenClaw config validation: PASS");
|
|
1267
|
+
return state;
|
|
1268
|
+
}
|
|
1269
|
+
logger.error("OpenClaw config validation: FAIL");
|
|
1270
|
+
for (const err of result.errors) {
|
|
1271
|
+
logger.error(` - ${err}`);
|
|
1272
|
+
}
|
|
1273
|
+
if (state.artifacts.configBackupPath && state.artifacts.openClawConfigPath) {
|
|
1274
|
+
logger.warn("Rolling back to previous config...");
|
|
1275
|
+
try {
|
|
1276
|
+
copyFileSync2(state.artifacts.configBackupPath, state.artifacts.openClawConfigPath);
|
|
1277
|
+
logger.info("Config rolled back successfully");
|
|
1278
|
+
} catch (rollbackErr) {
|
|
1279
|
+
logger.error(`Rollback failed: ${rollbackErr}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
throw {
|
|
1283
|
+
code: "CONFIG_VALIDATION_FAILED",
|
|
1284
|
+
message: `Config validation failed: ${result.errors.join("; ")}`,
|
|
1285
|
+
recoverable: true
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/steps/smoke.ts
|
|
1290
|
+
import ora2 from "ora";
|
|
1291
|
+
var openClawAdapter = new OpenClawAdapter();
|
|
1292
|
+
async function smokeTest9Router(state) {
|
|
1293
|
+
const spinner = ora2("Running 9Router smoke test...").start();
|
|
1294
|
+
const apiUrl = state.artifacts.nineRouterBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
1295
|
+
const apiKey = state.artifacts.nineRouterApiKey ?? "";
|
|
1296
|
+
try {
|
|
1297
|
+
const controller = new AbortController();
|
|
1298
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUTS.SMOKE_9ROUTER);
|
|
1299
|
+
const response = await fetch(`${apiUrl}/chat/completions`, {
|
|
1300
|
+
method: "POST",
|
|
1301
|
+
headers: {
|
|
1302
|
+
"Content-Type": "application/json",
|
|
1303
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
1304
|
+
},
|
|
1305
|
+
body: JSON.stringify({
|
|
1306
|
+
model: "gc/gemini-3-flash-preview",
|
|
1307
|
+
messages: [
|
|
1308
|
+
{ role: "user", content: "Reply with the word OK only." }
|
|
1309
|
+
]
|
|
1310
|
+
}),
|
|
1311
|
+
signal: controller.signal
|
|
1312
|
+
});
|
|
1313
|
+
clearTimeout(timer);
|
|
1314
|
+
if (response.ok) {
|
|
1315
|
+
const data = await response.json();
|
|
1316
|
+
spinner.succeed("9Router smoke test: PASS");
|
|
1317
|
+
logger.debug(`Smoke response: ${JSON.stringify(data).slice(0, 200)}`);
|
|
1318
|
+
return state;
|
|
1319
|
+
}
|
|
1320
|
+
spinner.fail(`9Router smoke test: HTTP ${response.status}`);
|
|
1321
|
+
throw {
|
|
1322
|
+
code: "SMOKE_9ROUTER_FAILED",
|
|
1323
|
+
message: `9Router smoke test returned HTTP ${response.status}`,
|
|
1324
|
+
recoverable: true
|
|
1325
|
+
};
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
if (err?.code === "SMOKE_9ROUTER_FAILED") throw err;
|
|
1328
|
+
spinner.fail("9Router smoke test: FAIL");
|
|
1329
|
+
throw {
|
|
1330
|
+
code: "SMOKE_9ROUTER_FAILED",
|
|
1331
|
+
message: `9Router smoke test failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1332
|
+
recoverable: true
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async function smokeTestOpenClaw(state) {
|
|
1337
|
+
const spinner = ora2("Running OpenClaw smoke test...").start();
|
|
1338
|
+
const result = await openClawAdapter.runSmokePrompt("Reply with the word OK only.");
|
|
1339
|
+
if (result.success) {
|
|
1340
|
+
spinner.succeed(`OpenClaw smoke test: PASS (${result.latencyMs}ms)`);
|
|
1341
|
+
return state;
|
|
1342
|
+
}
|
|
1343
|
+
spinner.fail("OpenClaw smoke test: FAIL");
|
|
1344
|
+
throw {
|
|
1345
|
+
code: "SMOKE_OPENCLAW_FAILED",
|
|
1346
|
+
message: `OpenClaw smoke test failed: ${result.error}`,
|
|
1347
|
+
recoverable: true
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/cli.ts
|
|
1352
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
|
|
1353
|
+
|
|
1354
|
+
// src/web/server.ts
|
|
1355
|
+
import { createServer as createServer2 } from "http";
|
|
1356
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1357
|
+
import { join as join4, dirname as dirname4 } from "path";
|
|
1358
|
+
import { fileURLToPath } from "url";
|
|
1359
|
+
import { createHash as createHash2 } from "crypto";
|
|
1360
|
+
var wsClients = [];
|
|
1361
|
+
var heartbeatInterval = null;
|
|
1362
|
+
function wsAcceptKey(key) {
|
|
1363
|
+
return createHash2("sha1").update(key + "258EAFA5-E914-47DA-95CA-5AB5DC085B11").digest("base64");
|
|
1364
|
+
}
|
|
1365
|
+
function encodeWsFrame(message) {
|
|
1366
|
+
const data = Buffer.from(message, "utf-8");
|
|
1367
|
+
const len = data.length;
|
|
1368
|
+
let header;
|
|
1369
|
+
if (len < 126) {
|
|
1370
|
+
header = Buffer.alloc(2);
|
|
1371
|
+
header[0] = 129;
|
|
1372
|
+
header[1] = len;
|
|
1373
|
+
} else if (len < 65536) {
|
|
1374
|
+
header = Buffer.alloc(4);
|
|
1375
|
+
header[0] = 129;
|
|
1376
|
+
header[1] = 126;
|
|
1377
|
+
header.writeUInt16BE(len, 2);
|
|
1378
|
+
} else {
|
|
1379
|
+
header = Buffer.alloc(10);
|
|
1380
|
+
header[0] = 129;
|
|
1381
|
+
header[1] = 127;
|
|
1382
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
1383
|
+
}
|
|
1384
|
+
return Buffer.concat([header, data]);
|
|
1385
|
+
}
|
|
1386
|
+
function decodeWsFrame(buffer) {
|
|
1387
|
+
if (buffer.length < 2) return null;
|
|
1388
|
+
const opcode = buffer[0] & 15;
|
|
1389
|
+
const masked = (buffer[1] & 128) !== 0;
|
|
1390
|
+
let payloadLength = buffer[1] & 127;
|
|
1391
|
+
let offset = 2;
|
|
1392
|
+
if (payloadLength === 126) {
|
|
1393
|
+
payloadLength = buffer.readUInt16BE(2);
|
|
1394
|
+
offset = 4;
|
|
1395
|
+
} else if (payloadLength === 127) {
|
|
1396
|
+
payloadLength = Number(buffer.readBigUInt64BE(2));
|
|
1397
|
+
offset = 10;
|
|
1398
|
+
}
|
|
1399
|
+
let maskKey = null;
|
|
1400
|
+
if (masked) {
|
|
1401
|
+
maskKey = buffer.subarray(offset, offset + 4);
|
|
1402
|
+
offset += 4;
|
|
1403
|
+
}
|
|
1404
|
+
const payload = buffer.subarray(offset, offset + payloadLength);
|
|
1405
|
+
if (maskKey) {
|
|
1406
|
+
for (let i = 0; i < payload.length; i++) {
|
|
1407
|
+
payload[i] ^= maskKey[i % 4];
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return { opcode, payload: payload.toString("utf-8") };
|
|
1411
|
+
}
|
|
1412
|
+
function wsBroadcast(message) {
|
|
1413
|
+
const frame = encodeWsFrame(message);
|
|
1414
|
+
for (const client of wsClients) {
|
|
1415
|
+
try {
|
|
1416
|
+
client.socket.write(frame);
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function wsCleanup() {
|
|
1422
|
+
for (let i = wsClients.length - 1; i >= 0; i--) {
|
|
1423
|
+
if (!wsClients[i].alive) {
|
|
1424
|
+
try {
|
|
1425
|
+
wsClients[i].socket.end();
|
|
1426
|
+
} catch {
|
|
1427
|
+
}
|
|
1428
|
+
wsClients.splice(i, 1);
|
|
1429
|
+
} else {
|
|
1430
|
+
wsClients[i].alive = false;
|
|
1431
|
+
try {
|
|
1432
|
+
const ping = Buffer.alloc(2);
|
|
1433
|
+
ping[0] = 137;
|
|
1434
|
+
ping[1] = 0;
|
|
1435
|
+
wsClients[i].socket.write(ping);
|
|
1436
|
+
} catch {
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function resolvePagesDir() {
|
|
1442
|
+
const currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
1443
|
+
const candidates = [
|
|
1444
|
+
join4(currentDir, "pages"),
|
|
1445
|
+
join4(currentDir, "web", "pages"),
|
|
1446
|
+
join4(currentDir, "..", "pages"),
|
|
1447
|
+
join4(process.cwd(), "dist", "pages"),
|
|
1448
|
+
join4(process.cwd(), "src", "web", "pages")
|
|
1449
|
+
];
|
|
1450
|
+
for (const dir of candidates) {
|
|
1451
|
+
if (existsSync2(dir)) return dir;
|
|
1452
|
+
}
|
|
1453
|
+
return join4(process.cwd(), "src", "web", "pages");
|
|
1454
|
+
}
|
|
1455
|
+
var PAGES_DIR = resolvePagesDir();
|
|
1456
|
+
function loadPage(name) {
|
|
1457
|
+
const filePath = join4(PAGES_DIR, `${name}.html`);
|
|
1458
|
+
try {
|
|
1459
|
+
if (existsSync2(filePath)) {
|
|
1460
|
+
return readFileSync2(filePath, "utf-8");
|
|
1461
|
+
}
|
|
1462
|
+
return `<!DOCTYPE html><html><body><h1>${name}</h1><p>Not found: ${filePath}</p></body></html>`;
|
|
1463
|
+
} catch {
|
|
1464
|
+
return `<!DOCTYPE html><html><body><h1>Error: ${name}</h1></body></html>`;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function loadStateJson() {
|
|
1468
|
+
try {
|
|
1469
|
+
if (existsSync2(STATE_FILE_PATH)) {
|
|
1470
|
+
return readFileSync2(STATE_FILE_PATH, "utf-8");
|
|
1471
|
+
}
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
return JSON.stringify({ currentState: "IDLE", completedStates: [], steps: {} });
|
|
1475
|
+
}
|
|
1476
|
+
function startCallbackServer(port = 20199) {
|
|
1477
|
+
logger.debug(`Pages directory resolved to: ${PAGES_DIR}`);
|
|
1478
|
+
const server = createServer2((req, res) => {
|
|
1479
|
+
const url = req.url ?? "/";
|
|
1480
|
+
if (url.startsWith("/api/state")) {
|
|
1481
|
+
res.writeHead(200, {
|
|
1482
|
+
"Content-Type": "application/json",
|
|
1483
|
+
"Access-Control-Allow-Origin": "*"
|
|
1484
|
+
});
|
|
1485
|
+
res.end(loadStateJson());
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (url.startsWith("/api/ws-info")) {
|
|
1489
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1490
|
+
res.end(JSON.stringify({ clients: wsClients.length, wsPath: "/ws" }));
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (url === "/" || url.includes("/dashboard")) {
|
|
1494
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1495
|
+
res.end(loadPage("dashboard"));
|
|
1496
|
+
} else if (url.includes("/success")) {
|
|
1497
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1498
|
+
res.end(loadPage("success"));
|
|
1499
|
+
} else if (url.includes("/error")) {
|
|
1500
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1501
|
+
res.end(loadPage("error"));
|
|
1502
|
+
} else if (url.includes("/waiting") || url.includes("/auth")) {
|
|
1503
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1504
|
+
res.end(loadPage("waiting"));
|
|
1505
|
+
} else {
|
|
1506
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1507
|
+
res.end(loadPage("dashboard"));
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
server.on("upgrade", (req, socket) => {
|
|
1511
|
+
if (req.url !== "/ws") {
|
|
1512
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
const key = req.headers["sec-websocket-key"];
|
|
1516
|
+
if (!key) {
|
|
1517
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const acceptKey = wsAcceptKey(key);
|
|
1521
|
+
socket.write(
|
|
1522
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
1523
|
+
Upgrade: websocket\r
|
|
1524
|
+
Connection: Upgrade\r
|
|
1525
|
+
Sec-WebSocket-Accept: ${acceptKey}\r
|
|
1526
|
+
\r
|
|
1527
|
+
`
|
|
1528
|
+
);
|
|
1529
|
+
const clientId = `ws_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1530
|
+
const client = { id: clientId, socket, alive: true };
|
|
1531
|
+
wsClients.push(client);
|
|
1532
|
+
logger.debug(`WebSocket client connected: ${clientId} (total: ${wsClients.length})`);
|
|
1533
|
+
const initialState = loadStateJson();
|
|
1534
|
+
socket.write(encodeWsFrame(JSON.stringify({ type: "state", data: JSON.parse(initialState) })));
|
|
1535
|
+
socket.on("data", (data) => {
|
|
1536
|
+
const frame = decodeWsFrame(data);
|
|
1537
|
+
if (!frame) return;
|
|
1538
|
+
if (frame.opcode === 8) {
|
|
1539
|
+
try {
|
|
1540
|
+
socket.end();
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (frame.opcode === 10) {
|
|
1546
|
+
client.alive = true;
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (frame.opcode === 1) {
|
|
1550
|
+
client.alive = true;
|
|
1551
|
+
if (frame.payload === "ping") {
|
|
1552
|
+
socket.write(encodeWsFrame(JSON.stringify({ type: "pong", ts: Date.now() })));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
socket.on("close", () => {
|
|
1557
|
+
const idx = wsClients.indexOf(client);
|
|
1558
|
+
if (idx >= 0) wsClients.splice(idx, 1);
|
|
1559
|
+
logger.debug(`WebSocket client disconnected: ${clientId} (total: ${wsClients.length})`);
|
|
1560
|
+
});
|
|
1561
|
+
socket.on("error", () => {
|
|
1562
|
+
const idx = wsClients.indexOf(client);
|
|
1563
|
+
if (idx >= 0) wsClients.splice(idx, 1);
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
onStateChange((state) => {
|
|
1567
|
+
wsBroadcast(JSON.stringify({ type: "state", data: state }));
|
|
1568
|
+
});
|
|
1569
|
+
heartbeatInterval = setInterval(wsCleanup, 3e4);
|
|
1570
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1571
|
+
logger.debug(`Web server listening on http://127.0.0.1:${port}`);
|
|
1572
|
+
logger.debug(`WebSocket available at ws://127.0.0.1:${port}/ws`);
|
|
1573
|
+
});
|
|
1574
|
+
return {
|
|
1575
|
+
close: () => {
|
|
1576
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
1577
|
+
wsClients.forEach((c) => {
|
|
1578
|
+
try {
|
|
1579
|
+
c.socket.end();
|
|
1580
|
+
} catch {
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
wsClients.length = 0;
|
|
1584
|
+
server.close();
|
|
1585
|
+
},
|
|
1586
|
+
port
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/cli.ts
|
|
1591
|
+
var handlers = {
|
|
1592
|
+
PRECHECK: precheck,
|
|
1593
|
+
INSTALL_9ROUTER: install9router,
|
|
1594
|
+
START_9ROUTER: start9router,
|
|
1595
|
+
WAIT_9ROUTER_READY: wait9router,
|
|
1596
|
+
OPEN_AUTH_PAGE: openAuth,
|
|
1597
|
+
WAIT_GEMINI_AUTH: waitAuth,
|
|
1598
|
+
ENSURE_9ROUTER_API_KEY: ensureApiKey,
|
|
1599
|
+
INSTALL_OPENCLAW: installOpenClaw,
|
|
1600
|
+
LOCATE_OPENCLAW_CONFIG: locateOpenClawConfig,
|
|
1601
|
+
PATCH_OPENCLAW_PROVIDER: patchOpenClaw,
|
|
1602
|
+
SET_DEFAULT_MODEL: async (state) => {
|
|
1603
|
+
logger.info("Default model configuration verified");
|
|
1604
|
+
return state;
|
|
1605
|
+
},
|
|
1606
|
+
VALIDATE_OPENCLAW_CONFIG: validate,
|
|
1607
|
+
SMOKE_TEST_9ROUTER: smokeTest9Router,
|
|
1608
|
+
SMOKE_TEST_OPENCLAW: smokeTestOpenClaw
|
|
1609
|
+
};
|
|
1610
|
+
var program = new Command();
|
|
1611
|
+
program.name(APP.NAME).version(APP.VERSION).description(APP.DESCRIPTION);
|
|
1612
|
+
program.command("install").description("Run full setup: install 9router, authenticate, install & configure OpenClaw").action(async () => {
|
|
1613
|
+
try {
|
|
1614
|
+
await runInstall(handlers);
|
|
1615
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
const e = err;
|
|
1618
|
+
if (e.recoverable) {
|
|
1619
|
+
console.log("");
|
|
1620
|
+
console.log(chalk3.yellow(` Setup paused at: ${e.code ?? "unknown"}`));
|
|
1621
|
+
console.log(chalk3.yellow(` Reason: ${e.message}`));
|
|
1622
|
+
console.log("");
|
|
1623
|
+
console.log(chalk3.gray(" Next steps:"));
|
|
1624
|
+
console.log(chalk3.gray(" oneclick-openclaw resume"));
|
|
1625
|
+
console.log(chalk3.gray(" oneclick-openclaw doctor"));
|
|
1626
|
+
console.log("");
|
|
1627
|
+
process.exit(EXIT_CODES.OAUTH_FAILED);
|
|
1628
|
+
} else {
|
|
1629
|
+
console.log("");
|
|
1630
|
+
console.log(chalk3.red(` Setup failed: ${e.message}`));
|
|
1631
|
+
console.log("");
|
|
1632
|
+
console.log(chalk3.gray(" See logs:"));
|
|
1633
|
+
console.log(chalk3.gray(" oneclick-openclaw logs"));
|
|
1634
|
+
console.log("");
|
|
1635
|
+
process.exit(EXIT_CODES.FATAL_GENERIC);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
program.command("resume").description("Resume setup from the last checkpoint").action(async () => {
|
|
1640
|
+
try {
|
|
1641
|
+
await runResume(handlers);
|
|
1642
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
const e = err;
|
|
1645
|
+
logger.error(`Resume failed: ${e.message}`);
|
|
1646
|
+
process.exit(EXIT_CODES.FATAL_GENERIC);
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
program.command("doctor").description("Check environment, services, and configuration health").action(async () => {
|
|
1650
|
+
console.log("");
|
|
1651
|
+
console.log(chalk3.bold(" OneClick OpenClaw \u2014 System Doctor"));
|
|
1652
|
+
console.log(chalk3.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1653
|
+
console.log("");
|
|
1654
|
+
const checks = [];
|
|
1655
|
+
const nrAdapter = new NineRouterAdapter();
|
|
1656
|
+
const ocAdapter = new OpenClawAdapter();
|
|
1657
|
+
const nodeOk = await commandExists("node");
|
|
1658
|
+
checks.push({ category: "Environment", name: "Node.js", status: nodeOk ? "OK" : "FAIL" });
|
|
1659
|
+
const npmOk = await commandExists("npm");
|
|
1660
|
+
checks.push({ category: "Environment", name: "npm", status: npmOk ? "OK" : "FAIL" });
|
|
1661
|
+
const curlOk = await commandExists("curl");
|
|
1662
|
+
checks.push({ category: "Environment", name: "curl", status: curlOk ? "OK" : "WARN", detail: curlOk ? void 0 : "Optional" });
|
|
1663
|
+
const nrInstalled = await nrAdapter.isInstalled();
|
|
1664
|
+
checks.push({ category: "9Router", name: "Installed", status: nrInstalled ? "OK" : "FAIL" });
|
|
1665
|
+
const nrRunning = await nrAdapter.isRunning();
|
|
1666
|
+
checks.push({ category: "9Router", name: "Running", status: nrRunning ? "OK" : "FAIL" });
|
|
1667
|
+
if (nrRunning) {
|
|
1668
|
+
checks.push({ category: "9Router", name: "Dashboard", status: "OK", detail: nrAdapter.getDashboardUrl() });
|
|
1669
|
+
checks.push({ category: "9Router", name: "API", status: "OK", detail: nrAdapter.getApiBaseUrl() });
|
|
1670
|
+
const provStatus = await nrAdapter.getProviderStatus("google-gemini");
|
|
1671
|
+
checks.push({
|
|
1672
|
+
category: "9Router",
|
|
1673
|
+
name: "Auth (Google)",
|
|
1674
|
+
status: provStatus.connected ? "OK" : "WARN",
|
|
1675
|
+
detail: provStatus.connected ? "Connected" : "Not connected"
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
const ocInstalled = await ocAdapter.isInstalled();
|
|
1679
|
+
checks.push({ category: "OpenClaw", name: "Installed", status: ocInstalled ? "OK" : "FAIL" });
|
|
1680
|
+
const configPath = await ocAdapter.getConfigPath();
|
|
1681
|
+
const configExists = fileExists(configPath);
|
|
1682
|
+
checks.push({
|
|
1683
|
+
category: "OpenClaw",
|
|
1684
|
+
name: "Config path",
|
|
1685
|
+
status: configExists ? "OK" : "WARN",
|
|
1686
|
+
detail: configPath
|
|
1687
|
+
});
|
|
1688
|
+
if (configExists) {
|
|
1689
|
+
const valResult = await ocAdapter.validateConfig();
|
|
1690
|
+
checks.push({
|
|
1691
|
+
category: "OpenClaw",
|
|
1692
|
+
name: "Config valid",
|
|
1693
|
+
status: valResult.valid ? "OK" : "FAIL",
|
|
1694
|
+
detail: valResult.valid ? void 0 : valResult.errors.join(", ")
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
const stateExists = existsSync3(STATE_FILE_PATH);
|
|
1698
|
+
if (stateExists) {
|
|
1699
|
+
const stateRaw = readFileSync3(STATE_FILE_PATH, "utf-8");
|
|
1700
|
+
try {
|
|
1701
|
+
const stateData = JSON.parse(stateRaw);
|
|
1702
|
+
checks.push({
|
|
1703
|
+
category: "Orchestrator",
|
|
1704
|
+
name: "State",
|
|
1705
|
+
status: "OK",
|
|
1706
|
+
detail: `Current: ${stateData.currentState}`
|
|
1707
|
+
});
|
|
1708
|
+
} catch {
|
|
1709
|
+
checks.push({ category: "Orchestrator", name: "State", status: "FAIL", detail: "Corrupted" });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
let currentCat = "";
|
|
1713
|
+
for (const check of checks) {
|
|
1714
|
+
if (check.category !== currentCat) {
|
|
1715
|
+
currentCat = check.category;
|
|
1716
|
+
console.log(chalk3.bold(` ${currentCat}`));
|
|
1717
|
+
}
|
|
1718
|
+
const icon = check.status === "OK" ? chalk3.green("\u2714") : check.status === "WARN" ? chalk3.yellow("\u26A0") : check.status === "SKIP" ? chalk3.gray("\u25CB") : chalk3.red("\u2716");
|
|
1719
|
+
const detail = check.detail ? chalk3.gray(` (${check.detail})`) : "";
|
|
1720
|
+
console.log(` ${icon} ${check.name}${detail}`);
|
|
1721
|
+
}
|
|
1722
|
+
console.log("");
|
|
1723
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1724
|
+
});
|
|
1725
|
+
var authCmd = program.command("auth").description("Authentication commands");
|
|
1726
|
+
authCmd.command("google").description("Re-open Google OAuth flow").action(async () => {
|
|
1727
|
+
try {
|
|
1728
|
+
const state = loadOrCreateState();
|
|
1729
|
+
await openAuth(state);
|
|
1730
|
+
await waitAuth(state);
|
|
1731
|
+
logger.success("Google authentication completed");
|
|
1732
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
const e = err;
|
|
1735
|
+
logger.error(`Auth failed: ${e.message}`);
|
|
1736
|
+
process.exit(EXIT_CODES.OAUTH_FAILED);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
program.command("logs").description("Show log file location and recent entries").action(() => {
|
|
1740
|
+
console.log("");
|
|
1741
|
+
console.log(chalk3.bold(" Log files:"));
|
|
1742
|
+
console.log(` ${chalk3.cyan(LOG_FILE_PATH)}`);
|
|
1743
|
+
console.log("");
|
|
1744
|
+
if (existsSync3(LOG_FILE_PATH)) {
|
|
1745
|
+
const content = readFileSync3(LOG_FILE_PATH, "utf-8");
|
|
1746
|
+
const lines = content.split("\n");
|
|
1747
|
+
const tail = lines.slice(-20).join("\n");
|
|
1748
|
+
console.log(chalk3.gray(" Last 20 lines:"));
|
|
1749
|
+
console.log(chalk3.gray(tail));
|
|
1750
|
+
} else {
|
|
1751
|
+
console.log(chalk3.gray(" No log file found yet."));
|
|
1752
|
+
}
|
|
1753
|
+
console.log("");
|
|
1754
|
+
});
|
|
1755
|
+
program.command("dashboard").description("Open the web dashboard to monitor system status").option("-p, --port <port>", "Server port", "20199").action(async (opts) => {
|
|
1756
|
+
const port = parseInt(opts.port, 10);
|
|
1757
|
+
const handle = startCallbackServer(port);
|
|
1758
|
+
const url = `http://127.0.0.1:${handle.port}/dashboard`;
|
|
1759
|
+
console.log("");
|
|
1760
|
+
console.log(chalk3.bold(" OneClick OpenClaw \u2014 Dashboard"));
|
|
1761
|
+
console.log(chalk3.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1762
|
+
console.log(` ${chalk3.cyan(url)}`);
|
|
1763
|
+
console.log("");
|
|
1764
|
+
console.log(chalk3.gray(" Press Ctrl+C to stop"));
|
|
1765
|
+
console.log("");
|
|
1766
|
+
openBrowser(url);
|
|
1767
|
+
});
|
|
1768
|
+
program.command("reset").description("Reset orchestrator state (does not uninstall 9router or OpenClaw)").action(() => {
|
|
1769
|
+
resetState();
|
|
1770
|
+
logger.success("State reset. You can now run 'install' again.");
|
|
1771
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1772
|
+
});
|
|
1773
|
+
program.command("uninstall").description("Remove orchestrator artifacts").option("--keep-openclaw", "Keep OpenClaw installation").option("--keep-9router", "Keep 9router installation").option("--full", "Remove everything").action((opts) => {
|
|
1774
|
+
resetState();
|
|
1775
|
+
try {
|
|
1776
|
+
rmSync(LOG_DIR, { recursive: true, force: true });
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
logger.success("Orchestrator artifacts removed.");
|
|
1780
|
+
if (!opts.keep9router && opts.full) {
|
|
1781
|
+
logger.info("To uninstall 9router: npm uninstall -g 9router");
|
|
1782
|
+
}
|
|
1783
|
+
if (!opts.keepOpenclaw && opts.full) {
|
|
1784
|
+
logger.info("To uninstall OpenClaw: npm uninstall -g openclaw");
|
|
1785
|
+
}
|
|
1786
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
1787
|
+
});
|
|
1788
|
+
program.parse();
|