@opencode-manager/ocm-cli 0.1.2 → 0.1.4
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/README.md +1 -1
- package/dist/ocm.js +104 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,6 @@ export default [ocm]
|
|
|
69
69
|
## Requirements
|
|
70
70
|
|
|
71
71
|
- `opencode` available on `PATH`
|
|
72
|
-
- `git` and `tar` available on `PATH`
|
|
72
|
+
- `git` and `tar` (with gzip support, i.e. the `-z` flag) available on `PATH`
|
|
73
73
|
- macOS `security` CLI for Keychain-backed token storage
|
|
74
74
|
- An OpenCode Manager URL and bearer token
|
package/dist/ocm.js
CHANGED
|
@@ -165,12 +165,12 @@ class ManagerApi {
|
|
|
165
165
|
if (!res.ok)
|
|
166
166
|
throw await formatErrorResponse(res, `mirror part ${index}`);
|
|
167
167
|
}
|
|
168
|
-
async mirrorCommit(repoId, uploadId, totalParts) {
|
|
168
|
+
async mirrorCommit(repoId, uploadId, totalParts, gzip) {
|
|
169
169
|
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror/commit`;
|
|
170
170
|
const res = await fetch(url, {
|
|
171
171
|
method: "POST",
|
|
172
172
|
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
173
|
-
body: JSON.stringify({ uploadId, totalParts })
|
|
173
|
+
body: JSON.stringify({ uploadId, totalParts, gzip })
|
|
174
174
|
});
|
|
175
175
|
if (!res.ok)
|
|
176
176
|
throw await formatErrorResponse(res, "mirror commit");
|
|
@@ -180,8 +180,9 @@ class ManagerApi {
|
|
|
180
180
|
const url = `${this.baseUrl}/api/internal/repos/${repoId}/mirror/uploads/${uploadId}`;
|
|
181
181
|
await fetch(url, { method: "DELETE", headers: this.headers() }).catch(() => {});
|
|
182
182
|
}
|
|
183
|
-
async mirrorDown(repoId) {
|
|
184
|
-
const
|
|
183
|
+
async mirrorDown(repoId, gzip) {
|
|
184
|
+
const query = gzip ? "?compress=gzip" : "";
|
|
185
|
+
const res = await fetch(`${this.baseUrl}/api/internal/repos/${repoId}/mirror${query}`, {
|
|
185
186
|
headers: this.headers()
|
|
186
187
|
});
|
|
187
188
|
if (!res.ok)
|
|
@@ -195,7 +196,7 @@ import { spawnSync as spawnSync3, spawn } from "child_process";
|
|
|
195
196
|
import { existsSync as existsSync2 } from "fs";
|
|
196
197
|
import * as fsp from "fs/promises";
|
|
197
198
|
import { Readable } from "stream";
|
|
198
|
-
import { join as join2 } from "path";
|
|
199
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
199
200
|
import { tmpdir } from "os";
|
|
200
201
|
|
|
201
202
|
// src/local-repo.ts
|
|
@@ -242,6 +243,7 @@ function urlsEqual(a, b) {
|
|
|
242
243
|
var HARDCODED_EXCLUDES = ["node_modules", "dist", ".next", ".venv", "__pycache__", ".turbo", ".DS_Store", "._*"];
|
|
243
244
|
var PART_RETRIES = 3;
|
|
244
245
|
var PART_BACKOFF_MS = [500, 2000, 8000];
|
|
246
|
+
var MIRROR_GZIP = true;
|
|
245
247
|
function getGitignoreExclusions(repoRoot) {
|
|
246
248
|
const res = spawnSync3("git", ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory"], {
|
|
247
249
|
cwd: repoRoot,
|
|
@@ -252,6 +254,21 @@ function getGitignoreExclusions(repoRoot) {
|
|
|
252
254
|
return (res.stdout ?? "").split(`
|
|
253
255
|
`).filter((line) => line.length > 0);
|
|
254
256
|
}
|
|
257
|
+
async function carryOverIgnored(fromDir, toDir) {
|
|
258
|
+
if (!existsSync2(fromDir))
|
|
259
|
+
return;
|
|
260
|
+
for (const rel of getGitignoreExclusions(fromDir)) {
|
|
261
|
+
const clean = rel.replace(/\/+$/, "");
|
|
262
|
+
if (!clean)
|
|
263
|
+
continue;
|
|
264
|
+
const src = join2(fromDir, clean);
|
|
265
|
+
const dest = join2(toDir, clean);
|
|
266
|
+
if (!existsSync2(src) || existsSync2(dest))
|
|
267
|
+
continue;
|
|
268
|
+
await fsp.mkdir(dirname2(dest), { recursive: true });
|
|
269
|
+
await fsp.rename(src, dest).catch(() => {});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
255
272
|
|
|
256
273
|
class MirrorAbort extends Error {
|
|
257
274
|
constructor(message) {
|
|
@@ -331,7 +348,10 @@ function createPartFlusher(api, repoId, uploadId, chunkSize) {
|
|
|
331
348
|
async function mirrorUp(plan, opts) {
|
|
332
349
|
const repoId = opts.create ? 0 : plan.matched[0].repoId;
|
|
333
350
|
const begin = await opts.api.mirrorBegin(repoId, { force: opts.force, create: opts.create });
|
|
351
|
+
let bytesSent = 0;
|
|
334
352
|
const tarArgs = ["-c", "-C", plan.repoRoot];
|
|
353
|
+
if (MIRROR_GZIP)
|
|
354
|
+
tarArgs.unshift("-z");
|
|
335
355
|
for (const dir of HARDCODED_EXCLUDES)
|
|
336
356
|
tarArgs.push("--exclude", dir);
|
|
337
357
|
const ignoredPaths = getGitignoreExclusions(plan.repoRoot);
|
|
@@ -364,10 +384,13 @@ async function mirrorUp(plan, opts) {
|
|
|
364
384
|
try {
|
|
365
385
|
for await (const chunk of child.stdout) {
|
|
366
386
|
await flusher.push(chunk);
|
|
387
|
+
bytesSent += chunk.length;
|
|
388
|
+
opts.onProgress?.({ bytesSent });
|
|
367
389
|
}
|
|
368
390
|
await tarExit;
|
|
369
391
|
const totalParts = await flusher.finish();
|
|
370
|
-
|
|
392
|
+
opts.onProgress?.({ bytesSent });
|
|
393
|
+
const result = await opts.api.mirrorCommit(begin.repoId, begin.uploadId, totalParts, MIRROR_GZIP);
|
|
371
394
|
return result;
|
|
372
395
|
} catch (err) {
|
|
373
396
|
if (!child.killed)
|
|
@@ -387,8 +410,11 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
387
410
|
const staging = `${repoRoot}.ocm-recv-${Date.now()}`;
|
|
388
411
|
await fsp.mkdir(staging, { recursive: true });
|
|
389
412
|
try {
|
|
390
|
-
const tarball = await api.mirrorDown(repoId);
|
|
391
|
-
const
|
|
413
|
+
const tarball = await api.mirrorDown(repoId, MIRROR_GZIP);
|
|
414
|
+
const tarArgs = ["-x", "-f", "-", "-C", staging];
|
|
415
|
+
if (MIRROR_GZIP)
|
|
416
|
+
tarArgs.unshift("-z");
|
|
417
|
+
const child = spawn("tar", tarArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
392
418
|
const stderrChunks = [];
|
|
393
419
|
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
394
420
|
const tarDone = new Promise((resolve, reject) => {
|
|
@@ -403,6 +429,11 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
403
429
|
child.on("error", reject);
|
|
404
430
|
});
|
|
405
431
|
const stdinWritable = Readable.fromWeb(tarball);
|
|
432
|
+
let received = 0;
|
|
433
|
+
stdinWritable.on("data", (chunk) => {
|
|
434
|
+
received += chunk.length;
|
|
435
|
+
opts.onProgress?.(received);
|
|
436
|
+
});
|
|
406
437
|
stdinWritable.pipe(child.stdin);
|
|
407
438
|
await tarDone;
|
|
408
439
|
const backupDir = `${repoRoot}.ocm-backup-${Date.now()}`;
|
|
@@ -418,6 +449,7 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
418
449
|
for (const entry of stagingEntries) {
|
|
419
450
|
await fsp.rename(join2(staging, entry), join2(repoRoot, entry));
|
|
420
451
|
}
|
|
452
|
+
await carryOverIgnored(backupDir, repoRoot);
|
|
421
453
|
await fsp.rm(backupDir, { recursive: true, force: true }).catch(() => {});
|
|
422
454
|
await fsp.rm(staging, { recursive: true, force: true }).catch(() => {});
|
|
423
455
|
} catch (swapError) {
|
|
@@ -434,6 +466,56 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
434
466
|
}
|
|
435
467
|
}
|
|
436
468
|
|
|
469
|
+
// src/progress.ts
|
|
470
|
+
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
471
|
+
function formatBytes(bytes) {
|
|
472
|
+
if (bytes < 1024) {
|
|
473
|
+
return `${bytes} B`;
|
|
474
|
+
}
|
|
475
|
+
if (bytes < 1024 * 1024) {
|
|
476
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
477
|
+
}
|
|
478
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
479
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
480
|
+
}
|
|
481
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
482
|
+
}
|
|
483
|
+
function createProgressReporter(label, out = process.stderr, now = Date.now) {
|
|
484
|
+
let finished = false;
|
|
485
|
+
let lastRenderAt = -Infinity;
|
|
486
|
+
let lastNonTtyTickAt = -Infinity;
|
|
487
|
+
let frameIndex = 0;
|
|
488
|
+
const isTTY = out.isTTY === true;
|
|
489
|
+
return {
|
|
490
|
+
tick(bytes) {
|
|
491
|
+
if (finished)
|
|
492
|
+
return;
|
|
493
|
+
const t = now();
|
|
494
|
+
if (isTTY) {
|
|
495
|
+
if (t - lastRenderAt < 80)
|
|
496
|
+
return;
|
|
497
|
+
lastRenderAt = t;
|
|
498
|
+
out.write(`\r\x1B[K${label}: ${FRAMES[frameIndex]} ${formatBytes(bytes)}`);
|
|
499
|
+
frameIndex = (frameIndex + 1) % FRAMES.length;
|
|
500
|
+
} else {
|
|
501
|
+
if (t - lastNonTtyTickAt < 1000)
|
|
502
|
+
return;
|
|
503
|
+
lastNonTtyTickAt = t;
|
|
504
|
+
out.write(`${label}: ${formatBytes(bytes)}
|
|
505
|
+
`);
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
done() {
|
|
509
|
+
if (finished)
|
|
510
|
+
return;
|
|
511
|
+
finished = true;
|
|
512
|
+
if (isTTY) {
|
|
513
|
+
out.write("\r\x1B[K");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
437
519
|
// src/resolve-target.ts
|
|
438
520
|
function resolveTarget(input) {
|
|
439
521
|
const repoRoot = getRepoRoot(input.cwd);
|
|
@@ -464,7 +546,7 @@ function toTarget(last) {
|
|
|
464
546
|
// package.json
|
|
465
547
|
var package_default = {
|
|
466
548
|
name: "@opencode-manager/ocm-cli",
|
|
467
|
-
version: "0.1.
|
|
549
|
+
version: "0.1.4",
|
|
468
550
|
description: "OpenCode Manager CLI: attach a local OpenCode TUI to a Manager-hosted repo.",
|
|
469
551
|
license: "MIT",
|
|
470
552
|
repository: {
|
|
@@ -760,6 +842,8 @@ async function cmdPush(args) {
|
|
|
760
842
|
const token = requireToken(state);
|
|
761
843
|
const api = new ManagerApi(state.managerUrl, token);
|
|
762
844
|
const repos = await fetchRepos(state.managerUrl, token);
|
|
845
|
+
const progress = createProgressReporter("push");
|
|
846
|
+
const onProgress = (p) => progress.tick(p.bytesSent);
|
|
763
847
|
const remotes = repos.map((r) => ({
|
|
764
848
|
repoId: r.repoId,
|
|
765
849
|
name: r.name,
|
|
@@ -789,11 +873,14 @@ async function cmdPush(args) {
|
|
|
789
873
|
const result = await mirrorUp(plan, {
|
|
790
874
|
api,
|
|
791
875
|
force,
|
|
792
|
-
create: { name, originUrl: plan.localOrigin, branch }
|
|
876
|
+
create: { name, originUrl: plan.localOrigin, branch },
|
|
877
|
+
onProgress
|
|
793
878
|
});
|
|
879
|
+
progress.done();
|
|
794
880
|
info(`pushed ${plan.repoRoot} -> ${result.created ? "created" : "updated"} (repoId=${result.repoId}, branch=${result.branch})`);
|
|
795
881
|
} else if (plan.matched.length === 1) {
|
|
796
|
-
const result = await mirrorUp(plan, { api, force });
|
|
882
|
+
const result = await mirrorUp(plan, { api, force, onProgress });
|
|
883
|
+
progress.done();
|
|
797
884
|
info(`pushed ${plan.repoRoot} -> ${plan.matched[0].name} (repoId=${result.repoId}, branch=${result.branch})`);
|
|
798
885
|
} else {
|
|
799
886
|
const names = plan.matched.map((r) => `${r.name} (id=${r.repoId})`).join(", ");
|
|
@@ -824,7 +911,12 @@ async function cmdPull(args) {
|
|
|
824
911
|
const names = plan.matched.map((r) => `${r.name} (id=${r.repoId})`).join(", ");
|
|
825
912
|
die(`multiple Manager repos match origin ${plan.localOrigin}: ${names}; disambiguate with \`ocm pull <repoId>\``);
|
|
826
913
|
}
|
|
827
|
-
|
|
914
|
+
const progress = createProgressReporter("pull");
|
|
915
|
+
try {
|
|
916
|
+
await mirrorDown(plan.matched[0].repoId, plan.repoRoot, api, { force, onProgress: (bytes) => progress.tick(bytes) });
|
|
917
|
+
} finally {
|
|
918
|
+
progress.done();
|
|
919
|
+
}
|
|
828
920
|
info(`pulled ${plan.matched[0].name} -> ${plan.repoRoot}`);
|
|
829
921
|
}
|
|
830
922
|
async function main() {
|