@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.
Files changed (2) hide show
  1. package/dist/ocm.js +147 -30
  2. 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 mirrorUp(repoId, body, opts) {
109
- const params = new URLSearchParams;
110
- if (opts.force)
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
- params.set("create", "1");
114
- params.set("name", opts.create.name);
141
+ body.create = true;
142
+ body.name = opts.create.name;
115
143
  if (opts.create.originUrl)
116
- params.set("originUrl", opts.create.originUrl);
144
+ body.originUrl = opts.create.originUrl;
117
145
  if (opts.create.branch)
118
- params.set("branch", opts.create.branch);
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
- ...this.headers(),
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 new Error(`mirror ${res.status}: ${await res.text()}`);
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 new Error(`mirror ${res.status}: ${await res.text()}`);
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, { stdio: ["pipe", "pipe", "pipe"] });
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 body = Readable.toWeb(child.stdout);
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 [result] = await Promise.all([
253
- opts.api.mirrorUp(repoId, body, {
254
- force: opts.force,
255
- create: opts.create
256
- }),
257
- tarExit
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.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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-manager/ocm-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "OpenCode Manager CLI: attach a local OpenCode TUI to a Manager-hosted repo.",
5
5
  "license": "MIT",
6
6
  "repository": {