@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/ocm.js +104 -12
  3. 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 res = await fetch(`${this.baseUrl}/api/internal/repos/${repoId}/mirror`, {
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
- const result = await opts.api.mirrorCommit(begin.repoId, begin.uploadId, totalParts);
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 child = spawn("tar", ["-x", "-f", "-", "-C", staging], { stdio: ["pipe", "pipe", "pipe"] });
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.2",
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
- await mirrorDown(plan.matched[0].repoId, plan.repoRoot, api, { force });
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() {
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.4",
4
4
  "description": "OpenCode Manager CLI: attach a local OpenCode TUI to a Manager-hosted repo.",
5
5
  "license": "MIT",
6
6
  "repository": {