@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.
Files changed (2) hide show
  1. package/dist/ocm.js +144 -6
  2. 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.2",
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
- await mirrorDown(plan.matched[0].repoId, plan.repoRoot, api, { force });
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-manager/ocm-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "OpenCode Manager CLI: attach a local OpenCode TUI to a Manager-hosted repo.",
5
5
  "license": "MIT",
6
6
  "repository": {