@frumu/tandem 0.4.43 → 0.5.0

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/bin/tandem.js CHANGED
@@ -525,6 +525,72 @@ async function buildDiagnostics(env = process.env) {
525
525
  };
526
526
  }
527
527
 
528
+ function buildWorktreeCleanupPayload(cli) {
529
+ const repoRoot = String(cli.value("repo-root") || "").trim();
530
+ return {
531
+ repo_root: repoRoot || undefined,
532
+ dry_run: !cli.has("apply"),
533
+ remove_orphan_dirs: !cli.has("keep-orphan-dirs"),
534
+ };
535
+ }
536
+
537
+ async function requestWorktreeCleanup(cli, env = process.env) {
538
+ const paths = resolveTandemPaths(env);
539
+ const engineUrl = `http://${paths.engineHost}:${paths.enginePort}`;
540
+ const response = await fetch(`${engineUrl}/worktree/cleanup`, {
541
+ method: "POST",
542
+ headers: { "content-type": "application/json" },
543
+ body: JSON.stringify(buildWorktreeCleanupPayload(cli)),
544
+ signal: AbortSignal.timeout(15_000),
545
+ });
546
+ if (!response.ok) {
547
+ const text = await response.text().catch(() => "");
548
+ throw new Error(`Worktree cleanup failed (${response.status})${text ? `: ${text}` : ""}`);
549
+ }
550
+ return await response.json();
551
+ }
552
+
553
+ function printWorktreeCleanupReport(report, json = false) {
554
+ if (json) {
555
+ console.log(JSON.stringify(report, null, 2));
556
+ return;
557
+ }
558
+ const staleCount = Array.isArray(report.stale_paths) ? report.stale_paths.length : 0;
559
+ const activeCount = Array.isArray(report.active_paths) ? report.active_paths.length : 0;
560
+ const removedCount =
561
+ (Array.isArray(report.cleaned_worktrees) ? report.cleaned_worktrees.length : 0) +
562
+ (Array.isArray(report.orphan_dirs_removed) ? report.orphan_dirs_removed.length : 0);
563
+ const failureCount = Array.isArray(report.failures) ? report.failures.length : 0;
564
+ printLines([
565
+ `[Tandem] worktree cleanup: ${report.dry_run ? "preview" : "applied"}`,
566
+ `[Tandem] repo root: ${report.repo_root || "unknown"}`,
567
+ `[Tandem] managed root: ${report.managed_root || "unknown"}`,
568
+ `[Tandem] active tracked: ${activeCount}`,
569
+ `[Tandem] stale candidates: ${staleCount}`,
570
+ `[Tandem] removed: ${removedCount}`,
571
+ `[Tandem] failures: ${failureCount}`,
572
+ ]);
573
+ const logRows = [];
574
+ for (const row of Array.isArray(report.cleaned_worktrees) ? report.cleaned_worktrees : []) {
575
+ logRows.push(` removed worktree: ${row.path}${row.branch ? ` (${row.branch})` : ""}`);
576
+ }
577
+ for (const row of Array.isArray(report.orphan_dirs_removed) ? report.orphan_dirs_removed : []) {
578
+ logRows.push(` removed orphan dir: ${row.path}`);
579
+ }
580
+ if (report.dry_run) {
581
+ for (const row of Array.isArray(report.stale_paths) ? report.stale_paths : []) {
582
+ logRows.push(` stale candidate: ${row.path}${row.branch ? ` (${row.branch})` : ""}`);
583
+ }
584
+ for (const path of Array.isArray(report.orphan_dirs) ? report.orphan_dirs : []) {
585
+ logRows.push(` orphan dir: ${path}`);
586
+ }
587
+ }
588
+ for (const row of Array.isArray(report.failures) ? report.failures : []) {
589
+ logRows.push(` failure: ${row.path || row.code || "unknown"}${row.error ? ` -> ${row.error}` : row.stderr ? ` -> ${row.stderr}` : ""}`);
590
+ }
591
+ if (logRows.length) printLines(logRows);
592
+ }
593
+
528
594
  async function printDiagnostics(report, json = false) {
529
595
  if (json) {
530
596
  console.log(JSON.stringify(report, null, 2));
@@ -744,7 +810,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
744
810
 
745
811
  if (!command) {
746
812
  console.log(`[Tandem] ${packageInfo.name} ${packageInfo.version}`);
747
- console.log("[Tandem] Use: tandem doctor | tandem status | tandem service install | tandem install panel");
813
+ console.log("[Tandem] Use: tandem doctor | tandem doctor worktrees | tandem status | tandem service install | tandem install panel");
748
814
  return 0;
749
815
  }
750
816
 
@@ -754,6 +820,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
754
820
  "",
755
821
  "Commands:",
756
822
  " tandem doctor",
823
+ " tandem doctor worktrees [--repo-root /abs/path] [--apply] [--json]",
757
824
  " tandem status",
758
825
  " tandem service install|start|stop|restart|status|logs",
759
826
  " tandem install panel",
@@ -767,6 +834,12 @@ async function main(argv = process.argv.slice(2), env = process.env) {
767
834
  }
768
835
 
769
836
  if (command === "doctor") {
837
+ const subcommand = String(argv[1] || "").trim().toLowerCase();
838
+ if (subcommand === "worktrees" || subcommand === "worktree") {
839
+ const report = await requestWorktreeCleanup(cli, env);
840
+ printWorktreeCleanupReport(report, cli.has("json"));
841
+ return Array.isArray(report.failures) && report.failures.length ? 1 : 0;
842
+ }
770
843
  const report = await buildDiagnostics(env);
771
844
  await printDiagnostics(report, cli.has("json"));
772
845
  return report.engine.reachable ? 0 : 1;
@@ -853,10 +926,13 @@ module.exports = {
853
926
  parseArgs,
854
927
  printDiagnostics,
855
928
  printStatus,
929
+ printWorktreeCleanupReport,
856
930
  queryEngineServiceState,
931
+ requestWorktreeCleanup,
857
932
  resolveTandemHomeDir,
858
933
  resolveTandemPaths,
859
934
  runAddonCli,
860
935
  runCommand,
936
+ buildWorktreeCleanupPayload,
861
937
  updatePackage,
862
938
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frumu/tandem",
3
- "version": "0.4.43",
3
+ "version": "0.5.0",
4
4
  "description": "Tandem master CLI and engine binary distribution",
5
5
  "homepage": "https://tandem.ac",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const https = require('https');
4
- const { execSync } = require('child_process');
4
+ const { execFileSync, execSync } = require('child_process');
5
5
 
6
6
  // Configuration
7
7
  const REPO = "frumu-ai/tandem";
@@ -70,9 +70,47 @@ if (!fs.existsSync(binDir)) {
70
70
  fs.mkdirSync(binDir, { recursive: true });
71
71
  }
72
72
 
73
- if (fs.existsSync(destPath)) {
74
- console.log("Binary already present.");
75
- process.exit(0);
73
+ function parseVersion(raw) {
74
+ const match = String(raw || '').match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
75
+ return match ? match[1] : "";
76
+ }
77
+
78
+ function installedBinaryVersion(binaryPath, execFile = execFileSync) {
79
+ if (!fs.existsSync(binaryPath)) return "";
80
+ try {
81
+ const output = execFile(binaryPath, ['--version'], {
82
+ encoding: 'utf8',
83
+ stdio: ['ignore', 'pipe', 'ignore'],
84
+ timeout: 5000,
85
+ });
86
+ return parseVersion(output);
87
+ } catch {
88
+ return "";
89
+ }
90
+ }
91
+
92
+ function shouldDownloadBinary(binaryPath, packageVersion, readVersion = installedBinaryVersion) {
93
+ if (!fs.existsSync(binaryPath)) {
94
+ return { download: true, reason: "missing" };
95
+ }
96
+
97
+ const stats = fs.statSync(binaryPath);
98
+ if (stats.size < MIN_SIZE) {
99
+ return { download: true, reason: `too small (${stats.size} bytes)` };
100
+ }
101
+
102
+ const installedVersion = readVersion(binaryPath);
103
+ if (!installedVersion) {
104
+ return { download: true, reason: "version check failed" };
105
+ }
106
+ if (installedVersion !== packageVersion) {
107
+ return {
108
+ download: true,
109
+ reason: `version mismatch (${installedVersion} != ${packageVersion})`,
110
+ };
111
+ }
112
+
113
+ return { download: false, reason: `version ${installedVersion} already installed` };
76
114
  }
77
115
 
78
116
  // Helper to fetch JSON from GitHub API
@@ -190,4 +228,26 @@ async function extract(archivePath) {
190
228
  }
191
229
  }
192
230
 
193
- download().then(extract).catch(warnAndExit);
231
+ async function main() {
232
+ const packageVersion = require('../package.json').version;
233
+ const decision = shouldDownloadBinary(destPath, packageVersion);
234
+ if (!decision.download) {
235
+ console.log(`Binary already present (${decision.reason}).`);
236
+ return;
237
+ }
238
+ if (decision.reason !== "missing") {
239
+ console.log(`Existing binary will be replaced: ${decision.reason}.`);
240
+ }
241
+ const archivePath = await download();
242
+ await extract(archivePath);
243
+ }
244
+
245
+ if (require.main === module) {
246
+ main().catch(warnAndExit);
247
+ }
248
+
249
+ module.exports = {
250
+ installedBinaryVersion,
251
+ parseVersion,
252
+ shouldDownloadBinary,
253
+ };