@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 +77 -1
- package/package.json +1 -1
- package/scripts/install.js +65 -5
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
package/scripts/install.js
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
+
};
|