@opencode-manager/ocm-cli 0.1.1 → 0.1.2
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/ocm.js +147 -30
- package/package.json +1 -1
package/dist/ocm.js
CHANGED
|
@@ -95,6 +95,35 @@ function deleteToken(account) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// src/manager-api.ts
|
|
98
|
+
class ManagerApiError extends Error {
|
|
99
|
+
status;
|
|
100
|
+
code;
|
|
101
|
+
operation;
|
|
102
|
+
constructor(message, status, code, operation) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.status = status;
|
|
105
|
+
this.code = code;
|
|
106
|
+
this.operation = operation;
|
|
107
|
+
this.name = "ManagerApiError";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function formatErrorResponse(res, operation) {
|
|
111
|
+
const text = await res.text().catch(() => "");
|
|
112
|
+
let code = null;
|
|
113
|
+
let detail = text;
|
|
114
|
+
if (text) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(text);
|
|
117
|
+
const errField = typeof parsed.error === "string" ? parsed.error : null;
|
|
118
|
+
const msgField = typeof parsed.message === "string" ? parsed.message : null;
|
|
119
|
+
code = errField;
|
|
120
|
+
detail = msgField ?? errField ?? text;
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
const message = detail ? `${operation} failed (${res.status}): ${detail}` : `${operation} failed (${res.status})`;
|
|
124
|
+
return new ManagerApiError(message, res.status, code, operation);
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
class ManagerApi {
|
|
99
128
|
baseUrl;
|
|
100
129
|
token;
|
|
@@ -105,39 +134,58 @@ class ManagerApi {
|
|
|
105
134
|
headers(extra = {}) {
|
|
106
135
|
return { Authorization: `Bearer ${this.token}`, ...extra };
|
|
107
136
|
}
|
|
108
|
-
async
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
params.set("force", "1");
|
|
137
|
+
async mirrorBegin(repoId, opts) {
|
|
138
|
+
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror/begin`;
|
|
139
|
+
const body = { force: opts.force === true };
|
|
112
140
|
if (opts.create) {
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
body.create = true;
|
|
142
|
+
body.name = opts.create.name;
|
|
115
143
|
if (opts.create.originUrl)
|
|
116
|
-
|
|
144
|
+
body.originUrl = opts.create.originUrl;
|
|
117
145
|
if (opts.create.branch)
|
|
118
|
-
|
|
146
|
+
body.branch = opts.create.branch;
|
|
119
147
|
}
|
|
120
|
-
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
121
|
-
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror${qs}`;
|
|
122
148
|
const res = await fetch(url, {
|
|
123
149
|
method: "POST",
|
|
124
|
-
headers: {
|
|
125
|
-
|
|
126
|
-
"Content-Type": "application/x-tar"
|
|
127
|
-
},
|
|
128
|
-
body,
|
|
129
|
-
duplex: "half"
|
|
150
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
151
|
+
body: JSON.stringify(body)
|
|
130
152
|
});
|
|
131
153
|
if (!res.ok)
|
|
132
|
-
throw
|
|
154
|
+
throw await formatErrorResponse(res, "mirror begin");
|
|
133
155
|
return await res.json();
|
|
134
156
|
}
|
|
157
|
+
async mirrorUploadPart(repoId, uploadId, index, chunk) {
|
|
158
|
+
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror/parts/${uploadId}/${index}`;
|
|
159
|
+
const ab = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
|
|
160
|
+
const res = await fetch(url, {
|
|
161
|
+
method: "PUT",
|
|
162
|
+
headers: { ...this.headers(), "Content-Type": "application/octet-stream" },
|
|
163
|
+
body: ab
|
|
164
|
+
});
|
|
165
|
+
if (!res.ok)
|
|
166
|
+
throw await formatErrorResponse(res, `mirror part ${index}`);
|
|
167
|
+
}
|
|
168
|
+
async mirrorCommit(repoId, uploadId, totalParts) {
|
|
169
|
+
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror/commit`;
|
|
170
|
+
const res = await fetch(url, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
173
|
+
body: JSON.stringify({ uploadId, totalParts })
|
|
174
|
+
});
|
|
175
|
+
if (!res.ok)
|
|
176
|
+
throw await formatErrorResponse(res, "mirror commit");
|
|
177
|
+
return await res.json();
|
|
178
|
+
}
|
|
179
|
+
async mirrorAbort(repoId, uploadId) {
|
|
180
|
+
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror/uploads/${uploadId}`;
|
|
181
|
+
await fetch(url, { method: "DELETE", headers: this.headers() }).catch(() => {});
|
|
182
|
+
}
|
|
135
183
|
async mirrorDown(repoId) {
|
|
136
184
|
const res = await fetch(`${this.baseUrl}/api/internal/repos/${repoId}/mirror`, {
|
|
137
185
|
headers: this.headers()
|
|
138
186
|
});
|
|
139
187
|
if (!res.ok)
|
|
140
|
-
throw
|
|
188
|
+
throw await formatErrorResponse(res, "mirror download");
|
|
141
189
|
return res.body;
|
|
142
190
|
}
|
|
143
191
|
}
|
|
@@ -191,7 +239,9 @@ function urlsEqual(a, b) {
|
|
|
191
239
|
}
|
|
192
240
|
|
|
193
241
|
// src/mirror.ts
|
|
194
|
-
var HARDCODED_EXCLUDES = ["node_modules", "dist", ".next", ".venv", "__pycache__", ".turbo"];
|
|
242
|
+
var HARDCODED_EXCLUDES = ["node_modules", "dist", ".next", ".venv", "__pycache__", ".turbo", ".DS_Store", "._*"];
|
|
243
|
+
var PART_RETRIES = 3;
|
|
244
|
+
var PART_BACKOFF_MS = [500, 2000, 8000];
|
|
195
245
|
function getGitignoreExclusions(repoRoot) {
|
|
196
246
|
const res = spawnSync3("git", ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory"], {
|
|
197
247
|
cwd: repoRoot,
|
|
@@ -219,7 +269,68 @@ function prepareMirror(cwd, remotes) {
|
|
|
219
269
|
const matched = remotes.filter((r) => urlsEqual(localOrigin, r.originUrl));
|
|
220
270
|
return { repoRoot, localOrigin, matched };
|
|
221
271
|
}
|
|
272
|
+
function delay(ms) {
|
|
273
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
274
|
+
}
|
|
275
|
+
function isRetryablePartError(err) {
|
|
276
|
+
if (err instanceof ManagerApiError) {
|
|
277
|
+
return err.status >= 500 || err.status === 408 || err.status === 429;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
async function uploadPartWithRetry(api, repoId, uploadId, index, chunk) {
|
|
282
|
+
let lastError;
|
|
283
|
+
for (let attempt = 0;attempt < PART_RETRIES; attempt++) {
|
|
284
|
+
try {
|
|
285
|
+
await api.mirrorUploadPart(repoId, uploadId, index, chunk);
|
|
286
|
+
return;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
lastError = err;
|
|
289
|
+
if (!isRetryablePartError(err))
|
|
290
|
+
break;
|
|
291
|
+
if (attempt < PART_RETRIES - 1) {
|
|
292
|
+
await delay(PART_BACKOFF_MS[attempt]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
throw lastError instanceof Error ? lastError : new Error(`part ${index} failed: ${String(lastError)}`);
|
|
297
|
+
}
|
|
298
|
+
function createPartFlusher(api, repoId, uploadId, chunkSize) {
|
|
299
|
+
let acc = [];
|
|
300
|
+
let accLen = 0;
|
|
301
|
+
let index = 0;
|
|
302
|
+
const flush = async () => {
|
|
303
|
+
if (accLen === 0)
|
|
304
|
+
return;
|
|
305
|
+
const buf = Buffer.concat(acc, accLen);
|
|
306
|
+
acc = [];
|
|
307
|
+
accLen = 0;
|
|
308
|
+
await uploadPartWithRetry(api, repoId, uploadId, index, buf);
|
|
309
|
+
index += 1;
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
async push(buf) {
|
|
313
|
+
let offset = 0;
|
|
314
|
+
while (offset < buf.length) {
|
|
315
|
+
const room = chunkSize - accLen;
|
|
316
|
+
const take = Math.min(room, buf.length - offset);
|
|
317
|
+
acc.push(buf.subarray(offset, offset + take));
|
|
318
|
+
accLen += take;
|
|
319
|
+
offset += take;
|
|
320
|
+
if (accLen >= chunkSize) {
|
|
321
|
+
await flush();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
async finish() {
|
|
326
|
+
await flush();
|
|
327
|
+
return index;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
222
331
|
async function mirrorUp(plan, opts) {
|
|
332
|
+
const repoId = opts.create ? 0 : plan.matched[0].repoId;
|
|
333
|
+
const begin = await opts.api.mirrorBegin(repoId, { force: opts.force, create: opts.create });
|
|
223
334
|
const tarArgs = ["-c", "-C", plan.repoRoot];
|
|
224
335
|
for (const dir of HARDCODED_EXCLUDES)
|
|
225
336
|
tarArgs.push("--exclude", dir);
|
|
@@ -232,7 +343,10 @@ async function mirrorUp(plan, opts) {
|
|
|
232
343
|
tarArgs.push("--exclude-from", excludeFile);
|
|
233
344
|
}
|
|
234
345
|
tarArgs.push(".");
|
|
235
|
-
const child = spawn("tar", tarArgs, {
|
|
346
|
+
const child = spawn("tar", tarArgs, {
|
|
347
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
348
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
349
|
+
});
|
|
236
350
|
const stderrChunks = [];
|
|
237
351
|
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
238
352
|
const tarExit = new Promise((resolve, reject) => {
|
|
@@ -246,17 +360,20 @@ async function mirrorUp(plan, opts) {
|
|
|
246
360
|
});
|
|
247
361
|
child.on("error", reject);
|
|
248
362
|
});
|
|
249
|
-
const
|
|
250
|
-
const repoId = opts.create ? 0 : plan.matched[0].repoId;
|
|
363
|
+
const flusher = createPartFlusher(opts.api, begin.repoId, begin.uploadId, begin.chunkSize);
|
|
251
364
|
try {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
]);
|
|
365
|
+
for await (const chunk of child.stdout) {
|
|
366
|
+
await flusher.push(chunk);
|
|
367
|
+
}
|
|
368
|
+
await tarExit;
|
|
369
|
+
const totalParts = await flusher.finish();
|
|
370
|
+
const result = await opts.api.mirrorCommit(begin.repoId, begin.uploadId, totalParts);
|
|
259
371
|
return result;
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (!child.killed)
|
|
374
|
+
child.kill("SIGKILL");
|
|
375
|
+
await opts.api.mirrorAbort(begin.repoId, begin.uploadId);
|
|
376
|
+
throw err;
|
|
260
377
|
} finally {
|
|
261
378
|
if (excludeFile) {
|
|
262
379
|
await fsp.rm(excludeFile, { force: true }).catch(() => {});
|
|
@@ -347,7 +464,7 @@ function toTarget(last) {
|
|
|
347
464
|
// package.json
|
|
348
465
|
var package_default = {
|
|
349
466
|
name: "@opencode-manager/ocm-cli",
|
|
350
|
-
version: "0.1.
|
|
467
|
+
version: "0.1.2",
|
|
351
468
|
description: "OpenCode Manager CLI: attach a local OpenCode TUI to a Manager-hosted repo.",
|
|
352
469
|
license: "MIT",
|
|
353
470
|
repository: {
|