@slock-ai/computer 0.0.16-alpha.0 → 0.0.17
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/dist/index.js +621 -292
- package/dist/lib/index.d.ts +173 -73
- package/dist/lib/index.js +380 -108
- package/package.json +2 -2
package/dist/lib/index.js
CHANGED
|
@@ -31,73 +31,253 @@ var StateReaderError = class extends Error {
|
|
|
31
31
|
this.code = code;
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
-
var MIGRATION_DETECTION_KINDS = ["
|
|
34
|
+
var MIGRATION_DETECTION_KINDS = ["candidates", "server-unavailable"];
|
|
35
35
|
function isMigrationDetectionKind(value) {
|
|
36
36
|
return typeof value === "string" && MIGRATION_DETECTION_KINDS.includes(value);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/lib/migration.ts
|
|
40
|
-
import { readdir, readFile } from "fs/promises";
|
|
41
|
-
import { join } from "path";
|
|
40
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
41
|
+
import { join, resolve } from "path";
|
|
42
42
|
var MACHINE_DIR_PREFIX = "machine-";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
var FINGERPRINT_HEX_RE = /^[0-9a-f]{16}$/;
|
|
44
|
+
async function readLocalOwners(installRoot) {
|
|
45
|
+
const machinesDir = join(installRoot, "machines");
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await readdir(machinesDir);
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const owners = [];
|
|
53
|
+
for (const name of entries) {
|
|
54
|
+
if (!name.startsWith(MACHINE_DIR_PREFIX)) continue;
|
|
55
|
+
const ownerPath = join(machinesDir, name, "daemon.lock", "owner.json");
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readFile(ownerPath, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const fp = parsed.apiKeyFingerprint;
|
|
69
|
+
if (typeof fp !== "string" || !FINGERPRINT_HEX_RE.test(fp)) continue;
|
|
70
|
+
owners.push({ apiKeyFingerprint: fp, localPath: ownerPath });
|
|
71
|
+
}
|
|
72
|
+
return owners;
|
|
73
|
+
}
|
|
74
|
+
function indexLocalByFingerprint(owners) {
|
|
75
|
+
const map = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const owner of owners) {
|
|
77
|
+
if (!map.has(owner.apiKeyFingerprint)) {
|
|
78
|
+
map.set(owner.apiKeyFingerprint, owner);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return map;
|
|
82
|
+
}
|
|
83
|
+
function intersect(localByFp, roster) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const entry of roster) {
|
|
86
|
+
const local = localByFp.get(entry.apiKeyFingerprint);
|
|
87
|
+
if (!local) continue;
|
|
88
|
+
out.push({
|
|
89
|
+
apiKeyFingerprint: entry.apiKeyFingerprint,
|
|
90
|
+
daemonId: entry.daemonId,
|
|
91
|
+
localPath: local.localPath,
|
|
92
|
+
machineName: entry.machineName,
|
|
93
|
+
...entry.hostname ? { hostname: entry.hostname } : {},
|
|
94
|
+
...entry.lastSeenAt ? { lastSeenAt: entry.lastSeenAt } : {}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
out.sort((a, b) => {
|
|
98
|
+
const aSeen = a.lastSeenAt ?? "";
|
|
99
|
+
const bSeen = b.lastSeenAt ?? "";
|
|
100
|
+
if (aSeen !== bSeen) {
|
|
101
|
+
if (aSeen === "") return 1;
|
|
102
|
+
if (bSeen === "") return -1;
|
|
103
|
+
return aSeen < bSeen ? 1 : -1;
|
|
104
|
+
}
|
|
105
|
+
return a.apiKeyFingerprint < b.apiKeyFingerprint ? -1 : a.apiKeyFingerprint > b.apiKeyFingerprint ? 1 : 0;
|
|
106
|
+
});
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
async function detectLegacyMigration(installRoot, serverSlug, client) {
|
|
110
|
+
const localOwners = await readLocalOwners(installRoot);
|
|
111
|
+
if (localOwners.length === 0) {
|
|
112
|
+
return { kind: "candidates", candidates: [] };
|
|
113
|
+
}
|
|
114
|
+
const result = await client.list(serverSlug);
|
|
115
|
+
if (result.status !== "success") {
|
|
116
|
+
return { kind: "server-unavailable" };
|
|
117
|
+
}
|
|
118
|
+
const localByFp = indexLocalByFingerprint(localOwners);
|
|
119
|
+
return { kind: "candidates", candidates: intersect(localByFp, result.entries) };
|
|
120
|
+
}
|
|
121
|
+
async function validateManualMigratePath(inputPath, roster) {
|
|
122
|
+
const absInput = resolve(inputPath);
|
|
123
|
+
let stats;
|
|
124
|
+
try {
|
|
125
|
+
stats = await stat(absInput);
|
|
126
|
+
} catch {
|
|
127
|
+
return { ok: false, code: "MIGRATE_FROM_NOT_FOUND" };
|
|
128
|
+
}
|
|
129
|
+
const ownerPath = stats.isDirectory() ? join(absInput, "daemon.lock", "owner.json") : absInput;
|
|
130
|
+
const candidateOwnerPath = await firstReadableOwner([
|
|
131
|
+
ownerPath,
|
|
132
|
+
join(absInput, "owner.json"),
|
|
133
|
+
absInput
|
|
134
|
+
]);
|
|
135
|
+
if (!candidateOwnerPath) {
|
|
136
|
+
return { ok: false, code: "MIGRATE_FROM_INVALID" };
|
|
137
|
+
}
|
|
51
138
|
let raw;
|
|
52
139
|
try {
|
|
53
|
-
raw = await readFile(
|
|
140
|
+
raw = await readFile(candidateOwnerPath, "utf8");
|
|
54
141
|
} catch {
|
|
55
|
-
return {};
|
|
142
|
+
return { ok: false, code: "MIGRATE_FROM_INVALID" };
|
|
56
143
|
}
|
|
57
144
|
let parsed;
|
|
58
145
|
try {
|
|
59
146
|
parsed = JSON.parse(raw);
|
|
60
147
|
} catch {
|
|
61
|
-
return {};
|
|
148
|
+
return { ok: false, code: "MIGRATE_FROM_INVALID" };
|
|
149
|
+
}
|
|
150
|
+
const fp = parsed.apiKeyFingerprint;
|
|
151
|
+
if (typeof fp !== "string" || !FINGERPRINT_HEX_RE.test(fp)) {
|
|
152
|
+
return { ok: false, code: "MIGRATE_FROM_INVALID" };
|
|
153
|
+
}
|
|
154
|
+
const match = roster.find((r) => r.apiKeyFingerprint === fp);
|
|
155
|
+
if (!match) {
|
|
156
|
+
return { ok: false, code: "MIGRATE_FROM_NOT_OWNED" };
|
|
62
157
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
candidate: {
|
|
161
|
+
apiKeyFingerprint: match.apiKeyFingerprint,
|
|
162
|
+
daemonId: match.daemonId,
|
|
163
|
+
localPath: candidateOwnerPath,
|
|
164
|
+
machineName: match.machineName,
|
|
165
|
+
...match.hostname ? { hostname: match.hostname } : {},
|
|
166
|
+
...match.lastSeenAt ? { lastSeenAt: match.lastSeenAt } : {}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
66
169
|
}
|
|
67
|
-
async function
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
170
|
+
async function firstReadableOwner(paths) {
|
|
171
|
+
const seen = /* @__PURE__ */ new Set();
|
|
172
|
+
for (const p of paths) {
|
|
173
|
+
if (seen.has(p)) continue;
|
|
174
|
+
seen.add(p);
|
|
175
|
+
try {
|
|
176
|
+
const st = await stat(p);
|
|
177
|
+
if (st.isFile()) return p;
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
75
180
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/apiClient.ts
|
|
185
|
+
import { fetch } from "undici";
|
|
186
|
+
var LegacyMachinesClient = class {
|
|
187
|
+
constructor(baseUrl, accessToken) {
|
|
188
|
+
this.baseUrl = baseUrl;
|
|
189
|
+
this.accessToken = accessToken;
|
|
190
|
+
}
|
|
191
|
+
url(p) {
|
|
192
|
+
return new URL(p, this.baseUrl).toString();
|
|
193
|
+
}
|
|
194
|
+
async list(serverSlug) {
|
|
195
|
+
let res;
|
|
196
|
+
try {
|
|
197
|
+
res = await fetch(
|
|
198
|
+
this.url(`/api/computer/legacy-machines?serverSlug=${encodeURIComponent(serverSlug)}`),
|
|
199
|
+
{
|
|
200
|
+
method: "GET",
|
|
201
|
+
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
return { status: "error", code: "request_failed" };
|
|
206
|
+
}
|
|
207
|
+
const body = await res.json().catch(() => null);
|
|
208
|
+
if (res.status === 200 && body && Array.isArray(body.entries)) {
|
|
209
|
+
const entries = body.entries.map((raw) => {
|
|
210
|
+
if (!raw || typeof raw !== "object") return null;
|
|
211
|
+
const e = raw;
|
|
212
|
+
if (typeof e.daemonId !== "string" || typeof e.apiKeyFingerprint !== "string" || typeof e.machineName !== "string") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
daemonId: e.daemonId,
|
|
217
|
+
apiKeyFingerprint: e.apiKeyFingerprint,
|
|
218
|
+
machineName: e.machineName,
|
|
219
|
+
hostname: typeof e.hostname === "string" ? e.hostname : null,
|
|
220
|
+
lastSeenAt: typeof e.lastSeenAt === "string" ? e.lastSeenAt : null
|
|
221
|
+
};
|
|
222
|
+
}).filter((entry) => entry !== null);
|
|
223
|
+
return { status: "success", entries };
|
|
224
|
+
}
|
|
225
|
+
if (res.status === 401) return { status: "auth_required" };
|
|
226
|
+
if (res.status === 403) return { status: "not_authorized" };
|
|
227
|
+
if (res.status === 404) return { status: "disabled" };
|
|
228
|
+
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
229
|
+
return { status: "error", code };
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var RunnersClient = class {
|
|
233
|
+
constructor(baseUrl, computerApiKey) {
|
|
234
|
+
this.baseUrl = baseUrl;
|
|
235
|
+
this.computerApiKey = computerApiKey;
|
|
236
|
+
}
|
|
237
|
+
url(p) {
|
|
238
|
+
return new URL(p, this.baseUrl).toString();
|
|
239
|
+
}
|
|
240
|
+
async list() {
|
|
241
|
+
const res = await fetch(this.url("/internal/computer/runners"), {
|
|
242
|
+
method: "GET",
|
|
243
|
+
headers: { Authorization: `Bearer ${this.computerApiKey}` }
|
|
84
244
|
});
|
|
245
|
+
const body = await res.json().catch(() => null);
|
|
246
|
+
if (res.status === 200 && body && Array.isArray(body.runners)) {
|
|
247
|
+
return {
|
|
248
|
+
status: "success",
|
|
249
|
+
whitelist: Array.isArray(body.whitelist) ? body.whitelist : [],
|
|
250
|
+
runners: body.runners
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
|
|
254
|
+
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
255
|
+
return { status: "error", code };
|
|
85
256
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
257
|
+
async stop(agentId) {
|
|
258
|
+
const res = await fetch(this.url(`/internal/computer/runners/${encodeURIComponent(agentId)}/stop`), {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { Authorization: `Bearer ${this.computerApiKey}`, "Content-Type": "application/json" },
|
|
261
|
+
body: "{}"
|
|
262
|
+
});
|
|
263
|
+
if (res.status === 200) return { status: "success" };
|
|
264
|
+
if (res.status === 404) return { status: "not_found" };
|
|
265
|
+
if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
|
|
266
|
+
const body = await res.json().catch(() => null);
|
|
267
|
+
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
268
|
+
return { status: "error", code };
|
|
95
269
|
}
|
|
96
|
-
|
|
97
|
-
}
|
|
270
|
+
};
|
|
98
271
|
|
|
99
|
-
// src/
|
|
100
|
-
import { readFile as
|
|
272
|
+
// src/setup.ts
|
|
273
|
+
import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename3, rm as rm2, writeFile as writeFile8 } from "fs/promises";
|
|
274
|
+
import { dirname as dirname8 } from "path";
|
|
275
|
+
import { fetch as fetch3 } from "undici";
|
|
276
|
+
|
|
277
|
+
// src/serverState.ts
|
|
278
|
+
import { readFile as readFile2, readdir as readdir2, writeFile, mkdir, unlink, access, chmod } from "fs/promises";
|
|
279
|
+
import { dirname } from "path";
|
|
280
|
+
import { constants as fsConstants } from "fs";
|
|
101
281
|
|
|
102
282
|
// src/paths.ts
|
|
103
283
|
import { createHash } from "crypto";
|
|
@@ -128,26 +308,39 @@ function serverDir(slockHome, serverId) {
|
|
|
128
308
|
function serverAttachmentPath(slockHome, serverId) {
|
|
129
309
|
return path.join(serverDir(slockHome, serverId), "runner.state.json");
|
|
130
310
|
}
|
|
131
|
-
function
|
|
132
|
-
return path.join(serverDir(slockHome, serverId), "
|
|
311
|
+
function serverRunnerPidPath(slockHome, serverId) {
|
|
312
|
+
return path.join(serverDir(slockHome, serverId), "server-runner.pid");
|
|
133
313
|
}
|
|
134
|
-
function
|
|
135
|
-
return path.join(serverDir(slockHome, serverId), "
|
|
314
|
+
function serverRunnerLogPath(slockHome, serverId) {
|
|
315
|
+
return path.join(serverDir(slockHome, serverId), "server-runner.log");
|
|
136
316
|
}
|
|
137
317
|
function serverHealthPath(slockHome, serverId) {
|
|
138
318
|
return path.join(serverDir(slockHome, serverId), "health.json");
|
|
139
319
|
}
|
|
320
|
+
function serviceRunDir(slockHome) {
|
|
321
|
+
return path.join(computerDir(slockHome), "run");
|
|
322
|
+
}
|
|
140
323
|
function servicePidPath(slockHome) {
|
|
324
|
+
return path.join(serviceRunDir(slockHome), "service.pid");
|
|
325
|
+
}
|
|
326
|
+
function legacyServicePidPath(slockHome) {
|
|
141
327
|
return path.join(computerDir(slockHome), "service.pid");
|
|
142
328
|
}
|
|
329
|
+
function legacySupervisorPidPath(slockHome) {
|
|
330
|
+
return path.join(computerDir(slockHome), "supervisor.pid");
|
|
331
|
+
}
|
|
332
|
+
function servicePidReadFallback(slockHome) {
|
|
333
|
+
return [
|
|
334
|
+
servicePidPath(slockHome),
|
|
335
|
+
legacyServicePidPath(slockHome),
|
|
336
|
+
legacySupervisorPidPath(slockHome)
|
|
337
|
+
];
|
|
338
|
+
}
|
|
143
339
|
function serviceLogPath(slockHome) {
|
|
144
|
-
return path.join(
|
|
340
|
+
return path.join(serviceRunDir(slockHome), "service.log");
|
|
145
341
|
}
|
|
146
342
|
|
|
147
343
|
// src/serverState.ts
|
|
148
|
-
import { readFile as readFile2, readdir as readdir2, writeFile, mkdir, unlink, access, chmod } from "fs/promises";
|
|
149
|
-
import { dirname } from "path";
|
|
150
|
-
import { constants as fsConstants } from "fs";
|
|
151
344
|
function parseAttachment(raw) {
|
|
152
345
|
try {
|
|
153
346
|
const a = JSON.parse(raw);
|
|
@@ -198,12 +391,37 @@ async function listServerAttachments(slockHome) {
|
|
|
198
391
|
return out;
|
|
199
392
|
}
|
|
200
393
|
|
|
201
|
-
// src/
|
|
202
|
-
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2
|
|
394
|
+
// src/services/attach.ts
|
|
395
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
203
396
|
import { dirname as dirname2 } from "path";
|
|
397
|
+
|
|
398
|
+
// src/services/login.ts
|
|
399
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
400
|
+
import { dirname as dirname3 } from "path";
|
|
401
|
+
|
|
402
|
+
// src/services/adoptLegacy.ts
|
|
403
|
+
import { chmod as chmod3, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, appendFile, stat as stat2 } from "fs/promises";
|
|
404
|
+
import { createHash as createHash2 } from "crypto";
|
|
405
|
+
import { dirname as dirname4, join as join2 } from "path";
|
|
406
|
+
import { setTimeout as delay } from "timers/promises";
|
|
407
|
+
|
|
408
|
+
// src/service.ts
|
|
409
|
+
import { spawn as spawn2 } from "child_process";
|
|
410
|
+
import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile7, open, rename as rename2 } from "fs/promises";
|
|
411
|
+
import { dirname as dirname7, join as joinPath } from "path";
|
|
412
|
+
import { fileURLToPath } from "url";
|
|
413
|
+
|
|
414
|
+
// src/cleanup.ts
|
|
415
|
+
import { readdir as readdir3, stat as stat3, unlink as unlink3, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
|
|
416
|
+
import { spawn } from "child_process";
|
|
417
|
+
import { join as join3 } from "path";
|
|
418
|
+
|
|
419
|
+
// src/internal/process-primitives.ts
|
|
420
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
|
|
421
|
+
import { dirname as dirname5 } from "path";
|
|
204
422
|
async function readPidfileAt(pidfilePath) {
|
|
205
423
|
try {
|
|
206
|
-
const raw = (await
|
|
424
|
+
const raw = (await readFile5(pidfilePath, "utf8")).trim();
|
|
207
425
|
const pid = Number.parseInt(raw, 10);
|
|
208
426
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
209
427
|
} catch {
|
|
@@ -220,15 +438,18 @@ function isProcessAlive(pid) {
|
|
|
220
438
|
}
|
|
221
439
|
}
|
|
222
440
|
|
|
441
|
+
// src/cleanup.ts
|
|
442
|
+
var TMP_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
443
|
+
|
|
223
444
|
// src/health.ts
|
|
224
|
-
import { readFile as
|
|
225
|
-
import { dirname as
|
|
445
|
+
import { readFile as readFile6, writeFile as writeFile6, unlink as unlink4, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
|
|
446
|
+
import { dirname as dirname6 } from "path";
|
|
226
447
|
var CRASH_WINDOW_MS = 6e4;
|
|
227
448
|
var DEGRADED_THRESHOLD = 3;
|
|
228
449
|
async function readHealthFile(slockHome, serverId) {
|
|
229
450
|
if (!isValidServerId(serverId)) return { crashes: [] };
|
|
230
451
|
try {
|
|
231
|
-
const raw = await
|
|
452
|
+
const raw = await readFile6(serverHealthPath(slockHome, serverId), "utf8");
|
|
232
453
|
const parsed = JSON.parse(raw);
|
|
233
454
|
if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
|
|
234
455
|
return parsed;
|
|
@@ -248,10 +469,95 @@ async function isDegraded(slockHome, serverId, nowMs = Date.now()) {
|
|
|
248
469
|
return recent.length >= DEGRADED_THRESHOLD;
|
|
249
470
|
}
|
|
250
471
|
|
|
472
|
+
// src/internal/service-pid-fallback.ts
|
|
473
|
+
async function findLiveServicePidReadOnly(slockHome, deps = {}) {
|
|
474
|
+
const readPidfile = deps.readPidfile ?? readPidfileAt;
|
|
475
|
+
const isAlive = deps.isProcessAlive ?? isProcessAlive;
|
|
476
|
+
return walkFallback(slockHome, readPidfile, isAlive, async () => void 0);
|
|
477
|
+
}
|
|
478
|
+
async function walkFallback(slockHome, readPidfile, isAlive, clearStale) {
|
|
479
|
+
const candidates = servicePidReadFallback(slockHome);
|
|
480
|
+
let firstStalePidfile = null;
|
|
481
|
+
let firstStalePid = null;
|
|
482
|
+
for (const candidate of candidates) {
|
|
483
|
+
const candidatePid = await readPidfile(candidate);
|
|
484
|
+
if (candidatePid === null) continue;
|
|
485
|
+
if (isAlive(candidatePid)) {
|
|
486
|
+
return {
|
|
487
|
+
pid: candidatePid,
|
|
488
|
+
pidfilePath: candidate,
|
|
489
|
+
firstStalePidfile,
|
|
490
|
+
firstStalePid
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
await clearStale(candidate);
|
|
494
|
+
if (firstStalePidfile === null) {
|
|
495
|
+
firstStalePidfile = candidate;
|
|
496
|
+
firstStalePid = candidatePid;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
pid: null,
|
|
501
|
+
pidfilePath: candidates[0],
|
|
502
|
+
firstStalePidfile,
|
|
503
|
+
firstStalePid
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/services/detach.ts
|
|
508
|
+
import { fetch as fetch2 } from "undici";
|
|
509
|
+
|
|
510
|
+
// src/setup.ts
|
|
511
|
+
var MIGRATION_FRESH_TRIGGERS = [
|
|
512
|
+
"empty-intersection",
|
|
513
|
+
"explicit-zero",
|
|
514
|
+
"eof",
|
|
515
|
+
"non-tty",
|
|
516
|
+
"server-unavailable"
|
|
517
|
+
];
|
|
518
|
+
async function pickMigrationCandidateFromInput(candidates, read, write) {
|
|
519
|
+
write(
|
|
520
|
+
"Migration: detected legacy daemon(s) on this Computer that match the target server.\n"
|
|
521
|
+
);
|
|
522
|
+
candidates.forEach((c, i) => {
|
|
523
|
+
const host = c.hostname ? ` (${c.hostname})` : "";
|
|
524
|
+
const seen = c.lastSeenAt ? ` last seen ${c.lastSeenAt}` : "";
|
|
525
|
+
write(` ${i + 1}. ${c.machineName}${host}${seen}
|
|
526
|
+
`);
|
|
527
|
+
write(` local: ${c.localPath}
|
|
528
|
+
`);
|
|
529
|
+
});
|
|
530
|
+
write(" 0. Fresh attach (skip migration)\n");
|
|
531
|
+
write(" m. Migrate from a different on-disk path\n");
|
|
532
|
+
while (true) {
|
|
533
|
+
write("Choose [1]: ");
|
|
534
|
+
const { line, eof } = await read();
|
|
535
|
+
if (eof) return { kind: "fresh" };
|
|
536
|
+
const trimmed = line.trim();
|
|
537
|
+
const norm = trimmed.toLowerCase();
|
|
538
|
+
if (norm === "m") {
|
|
539
|
+
write("Path to legacy machine dir or owner.json: ");
|
|
540
|
+
const { line: pathLine } = await read();
|
|
541
|
+
return { kind: "manual", path: pathLine.trim() };
|
|
542
|
+
}
|
|
543
|
+
if (norm.length === 0) return { kind: "candidate", index: 0 };
|
|
544
|
+
if (norm === "0") return { kind: "fresh" };
|
|
545
|
+
const n = Number.parseInt(norm, 10);
|
|
546
|
+
if (Number.isFinite(n) && String(n) === norm && n >= 1 && n <= candidates.length) {
|
|
547
|
+
return { kind: "candidate", index: n - 1 };
|
|
548
|
+
}
|
|
549
|
+
write(
|
|
550
|
+
`Invalid selection "${trimmed}". Enter 1..${candidates.length}, 0, or m.
|
|
551
|
+
`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
251
556
|
// src/status.ts
|
|
557
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
252
558
|
async function readUserSession(path2) {
|
|
253
559
|
try {
|
|
254
|
-
const parsed = JSON.parse(await
|
|
560
|
+
const parsed = JSON.parse(await readFile9(path2, "utf8"));
|
|
255
561
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
256
562
|
return { state: "present", session: parsed, error: null };
|
|
257
563
|
}
|
|
@@ -274,6 +580,10 @@ async function pidStatus(pidfile) {
|
|
|
274
580
|
const pid = await readPidfileAt(pidfile);
|
|
275
581
|
return pid !== null && isProcessAlive(pid) ? { running: true, pid } : { running: false };
|
|
276
582
|
}
|
|
583
|
+
async function serviceState(slockHome) {
|
|
584
|
+
const { pid } = await findLiveServicePidReadOnly(slockHome);
|
|
585
|
+
return pid !== null ? { running: true, pid } : { running: false };
|
|
586
|
+
}
|
|
277
587
|
async function deriveHealth(slockHome, serverId, daemon) {
|
|
278
588
|
if (!daemon.running) return "offline";
|
|
279
589
|
if (await isDegraded(slockHome, serverId)) return "degraded";
|
|
@@ -284,19 +594,19 @@ async function buildStatusReport(installRoot) {
|
|
|
284
594
|
const session = sessionRead.session;
|
|
285
595
|
const attachments = await listServerAttachments(installRoot);
|
|
286
596
|
const service = {
|
|
287
|
-
...await
|
|
597
|
+
...await serviceState(installRoot),
|
|
288
598
|
logPath: serviceLogPath(installRoot)
|
|
289
599
|
};
|
|
290
600
|
const servers = [];
|
|
291
601
|
for (const a of attachments) {
|
|
292
|
-
const daemon = await pidStatus(
|
|
602
|
+
const daemon = await pidStatus(serverRunnerPidPath(installRoot, a.serverId));
|
|
293
603
|
servers.push({
|
|
294
604
|
serverId: a.serverId,
|
|
295
605
|
serverSlug: a.serverSlug ?? null,
|
|
296
606
|
serverMachineId: a.serverMachineId,
|
|
297
607
|
serverUrl: a.serverUrl,
|
|
298
608
|
attachedAt: a.attachedAt ?? null,
|
|
299
|
-
|
|
609
|
+
serverRunnerLogPath: serverRunnerLogPath(installRoot, a.serverId),
|
|
300
610
|
daemon,
|
|
301
611
|
health: await deriveHealth(installRoot, a.serverId, daemon)
|
|
302
612
|
});
|
|
@@ -313,48 +623,6 @@ async function buildStatusReport(installRoot) {
|
|
|
313
623
|
};
|
|
314
624
|
}
|
|
315
625
|
|
|
316
|
-
// src/apiClient.ts
|
|
317
|
-
import { fetch } from "undici";
|
|
318
|
-
var RunnersClient = class {
|
|
319
|
-
constructor(baseUrl, computerApiKey) {
|
|
320
|
-
this.baseUrl = baseUrl;
|
|
321
|
-
this.computerApiKey = computerApiKey;
|
|
322
|
-
}
|
|
323
|
-
url(p) {
|
|
324
|
-
return new URL(p, this.baseUrl).toString();
|
|
325
|
-
}
|
|
326
|
-
async list() {
|
|
327
|
-
const res = await fetch(this.url("/internal/computer/runners"), {
|
|
328
|
-
method: "GET",
|
|
329
|
-
headers: { Authorization: `Bearer ${this.computerApiKey}` }
|
|
330
|
-
});
|
|
331
|
-
const body = await res.json().catch(() => null);
|
|
332
|
-
if (res.status === 200 && body && Array.isArray(body.runners)) {
|
|
333
|
-
return {
|
|
334
|
-
status: "success",
|
|
335
|
-
whitelist: Array.isArray(body.whitelist) ? body.whitelist : [],
|
|
336
|
-
runners: body.runners
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
|
|
340
|
-
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
341
|
-
return { status: "error", code };
|
|
342
|
-
}
|
|
343
|
-
async stop(agentId) {
|
|
344
|
-
const res = await fetch(this.url(`/internal/computer/runners/${encodeURIComponent(agentId)}/stop`), {
|
|
345
|
-
method: "POST",
|
|
346
|
-
headers: { Authorization: `Bearer ${this.computerApiKey}`, "Content-Type": "application/json" },
|
|
347
|
-
body: "{}"
|
|
348
|
-
});
|
|
349
|
-
if (res.status === 200) return { status: "success" };
|
|
350
|
-
if (res.status === 404) return { status: "not_found" };
|
|
351
|
-
if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
|
|
352
|
-
const body = await res.json().catch(() => null);
|
|
353
|
-
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
354
|
-
return { status: "error", code };
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
626
|
// src/lib/readers.ts
|
|
359
627
|
async function readServiceStatus(installRoot) {
|
|
360
628
|
return buildStatusReport(installRoot);
|
|
@@ -442,7 +710,7 @@ function isServiceState(value) {
|
|
|
442
710
|
}
|
|
443
711
|
|
|
444
712
|
// src/upgradeLog.ts
|
|
445
|
-
import { chmod as
|
|
713
|
+
import { chmod as chmod5, mkdir as mkdir10, open as open2 } from "fs/promises";
|
|
446
714
|
var UPGRADE_ERROR_CODES = [
|
|
447
715
|
"UPGRADE_DEPS_CHANGED",
|
|
448
716
|
"UPGRADE_NETWORK_FAILED",
|
|
@@ -486,7 +754,9 @@ function assertUpgradeLogEntry(entry) {
|
|
|
486
754
|
}
|
|
487
755
|
export {
|
|
488
756
|
IPC_ERROR_CODES,
|
|
757
|
+
LegacyMachinesClient,
|
|
489
758
|
MIGRATION_DETECTION_KINDS,
|
|
759
|
+
MIGRATION_FRESH_TRIGGERS,
|
|
490
760
|
RUNNER_STATE_VALUES,
|
|
491
761
|
SERVICE_STATE_VALUES,
|
|
492
762
|
STATE_READER_ERROR_CODES,
|
|
@@ -500,6 +770,8 @@ export {
|
|
|
500
770
|
isRunnerState,
|
|
501
771
|
isServiceState,
|
|
502
772
|
listRunners,
|
|
773
|
+
pickMigrationCandidateFromInput,
|
|
503
774
|
readRunnerStatus,
|
|
504
|
-
readServiceStatus
|
|
775
|
+
readServiceStatus,
|
|
776
|
+
validateManualMigratePath
|
|
505
777
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slock-ai/computer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Slock Computer — standalone human/local-machine control-plane CLI (login + attach). Distinct from the agent-facing @slock-ai/cli.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"commander": "^12.1.0",
|
|
29
29
|
"proper-lockfile": "^4.1.2",
|
|
30
30
|
"undici": "^7.24.7",
|
|
31
|
-
"@slock-ai/daemon": "0.55.
|
|
31
|
+
"@slock-ai/daemon": "0.55.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.5.0",
|