@sentropic/remote-cli 0.0.3
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/attach-YI2AMS3B.js +22 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-E3GXAKJ3.js +258 -0
- package/dist/chunk-PG4CWJNC.js +197 -0
- package/dist/chunk-RLFQALHC.js +45 -0
- package/dist/chunk-VRTZ23J3.js +541 -0
- package/dist/chunk-Z277FK2S.js +294 -0
- package/dist/h2a-bridge-FPB5D3TB.js +20 -0
- package/dist/index.d.ts +730 -0
- package/dist/index.js +13966 -0
- package/dist/llm-gateway-runtime/index.d.ts +2 -0
- package/dist/llm-gateway-runtime/index.js +1295 -0
- package/dist/sync-status-WRQK4YRK.js +15 -0
- package/dist/workspace-O32VX2JG.js +38 -0
- package/dist/workspace-sync-incremental-W3SHAYHS.js +159 -0
- package/package.json +46 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attach,
|
|
3
|
+
createRemoteSession,
|
|
4
|
+
getRemoteSession,
|
|
5
|
+
listRemoteSessions,
|
|
6
|
+
refreshRemoteSession,
|
|
7
|
+
renameRemoteSession,
|
|
8
|
+
sessionTerminalHealth,
|
|
9
|
+
stopRemoteSession
|
|
10
|
+
} from "./chunk-VRTZ23J3.js";
|
|
11
|
+
import "./chunk-E3GXAKJ3.js";
|
|
12
|
+
import "./chunk-3RG5ZIWI.js";
|
|
13
|
+
export {
|
|
14
|
+
attach,
|
|
15
|
+
createRemoteSession,
|
|
16
|
+
getRemoteSession,
|
|
17
|
+
listRemoteSessions,
|
|
18
|
+
refreshRemoteSession,
|
|
19
|
+
renameRemoteSession,
|
|
20
|
+
sessionTerminalHealth,
|
|
21
|
+
stopRemoteSession
|
|
22
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__require
|
|
10
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var DEFAULT_LAYOUT = {
|
|
6
|
+
maxAgeHours: 48,
|
|
7
|
+
maxPerWindow: 12,
|
|
8
|
+
sharedWindows: 2,
|
|
9
|
+
multiSession: {},
|
|
10
|
+
groups: []
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_H2A_COMMAND = "h2a mcp-serve --auto-open --auto-upgrade --wake local-tmux";
|
|
13
|
+
var DEFAULT_SESSION_TARGET = "scaleway-kapsule";
|
|
14
|
+
function parseTunnel(raw) {
|
|
15
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
16
|
+
const t = raw;
|
|
17
|
+
if (typeof t.namespace !== "string" || typeof t.service !== "string" || typeof t.localPort !== "number" || typeof t.remotePort !== "number") {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
const tunnel = {
|
|
21
|
+
namespace: t.namespace,
|
|
22
|
+
service: t.service,
|
|
23
|
+
localPort: t.localPort,
|
|
24
|
+
remotePort: t.remotePort
|
|
25
|
+
};
|
|
26
|
+
if (typeof t.kubeconfig === "string") tunnel.kubeconfig = t.kubeconfig;
|
|
27
|
+
return tunnel;
|
|
28
|
+
}
|
|
29
|
+
function parseH2a(raw) {
|
|
30
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
31
|
+
const h = raw;
|
|
32
|
+
const h2a = {};
|
|
33
|
+
if (typeof h.enabled === "boolean") h2a.enabled = h.enabled;
|
|
34
|
+
if (typeof h.command === "string") h2a.command = h.command;
|
|
35
|
+
return Object.keys(h2a).length > 0 ? h2a : void 0;
|
|
36
|
+
}
|
|
37
|
+
function parseLlmMeshRuntime(raw) {
|
|
38
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
39
|
+
const r = raw;
|
|
40
|
+
const llmMesh = {};
|
|
41
|
+
if (typeof r.enabled === "boolean") llmMesh.enabled = r.enabled;
|
|
42
|
+
return Object.keys(llmMesh).length > 0 ? llmMesh : void 0;
|
|
43
|
+
}
|
|
44
|
+
function parsePluginMcp(raw) {
|
|
45
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
46
|
+
const m = raw;
|
|
47
|
+
if (typeof m.name !== "string" || typeof m.command !== "string" || !Array.isArray(m.args) || !m.args.every((a) => typeof a === "string")) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
const mcp = {
|
|
51
|
+
name: m.name,
|
|
52
|
+
command: m.command,
|
|
53
|
+
args: m.args
|
|
54
|
+
};
|
|
55
|
+
if (typeof m.scriptRel === "string") mcp.scriptRel = m.scriptRel;
|
|
56
|
+
return mcp;
|
|
57
|
+
}
|
|
58
|
+
function parsePlugins(raw) {
|
|
59
|
+
if (!Array.isArray(raw)) return void 0;
|
|
60
|
+
const plugins = [];
|
|
61
|
+
for (const item of raw) {
|
|
62
|
+
if (!item || typeof item !== "object") continue;
|
|
63
|
+
const p = item;
|
|
64
|
+
if (typeof p.pkg !== "string" || typeof p.version !== "string" || !Array.isArray(p.mcp)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const mcp = p.mcp.map(parsePluginMcp).filter((m) => m !== void 0);
|
|
68
|
+
const entry = { pkg: p.pkg, version: p.version, mcp };
|
|
69
|
+
const install = parsePluginInstall(p.install);
|
|
70
|
+
if (install) entry.install = install;
|
|
71
|
+
plugins.push(entry);
|
|
72
|
+
}
|
|
73
|
+
return plugins;
|
|
74
|
+
}
|
|
75
|
+
function parsePluginInstall(raw) {
|
|
76
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
77
|
+
const i = raw;
|
|
78
|
+
if (i.method !== "npm" && i.method !== "curl" && i.method !== "script") {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const install = { method: i.method };
|
|
82
|
+
if (typeof i.spec === "string") install.spec = i.spec;
|
|
83
|
+
return install;
|
|
84
|
+
}
|
|
85
|
+
function configHome() {
|
|
86
|
+
return process.env.REMOTE_CLI_CONFIG_HOME ?? homedir();
|
|
87
|
+
}
|
|
88
|
+
function resolveConfigPath() {
|
|
89
|
+
return join(configHome(), ".config", "sentropic", "remote-cli", "config.json");
|
|
90
|
+
}
|
|
91
|
+
function normalizeRemoteUrl(rawUrl) {
|
|
92
|
+
const trimmed = rawUrl.trim();
|
|
93
|
+
if (!trimmed) {
|
|
94
|
+
throw new Error("Remote URL cannot be empty.");
|
|
95
|
+
}
|
|
96
|
+
const parsed = new URL(trimmed);
|
|
97
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Unsupported remote URL protocol "${parsed.protocol}". Use http:// or https://.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return trimmed.replace(/\/+$/, "");
|
|
103
|
+
}
|
|
104
|
+
function readRemoteConfig() {
|
|
105
|
+
try {
|
|
106
|
+
const raw = readFileSync(resolveConfigPath(), "utf8");
|
|
107
|
+
const parsed = JSON.parse(raw);
|
|
108
|
+
if (parsed && typeof parsed === "object") {
|
|
109
|
+
const config = {};
|
|
110
|
+
if (typeof parsed.defaultRemote === "string")
|
|
111
|
+
config.defaultRemote = parsed.defaultRemote;
|
|
112
|
+
if (typeof parsed.token === "string") config.token = parsed.token;
|
|
113
|
+
if (typeof parsed.defaultTarget === "string")
|
|
114
|
+
config.defaultTarget = parsed.defaultTarget;
|
|
115
|
+
if (Array.isArray(parsed.defaultTools) && parsed.defaultTools.every((t) => typeof t === "string")) {
|
|
116
|
+
config.defaultTools = parsed.defaultTools;
|
|
117
|
+
}
|
|
118
|
+
const tunnel = parseTunnel(parsed.tunnel);
|
|
119
|
+
if (tunnel) config.tunnel = tunnel;
|
|
120
|
+
if (parsed.layout && typeof parsed.layout === "object")
|
|
121
|
+
config.layout = parsed.layout;
|
|
122
|
+
const plugins = parsePlugins(parsed.plugins);
|
|
123
|
+
if (plugins) config.plugins = plugins;
|
|
124
|
+
const h2a = parseH2a(parsed.h2a);
|
|
125
|
+
if (h2a) config.h2a = h2a;
|
|
126
|
+
const llmMesh = parseLlmMeshRuntime(parsed.llmMesh);
|
|
127
|
+
if (llmMesh) config.llmMesh = llmMesh;
|
|
128
|
+
if (typeof parsed.maxConcurrent === "number" && Number.isFinite(parsed.maxConcurrent) && parsed.maxConcurrent > 0) {
|
|
129
|
+
config.maxConcurrent = Math.trunc(parsed.maxConcurrent);
|
|
130
|
+
}
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
return {};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code === "ENOENT") return {};
|
|
136
|
+
throw new Error(`failed to read remote config: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function writeRemoteConfig(config) {
|
|
140
|
+
const path = resolveConfigPath();
|
|
141
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
142
|
+
writeFileSync(path, JSON.stringify(config, null, 2), "utf8");
|
|
143
|
+
}
|
|
144
|
+
function getDefaultRemote() {
|
|
145
|
+
const config = readRemoteConfig();
|
|
146
|
+
return config.defaultRemote;
|
|
147
|
+
}
|
|
148
|
+
function setDefaultRemote(rawUrl) {
|
|
149
|
+
const defaultRemote = normalizeRemoteUrl(rawUrl);
|
|
150
|
+
writeRemoteConfig({ ...readRemoteConfig(), defaultRemote });
|
|
151
|
+
return defaultRemote;
|
|
152
|
+
}
|
|
153
|
+
function clearDefaultRemote() {
|
|
154
|
+
const { defaultRemote: _drop, ...rest } = readRemoteConfig();
|
|
155
|
+
writeRemoteConfig(rest);
|
|
156
|
+
}
|
|
157
|
+
function getDefaultTarget() {
|
|
158
|
+
const configured = readRemoteConfig().defaultTarget;
|
|
159
|
+
return configured ?? DEFAULT_SESSION_TARGET;
|
|
160
|
+
}
|
|
161
|
+
function setDefaultTarget(value) {
|
|
162
|
+
writeRemoteConfig({ ...readRemoteConfig(), defaultTarget: value });
|
|
163
|
+
}
|
|
164
|
+
function getDefaultTools() {
|
|
165
|
+
return readRemoteConfig().defaultTools ?? [];
|
|
166
|
+
}
|
|
167
|
+
function setDefaultTools(tools) {
|
|
168
|
+
writeRemoteConfig({ ...readRemoteConfig(), defaultTools: tools });
|
|
169
|
+
}
|
|
170
|
+
function getPlugins() {
|
|
171
|
+
return readRemoteConfig().plugins ?? [];
|
|
172
|
+
}
|
|
173
|
+
function setPlugins(plugins) {
|
|
174
|
+
writeRemoteConfig({ ...readRemoteConfig(), plugins });
|
|
175
|
+
}
|
|
176
|
+
function getH2aConfig() {
|
|
177
|
+
const raw = readRemoteConfig().h2a ?? {};
|
|
178
|
+
return {
|
|
179
|
+
enabled: raw.enabled ?? false,
|
|
180
|
+
command: raw.command ?? DEFAULT_H2A_COMMAND
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function getLlmMeshRuntimeConfig() {
|
|
184
|
+
const raw = readRemoteConfig().llmMesh ?? {};
|
|
185
|
+
return {
|
|
186
|
+
enabled: raw.enabled ?? false
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function setLlmMeshRuntimeConfig(llmMesh) {
|
|
190
|
+
writeRemoteConfig({ ...readRemoteConfig(), llmMesh });
|
|
191
|
+
}
|
|
192
|
+
function getMaxConcurrent() {
|
|
193
|
+
const env = process.env.REMOTE_MAX_CONCURRENT;
|
|
194
|
+
if (env !== void 0) {
|
|
195
|
+
const n = Number.parseInt(env, 10);
|
|
196
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
197
|
+
}
|
|
198
|
+
return readRemoteConfig().maxConcurrent;
|
|
199
|
+
}
|
|
200
|
+
var DEFAULT_JOB_MAX_AGE_HOURS = 24;
|
|
201
|
+
function getJobMaxAgeHours() {
|
|
202
|
+
const env = process.env.REMOTE_JOB_MAX_AGE_HOURS;
|
|
203
|
+
if (env !== void 0) {
|
|
204
|
+
const n = Number.parseFloat(env);
|
|
205
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
206
|
+
}
|
|
207
|
+
return DEFAULT_JOB_MAX_AGE_HOURS;
|
|
208
|
+
}
|
|
209
|
+
function getTunnel() {
|
|
210
|
+
return readRemoteConfig().tunnel;
|
|
211
|
+
}
|
|
212
|
+
function setTunnel(tunnel) {
|
|
213
|
+
writeRemoteConfig({ ...readRemoteConfig(), tunnel });
|
|
214
|
+
}
|
|
215
|
+
function getLayoutConfig() {
|
|
216
|
+
const raw = readRemoteConfig().layout ?? {};
|
|
217
|
+
return {
|
|
218
|
+
maxAgeHours: typeof raw.maxAgeHours === "number" ? raw.maxAgeHours : DEFAULT_LAYOUT.maxAgeHours,
|
|
219
|
+
maxPerWindow: typeof raw.maxPerWindow === "number" ? raw.maxPerWindow : DEFAULT_LAYOUT.maxPerWindow,
|
|
220
|
+
sharedWindows: typeof raw.sharedWindows === "number" ? raw.sharedWindows : DEFAULT_LAYOUT.sharedWindows,
|
|
221
|
+
multiSession: raw.multiSession && typeof raw.multiSession === "object" ? raw.multiSession : DEFAULT_LAYOUT.multiSession,
|
|
222
|
+
groups: Array.isArray(raw.groups) ? raw.groups : DEFAULT_LAYOUT.groups
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function getToken() {
|
|
226
|
+
return process.env.REMOTE_TOKEN ?? readRemoteConfig().token;
|
|
227
|
+
}
|
|
228
|
+
function setToken(value) {
|
|
229
|
+
writeRemoteConfig({ ...readRemoteConfig(), token: value });
|
|
230
|
+
}
|
|
231
|
+
function authHeaders() {
|
|
232
|
+
const token = getToken();
|
|
233
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
DEFAULT_SESSION_TARGET,
|
|
238
|
+
resolveConfigPath,
|
|
239
|
+
getDefaultRemote,
|
|
240
|
+
setDefaultRemote,
|
|
241
|
+
clearDefaultRemote,
|
|
242
|
+
getDefaultTarget,
|
|
243
|
+
setDefaultTarget,
|
|
244
|
+
getDefaultTools,
|
|
245
|
+
setDefaultTools,
|
|
246
|
+
getPlugins,
|
|
247
|
+
setPlugins,
|
|
248
|
+
getH2aConfig,
|
|
249
|
+
getLlmMeshRuntimeConfig,
|
|
250
|
+
setLlmMeshRuntimeConfig,
|
|
251
|
+
getMaxConcurrent,
|
|
252
|
+
getJobMaxAgeHours,
|
|
253
|
+
getTunnel,
|
|
254
|
+
setTunnel,
|
|
255
|
+
getLayoutConfig,
|
|
256
|
+
setToken,
|
|
257
|
+
authHeaders
|
|
258
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authHeaders
|
|
3
|
+
} from "./chunk-E3GXAKJ3.js";
|
|
4
|
+
|
|
5
|
+
// src/workspace.ts
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
var MARKER_DIR = ".remote";
|
|
9
|
+
var MARKER_FILE = "workspace.json";
|
|
10
|
+
var BASE_FILE = "base.tgz";
|
|
11
|
+
var LINEAGES_DIR = ".remote/lineages";
|
|
12
|
+
function baseSnapshotPath(cwd) {
|
|
13
|
+
return join(cwd, MARKER_DIR, BASE_FILE);
|
|
14
|
+
}
|
|
15
|
+
function readBaseSnapshot(cwd) {
|
|
16
|
+
const path = baseSnapshotPath(cwd);
|
|
17
|
+
if (!existsSync(path)) return null;
|
|
18
|
+
return readFileSync(path);
|
|
19
|
+
}
|
|
20
|
+
function writeBaseSnapshot(cwd, archive) {
|
|
21
|
+
const path = baseSnapshotPath(cwd);
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
writeFileSync(path, archive);
|
|
24
|
+
}
|
|
25
|
+
function markerPath(cwd) {
|
|
26
|
+
return join(cwd, MARKER_DIR, MARKER_FILE);
|
|
27
|
+
}
|
|
28
|
+
function readWorkspaceMarker(cwd) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(markerPath(cwd), "utf8");
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (parsed && typeof parsed.remote === "string" && typeof parsed.workspaceId === "string") {
|
|
33
|
+
return {
|
|
34
|
+
remote: parsed.remote,
|
|
35
|
+
workspaceId: parsed.workspaceId,
|
|
36
|
+
...typeof parsed.path === "string" ? { path: parsed.path } : {},
|
|
37
|
+
...typeof parsed.home === "string" ? { home: parsed.home } : {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return void 0;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code === "ENOENT") return void 0;
|
|
43
|
+
throw new Error(
|
|
44
|
+
`failed to read ${MARKER_DIR}/${MARKER_FILE}: ${error.message}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function writeWorkspaceMarker(cwd, marker) {
|
|
49
|
+
const path = markerPath(cwd);
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
writeFileSync(path, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
52
|
+
}
|
|
53
|
+
function lineageFilePath(cwd, workspaceId) {
|
|
54
|
+
const safe = workspaceId.replace(/[^a-zA-Z0-9-]/g, "_");
|
|
55
|
+
return join(cwd, LINEAGES_DIR, `${safe}.json`);
|
|
56
|
+
}
|
|
57
|
+
function readLineageRecord(cwd, workspaceId) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = readFileSync(lineageFilePath(cwd, workspaceId), "utf8");
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (parsed && typeof parsed.lineageId === "string") {
|
|
62
|
+
return {
|
|
63
|
+
lineageId: parsed.lineageId,
|
|
64
|
+
...typeof parsed.remoteSessionId === "string" ? { remoteSessionId: parsed.remoteSessionId } : {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err.code === "ENOENT") return void 0;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function writeLineageRecord(cwd, workspaceId, record) {
|
|
74
|
+
const path = lineageFilePath(cwd, workspaceId);
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
writeFileSync(path, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
77
|
+
}
|
|
78
|
+
function joinUrl(base, path) {
|
|
79
|
+
return `${base.replace(/\/$/, "")}${path}`;
|
|
80
|
+
}
|
|
81
|
+
async function createWorkspace(baseUrl, body = {}, fetchImpl = fetch) {
|
|
82
|
+
const response = await fetchImpl(joinUrl(baseUrl, "/workspaces"), {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "content-type": "application/json", ...authHeaders() },
|
|
85
|
+
body: JSON.stringify(body)
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`createWorkspace: ${response.status} ${response.statusText}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const json = await response.json();
|
|
93
|
+
return json.workspace;
|
|
94
|
+
}
|
|
95
|
+
async function listWorkspaces(baseUrl, fetchImpl = fetch) {
|
|
96
|
+
const response = await fetchImpl(joinUrl(baseUrl, "/workspaces"), {
|
|
97
|
+
headers: { ...authHeaders() }
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(`listWorkspaces: ${response.status} ${response.statusText}`);
|
|
101
|
+
}
|
|
102
|
+
const json = await response.json();
|
|
103
|
+
return json.workspaces;
|
|
104
|
+
}
|
|
105
|
+
async function deleteWorkspace(baseUrl, workspaceId, fetchImpl = fetch) {
|
|
106
|
+
const response = await fetchImpl(
|
|
107
|
+
joinUrl(baseUrl, `/workspaces/${workspaceId}`),
|
|
108
|
+
{ method: "DELETE", headers: { ...authHeaders() } }
|
|
109
|
+
);
|
|
110
|
+
if (response.status === 404) return false;
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`deleteWorkspace: ${response.status} ${response.statusText}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
async function downloadWorkspaceExport(baseUrl, sessionId, fetchImpl = fetch) {
|
|
119
|
+
const response = await fetchImpl(
|
|
120
|
+
joinUrl(baseUrl, `/sessions/${sessionId}/workspace/export`),
|
|
121
|
+
{ headers: { ...authHeaders() } }
|
|
122
|
+
);
|
|
123
|
+
if (response.status === 404) return null;
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`downloadWorkspaceExport: ${response.status} ${response.statusText}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return Buffer.from(await response.arrayBuffer());
|
|
130
|
+
}
|
|
131
|
+
async function acquireWorkspaceLock(baseUrl, workspaceId, holder, ttlSeconds = 300, fetchImpl = fetch) {
|
|
132
|
+
const response = await fetchImpl(
|
|
133
|
+
joinUrl(baseUrl, `/workspaces/${workspaceId}/lock`),
|
|
134
|
+
{
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "content-type": "application/json", ...authHeaders() },
|
|
137
|
+
body: JSON.stringify({ holder, ttlSeconds })
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
if (response.ok) return { acquired: true };
|
|
141
|
+
if (response.status === 409) {
|
|
142
|
+
const body = await response.json();
|
|
143
|
+
return {
|
|
144
|
+
acquired: false,
|
|
145
|
+
holder: body.holder ?? "unknown",
|
|
146
|
+
since: body.acquiredAt ?? "unknown"
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
throw new Error(
|
|
150
|
+
`acquireWorkspaceLock: ${response.status} ${response.statusText}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
async function releaseWorkspaceLock(baseUrl, workspaceId, fetchImpl = fetch) {
|
|
154
|
+
await fetchImpl(joinUrl(baseUrl, `/workspaces/${workspaceId}/lock`), {
|
|
155
|
+
method: "DELETE",
|
|
156
|
+
headers: { ...authHeaders() }
|
|
157
|
+
}).catch(() => {
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function lockHolderId() {
|
|
161
|
+
const user = process.env.USER ?? process.env.USERNAME ?? "user";
|
|
162
|
+
const host = process.env.HOSTNAME ?? process.env.HOST ?? "local";
|
|
163
|
+
return `${user}@${host}`;
|
|
164
|
+
}
|
|
165
|
+
async function requestWorkspaceGc(baseUrl, body, fetchImpl = fetch) {
|
|
166
|
+
const response = await fetchImpl(joinUrl(baseUrl, "/workspaces/gc"), {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "content-type": "application/json", ...authHeaders() },
|
|
169
|
+
body: JSON.stringify(body)
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const detail = await response.json().then((json) => json.message ?? "").catch(() => "");
|
|
173
|
+
throw new Error(
|
|
174
|
+
`workspace gc: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return await response.json();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
baseSnapshotPath,
|
|
182
|
+
readBaseSnapshot,
|
|
183
|
+
writeBaseSnapshot,
|
|
184
|
+
markerPath,
|
|
185
|
+
readWorkspaceMarker,
|
|
186
|
+
writeWorkspaceMarker,
|
|
187
|
+
readLineageRecord,
|
|
188
|
+
writeLineageRecord,
|
|
189
|
+
createWorkspace,
|
|
190
|
+
listWorkspaces,
|
|
191
|
+
deleteWorkspace,
|
|
192
|
+
downloadWorkspaceExport,
|
|
193
|
+
acquireWorkspaceLock,
|
|
194
|
+
releaseWorkspaceLock,
|
|
195
|
+
lockHolderId,
|
|
196
|
+
requestWorkspaceGc
|
|
197
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/sync-status.ts
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
function syncStatusPath(sessionId) {
|
|
6
|
+
return join(homedir(), ".remote", "sync-status", `${sessionId}.json`);
|
|
7
|
+
}
|
|
8
|
+
function readSyncStatus(sessionId) {
|
|
9
|
+
const p = syncStatusPath(sessionId);
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeSyncStatus(sessionId, status) {
|
|
17
|
+
const p = syncStatusPath(sessionId);
|
|
18
|
+
mkdirSync(join(homedir(), ".remote", "sync-status"), { recursive: true });
|
|
19
|
+
const tmp = `${p}.tmp`;
|
|
20
|
+
writeFileSync(tmp, JSON.stringify(status, null, 2), "utf8");
|
|
21
|
+
renameSync(tmp, p);
|
|
22
|
+
}
|
|
23
|
+
function emptyMetrics() {
|
|
24
|
+
return {
|
|
25
|
+
pendingBytes: 0,
|
|
26
|
+
pendingCount: 0,
|
|
27
|
+
oldestPendingAge: 0,
|
|
28
|
+
lastAckedAt: null,
|
|
29
|
+
estimatedCatchup: 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function mergedState(classes) {
|
|
33
|
+
if (classes.includes("blocked")) return "blocked";
|
|
34
|
+
if (classes.includes("degraded")) return "degraded";
|
|
35
|
+
if (classes.includes("pending")) return "pending";
|
|
36
|
+
return "synced";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
syncStatusPath,
|
|
41
|
+
readSyncStatus,
|
|
42
|
+
writeSyncStatus,
|
|
43
|
+
emptyMetrics,
|
|
44
|
+
mergedState
|
|
45
|
+
};
|