@opencode-manager/ocm-cli 0.1.2 → 0.1.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/ocm.js +144 -6
- package/package.json +1 -1
package/dist/ocm.js
CHANGED
|
@@ -192,10 +192,10 @@ class ManagerApi {
|
|
|
192
192
|
|
|
193
193
|
// src/mirror.ts
|
|
194
194
|
import { spawnSync as spawnSync3, spawn } from "child_process";
|
|
195
|
-
import { existsSync as existsSync2 } from "fs";
|
|
195
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
196
196
|
import * as fsp from "fs/promises";
|
|
197
197
|
import { Readable } from "stream";
|
|
198
|
-
import { join as join2 } from "path";
|
|
198
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
199
199
|
import { tmpdir } from "os";
|
|
200
200
|
|
|
201
201
|
// src/local-repo.ts
|
|
@@ -252,6 +252,45 @@ function getGitignoreExclusions(repoRoot) {
|
|
|
252
252
|
return (res.stdout ?? "").split(`
|
|
253
253
|
`).filter((line) => line.length > 0);
|
|
254
254
|
}
|
|
255
|
+
async function carryOverIgnored(fromDir, toDir) {
|
|
256
|
+
if (!existsSync2(fromDir))
|
|
257
|
+
return;
|
|
258
|
+
for (const rel of getGitignoreExclusions(fromDir)) {
|
|
259
|
+
const clean = rel.replace(/\/+$/, "");
|
|
260
|
+
if (!clean)
|
|
261
|
+
continue;
|
|
262
|
+
const src = join2(fromDir, clean);
|
|
263
|
+
const dest = join2(toDir, clean);
|
|
264
|
+
if (!existsSync2(src) || existsSync2(dest))
|
|
265
|
+
continue;
|
|
266
|
+
await fsp.mkdir(dirname2(dest), { recursive: true });
|
|
267
|
+
await fsp.rename(src, dest).catch(() => {});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function listIncludedFiles(repoRoot) {
|
|
271
|
+
const tracked = spawnSync3("git", ["ls-files", "-z"], { cwd: repoRoot, encoding: "utf-8" });
|
|
272
|
+
const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard", "-z"], { cwd: repoRoot, encoding: "utf-8" });
|
|
273
|
+
const split = (out) => (out ?? "").split("\x00").filter((p) => p.length > 0);
|
|
274
|
+
const all = [...split(tracked.stdout), ...split(untracked.stdout)];
|
|
275
|
+
return all.filter((p) => {
|
|
276
|
+
const top = p.split("/")[0] ?? p;
|
|
277
|
+
return !HARDCODED_EXCLUDES.includes(top);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function estimateTarSize(repoRoot) {
|
|
281
|
+
const files = listIncludedFiles(repoRoot);
|
|
282
|
+
let total = 0;
|
|
283
|
+
for (const rel of files) {
|
|
284
|
+
let size = 0;
|
|
285
|
+
try {
|
|
286
|
+
size = statSync(join2(repoRoot, rel)).size;
|
|
287
|
+
} catch {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
total += 512 + Math.ceil(size / 512) * 512;
|
|
291
|
+
}
|
|
292
|
+
return total + 1024;
|
|
293
|
+
}
|
|
255
294
|
|
|
256
295
|
class MirrorAbort extends Error {
|
|
257
296
|
constructor(message) {
|
|
@@ -331,6 +370,8 @@ function createPartFlusher(api, repoId, uploadId, chunkSize) {
|
|
|
331
370
|
async function mirrorUp(plan, opts) {
|
|
332
371
|
const repoId = opts.create ? 0 : plan.matched[0].repoId;
|
|
333
372
|
const begin = await opts.api.mirrorBegin(repoId, { force: opts.force, create: opts.create });
|
|
373
|
+
const totalBytes = estimateTarSize(plan.repoRoot);
|
|
374
|
+
let bytesSent = 0;
|
|
334
375
|
const tarArgs = ["-c", "-C", plan.repoRoot];
|
|
335
376
|
for (const dir of HARDCODED_EXCLUDES)
|
|
336
377
|
tarArgs.push("--exclude", dir);
|
|
@@ -364,9 +405,12 @@ async function mirrorUp(plan, opts) {
|
|
|
364
405
|
try {
|
|
365
406
|
for await (const chunk of child.stdout) {
|
|
366
407
|
await flusher.push(chunk);
|
|
408
|
+
bytesSent += chunk.length;
|
|
409
|
+
opts.onProgress?.({ phase: "uploading", bytesSent, totalBytes });
|
|
367
410
|
}
|
|
368
411
|
await tarExit;
|
|
369
412
|
const totalParts = await flusher.finish();
|
|
413
|
+
opts.onProgress?.({ phase: "committing", bytesSent, totalBytes });
|
|
370
414
|
const result = await opts.api.mirrorCommit(begin.repoId, begin.uploadId, totalParts);
|
|
371
415
|
return result;
|
|
372
416
|
} catch (err) {
|
|
@@ -403,6 +447,11 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
403
447
|
child.on("error", reject);
|
|
404
448
|
});
|
|
405
449
|
const stdinWritable = Readable.fromWeb(tarball);
|
|
450
|
+
let received = 0;
|
|
451
|
+
stdinWritable.on("data", (chunk) => {
|
|
452
|
+
received += chunk.length;
|
|
453
|
+
opts.onProgress?.(received);
|
|
454
|
+
});
|
|
406
455
|
stdinWritable.pipe(child.stdin);
|
|
407
456
|
await tarDone;
|
|
408
457
|
const backupDir = `${repoRoot}.ocm-backup-${Date.now()}`;
|
|
@@ -418,6 +467,7 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
418
467
|
for (const entry of stagingEntries) {
|
|
419
468
|
await fsp.rename(join2(staging, entry), join2(repoRoot, entry));
|
|
420
469
|
}
|
|
470
|
+
await carryOverIgnored(backupDir, repoRoot);
|
|
421
471
|
await fsp.rm(backupDir, { recursive: true, force: true }).catch(() => {});
|
|
422
472
|
await fsp.rm(staging, { recursive: true, force: true }).catch(() => {});
|
|
423
473
|
} catch (swapError) {
|
|
@@ -434,6 +484,77 @@ async function mirrorDown(repoId, repoRoot, api, opts = { force: false }) {
|
|
|
434
484
|
}
|
|
435
485
|
}
|
|
436
486
|
|
|
487
|
+
// src/progress.ts
|
|
488
|
+
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
489
|
+
function formatBytes(bytes) {
|
|
490
|
+
if (bytes < 1024) {
|
|
491
|
+
return `${bytes} B`;
|
|
492
|
+
}
|
|
493
|
+
if (bytes < 1024 * 1024) {
|
|
494
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
495
|
+
}
|
|
496
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
497
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
498
|
+
}
|
|
499
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
500
|
+
}
|
|
501
|
+
function createProgressReporter(label, out = process.stderr, now = Date.now) {
|
|
502
|
+
let finished = false;
|
|
503
|
+
let lastRenderAt = -Infinity;
|
|
504
|
+
let lastBucket = -1;
|
|
505
|
+
let lastNonTtyTickAt = -Infinity;
|
|
506
|
+
let frameIndex = 0;
|
|
507
|
+
const isTTY = out.isTTY === true;
|
|
508
|
+
return {
|
|
509
|
+
update(current, total) {
|
|
510
|
+
if (finished)
|
|
511
|
+
return;
|
|
512
|
+
if (isTTY) {
|
|
513
|
+
const t = now();
|
|
514
|
+
if (t - lastRenderAt < 80)
|
|
515
|
+
return;
|
|
516
|
+
lastRenderAt = t;
|
|
517
|
+
const pct = total > 0 ? Math.min(99, Math.floor(current / total * 100)) : 0;
|
|
518
|
+
out.write(`\r\x1B[K${label}: ${pct}% (${formatBytes(current)} / ${formatBytes(total)})`);
|
|
519
|
+
} else {
|
|
520
|
+
const pct = total > 0 ? Math.min(99, Math.floor(current / total * 100)) : 0;
|
|
521
|
+
const bucket = Math.floor(pct / 10);
|
|
522
|
+
if (bucket === lastBucket)
|
|
523
|
+
return;
|
|
524
|
+
lastBucket = bucket;
|
|
525
|
+
out.write(`${label}: ${pct}% (${formatBytes(current)} / ${formatBytes(total)})
|
|
526
|
+
`);
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
tick(bytes) {
|
|
530
|
+
if (finished)
|
|
531
|
+
return;
|
|
532
|
+
const t = now();
|
|
533
|
+
if (isTTY) {
|
|
534
|
+
if (t - lastRenderAt < 80)
|
|
535
|
+
return;
|
|
536
|
+
lastRenderAt = t;
|
|
537
|
+
out.write(`\r\x1B[K${label}: ${FRAMES[frameIndex]} ${formatBytes(bytes)}`);
|
|
538
|
+
frameIndex = (frameIndex + 1) % FRAMES.length;
|
|
539
|
+
} else {
|
|
540
|
+
if (t - lastNonTtyTickAt < 1000)
|
|
541
|
+
return;
|
|
542
|
+
lastNonTtyTickAt = t;
|
|
543
|
+
out.write(`${label}: ${formatBytes(bytes)}
|
|
544
|
+
`);
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
done() {
|
|
548
|
+
if (finished)
|
|
549
|
+
return;
|
|
550
|
+
finished = true;
|
|
551
|
+
if (isTTY) {
|
|
552
|
+
out.write("\r\x1B[K");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
437
558
|
// src/resolve-target.ts
|
|
438
559
|
function resolveTarget(input) {
|
|
439
560
|
const repoRoot = getRepoRoot(input.cwd);
|
|
@@ -464,7 +585,7 @@ function toTarget(last) {
|
|
|
464
585
|
// package.json
|
|
465
586
|
var package_default = {
|
|
466
587
|
name: "@opencode-manager/ocm-cli",
|
|
467
|
-
version: "0.1.
|
|
588
|
+
version: "0.1.3",
|
|
468
589
|
description: "OpenCode Manager CLI: attach a local OpenCode TUI to a Manager-hosted repo.",
|
|
469
590
|
license: "MIT",
|
|
470
591
|
repository: {
|
|
@@ -760,6 +881,15 @@ async function cmdPush(args) {
|
|
|
760
881
|
const token = requireToken(state);
|
|
761
882
|
const api = new ManagerApi(state.managerUrl, token);
|
|
762
883
|
const repos = await fetchRepos(state.managerUrl, token);
|
|
884
|
+
const progress = createProgressReporter("push");
|
|
885
|
+
const onProgress = (p) => {
|
|
886
|
+
if (p.phase === "committing")
|
|
887
|
+
progress.tick(p.bytesSent);
|
|
888
|
+
else if (p.totalBytes > 0)
|
|
889
|
+
progress.update(p.bytesSent, p.totalBytes);
|
|
890
|
+
else
|
|
891
|
+
progress.tick(p.bytesSent);
|
|
892
|
+
};
|
|
763
893
|
const remotes = repos.map((r) => ({
|
|
764
894
|
repoId: r.repoId,
|
|
765
895
|
name: r.name,
|
|
@@ -789,11 +919,14 @@ async function cmdPush(args) {
|
|
|
789
919
|
const result = await mirrorUp(plan, {
|
|
790
920
|
api,
|
|
791
921
|
force,
|
|
792
|
-
create: { name, originUrl: plan.localOrigin, branch }
|
|
922
|
+
create: { name, originUrl: plan.localOrigin, branch },
|
|
923
|
+
onProgress
|
|
793
924
|
});
|
|
925
|
+
progress.done();
|
|
794
926
|
info(`pushed ${plan.repoRoot} -> ${result.created ? "created" : "updated"} (repoId=${result.repoId}, branch=${result.branch})`);
|
|
795
927
|
} else if (plan.matched.length === 1) {
|
|
796
|
-
const result = await mirrorUp(plan, { api, force });
|
|
928
|
+
const result = await mirrorUp(plan, { api, force, onProgress });
|
|
929
|
+
progress.done();
|
|
797
930
|
info(`pushed ${plan.repoRoot} -> ${plan.matched[0].name} (repoId=${result.repoId}, branch=${result.branch})`);
|
|
798
931
|
} else {
|
|
799
932
|
const names = plan.matched.map((r) => `${r.name} (id=${r.repoId})`).join(", ");
|
|
@@ -824,7 +957,12 @@ async function cmdPull(args) {
|
|
|
824
957
|
const names = plan.matched.map((r) => `${r.name} (id=${r.repoId})`).join(", ");
|
|
825
958
|
die(`multiple Manager repos match origin ${plan.localOrigin}: ${names}; disambiguate with \`ocm pull <repoId>\``);
|
|
826
959
|
}
|
|
827
|
-
|
|
960
|
+
const progress = createProgressReporter("pull");
|
|
961
|
+
try {
|
|
962
|
+
await mirrorDown(plan.matched[0].repoId, plan.repoRoot, api, { force, onProgress: (bytes) => progress.tick(bytes) });
|
|
963
|
+
} finally {
|
|
964
|
+
progress.done();
|
|
965
|
+
}
|
|
828
966
|
info(`pulled ${plan.matched[0].name} -> ${plan.repoRoot}`);
|
|
829
967
|
}
|
|
830
968
|
async function main() {
|