@taskless-app/swarm 0.4.10 → 0.6.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/README.md +136 -16
- package/dist/cli.js +3016 -806
- package/dist/overmind.js +2334 -573
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,41 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/cliIo.ts
|
|
13
|
+
import { createInterface } from "readline/promises";
|
|
14
|
+
import { execa } from "execa";
|
|
15
|
+
async function askTty(question) {
|
|
16
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
try {
|
|
18
|
+
return await rl.question(question);
|
|
19
|
+
} finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function askYesNo(question) {
|
|
24
|
+
return YES.includes((await askTty(`${question} [y/N]: `)).trim().toLowerCase());
|
|
25
|
+
}
|
|
26
|
+
async function runQuiet(command, opts = {}) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await execa(command, { shell: true, reject: false, timeout: opts.timeoutMs ?? PROBE_TIMEOUT_MS });
|
|
29
|
+
return { exitCode: res.exitCode ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return { exitCode: 1, stdout: "", stderr: e instanceof Error ? e.message : String(e) };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function execLoud(command) {
|
|
35
|
+
const res = await execa(command, { shell: true, reject: false, stdio: "inherit" });
|
|
36
|
+
return res.exitCode ?? 1;
|
|
37
|
+
}
|
|
38
|
+
var YES, PROBE_TIMEOUT_MS;
|
|
39
|
+
var init_cliIo = __esm({
|
|
40
|
+
"src/cliIo.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
YES = ["y", "yes", "\u0434", "\u0434\u0430"];
|
|
43
|
+
PROBE_TIMEOUT_MS = 1e4;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
12
47
|
// ../swarm-core/src/types.ts
|
|
13
48
|
function isMilestoneKind(kind) {
|
|
14
49
|
return MILESTONE_KIND_SET.has(kind);
|
|
@@ -361,9 +396,14 @@ var init_windows = __esm({
|
|
|
361
396
|
});
|
|
362
397
|
|
|
363
398
|
// ../swarm-core/src/slug.ts
|
|
399
|
+
function isTaskSlug(s) {
|
|
400
|
+
return !!s && TASK_SLUG_RE.test(s);
|
|
401
|
+
}
|
|
402
|
+
var TASK_SLUG_RE;
|
|
364
403
|
var init_slug = __esm({
|
|
365
404
|
"../swarm-core/src/slug.ts"() {
|
|
366
405
|
"use strict";
|
|
406
|
+
TASK_SLUG_RE = /^(?:[A-Za-z0-9]{1,8}-\d+|#[A-Za-z0-9]{4,})$/;
|
|
367
407
|
}
|
|
368
408
|
});
|
|
369
409
|
|
|
@@ -384,7 +424,7 @@ var init_text = __esm({
|
|
|
384
424
|
"use strict";
|
|
385
425
|
ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
386
426
|
CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
|
|
387
|
-
EVENT_TEXT_MAX =
|
|
427
|
+
EVENT_TEXT_MAX = 16e3;
|
|
388
428
|
}
|
|
389
429
|
});
|
|
390
430
|
|
|
@@ -538,6 +578,7 @@ function isHostOffline(lastSeenAt, nowMs, staleMs = HOST_HEARTBEAT_STALE_MS) {
|
|
|
538
578
|
return nowMs - at > staleMs;
|
|
539
579
|
}
|
|
540
580
|
function hostStateText(host, nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_MS) {
|
|
581
|
+
if (host.draining) return host.drainReason ? `\u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u043D\u0438\u0435 \u2014 ${host.drainReason}` : "\u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u043D\u0438\u0435";
|
|
541
582
|
if (!host.lastSeenAt) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
|
|
542
583
|
const at = Date.parse(host.lastSeenAt);
|
|
543
584
|
if (Number.isNaN(at)) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
|
|
@@ -563,8 +604,16 @@ var init_hosts = __esm({
|
|
|
563
604
|
}
|
|
564
605
|
});
|
|
565
606
|
|
|
607
|
+
// ../swarm-core/src/hostPool.ts
|
|
608
|
+
var init_hostPool = __esm({
|
|
609
|
+
"../swarm-core/src/hostPool.ts"() {
|
|
610
|
+
"use strict";
|
|
611
|
+
init_hosts();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
566
615
|
// ../swarm-core/src/hostCommands.ts
|
|
567
|
-
var HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS;
|
|
616
|
+
var HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED;
|
|
568
617
|
var init_hostCommands = __esm({
|
|
569
618
|
"../swarm-core/src/hostCommands.ts"() {
|
|
570
619
|
"use strict";
|
|
@@ -573,6 +622,8 @@ var init_hostCommands = __esm({
|
|
|
573
622
|
HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
|
|
574
623
|
HOST_COMMAND_TTL_MS = 30 * 6e4;
|
|
575
624
|
HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
|
|
625
|
+
RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
|
|
626
|
+
RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
|
|
576
627
|
}
|
|
577
628
|
});
|
|
578
629
|
|
|
@@ -584,6 +635,420 @@ var init_relief = __esm({
|
|
|
584
635
|
}
|
|
585
636
|
});
|
|
586
637
|
|
|
638
|
+
// ../swarm-core/src/cost.ts
|
|
639
|
+
var EMPTY_TASK_COST;
|
|
640
|
+
var init_cost = __esm({
|
|
641
|
+
"../swarm-core/src/cost.ts"() {
|
|
642
|
+
"use strict";
|
|
643
|
+
EMPTY_TASK_COST = Object.freeze({
|
|
644
|
+
costUsd: 0,
|
|
645
|
+
tokensIn: 0,
|
|
646
|
+
tokensOut: 0,
|
|
647
|
+
durationMs: 0,
|
|
648
|
+
runs: 0,
|
|
649
|
+
respawns: 0
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// ../swarm-core/src/runGuards.ts
|
|
655
|
+
var DEFAULT_RUN_LIMITS;
|
|
656
|
+
var init_runGuards = __esm({
|
|
657
|
+
"../swarm-core/src/runGuards.ts"() {
|
|
658
|
+
"use strict";
|
|
659
|
+
DEFAULT_RUN_LIMITS = {
|
|
660
|
+
maxCostUsd: 20,
|
|
661
|
+
runTimeoutMs: 8 * 60 * 6e4,
|
|
662
|
+
stepTimeoutMs: 90 * 6e4,
|
|
663
|
+
lostMs: 30 * 6e4
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// ../swarm-core/src/codeZones.ts
|
|
669
|
+
function hasZoneModel(m) {
|
|
670
|
+
return m.tasks > 0 && m.zones.length > 0;
|
|
671
|
+
}
|
|
672
|
+
function testFileFeature(basename5) {
|
|
673
|
+
const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
|
|
674
|
+
return head && !GENERIC_DIRS.has(head) ? head : null;
|
|
675
|
+
}
|
|
676
|
+
function pathZoneCandidate(path) {
|
|
677
|
+
const parts = path.split("/").filter(Boolean);
|
|
678
|
+
if (parts.length <= 1) return "root";
|
|
679
|
+
if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
|
|
680
|
+
const pkg = parts[1];
|
|
681
|
+
if (parts[2] === "src" && parts.length >= 5) {
|
|
682
|
+
const feature = parts[3];
|
|
683
|
+
if (!GENERIC_DIRS.has(feature)) return feature;
|
|
684
|
+
}
|
|
685
|
+
if (GENERIC_TEST_DIRS.has(parts[2])) {
|
|
686
|
+
const feature = testFileFeature(parts[parts.length - 1]);
|
|
687
|
+
if (feature) return feature;
|
|
688
|
+
}
|
|
689
|
+
return pkg;
|
|
690
|
+
}
|
|
691
|
+
if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
|
|
692
|
+
return parts[0];
|
|
693
|
+
}
|
|
694
|
+
function tokenize(text) {
|
|
695
|
+
const out = [];
|
|
696
|
+
for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
|
|
697
|
+
if (!raw) continue;
|
|
698
|
+
for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
|
|
699
|
+
const t = piece.toLowerCase();
|
|
700
|
+
if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
|
|
701
|
+
out.push(t);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
function normalizeZoneName(raw) {
|
|
707
|
+
if (!raw) return null;
|
|
708
|
+
const s = raw.trim().toLowerCase().replace(/^area:/, "");
|
|
709
|
+
return s || null;
|
|
710
|
+
}
|
|
711
|
+
function bump(m, k, by = 1) {
|
|
712
|
+
m.set(k, (m.get(k) ?? 0) + by);
|
|
713
|
+
}
|
|
714
|
+
function topKey(m) {
|
|
715
|
+
let best = null;
|
|
716
|
+
let bestN = 0;
|
|
717
|
+
let total = 0;
|
|
718
|
+
for (const [k, n] of m) {
|
|
719
|
+
total += n;
|
|
720
|
+
if (n > bestN) {
|
|
721
|
+
best = k;
|
|
722
|
+
bestN = n;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
|
|
726
|
+
}
|
|
727
|
+
function buildZoneModel(entries, opts = {}) {
|
|
728
|
+
const o = { ...DEFAULTS, ...opts };
|
|
729
|
+
const tasks = entries.filter((e) => e.files.length > 0);
|
|
730
|
+
if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
|
|
731
|
+
const fileTasks = /* @__PURE__ */ new Map();
|
|
732
|
+
const candidate = /* @__PURE__ */ new Map();
|
|
733
|
+
for (const t of tasks) {
|
|
734
|
+
for (const f of new Set(t.files)) {
|
|
735
|
+
bump(fileTasks, f);
|
|
736
|
+
if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const zoneOf = new Map(candidate);
|
|
740
|
+
for (const [file, seen] of fileTasks) {
|
|
741
|
+
if (seen < 2) continue;
|
|
742
|
+
const votes = /* @__PURE__ */ new Map();
|
|
743
|
+
for (const t of tasks) {
|
|
744
|
+
const files = [...new Set(t.files)];
|
|
745
|
+
if (!files.includes(file)) continue;
|
|
746
|
+
const others = files.filter((f) => f !== file);
|
|
747
|
+
if (!others.length) continue;
|
|
748
|
+
for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
|
|
749
|
+
}
|
|
750
|
+
const top = topKey(votes);
|
|
751
|
+
if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
|
|
752
|
+
}
|
|
753
|
+
const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
|
|
754
|
+
const taskZonePre = /* @__PURE__ */ new Map();
|
|
755
|
+
for (const t of tasks) {
|
|
756
|
+
const votes = /* @__PURE__ */ new Map();
|
|
757
|
+
for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
|
|
758
|
+
const top = topKey(votes);
|
|
759
|
+
if (top) taskZonePre.set(t.slug, top.key);
|
|
760
|
+
}
|
|
761
|
+
const fileZones = /* @__PURE__ */ new Map();
|
|
762
|
+
for (const t of tasks) {
|
|
763
|
+
const tz = taskZonePre.get(t.slug);
|
|
764
|
+
if (!tz) continue;
|
|
765
|
+
for (const f of new Set(t.files)) {
|
|
766
|
+
let m = fileZones.get(f);
|
|
767
|
+
if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
|
|
768
|
+
bump(m, tz);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const spineSet = /* @__PURE__ */ new Set();
|
|
772
|
+
for (const [file, seen] of fileTasks) {
|
|
773
|
+
if (seen < o.spineMinTasks) continue;
|
|
774
|
+
const zonesOfFile = fileZones.get(file);
|
|
775
|
+
const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
|
|
776
|
+
if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
|
|
777
|
+
if (dominant && dominant.share >= o.spineDominantShare) continue;
|
|
778
|
+
spineSet.add(file);
|
|
779
|
+
}
|
|
780
|
+
const fileZone = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const [f, z] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z);
|
|
782
|
+
const taskZone = /* @__PURE__ */ new Map();
|
|
783
|
+
for (const t of tasks) {
|
|
784
|
+
const votes = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const f of new Set(t.files)) {
|
|
786
|
+
const z = fileZone.get(f);
|
|
787
|
+
if (z) bump(votes, z, voteWeight(f));
|
|
788
|
+
}
|
|
789
|
+
const top = topKey(votes);
|
|
790
|
+
taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
|
|
791
|
+
}
|
|
792
|
+
const labelVotes = /* @__PURE__ */ new Map();
|
|
793
|
+
for (const t of tasks) {
|
|
794
|
+
const manual = normalizeZoneName(t.area);
|
|
795
|
+
const z = taskZone.get(t.slug);
|
|
796
|
+
if (!manual || !z || z === SPINE_ZONE || manual === SPINE_ZONE) continue;
|
|
797
|
+
let m = labelVotes.get(z);
|
|
798
|
+
if (!m) labelVotes.set(z, m = /* @__PURE__ */ new Map());
|
|
799
|
+
bump(m, manual);
|
|
800
|
+
}
|
|
801
|
+
const rename = /* @__PURE__ */ new Map();
|
|
802
|
+
const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
|
|
803
|
+
const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
|
|
804
|
+
const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
|
|
805
|
+
return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
|
|
806
|
+
});
|
|
807
|
+
for (const [zone, votes] of clustersByEvidence) {
|
|
808
|
+
const top = topKey(votes);
|
|
809
|
+
if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
|
|
810
|
+
rename.set(zone, top.key);
|
|
811
|
+
takenNames.add(top.key);
|
|
812
|
+
}
|
|
813
|
+
const renamed = (z) => rename.get(z) ?? z;
|
|
814
|
+
if (rename.size) {
|
|
815
|
+
for (const [f, z] of fileZone) fileZone.set(f, renamed(z));
|
|
816
|
+
for (const [s, z] of taskZone) taskZone.set(s, renamed(z));
|
|
817
|
+
}
|
|
818
|
+
const zoneTerms = /* @__PURE__ */ new Map();
|
|
819
|
+
const addTerm = (zone, term, w) => {
|
|
820
|
+
let m = zoneTerms.get(zone);
|
|
821
|
+
if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
|
|
822
|
+
bump(m, term, w);
|
|
823
|
+
};
|
|
824
|
+
for (const [f, z] of fileZone) for (const term of tokenize(f)) addTerm(z, term, 1);
|
|
825
|
+
for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
|
|
826
|
+
for (const t of tasks) {
|
|
827
|
+
const z = taskZone.get(t.slug);
|
|
828
|
+
if (!z || !t.text) continue;
|
|
829
|
+
for (const term of tokenize(t.text)) addTerm(z, term, 2);
|
|
830
|
+
}
|
|
831
|
+
const termZones = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
|
|
833
|
+
const zoneFiles = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const [f, z] of fileZone) {
|
|
835
|
+
const arr = zoneFiles.get(z) ?? [];
|
|
836
|
+
arr.push(f);
|
|
837
|
+
zoneFiles.set(z, arr);
|
|
838
|
+
}
|
|
839
|
+
const zoneTaskCount = /* @__PURE__ */ new Map();
|
|
840
|
+
for (const z of taskZone.values()) bump(zoneTaskCount, z);
|
|
841
|
+
const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
|
|
842
|
+
const zones = [...zoneFiles.entries()].map(([zone, files]) => ({ zone, tasks: zoneTaskCount.get(zone) ?? 0, files: files.sort(byFreq) })).sort((a, b) => b.tasks - a.tasks || a.zone.localeCompare(b.zone));
|
|
843
|
+
const spine = [...spineSet].sort(byFreq);
|
|
844
|
+
if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
|
|
845
|
+
const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
|
|
846
|
+
return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
|
|
847
|
+
}
|
|
848
|
+
function zoneForFiles(model, files) {
|
|
849
|
+
if (!hasZoneModel(model) || !files.length) return null;
|
|
850
|
+
const spineSet = new Set(model.spine);
|
|
851
|
+
const touchedSpine = files.filter((f) => spineSet.has(f));
|
|
852
|
+
const votes = /* @__PURE__ */ new Map();
|
|
853
|
+
for (const f of new Set(files)) {
|
|
854
|
+
const z = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
|
|
855
|
+
if (z) bump(votes, z);
|
|
856
|
+
}
|
|
857
|
+
const top = topKey(votes);
|
|
858
|
+
if (!top) {
|
|
859
|
+
return {
|
|
860
|
+
zone: SPINE_ZONE,
|
|
861
|
+
confidence: 1,
|
|
862
|
+
why: `\u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B \u2014 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${touchedSpine.slice(0, 3).join(", ")})`,
|
|
863
|
+
spine: true
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
zone: top.key,
|
|
868
|
+
confidence: top.share,
|
|
869
|
+
why: `${top.count} \u0438\u0437 ${[...votes.values()].reduce((a, b) => a + b, 0)} \u0444\u0430\u0439\u043B\u043E\u0432 \u2014 \u0437\u043E\u043D\u0430 ${top.key}`,
|
|
870
|
+
spine: touchedSpine.length > 0
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function suggestZonesFromText(model, text, limit = 3) {
|
|
874
|
+
if (!hasZoneModel(model)) return [];
|
|
875
|
+
const terms = tokenize(text);
|
|
876
|
+
if (!terms.length) return [];
|
|
877
|
+
const zoneCount = model.zoneTerms.size || 1;
|
|
878
|
+
const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
|
|
879
|
+
const scores = /* @__PURE__ */ new Map();
|
|
880
|
+
const hits = /* @__PURE__ */ new Map();
|
|
881
|
+
const tasksOfZone = new Map(model.zones.map((z) => [z.zone, z.tasks]));
|
|
882
|
+
for (const [zone, dict] of model.zoneTerms) {
|
|
883
|
+
if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
|
|
884
|
+
let dot = 0;
|
|
885
|
+
let norm = 0;
|
|
886
|
+
const matched = [];
|
|
887
|
+
for (const [term, w] of dict) {
|
|
888
|
+
const v = Math.log(1 + w) * idf(term);
|
|
889
|
+
norm += v * v;
|
|
890
|
+
}
|
|
891
|
+
if (!norm) continue;
|
|
892
|
+
for (const term of new Set(terms)) {
|
|
893
|
+
const w = dict.get(term);
|
|
894
|
+
if (!w) continue;
|
|
895
|
+
dot += Math.log(1 + w) * idf(term) * idf(term);
|
|
896
|
+
matched.push(term);
|
|
897
|
+
}
|
|
898
|
+
const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
|
|
899
|
+
if (score > 0) {
|
|
900
|
+
scores.set(zone, score);
|
|
901
|
+
hits.set(zone, matched);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const total = [...scores.values()].reduce((a, b) => a + b, 0);
|
|
905
|
+
if (!total) return [];
|
|
906
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
|
|
907
|
+
zone,
|
|
908
|
+
confidence: s / total,
|
|
909
|
+
why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
|
|
910
|
+
spine: zone === SPINE_ZONE
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
function similarTasks(model, text, limit = 5) {
|
|
914
|
+
const terms = new Set(tokenize(text));
|
|
915
|
+
if (!terms.size) return [];
|
|
916
|
+
const scored = [];
|
|
917
|
+
for (const [i, e] of model.entries.entries()) {
|
|
918
|
+
const other = model.entryTerms[i];
|
|
919
|
+
if (!other?.size) continue;
|
|
920
|
+
let inter = 0;
|
|
921
|
+
for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
|
|
922
|
+
if (inter <= 0) continue;
|
|
923
|
+
scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
|
|
924
|
+
}
|
|
925
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
926
|
+
}
|
|
927
|
+
function predictFiles(model, opts) {
|
|
928
|
+
if (!hasZoneModel(model)) return [];
|
|
929
|
+
const limit = opts.limit ?? 40;
|
|
930
|
+
const best = /* @__PURE__ */ new Map();
|
|
931
|
+
const put = (path, weight, why) => {
|
|
932
|
+
const cur = best.get(path);
|
|
933
|
+
if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
|
|
934
|
+
};
|
|
935
|
+
if (opts.text) {
|
|
936
|
+
const sim = similarTasks(model, opts.text, opts.similar ?? 5);
|
|
937
|
+
const topScore = sim[0]?.score ?? 0;
|
|
938
|
+
for (const { entry, score } of sim) {
|
|
939
|
+
const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
|
|
940
|
+
for (const f of new Set(entry.files)) put(f, Math.max(0.35, w * 0.9), `\u043F\u043E\u0445\u043E\u0436\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 ${entry.slug}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
|
|
944
|
+
if (zone) {
|
|
945
|
+
const stat = model.zones.find((z) => z.zone === zone);
|
|
946
|
+
if (stat) stat.files.slice(0, limit).forEach((f, i) => put(f, Math.max(0.12, 0.45 - i * 0.02), `\u0437\u043E\u043D\u0430 ${zone}`));
|
|
947
|
+
}
|
|
948
|
+
return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
|
|
949
|
+
}
|
|
950
|
+
function manualIsBroader(manual, files) {
|
|
951
|
+
if (!files.length) return false;
|
|
952
|
+
const withSegment = files.filter((f) => f.split("/").some((seg) => seg.toLowerCase() === manual)).length;
|
|
953
|
+
return withSegment / files.length >= 0.6;
|
|
954
|
+
}
|
|
955
|
+
function checkZoneAgreement(model, entries = model.entries) {
|
|
956
|
+
const rows = [];
|
|
957
|
+
for (const e of entries) {
|
|
958
|
+
const manual = normalizeZoneName(e.area);
|
|
959
|
+
if (!manual) continue;
|
|
960
|
+
const guess = zoneForFiles(model, e.files);
|
|
961
|
+
const suggested = guess?.zone ?? null;
|
|
962
|
+
const verdict = suggested === manual ? "match" : suggested && manualIsBroader(manual, e.files) ? "compatible" : "mismatch";
|
|
963
|
+
rows.push({ slug: e.slug, manual, suggested, match: verdict === "match", verdict });
|
|
964
|
+
}
|
|
965
|
+
const matched = rows.filter((r) => r.verdict === "match").length;
|
|
966
|
+
const compatible = rows.filter((r) => r.verdict === "compatible").length;
|
|
967
|
+
const n = rows.length;
|
|
968
|
+
return { checked: n, matched, compatible, rate: n ? matched / n : 0, looseRate: n ? (matched + compatible) / n : 0, rows };
|
|
969
|
+
}
|
|
970
|
+
var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS;
|
|
971
|
+
var init_codeZones = __esm({
|
|
972
|
+
"../swarm-core/src/codeZones.ts"() {
|
|
973
|
+
"use strict";
|
|
974
|
+
SPINE_ZONE = "core";
|
|
975
|
+
DEFAULTS = {
|
|
976
|
+
minTasks: 5,
|
|
977
|
+
spineMinTasks: 3,
|
|
978
|
+
spineMinZones: 3,
|
|
979
|
+
spineDominantShare: 0.6
|
|
980
|
+
};
|
|
981
|
+
EMPTY_ZONE_MODEL = {
|
|
982
|
+
tasks: 0,
|
|
983
|
+
zones: [],
|
|
984
|
+
fileZone: /* @__PURE__ */ new Map(),
|
|
985
|
+
spine: [],
|
|
986
|
+
taskZone: /* @__PURE__ */ new Map(),
|
|
987
|
+
zoneTerms: /* @__PURE__ */ new Map(),
|
|
988
|
+
termZones: /* @__PURE__ */ new Map(),
|
|
989
|
+
entries: [],
|
|
990
|
+
entryTerms: []
|
|
991
|
+
};
|
|
992
|
+
GENERIC_DIRS = /* @__PURE__ */ new Set([
|
|
993
|
+
"src",
|
|
994
|
+
"source",
|
|
995
|
+
"lib",
|
|
996
|
+
"libs",
|
|
997
|
+
"util",
|
|
998
|
+
"utils",
|
|
999
|
+
"common",
|
|
1000
|
+
"internal",
|
|
1001
|
+
"app",
|
|
1002
|
+
"test",
|
|
1003
|
+
"tests",
|
|
1004
|
+
"__tests__",
|
|
1005
|
+
"spec",
|
|
1006
|
+
"specs",
|
|
1007
|
+
"index"
|
|
1008
|
+
]);
|
|
1009
|
+
WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
|
|
1010
|
+
GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
|
|
1011
|
+
STOP_TERMS = /* @__PURE__ */ new Set([
|
|
1012
|
+
"the",
|
|
1013
|
+
"and",
|
|
1014
|
+
"for",
|
|
1015
|
+
"with",
|
|
1016
|
+
"from",
|
|
1017
|
+
"into",
|
|
1018
|
+
"src",
|
|
1019
|
+
"test",
|
|
1020
|
+
"tests",
|
|
1021
|
+
"index",
|
|
1022
|
+
"packages",
|
|
1023
|
+
"package",
|
|
1024
|
+
"json",
|
|
1025
|
+
"yml",
|
|
1026
|
+
"yaml",
|
|
1027
|
+
"tsx",
|
|
1028
|
+
"mjs",
|
|
1029
|
+
"cjs",
|
|
1030
|
+
"\u0447\u0442\u043E",
|
|
1031
|
+
"\u043A\u0430\u043A",
|
|
1032
|
+
"\u0434\u043B\u044F",
|
|
1033
|
+
"\u043F\u0440\u0438",
|
|
1034
|
+
"\u044D\u0442\u043E",
|
|
1035
|
+
"\u0438\u043B\u0438",
|
|
1036
|
+
"\u0435\u0441\u043B\u0438",
|
|
1037
|
+
"\u0447\u0442\u043E\u0431\u044B",
|
|
1038
|
+
"\u0442\u043E\u0436\u0435",
|
|
1039
|
+
"\u0435\u0433\u043E",
|
|
1040
|
+
"\u0432\u0441\u0435",
|
|
1041
|
+
"feat",
|
|
1042
|
+
"fix",
|
|
1043
|
+
"chore",
|
|
1044
|
+
"docs",
|
|
1045
|
+
"refactor",
|
|
1046
|
+
"\u043F\u0435\u0440\u0444",
|
|
1047
|
+
"wip"
|
|
1048
|
+
]);
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
|
|
587
1052
|
// ../swarm-core/src/index.ts
|
|
588
1053
|
var init_src = __esm({
|
|
589
1054
|
"../swarm-core/src/index.ts"() {
|
|
@@ -600,156 +1065,638 @@ var init_src = __esm({
|
|
|
600
1065
|
init_names();
|
|
601
1066
|
init_baseBranch();
|
|
602
1067
|
init_hosts();
|
|
1068
|
+
init_hostPool();
|
|
603
1069
|
init_hostCommands();
|
|
604
1070
|
init_relief();
|
|
1071
|
+
init_cost();
|
|
1072
|
+
init_runGuards();
|
|
1073
|
+
init_codeZones();
|
|
605
1074
|
}
|
|
606
1075
|
});
|
|
607
1076
|
|
|
608
|
-
// src/
|
|
609
|
-
import {
|
|
610
|
-
import {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
1077
|
+
// src/claudeBin.ts
|
|
1078
|
+
import { existsSync, readdirSync } from "fs";
|
|
1079
|
+
import { win32 as winPath } from "path";
|
|
1080
|
+
function compareVersionDirs(a, b) {
|
|
1081
|
+
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
1082
|
+
const pa = parts(a);
|
|
1083
|
+
const pb = parts(b);
|
|
1084
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1085
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1086
|
+
if (d !== 0) return d;
|
|
1087
|
+
}
|
|
1088
|
+
return 0;
|
|
1089
|
+
}
|
|
1090
|
+
function realListDir(dir) {
|
|
615
1091
|
try {
|
|
616
|
-
return
|
|
1092
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
617
1093
|
} catch {
|
|
618
|
-
return
|
|
1094
|
+
return [];
|
|
619
1095
|
}
|
|
620
1096
|
}
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
1097
|
+
function winVersionRoots(env, listDir) {
|
|
1098
|
+
const roots = [];
|
|
1099
|
+
const appData = env.APPDATA;
|
|
1100
|
+
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
1101
|
+
const localAppData = env.LOCALAPPDATA;
|
|
1102
|
+
if (localAppData) {
|
|
1103
|
+
const packages = winPath.join(localAppData, "Packages");
|
|
1104
|
+
for (const name of listDir(packages)) {
|
|
1105
|
+
if (!name.startsWith("Claude")) continue;
|
|
1106
|
+
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return roots;
|
|
635
1110
|
}
|
|
636
|
-
function
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
1111
|
+
function findWindowsClaude(opts = {}) {
|
|
1112
|
+
const env = opts.env ?? process.env;
|
|
1113
|
+
const listDir = opts.listDir ?? realListDir;
|
|
1114
|
+
const fileExists = opts.fileExists ?? existsSync;
|
|
1115
|
+
let best = null;
|
|
1116
|
+
for (const root of winVersionRoots(env, listDir)) {
|
|
1117
|
+
for (const version of listDir(root)) {
|
|
1118
|
+
const candidate = winPath.join(root, version, "claude.exe");
|
|
1119
|
+
if (!fileExists(candidate)) continue;
|
|
1120
|
+
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return best?.path ?? null;
|
|
640
1124
|
}
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1125
|
+
function resolveClaudeBin(opts = {}) {
|
|
1126
|
+
const env = opts.env ?? process.env;
|
|
1127
|
+
const platform = opts.platform ?? process.platform;
|
|
1128
|
+
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
1129
|
+
if (explicit) return explicit;
|
|
1130
|
+
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
1131
|
+
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
1132
|
+
windowsScanDone = true;
|
|
1133
|
+
const found = findWindowsClaude({ ...opts, env });
|
|
1134
|
+
cachedWindowsBin = found;
|
|
1135
|
+
if (found) return found;
|
|
1136
|
+
if (!warnedNoWindowsClaude) {
|
|
1137
|
+
warnedNoWindowsClaude = true;
|
|
1138
|
+
console.warn(
|
|
1139
|
+
"[overmind] claude.exe \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0438 \u0432 %APPDATA%\\Claude\\claude-code, \u043D\u0438 \u0432 MSIX-\u043A\u043E\u043F\u0438\u0438 \u043F\u043E\u0434 %LOCALAPPDATA%\\Packages\\Claude* \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u0433\u043E\u043B\u043E\u0435 \xABclaude\xBB (\u043F\u043E\u0438\u0441\u043A \u043F\u043E PATH+PATHEXT). \u0423\u043F\u0430\u0434\u0451\u0442 ENOENT (bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0432 PATH \u043D\u0435 \u0432\u0438\u0434\u043D\u0430 spawn'\u0443) \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_CLAUDE_BIN \u043F\u0443\u0442\u0451\u043C \u043A \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u043C\u0443 claude.exe."
|
|
1140
|
+
);
|
|
646
1141
|
}
|
|
1142
|
+
return CLAUDE_FALLBACK;
|
|
647
1143
|
}
|
|
648
|
-
var
|
|
649
|
-
var
|
|
650
|
-
"src/
|
|
1144
|
+
var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
|
|
1145
|
+
var init_claudeBin = __esm({
|
|
1146
|
+
"src/claudeBin.ts"() {
|
|
651
1147
|
"use strict";
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
DRONE_HOME = process.env.SWARM_DRONE_HOME || join(SWARM_HOME, "drone-home");
|
|
657
|
-
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
658
|
-
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
659
|
-
WORKTREES_NOINDEX_DIR = join(SWARM_HOME, "worktrees.noindex");
|
|
660
|
-
WORKTREES_LEGACY_DIR = join(SWARM_HOME, "worktrees");
|
|
661
|
-
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
662
|
-
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
|
|
663
|
-
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join(SWARM_HOME, "overmind.log");
|
|
664
|
-
DIST_BUILD = true;
|
|
665
|
-
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
666
|
-
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
667
|
-
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
668
|
-
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
669
|
-
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
670
|
-
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
671
|
-
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
672
|
-
SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
|
|
673
|
-
SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
674
|
-
SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
675
|
-
SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
676
|
-
SWARM_RELIEF_CPU_BUSY = envNum("SWARM_RELIEF_CPU_BUSY", 0.95);
|
|
677
|
-
SWARM_RELIEF_COST_WINDOW_MS = envNum("SWARM_RELIEF_COST_WINDOW_MS", 60 * 6e4);
|
|
678
|
-
SWARM_RELIEF_ALERT_PAUSES = Math.trunc(envNum("SWARM_RELIEF_ALERT_PAUSES", 6));
|
|
679
|
-
SWARM_RELIEF_ALERT_LOST_MS = envNum("SWARM_RELIEF_ALERT_LOST_MS", 10 * 6e4);
|
|
680
|
-
SWARM_RELIEF_ALERT_MIN_STEPS = envNum("SWARM_RELIEF_ALERT_MIN_STEPS", 1);
|
|
681
|
-
SWARM_RELIEF_ALERT_SUSTAIN_MS = envNum("SWARM_RELIEF_ALERT_SUSTAIN_MS", 10 * 6e4);
|
|
682
|
-
SWARM_LOW_DISK_GB = envNum("SWARM_LOW_DISK_GB", 10);
|
|
683
|
-
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
684
|
-
SWARM_DRONE_BRIDGE_MS = envNum("SWARM_DRONE_BRIDGE_MS", 5e3);
|
|
685
|
-
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
686
|
-
SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
687
|
-
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
688
|
-
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
689
|
-
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
690
|
-
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
691
|
-
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join(SWARM_HOME, "drone-channel.key");
|
|
692
|
-
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
693
|
-
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
694
|
-
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
695
|
-
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
696
|
-
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
697
|
-
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
698
|
-
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
699
|
-
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
700
|
-
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
701
|
-
TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
702
|
-
TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
703
|
-
TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
704
|
-
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
705
|
-
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
706
|
-
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
707
|
-
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
|
|
708
|
-
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
709
|
-
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
710
|
-
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
711
|
-
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
712
|
-
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
713
|
-
SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
|
|
714
|
-
SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
|
|
715
|
-
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
716
|
-
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
717
|
-
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
718
|
-
SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
|
|
719
|
-
SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
|
|
720
|
-
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
721
|
-
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
722
|
-
SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
|
|
723
|
-
SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
|
|
1148
|
+
CLAUDE_FALLBACK = "claude";
|
|
1149
|
+
warnedNoWindowsClaude = false;
|
|
1150
|
+
cachedWindowsBin = null;
|
|
1151
|
+
windowsScanDone = false;
|
|
724
1152
|
}
|
|
725
1153
|
});
|
|
726
1154
|
|
|
727
|
-
// src/
|
|
728
|
-
function
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1155
|
+
// src/pricing.ts
|
|
1156
|
+
function costFromUsage(usage2, model, table = MODEL_PRICING) {
|
|
1157
|
+
const pricing = model ? table[model] : void 0;
|
|
1158
|
+
if (!pricing) {
|
|
1159
|
+
return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
|
|
1160
|
+
}
|
|
1161
|
+
const input = usage2.input_tokens ?? 0;
|
|
1162
|
+
const output = usage2.output_tokens ?? 0;
|
|
1163
|
+
const cacheWrite = usage2.cache_creation_input_tokens ?? 0;
|
|
1164
|
+
const cacheRead = usage2.cache_read_input_tokens ?? 0;
|
|
1165
|
+
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
1166
|
+
return { costUsd };
|
|
1167
|
+
}
|
|
1168
|
+
var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
|
|
1169
|
+
var init_pricing = __esm({
|
|
1170
|
+
"src/pricing.ts"() {
|
|
1171
|
+
"use strict";
|
|
1172
|
+
MODEL_PRICING = {
|
|
1173
|
+
// Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
|
|
1174
|
+
// Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
|
|
1175
|
+
// ветвление по времени внутри расчёта стоимости.
|
|
1176
|
+
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
1177
|
+
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1178
|
+
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
1179
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1180
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1181
|
+
};
|
|
1182
|
+
CACHE_READ_FACTOR = 0.1;
|
|
1183
|
+
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
736
1184
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// src/claudeResult.ts
|
|
1188
|
+
function parseClaudeResultJson(raw) {
|
|
1189
|
+
const trimmed = raw.trim();
|
|
1190
|
+
if (!trimmed) return null;
|
|
1191
|
+
try {
|
|
1192
|
+
const parsed = JSON.parse(trimmed);
|
|
1193
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1194
|
+
} catch {
|
|
1195
|
+
return null;
|
|
745
1196
|
}
|
|
746
1197
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1198
|
+
function launchCostUsd(result, model) {
|
|
1199
|
+
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
1200
|
+
return { costUsd: result.total_cost_usd };
|
|
1201
|
+
}
|
|
1202
|
+
if (!result.usage) return { costUsd: 0 };
|
|
1203
|
+
return costFromUsage(result.usage, model);
|
|
1204
|
+
}
|
|
1205
|
+
function launchSpend(result) {
|
|
1206
|
+
const u = result.usage ?? {};
|
|
1207
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
1208
|
+
return {
|
|
1209
|
+
tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
|
|
1210
|
+
tokensOut: num(u.output_tokens),
|
|
1211
|
+
durationMs: num(result.duration_ms)
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
var init_claudeResult = __esm({
|
|
1215
|
+
"src/claudeResult.ts"() {
|
|
750
1216
|
"use strict";
|
|
751
|
-
|
|
752
|
-
|
|
1217
|
+
init_pricing();
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// src/executors/claudeCode.ts
|
|
1222
|
+
import { readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
|
|
1223
|
+
import { homedir } from "os";
|
|
1224
|
+
import { join } from "path";
|
|
1225
|
+
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
1226
|
+
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
1227
|
+
const base3 = [
|
|
1228
|
+
"--dangerously-skip-permissions",
|
|
1229
|
+
...ISOLATION_FLAGS,
|
|
1230
|
+
"--mcp-config",
|
|
1231
|
+
cfgPath,
|
|
1232
|
+
...settings ? ["--settings", settings] : [],
|
|
1233
|
+
...pluginFlags,
|
|
1234
|
+
"--output-format",
|
|
1235
|
+
"json",
|
|
1236
|
+
"-p",
|
|
1237
|
+
prompt
|
|
1238
|
+
];
|
|
1239
|
+
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
1240
|
+
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
1241
|
+
}
|
|
1242
|
+
function droneDenyRules(paths) {
|
|
1243
|
+
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
1244
|
+
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
1245
|
+
}
|
|
1246
|
+
function droneSettingsJson(paths) {
|
|
1247
|
+
return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
|
|
1248
|
+
}
|
|
1249
|
+
function transcriptDir(worktree, projectsDir) {
|
|
1250
|
+
return join(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
1251
|
+
}
|
|
1252
|
+
function sessionTranscriptExists(worktree, projectsDir = join(homedir(), ".claude", "projects")) {
|
|
1253
|
+
try {
|
|
1254
|
+
return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
1255
|
+
} catch {
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function transcriptMtimeMs(worktree, projectsDir = join(homedir(), ".claude", "projects")) {
|
|
1260
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
1261
|
+
let files;
|
|
1262
|
+
try {
|
|
1263
|
+
files = readdirSync2(dir);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
let latest = null;
|
|
1268
|
+
for (const f of files) {
|
|
1269
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1270
|
+
try {
|
|
1271
|
+
const mtime2 = statSync(join(dir, f)).mtimeMs;
|
|
1272
|
+
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return latest;
|
|
1277
|
+
}
|
|
1278
|
+
function writeMcpConfig(spec) {
|
|
1279
|
+
const cfg = {
|
|
1280
|
+
mcpServers: {
|
|
1281
|
+
[spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
const path = join(spec.worktree, MCP_CONFIG_FILE);
|
|
1285
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2));
|
|
1286
|
+
return path;
|
|
1287
|
+
}
|
|
1288
|
+
var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
|
|
1289
|
+
var init_claudeCode = __esm({
|
|
1290
|
+
"src/executors/claudeCode.ts"() {
|
|
1291
|
+
"use strict";
|
|
1292
|
+
init_claudeBin();
|
|
1293
|
+
init_claudeResult();
|
|
1294
|
+
ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
1295
|
+
MCP_CONFIG_FILE = ".swarm-mcp.json";
|
|
1296
|
+
claudeCodeExecutor = {
|
|
1297
|
+
id: "claude-code",
|
|
1298
|
+
title: "Claude Code (claude)",
|
|
1299
|
+
binEnv: "SWARM_CLAUDE_BIN",
|
|
1300
|
+
install: { what: "Claude Code CLI \u2014 \u0438\u043C \u0434\u0440\u043E\u043D \u043F\u0438\u0448\u0435\u0442 \u043A\u043E\u0434", pkg: "@anthropic-ai/claude-code" },
|
|
1301
|
+
resolveBin: () => resolveClaudeBin(),
|
|
1302
|
+
// Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
|
|
1303
|
+
// проброс колбэком), и `this` тогда уедет в undefined.
|
|
1304
|
+
plan: (spec) => {
|
|
1305
|
+
const cfgPath = writeMcpConfig(spec);
|
|
1306
|
+
const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
|
|
1307
|
+
return {
|
|
1308
|
+
bin: resolveClaudeBin(),
|
|
1309
|
+
args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
|
|
1310
|
+
};
|
|
1311
|
+
},
|
|
1312
|
+
parseResult(raw, model) {
|
|
1313
|
+
const result = parseClaudeResultJson(raw);
|
|
1314
|
+
if (!result) return null;
|
|
1315
|
+
const est = launchCostUsd(result, model);
|
|
1316
|
+
const spend = launchSpend(result);
|
|
1317
|
+
return {
|
|
1318
|
+
costUsd: est.costUsd,
|
|
1319
|
+
...est.warning ? { warning: est.warning } : {},
|
|
1320
|
+
tokensIn: spend.tokensIn,
|
|
1321
|
+
tokensOut: spend.tokensOut,
|
|
1322
|
+
durationMs: spend.durationMs
|
|
1323
|
+
};
|
|
1324
|
+
},
|
|
1325
|
+
session: {
|
|
1326
|
+
exists: (worktree) => sessionTranscriptExists(worktree),
|
|
1327
|
+
mtimeMs: (worktree) => transcriptMtimeMs(worktree)
|
|
1328
|
+
},
|
|
1329
|
+
// `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
|
|
1330
|
+
// ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
|
|
1331
|
+
// (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
|
|
1332
|
+
probe: {
|
|
1333
|
+
args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
|
|
1334
|
+
// TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
|
|
1335
|
+
limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// src/swarmHome.ts
|
|
1342
|
+
import { homedir as homedir2 } from "os";
|
|
1343
|
+
import { join as join2 } from "path";
|
|
1344
|
+
import { mkdirSync } from "fs";
|
|
1345
|
+
var SWARM_HOME;
|
|
1346
|
+
var init_swarmHome = __esm({
|
|
1347
|
+
"src/swarmHome.ts"() {
|
|
1348
|
+
"use strict";
|
|
1349
|
+
SWARM_HOME = process.env.SWARM_HOME || join2(homedir2(), ".taskless-swarm");
|
|
1350
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
// src/executors/codex.ts
|
|
1355
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
|
|
1356
|
+
import { join as join3 } from "path";
|
|
1357
|
+
function resolveCodexBin(env = process.env) {
|
|
1358
|
+
return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
|
|
1359
|
+
}
|
|
1360
|
+
function codexHomePath(worktree, swarmHome = SWARM_HOME) {
|
|
1361
|
+
return join3(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
|
|
1362
|
+
}
|
|
1363
|
+
function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
|
|
1364
|
+
const home = codexHomePath(worktree, swarmHome);
|
|
1365
|
+
mkdirSync2(home, { recursive: true });
|
|
1366
|
+
const real = (env.CODEX_HOME ?? "").trim() || join3(realHome, ".codex");
|
|
1367
|
+
for (const file of ["auth.json", "config.toml"]) {
|
|
1368
|
+
const target = join3(real, file);
|
|
1369
|
+
const link = join3(home, file);
|
|
1370
|
+
if (!existsSync2(target) || existsSync2(link)) continue;
|
|
1371
|
+
try {
|
|
1372
|
+
symlinkSync(target, link);
|
|
1373
|
+
} catch {
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return home;
|
|
1377
|
+
}
|
|
1378
|
+
function tomlValue(v) {
|
|
1379
|
+
if (typeof v === "string") {
|
|
1380
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1381
|
+
return `"${escaped}"`;
|
|
1382
|
+
}
|
|
1383
|
+
if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
|
|
1384
|
+
return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
|
|
1385
|
+
}
|
|
1386
|
+
function mcpConfigArgs(spec) {
|
|
1387
|
+
const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
|
|
1388
|
+
return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
|
|
1389
|
+
}
|
|
1390
|
+
function skillsNotice(skillDirs) {
|
|
1391
|
+
if (skillDirs.length === 0) return "";
|
|
1392
|
+
return "\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u043E\u043B\u0438 \u043B\u0435\u0436\u0430\u0442 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C\u0438 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 (\u044D\u0442\u043E\u0442 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0438\u0445 \u0441\u0430\u043C): " + skillDirs.join(", ") + ". \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u0438\u0445 SKILL.md \u041F\u0415\u0420\u0415\u0414 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 \u2014 \u0442\u0430\u043C \u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430 \u0440\u043E\u044F \u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.";
|
|
1393
|
+
}
|
|
1394
|
+
function readUsage(node) {
|
|
1395
|
+
if (typeof node !== "object" || node === null) return null;
|
|
1396
|
+
const rec = node;
|
|
1397
|
+
const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
|
|
1398
|
+
if (direct && typeof direct === "object") return direct;
|
|
1399
|
+
const info = rec.info;
|
|
1400
|
+
if (info && typeof info === "object") return readUsage(info);
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
function parseCodexStream(raw) {
|
|
1404
|
+
let usage2 = null;
|
|
1405
|
+
let completed = false;
|
|
1406
|
+
for (const line of raw.split("\n")) {
|
|
1407
|
+
const trimmed = line.trim();
|
|
1408
|
+
if (!trimmed) continue;
|
|
1409
|
+
let parsed;
|
|
1410
|
+
try {
|
|
1411
|
+
parsed = JSON.parse(trimmed);
|
|
1412
|
+
} catch {
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
1416
|
+
const rec = parsed;
|
|
1417
|
+
const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
|
|
1418
|
+
const type = typeof msg.type === "string" ? msg.type : null;
|
|
1419
|
+
const found = readUsage(msg);
|
|
1420
|
+
if (found) usage2 = found;
|
|
1421
|
+
if (type && TERMINAL_EVENTS.has(type)) completed = true;
|
|
1422
|
+
}
|
|
1423
|
+
return { usage: usage2, completed };
|
|
1424
|
+
}
|
|
1425
|
+
function newestRolloutMtime(dir, depth = 4) {
|
|
1426
|
+
let entries;
|
|
1427
|
+
try {
|
|
1428
|
+
entries = readdirSync3(dir);
|
|
1429
|
+
} catch {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
let latest = null;
|
|
1433
|
+
for (const name of entries) {
|
|
1434
|
+
const full = join3(dir, name);
|
|
1435
|
+
let st;
|
|
1436
|
+
try {
|
|
1437
|
+
st = statSync2(full);
|
|
1438
|
+
} catch {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (st.isDirectory()) {
|
|
1442
|
+
if (depth <= 0) continue;
|
|
1443
|
+
const nested = newestRolloutMtime(full, depth - 1);
|
|
1444
|
+
if (nested !== null && (latest === null || nested > latest)) latest = nested;
|
|
1445
|
+
} else if (name.endsWith(".jsonl")) {
|
|
1446
|
+
if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return latest;
|
|
1450
|
+
}
|
|
1451
|
+
var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
|
|
1452
|
+
var init_codex = __esm({
|
|
1453
|
+
"src/executors/codex.ts"() {
|
|
1454
|
+
"use strict";
|
|
1455
|
+
init_swarmHome();
|
|
1456
|
+
init_pricing();
|
|
1457
|
+
BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
|
|
1458
|
+
SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
|
|
1459
|
+
CODEX_MODEL_PRICING = {};
|
|
1460
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
|
|
1461
|
+
codexExecutor = {
|
|
1462
|
+
id: "codex",
|
|
1463
|
+
title: "Codex CLI (codex)",
|
|
1464
|
+
binEnv: "SWARM_CODEX_BIN",
|
|
1465
|
+
install: { what: "Codex CLI \u2014 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C \u0434\u0440\u043E\u043D\u0430", pkg: "@openai/codex" },
|
|
1466
|
+
resolveBin: () => resolveCodexBin(),
|
|
1467
|
+
// Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
|
|
1468
|
+
plan: (spec) => {
|
|
1469
|
+
const notice = skillsNotice(spec.skillDirs);
|
|
1470
|
+
const prompt = notice ? `${spec.prompt}
|
|
1471
|
+
|
|
1472
|
+
${notice}` : spec.prompt;
|
|
1473
|
+
const args = [
|
|
1474
|
+
"exec",
|
|
1475
|
+
// Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
|
|
1476
|
+
// последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
|
|
1477
|
+
...spec.continueSession ? ["resume", "--last"] : [],
|
|
1478
|
+
"--json",
|
|
1479
|
+
SKIP_GIT_CHECK_FLAG,
|
|
1480
|
+
BYPASS_FLAG,
|
|
1481
|
+
...spec.model ? ["-m", spec.model] : [],
|
|
1482
|
+
...mcpConfigArgs(spec),
|
|
1483
|
+
prompt
|
|
1484
|
+
];
|
|
1485
|
+
return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
|
|
1486
|
+
},
|
|
1487
|
+
parseResult(raw, model) {
|
|
1488
|
+
const { usage: usage2, completed } = parseCodexStream(raw);
|
|
1489
|
+
if (!completed) return null;
|
|
1490
|
+
if (!usage2) return { costUsd: 0, warning: "codex: \u043F\u043E\u0442\u043E\u043A \u0431\u0435\u0437 usage \u2014 cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D" };
|
|
1491
|
+
const cached = usage2.cached_input_tokens ?? 0;
|
|
1492
|
+
const common = {
|
|
1493
|
+
input_tokens: Math.max(0, (usage2.input_tokens ?? 0) - cached),
|
|
1494
|
+
output_tokens: usage2.output_tokens ?? 0,
|
|
1495
|
+
cache_read_input_tokens: cached
|
|
1496
|
+
};
|
|
1497
|
+
const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
|
|
1498
|
+
return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
|
|
1499
|
+
},
|
|
1500
|
+
session: {
|
|
1501
|
+
exists: (worktree) => newestRolloutMtime(join3(codexHomePath(worktree), "sessions")) !== null,
|
|
1502
|
+
mtimeMs: (worktree) => newestRolloutMtime(join3(codexHomePath(worktree), "sessions"))
|
|
1503
|
+
},
|
|
1504
|
+
probe: {
|
|
1505
|
+
// Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
|
|
1506
|
+
// провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
|
|
1507
|
+
args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
|
|
1508
|
+
limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// src/executors/types.ts
|
|
1515
|
+
var init_types2 = __esm({
|
|
1516
|
+
"src/executors/types.ts"() {
|
|
1517
|
+
"use strict";
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
// src/executors/index.ts
|
|
1522
|
+
function resolveExecutor(id) {
|
|
1523
|
+
const key = (id ?? "").trim();
|
|
1524
|
+
if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
|
|
1525
|
+
const found = EXECUTORS[key];
|
|
1526
|
+
if (!found) {
|
|
1527
|
+
throw new Error(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C "${key}" (SWARM_EXECUTOR). \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B: ${Object.keys(EXECUTORS).join(", ")}`);
|
|
1528
|
+
}
|
|
1529
|
+
return found;
|
|
1530
|
+
}
|
|
1531
|
+
var DEFAULT_EXECUTOR_ID, EXECUTORS;
|
|
1532
|
+
var init_executors = __esm({
|
|
1533
|
+
"src/executors/index.ts"() {
|
|
1534
|
+
"use strict";
|
|
1535
|
+
init_claudeCode();
|
|
1536
|
+
init_codex();
|
|
1537
|
+
init_types2();
|
|
1538
|
+
init_claudeCode();
|
|
1539
|
+
init_codex();
|
|
1540
|
+
DEFAULT_EXECUTOR_ID = "claude-code";
|
|
1541
|
+
EXECUTORS = {
|
|
1542
|
+
[claudeCodeExecutor.id]: claudeCodeExecutor,
|
|
1543
|
+
[codexExecutor.id]: codexExecutor
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
// src/config.ts
|
|
1549
|
+
import { basename, join as join4, resolve } from "path";
|
|
1550
|
+
import { readFileSync, realpathSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1551
|
+
function normalizeRepo(repo) {
|
|
1552
|
+
const r = (repo ?? "").trim();
|
|
1553
|
+
if (!r) return "";
|
|
1554
|
+
try {
|
|
1555
|
+
return realpathSync(r);
|
|
1556
|
+
} catch {
|
|
1557
|
+
return resolve(r);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
1561
|
+
return join4(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
1562
|
+
}
|
|
1563
|
+
function resolveEffectiveWorktreesDir(deps) {
|
|
1564
|
+
if (deps.envOverride) return deps.envOverride;
|
|
1565
|
+
if (!deps.exists(deps.legacyDir) || !deps.isNonEmptyDir(deps.legacyDir)) return deps.targetDir;
|
|
1566
|
+
if (deps.liveDronesUnder(deps.legacyDir)) return deps.legacyDir;
|
|
1567
|
+
return deps.targetDir;
|
|
1568
|
+
}
|
|
1569
|
+
function envNum(name, def) {
|
|
1570
|
+
const raw = process.env[name];
|
|
1571
|
+
if (raw === void 0 || raw === "") return def;
|
|
1572
|
+
const n = Number(raw);
|
|
1573
|
+
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
1574
|
+
}
|
|
1575
|
+
function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
1576
|
+
const base3 = api.replace(/\/$/, "");
|
|
1577
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
1578
|
+
return `${base3}${p}`;
|
|
1579
|
+
}
|
|
1580
|
+
function tokenFromFile() {
|
|
1581
|
+
try {
|
|
1582
|
+
return readFileSync(join4(SWARM_HOME, "token"), "utf8").trim();
|
|
1583
|
+
} catch {
|
|
1584
|
+
return "";
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
|
|
1588
|
+
var init_config = __esm({
|
|
1589
|
+
"src/config.ts"() {
|
|
1590
|
+
"use strict";
|
|
1591
|
+
init_src();
|
|
1592
|
+
init_executors();
|
|
1593
|
+
init_swarmHome();
|
|
1594
|
+
init_swarmHome();
|
|
1595
|
+
DB_PATH = process.env.SWARM_DB || join4(SWARM_HOME, "swarm.db");
|
|
1596
|
+
EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
|
|
1597
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join4(SWARM_HOME, "drone-home");
|
|
1598
|
+
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
1599
|
+
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
1600
|
+
WORKTREES_NOINDEX_DIR = join4(SWARM_HOME, "worktrees.noindex");
|
|
1601
|
+
WORKTREES_LEGACY_DIR = join4(SWARM_HOME, "worktrees");
|
|
1602
|
+
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
1603
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join4(SWARM_HOME, "run-logs");
|
|
1604
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join4(SWARM_HOME, "overmind.log");
|
|
1605
|
+
DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
|
|
1606
|
+
DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
|
|
1607
|
+
DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
|
|
1608
|
+
DIST_BUILD = true;
|
|
1609
|
+
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
1610
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join4(SWARM_HOME, "outbox.jsonl");
|
|
1611
|
+
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
1612
|
+
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
1613
|
+
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
1614
|
+
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
1615
|
+
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
1616
|
+
SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
|
|
1617
|
+
SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
1618
|
+
SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
1619
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
1620
|
+
SWARM_RELIEF_CPU_BUSY = envNum("SWARM_RELIEF_CPU_BUSY", 0.95);
|
|
1621
|
+
SWARM_RELIEF_COST_WINDOW_MS = envNum("SWARM_RELIEF_COST_WINDOW_MS", 60 * 6e4);
|
|
1622
|
+
SWARM_RELIEF_ALERT_PAUSES = Math.trunc(envNum("SWARM_RELIEF_ALERT_PAUSES", 6));
|
|
1623
|
+
SWARM_RELIEF_ALERT_LOST_MS = envNum("SWARM_RELIEF_ALERT_LOST_MS", 10 * 6e4);
|
|
1624
|
+
SWARM_RELIEF_ALERT_MIN_STEPS = envNum("SWARM_RELIEF_ALERT_MIN_STEPS", 1);
|
|
1625
|
+
SWARM_RELIEF_ALERT_SUSTAIN_MS = envNum("SWARM_RELIEF_ALERT_SUSTAIN_MS", 10 * 6e4);
|
|
1626
|
+
SWARM_LOW_DISK_GB = envNum("SWARM_LOW_DISK_GB", 10);
|
|
1627
|
+
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
1628
|
+
SWARM_DRONE_BRIDGE_MS = envNum("SWARM_DRONE_BRIDGE_MS", 5e3);
|
|
1629
|
+
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
1630
|
+
SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
|
|
1631
|
+
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
1632
|
+
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
1633
|
+
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
1634
|
+
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
1635
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join4(SWARM_HOME, "drone-channel.key");
|
|
1636
|
+
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
1637
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join4(SWARM_HOME, "overmind.heartbeat");
|
|
1638
|
+
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
1639
|
+
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
1640
|
+
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
1641
|
+
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1642
|
+
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
1643
|
+
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join4(SWARM_HOME, "repo-map.json");
|
|
1644
|
+
SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
|
|
1645
|
+
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
1646
|
+
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
1647
|
+
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
1648
|
+
TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
1649
|
+
TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
1650
|
+
TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
1651
|
+
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
1652
|
+
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
1653
|
+
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
1654
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join4(SWARM_HOME, "review-cutoff.json");
|
|
1655
|
+
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
1656
|
+
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
1657
|
+
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
1658
|
+
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
1659
|
+
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
1660
|
+
SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
|
|
1661
|
+
SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
|
|
1662
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
1663
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
1664
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
1665
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
|
|
1666
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
|
|
1667
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
1668
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
1669
|
+
SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
|
|
1670
|
+
SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
// src/names.ts
|
|
1675
|
+
function pickName(rows) {
|
|
1676
|
+
const live = /* @__PURE__ */ new Set();
|
|
1677
|
+
const lastUse = /* @__PURE__ */ new Map();
|
|
1678
|
+
for (const r of rows) {
|
|
1679
|
+
if (!r.name) continue;
|
|
1680
|
+
if (NAME_LIVE.has(r.status)) live.add(r.name);
|
|
1681
|
+
const b = baseName(r.name);
|
|
1682
|
+
lastUse.set(b, Math.max(lastUse.get(b) ?? 0, r.id));
|
|
1683
|
+
}
|
|
1684
|
+
const ordered = [...DRONE_POOL].sort(
|
|
1685
|
+
(a, b) => (lastUse.get(a) ?? 0) - (lastUse.get(b) ?? 0) || DRONE_POOL.indexOf(a) - DRONE_POOL.indexOf(b)
|
|
1686
|
+
);
|
|
1687
|
+
for (let g = 1; ; g++) {
|
|
1688
|
+
for (const base3 of ordered) {
|
|
1689
|
+
const cand = genName(base3, g);
|
|
1690
|
+
if (!live.has(cand)) return cand;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
var NAME_LIVE;
|
|
1695
|
+
var init_names2 = __esm({
|
|
1696
|
+
"src/names.ts"() {
|
|
1697
|
+
"use strict";
|
|
1698
|
+
init_src();
|
|
1699
|
+
NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
753
1700
|
}
|
|
754
1701
|
});
|
|
755
1702
|
|
|
@@ -798,14 +1745,471 @@ var init_storm = __esm({
|
|
|
798
1745
|
}
|
|
799
1746
|
});
|
|
800
1747
|
|
|
1748
|
+
// src/repoRegistry.ts
|
|
1749
|
+
import { $ as $2 } from "execa";
|
|
1750
|
+
function parseRepoFullName(remoteUrl) {
|
|
1751
|
+
const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
|
|
1752
|
+
if (!m) return null;
|
|
1753
|
+
return `${m[1]}/${m[2]}`.toLowerCase();
|
|
1754
|
+
}
|
|
1755
|
+
async function resolveRepoFullName(path) {
|
|
1756
|
+
const res = await $2({ reject: false })`git -C ${path} remote get-url origin`;
|
|
1757
|
+
if (res.exitCode !== 0) return null;
|
|
1758
|
+
return parseRepoFullName(res.stdout);
|
|
1759
|
+
}
|
|
1760
|
+
async function buildRepoRegistry(paths) {
|
|
1761
|
+
const resolved = await Promise.all(
|
|
1762
|
+
paths.map(async (raw) => {
|
|
1763
|
+
const path = normalizeRepo(raw);
|
|
1764
|
+
if (!path) return null;
|
|
1765
|
+
const repoFullName = await resolveRepoFullName(path);
|
|
1766
|
+
return repoFullName ? { repoFullName, path } : null;
|
|
1767
|
+
})
|
|
1768
|
+
);
|
|
1769
|
+
return resolved.filter((e) => e !== null);
|
|
1770
|
+
}
|
|
1771
|
+
var init_repoRegistry = __esm({
|
|
1772
|
+
"src/repoRegistry.ts"() {
|
|
1773
|
+
"use strict";
|
|
1774
|
+
init_config();
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
// src/repoScan.ts
|
|
1779
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
|
|
1780
|
+
import { join as join6 } from "path";
|
|
1781
|
+
var init_repoScan = __esm({
|
|
1782
|
+
"src/repoScan.ts"() {
|
|
1783
|
+
"use strict";
|
|
1784
|
+
init_repoRegistry();
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
// src/repoDiscovery.ts
|
|
1789
|
+
import { homedir as homedir3 } from "os";
|
|
1790
|
+
import { readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1791
|
+
import { isAbsolute, join as join7, resolve as resolve2 } from "path";
|
|
1792
|
+
function repoMapLines(rows) {
|
|
1793
|
+
if (!rows?.length) return [];
|
|
1794
|
+
const missing = rows.filter((r) => !r.path).length;
|
|
1795
|
+
const head = `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F: ${rows.length}` + (missing ? `, \u0431\u0435\u0437 \u043A\u043B\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: ${missing}` : "");
|
|
1796
|
+
const body = rows.map((r) => {
|
|
1797
|
+
if (!r.path) return ` \u2717 ${r.repoFullName} \u2014 \u043A\u043B\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u041D\u0415\u0422 (\u0441\u0430\u043C \u043D\u0435 \u043A\u043B\u043E\u043D\u0438\u0440\u0443\u044E)`;
|
|
1798
|
+
const how = r.source ? ` (${SOURCE_TEXT[r.source]})` : "";
|
|
1799
|
+
const alt = r.alternatives.length ? ` \u26A0 \u0435\u0441\u0442\u044C \u0435\u0449\u0451 ${r.alternatives.length}: ${r.alternatives.join(", ")} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0432 \u043F\u0435\u0440\u0432\u043E\u043C` : "";
|
|
1800
|
+
return ` ${r.alternatives.length ? "\u26A0" : "\u2713"} ${r.repoFullName} \u2192 ${r.path}${how}${alt}`;
|
|
1801
|
+
});
|
|
1802
|
+
return [head, ...body];
|
|
1803
|
+
}
|
|
1804
|
+
function readRepoMapCache(path) {
|
|
1805
|
+
try {
|
|
1806
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
1807
|
+
if (!Array.isArray(raw?.entries)) return [];
|
|
1808
|
+
return raw.entries.filter(
|
|
1809
|
+
(e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
|
|
1810
|
+
);
|
|
1811
|
+
} catch {
|
|
1812
|
+
return [];
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
var SOURCE_TEXT;
|
|
1816
|
+
var init_repoDiscovery = __esm({
|
|
1817
|
+
"src/repoDiscovery.ts"() {
|
|
1818
|
+
"use strict";
|
|
1819
|
+
init_src();
|
|
1820
|
+
init_config();
|
|
1821
|
+
init_repoScan();
|
|
1822
|
+
SOURCE_TEXT = {
|
|
1823
|
+
configured: "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
|
|
1824
|
+
cache: "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B",
|
|
1825
|
+
scan: "\u043D\u0430\u0439\u0434\u0435\u043D \u0441\u043A\u0430\u043D\u043E\u043C"
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// package.json
|
|
1831
|
+
var package_default;
|
|
1832
|
+
var init_package = __esm({
|
|
1833
|
+
"package.json"() {
|
|
1834
|
+
package_default = {
|
|
1835
|
+
name: "@taskless-app/swarm",
|
|
1836
|
+
version: "0.6.0",
|
|
1837
|
+
type: "module",
|
|
1838
|
+
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
1839
|
+
keywords: [
|
|
1840
|
+
"ai-agents",
|
|
1841
|
+
"coding-agents",
|
|
1842
|
+
"swarm",
|
|
1843
|
+
"orchestration",
|
|
1844
|
+
"autonomous-agents",
|
|
1845
|
+
"mcp",
|
|
1846
|
+
"taskless",
|
|
1847
|
+
"cli"
|
|
1848
|
+
],
|
|
1849
|
+
license: "SEE LICENSE IN LICENSE",
|
|
1850
|
+
author: "Ilya Smurov <ilya@smurov.com>",
|
|
1851
|
+
homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
|
|
1852
|
+
repository: {
|
|
1853
|
+
type: "git",
|
|
1854
|
+
url: "git+https://github.com/ilyasmurov/taskless.git",
|
|
1855
|
+
directory: "packages/swarm-daemon"
|
|
1856
|
+
},
|
|
1857
|
+
engines: {
|
|
1858
|
+
node: ">=22"
|
|
1859
|
+
},
|
|
1860
|
+
bin: {
|
|
1861
|
+
swarm: "dist/cli.js"
|
|
1862
|
+
},
|
|
1863
|
+
files: [
|
|
1864
|
+
"dist",
|
|
1865
|
+
"docs/HERO.md",
|
|
1866
|
+
"roles"
|
|
1867
|
+
],
|
|
1868
|
+
scripts: {
|
|
1869
|
+
overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
|
|
1870
|
+
swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
|
|
1871
|
+
build: "tsup",
|
|
1872
|
+
start: "node dist/overmind.js",
|
|
1873
|
+
prepublishOnly: "pnpm run build",
|
|
1874
|
+
test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
|
|
1875
|
+
"test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
|
|
1876
|
+
"test:watch": "vitest",
|
|
1877
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
1878
|
+
lint: "echo 'no lint in swarm-daemon yet'"
|
|
1879
|
+
},
|
|
1880
|
+
dependencies: {
|
|
1881
|
+
"@hono/node-server": "^2.0.12",
|
|
1882
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1883
|
+
execa: "^10.0.0",
|
|
1884
|
+
zod: "3.25.76"
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
// src/version.ts
|
|
1891
|
+
var PACKAGE_VERSION, MIN_NODE_MAJOR;
|
|
1892
|
+
var init_version = __esm({
|
|
1893
|
+
"src/version.ts"() {
|
|
1894
|
+
"use strict";
|
|
1895
|
+
init_package();
|
|
1896
|
+
PACKAGE_VERSION = package_default.version;
|
|
1897
|
+
MIN_NODE_MAJOR = Number(/(\d+)/.exec(package_default.engines?.node ?? "")?.[1] ?? 0);
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
// src/droneChannel.ts
|
|
1902
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1903
|
+
import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
1904
|
+
import { dirname as dirname2 } from "path";
|
|
1905
|
+
import { serve } from "@hono/node-server";
|
|
1906
|
+
function loadChannelSecret(path) {
|
|
1907
|
+
try {
|
|
1908
|
+
const s = readFileSync8(path, "utf8").trim();
|
|
1909
|
+
if (s.length >= 32) return s;
|
|
1910
|
+
} catch {
|
|
1911
|
+
}
|
|
1912
|
+
const secret = randomBytes(32).toString("hex");
|
|
1913
|
+
mkdirSync5(dirname2(path), { recursive: true });
|
|
1914
|
+
writeFileSync6(path, secret, { mode: 384 });
|
|
1915
|
+
try {
|
|
1916
|
+
chmodSync(path, 384);
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
1919
|
+
return secret;
|
|
1920
|
+
}
|
|
1921
|
+
function sign(runKey, secret) {
|
|
1922
|
+
return createHmac("sha256", secret).update(runKey).digest("base64url");
|
|
1923
|
+
}
|
|
1924
|
+
function mintChannelToken(runKey, secret) {
|
|
1925
|
+
return `${Buffer.from(runKey, "utf8").toString("base64url")}.${sign(runKey, secret)}`;
|
|
1926
|
+
}
|
|
1927
|
+
function verifyChannelToken(token, secret) {
|
|
1928
|
+
const dot = token.lastIndexOf(".");
|
|
1929
|
+
if (dot <= 0) return null;
|
|
1930
|
+
const runKey = Buffer.from(token.slice(0, dot), "base64url").toString("utf8");
|
|
1931
|
+
if (!runKey) return null;
|
|
1932
|
+
const got = Buffer.from(token.slice(dot + 1));
|
|
1933
|
+
const want = Buffer.from(sign(runKey, secret));
|
|
1934
|
+
if (got.length !== want.length || !timingSafeEqual(got, want)) return null;
|
|
1935
|
+
return runKey;
|
|
1936
|
+
}
|
|
1937
|
+
function mintLocalCliToken(secret) {
|
|
1938
|
+
return mintChannelToken(LOCAL_CLI_KEY, secret);
|
|
1939
|
+
}
|
|
1940
|
+
function verifyLocalCliToken(token, secret) {
|
|
1941
|
+
return verifyChannelToken(token, secret) === LOCAL_CLI_KEY;
|
|
1942
|
+
}
|
|
1943
|
+
var LOCAL_CLI_KEY, EVENT_KIND_SET;
|
|
1944
|
+
var init_droneChannel = __esm({
|
|
1945
|
+
"src/droneChannel.ts"() {
|
|
1946
|
+
"use strict";
|
|
1947
|
+
init_src();
|
|
1948
|
+
LOCAL_CLI_KEY = "cli";
|
|
1949
|
+
EVENT_KIND_SET = new Set(EVENT_KINDS);
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
// src/agentToken.ts
|
|
1954
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
1955
|
+
import { join as join14 } from "path";
|
|
1956
|
+
function loadAgentToken() {
|
|
1957
|
+
try {
|
|
1958
|
+
const s = JSON.parse(readFileSync10(STORE_PATH2, "utf8"));
|
|
1959
|
+
return s.token ? s : null;
|
|
1960
|
+
} catch {
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function saveAgentToken(s) {
|
|
1965
|
+
writeFileSync8(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
|
|
1966
|
+
}
|
|
1967
|
+
function clearAgentToken() {
|
|
1968
|
+
try {
|
|
1969
|
+
unlinkSync3(STORE_PATH2);
|
|
1970
|
+
} catch {
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
async function mintAgentToken(args, fetchFn = fetch) {
|
|
1974
|
+
const res = await fetchFn(`${base2(args.api)}/api/swarm/agent/token`, {
|
|
1975
|
+
method: "POST",
|
|
1976
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${args.bearer}` },
|
|
1977
|
+
body: JSON.stringify(args.label ? { label: args.label } : {})
|
|
1978
|
+
});
|
|
1979
|
+
const text = await res.text();
|
|
1980
|
+
if (!res.ok) throw new Error(`agent token: HTTP ${res.status} ${text.slice(0, 200)}`);
|
|
1981
|
+
const j = JSON.parse(text);
|
|
1982
|
+
if (!j.token) throw new Error("agent token: \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 token");
|
|
1983
|
+
return { token: j.token, ...j.tokenId ? { tokenId: j.tokenId } : {} };
|
|
1984
|
+
}
|
|
1985
|
+
async function probeAgentToken(args, fetchFn = fetch) {
|
|
1986
|
+
try {
|
|
1987
|
+
const res = await fetchFn(`${base2(args.api)}/api/auth/me`, {
|
|
1988
|
+
headers: { Authorization: `Bearer ${args.token}` }
|
|
1989
|
+
});
|
|
1990
|
+
if (res.status === 401 || res.status === 403) return "revoked";
|
|
1991
|
+
return res.ok ? "ok" : "unreachable";
|
|
1992
|
+
} catch {
|
|
1993
|
+
return "unreachable";
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
var STORE_PATH2, base2;
|
|
1997
|
+
var init_agentToken = __esm({
|
|
1998
|
+
"src/agentToken.ts"() {
|
|
1999
|
+
"use strict";
|
|
2000
|
+
init_config();
|
|
2001
|
+
STORE_PATH2 = join14(SWARM_HOME, "agent.json");
|
|
2002
|
+
base2 = (api) => api.replace(/\/$/, "");
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
// src/screenshot.ts
|
|
2007
|
+
async function captureScreenshots(launchBrowser, opts) {
|
|
2008
|
+
const viewports = opts.viewports ?? DEFAULT_VIEWPORTS;
|
|
2009
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_SHOT_TIMEOUT_MS;
|
|
2010
|
+
const fullPage = opts.fullPage ?? true;
|
|
2011
|
+
const browser = await launchBrowser();
|
|
2012
|
+
try {
|
|
2013
|
+
const results = [];
|
|
2014
|
+
for (const vp of viewports) {
|
|
2015
|
+
const ctx = await browser.newContext({
|
|
2016
|
+
viewport: { width: vp.width, height: vp.height },
|
|
2017
|
+
...opts.storageStatePath ? { storageState: opts.storageStatePath } : {}
|
|
2018
|
+
});
|
|
2019
|
+
try {
|
|
2020
|
+
const page = await ctx.newPage();
|
|
2021
|
+
await page.goto(opts.url, { timeout: timeoutMs, waitUntil: "load" });
|
|
2022
|
+
await page.waitForNetworkSettle({
|
|
2023
|
+
quietMs: NETWORK_SETTLE_QUIET_MS,
|
|
2024
|
+
timeoutMs: Math.min(timeoutMs, NETWORK_SETTLE_TIMEOUT_MS)
|
|
2025
|
+
});
|
|
2026
|
+
const path = `${opts.outPrefix}-${vp.name}.png`;
|
|
2027
|
+
await page.screenshot({ path, fullPage, timeout: timeoutMs });
|
|
2028
|
+
results.push({ viewport: vp.name, path });
|
|
2029
|
+
} finally {
|
|
2030
|
+
await ctx.close();
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return results;
|
|
2034
|
+
} finally {
|
|
2035
|
+
await browser.close();
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
function isEventStream(headers2) {
|
|
2039
|
+
for (const [k, v] of Object.entries(headers2)) {
|
|
2040
|
+
if (k.toLowerCase() === "content-type") return v.toLowerCase().startsWith("text/event-stream");
|
|
2041
|
+
}
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
function createNetworkSettleTracker(target) {
|
|
2045
|
+
const pending2 = /* @__PURE__ */ new Set();
|
|
2046
|
+
const onChange = /* @__PURE__ */ new Set();
|
|
2047
|
+
const notify = () => {
|
|
2048
|
+
for (const cb of [...onChange]) cb();
|
|
2049
|
+
};
|
|
2050
|
+
target.on("request", ((req) => {
|
|
2051
|
+
pending2.add(req);
|
|
2052
|
+
notify();
|
|
2053
|
+
}));
|
|
2054
|
+
target.on("response", ((res) => {
|
|
2055
|
+
if (isEventStream(res.headers())) {
|
|
2056
|
+
pending2.delete(res.request());
|
|
2057
|
+
notify();
|
|
2058
|
+
}
|
|
2059
|
+
}));
|
|
2060
|
+
const onDone = ((req) => {
|
|
2061
|
+
pending2.delete(req);
|
|
2062
|
+
notify();
|
|
2063
|
+
});
|
|
2064
|
+
target.on("requestfinished", onDone);
|
|
2065
|
+
target.on("requestfailed", onDone);
|
|
2066
|
+
return {
|
|
2067
|
+
waitForSettle({ quietMs, timeoutMs }) {
|
|
2068
|
+
return new Promise((resolve4) => {
|
|
2069
|
+
let quietTimer;
|
|
2070
|
+
const hardTimer = setTimeout(finish, timeoutMs);
|
|
2071
|
+
function finish() {
|
|
2072
|
+
clearTimeout(quietTimer);
|
|
2073
|
+
clearTimeout(hardTimer);
|
|
2074
|
+
onChange.delete(check);
|
|
2075
|
+
resolve4();
|
|
2076
|
+
}
|
|
2077
|
+
function check() {
|
|
2078
|
+
clearTimeout(quietTimer);
|
|
2079
|
+
if (pending2.size === 0) quietTimer = setTimeout(finish, quietMs);
|
|
2080
|
+
}
|
|
2081
|
+
onChange.add(check);
|
|
2082
|
+
check();
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
function wrapPage(page) {
|
|
2088
|
+
const tracker = createNetworkSettleTracker(page);
|
|
2089
|
+
return {
|
|
2090
|
+
goto: (url, gotoOpts) => page.goto(url, gotoOpts),
|
|
2091
|
+
screenshot: (shotOpts) => page.screenshot(shotOpts),
|
|
2092
|
+
waitForNetworkSettle: (settleOpts) => tracker.waitForSettle(settleOpts)
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function wrapContext(ctx) {
|
|
2096
|
+
return {
|
|
2097
|
+
newPage: async () => wrapPage(await ctx.newPage()),
|
|
2098
|
+
close: () => ctx.close()
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function wrapBrowser(browser) {
|
|
2102
|
+
return {
|
|
2103
|
+
newContext: async (ctxOpts) => wrapContext(await browser.newContext(ctxOpts)),
|
|
2104
|
+
close: () => browser.close()
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
async function launchChromium(opts = {}) {
|
|
2108
|
+
let mod;
|
|
2109
|
+
try {
|
|
2110
|
+
mod = await import("playwright-core");
|
|
2111
|
+
} catch {
|
|
2112
|
+
throw new Error(
|
|
2113
|
+
"playwright-core \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438 \u043E\u0434\u0438\u043D \u0440\u0430\u0437: (\u0432 \u044D\u0442\u043E\u043C \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u043E) pnpm --filter @taskless-app/swarm add -D playwright-core; (\u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E) npm i -D playwright-core \u2014 \u0438 \u0432 \u043E\u0431\u043E\u0438\u0445 \u0441\u043B\u0443\u0447\u0430\u044F\u0445 npx playwright install chromium."
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
const browser = await mod.chromium.launch({ headless: true, ...opts.timeoutMs !== void 0 ? { timeout: opts.timeoutMs } : {} });
|
|
2117
|
+
return wrapBrowser(browser);
|
|
2118
|
+
}
|
|
2119
|
+
var DESKTOP_VIEWPORT, MOBILE_VIEWPORT, DEFAULT_VIEWPORTS, DEFAULT_SHOT_TIMEOUT_MS, NETWORK_SETTLE_QUIET_MS, NETWORK_SETTLE_TIMEOUT_MS;
|
|
2120
|
+
var init_screenshot = __esm({
|
|
2121
|
+
"src/screenshot.ts"() {
|
|
2122
|
+
"use strict";
|
|
2123
|
+
DESKTOP_VIEWPORT = { name: "desktop", width: 1360, height: 940 };
|
|
2124
|
+
MOBILE_VIEWPORT = { name: "mobile", width: 390, height: 844 };
|
|
2125
|
+
DEFAULT_VIEWPORTS = [DESKTOP_VIEWPORT, MOBILE_VIEWPORT];
|
|
2126
|
+
DEFAULT_SHOT_TIMEOUT_MS = 2e4;
|
|
2127
|
+
NETWORK_SETTLE_QUIET_MS = 500;
|
|
2128
|
+
NETWORK_SETTLE_TIMEOUT_MS = 8e3;
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
// src/daemonLink.ts
|
|
2133
|
+
function headers(extra = {}) {
|
|
2134
|
+
return { "X-Swarm-Local": mintLocalCliToken(loadChannelSecret(DRONE_CHANNEL_SECRET_PATH)), ...extra };
|
|
2135
|
+
}
|
|
2136
|
+
async function daemonRuns(status) {
|
|
2137
|
+
try {
|
|
2138
|
+
const qs = status ? `?status=${encodeURIComponent(status)}` : "";
|
|
2139
|
+
const res = await fetch(`${BASE}/runs${qs}`, { headers: headers(), signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
2140
|
+
if (!res.ok) return null;
|
|
2141
|
+
const body = await res.json();
|
|
2142
|
+
if (!Array.isArray(body.runs)) return null;
|
|
2143
|
+
return { runs: body.runs, backend: String(body.backend ?? ""), hostId: body.hostId ?? null };
|
|
2144
|
+
} catch {
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
async function post(path, form) {
|
|
2149
|
+
try {
|
|
2150
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
2151
|
+
method: "POST",
|
|
2152
|
+
headers: headers({ "Content-Type": "application/x-www-form-urlencoded" }),
|
|
2153
|
+
body: new URLSearchParams(form),
|
|
2154
|
+
signal: AbortSignal.timeout(ACTION_TIMEOUT_MS)
|
|
2155
|
+
});
|
|
2156
|
+
if (res.status === 503) return null;
|
|
2157
|
+
if (res.status === 401) return { ok: false, why: "\u043C\u043E\u0440\u0434\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B\u0430 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u043B\u044E\u0447 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C ~/.taskless-swarm/drone-channel.key" };
|
|
2158
|
+
return await res.json();
|
|
2159
|
+
} catch {
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function daemonCancel(slug, reason) {
|
|
2164
|
+
return post("/cancel", { slug, reason });
|
|
2165
|
+
}
|
|
2166
|
+
function daemonWake(runId) {
|
|
2167
|
+
return post("/wake", { runId: String(runId) });
|
|
2168
|
+
}
|
|
2169
|
+
function daemonPause(reason) {
|
|
2170
|
+
return post("/pause", { source: "cli", ...reason ? { reason } : {} });
|
|
2171
|
+
}
|
|
2172
|
+
function daemonResume() {
|
|
2173
|
+
return post("/resume", { source: "cli" });
|
|
2174
|
+
}
|
|
2175
|
+
async function daemonStop(opts) {
|
|
2176
|
+
const res = await post("/shutdown", { force: opts.force ? "1" : "0", wait: opts.wait ? "1" : "0" });
|
|
2177
|
+
if (!res) return null;
|
|
2178
|
+
const raw = res;
|
|
2179
|
+
if (raw.stopping === void 0 && raw.waiting === void 0) return null;
|
|
2180
|
+
return { stopping: !!raw.stopping, waiting: !!raw.waiting, sessions: Array.isArray(raw.sessions) ? raw.sessions : [] };
|
|
2181
|
+
}
|
|
2182
|
+
async function daemonStatus2() {
|
|
2183
|
+
try {
|
|
2184
|
+
const res = await fetch(`${BASE}/status`, { headers: headers(), signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
2185
|
+
if (!res.ok) return null;
|
|
2186
|
+
const body = await res.json();
|
|
2187
|
+
return typeof body?.paused === "boolean" ? body : null;
|
|
2188
|
+
} catch {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
var TIMEOUT_MS, ACTION_TIMEOUT_MS, BASE, NO_DAEMON_NOTE;
|
|
2193
|
+
var init_daemonLink = __esm({
|
|
2194
|
+
"src/daemonLink.ts"() {
|
|
2195
|
+
"use strict";
|
|
2196
|
+
init_config();
|
|
2197
|
+
init_droneChannel();
|
|
2198
|
+
TIMEOUT_MS = 5e3;
|
|
2199
|
+
ACTION_TIMEOUT_MS = 2e4;
|
|
2200
|
+
BASE = `http://127.0.0.1:${WEB_PORT}`;
|
|
2201
|
+
NO_DAEMON_NOTE = `\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT}) \u2014 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E \u041B\u041E\u041A\u0410\u041B\u042C\u041D\u042B\u0419 \u0441\u0442\u043E\u0440 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u044B, \u043E\u043D \u043C\u043E\u0436\u0435\u0442 \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u0442\u044C\u0441\u044F \u0441 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u043E\u043C. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0434\u0435\u043C\u043E\u043D\u0430: swarm start`;
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
|
|
801
2205
|
// src/db.ts
|
|
802
2206
|
var db_exports = {};
|
|
803
2207
|
__export(db_exports, {
|
|
804
2208
|
SwarmDb: () => SwarmDb
|
|
805
2209
|
});
|
|
806
2210
|
import { createRequire as createRequire2 } from "module";
|
|
807
|
-
import { existsSync as
|
|
808
|
-
import { resolve as
|
|
2211
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2212
|
+
import { resolve as resolve3 } from "path";
|
|
809
2213
|
function rowToQuestion(row) {
|
|
810
2214
|
return {
|
|
811
2215
|
id: row.id,
|
|
@@ -838,6 +2242,10 @@ function rowToRun(row) {
|
|
|
838
2242
|
depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
|
|
839
2243
|
model: row.model ?? null,
|
|
840
2244
|
cost_usd: row.cost_usd ?? 0,
|
|
2245
|
+
// Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
|
|
2246
|
+
tokens_in: row.tokens_in ?? 0,
|
|
2247
|
+
tokens_out: row.tokens_out ?? 0,
|
|
2248
|
+
duration_ms: row.duration_ms ?? 0,
|
|
841
2249
|
pr_url: row.pr_url ?? null,
|
|
842
2250
|
superseded_by: row.superseded_by ?? null,
|
|
843
2251
|
decision: row.decision ? JSON.parse(row.decision) : null,
|
|
@@ -903,6 +2311,9 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
903
2311
|
depends_on TEXT,
|
|
904
2312
|
model TEXT,
|
|
905
2313
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
2314
|
+
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
2315
|
+
tokens_out INTEGER NOT NULL DEFAULT 0,
|
|
2316
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
906
2317
|
pr_url TEXT,
|
|
907
2318
|
superseded_by INTEGER,
|
|
908
2319
|
decision TEXT,
|
|
@@ -996,7 +2407,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
996
2407
|
// Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
|
|
997
2408
|
"ALTER TABLE runs ADD COLUMN base_branch TEXT",
|
|
998
2409
|
// Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
|
|
999
|
-
"ALTER TABLE runs ADD COLUMN workspace_id TEXT"
|
|
2410
|
+
"ALTER TABLE runs ADD COLUMN workspace_id TEXT",
|
|
2411
|
+
// Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
|
|
2412
|
+
"ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
|
|
2413
|
+
"ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
|
|
2414
|
+
"ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
|
|
1000
2415
|
]) {
|
|
1001
2416
|
try {
|
|
1002
2417
|
this.db.exec(alter);
|
|
@@ -1285,11 +2700,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
1285
2700
|
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
1286
2701
|
*/
|
|
1287
2702
|
importDb(sourcePath, repo) {
|
|
1288
|
-
const abs =
|
|
2703
|
+
const abs = resolve3(sourcePath);
|
|
1289
2704
|
const repoN = normalizeRepo(repo);
|
|
1290
2705
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
1291
2706
|
if (already) return { imported: false, runs: already.run_count };
|
|
1292
|
-
if (!
|
|
2707
|
+
if (!existsSync10(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
1293
2708
|
const src = new this.databaseCtor(abs);
|
|
1294
2709
|
try {
|
|
1295
2710
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
@@ -1615,15 +3030,891 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
1615
3030
|
}
|
|
1616
3031
|
});
|
|
1617
3032
|
|
|
3033
|
+
// src/platform.ts
|
|
3034
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
3035
|
+
import { delimiter, join as join19 } from "path";
|
|
3036
|
+
function safeReadText(path) {
|
|
3037
|
+
try {
|
|
3038
|
+
return readFileSync14(path, "utf8");
|
|
3039
|
+
} catch {
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
3044
|
+
if (name.includes("/") || name.includes("\\")) return existsSync11(name) ? name : null;
|
|
3045
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
3046
|
+
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
3047
|
+
for (const dir of dirs) {
|
|
3048
|
+
for (const ext of exts) {
|
|
3049
|
+
const candidate = join19(dir, name + ext);
|
|
3050
|
+
if (existsSync11(candidate)) return candidate;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
function hasCommandInPath(name, env = process.env, platform = process.platform) {
|
|
3056
|
+
return resolveCommandPath(name, env, platform) !== null;
|
|
3057
|
+
}
|
|
3058
|
+
function isWsl(io = {}) {
|
|
3059
|
+
const env = io.env ?? process.env;
|
|
3060
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
3061
|
+
const readText = io.readText ?? safeReadText;
|
|
3062
|
+
const version = readText("/proc/version") ?? "";
|
|
3063
|
+
return /microsoft/i.test(version);
|
|
3064
|
+
}
|
|
3065
|
+
function detectPlatformKind(io = {}) {
|
|
3066
|
+
const platform = io.platform ?? process.platform;
|
|
3067
|
+
if (platform === "darwin") return "macos";
|
|
3068
|
+
if (platform === "win32") return "windows";
|
|
3069
|
+
return isWsl(io) ? "wsl" : "linux";
|
|
3070
|
+
}
|
|
3071
|
+
function detectSystemPm(kind, io = {}) {
|
|
3072
|
+
const env = io.env ?? process.env;
|
|
3073
|
+
const platform = io.platform ?? process.platform;
|
|
3074
|
+
const has = io.hasCommand ?? ((n) => hasCommandInPath(n, env, platform));
|
|
3075
|
+
if (kind === "macos") return has("brew") ? "brew" : null;
|
|
3076
|
+
if (kind === "windows") return has("winget") ? "winget" : null;
|
|
3077
|
+
return LINUX_PMS.find((p) => has(p.cmd))?.pm ?? null;
|
|
3078
|
+
}
|
|
3079
|
+
function detectPlatform(io = {}) {
|
|
3080
|
+
const kind = detectPlatformKind(io);
|
|
3081
|
+
const pm = detectSystemPm(kind, io);
|
|
3082
|
+
const isRoot = io.isRoot ?? (() => typeof process.getuid === "function" && process.getuid() === 0);
|
|
3083
|
+
const needsSudo = pm !== null && pm !== "brew" && pm !== "winget" && !isRoot();
|
|
3084
|
+
return { kind, pm, needsSudo, label: PLATFORM_LABEL[kind] };
|
|
3085
|
+
}
|
|
3086
|
+
function systemInstallCommand(info, packages) {
|
|
3087
|
+
if (!info.pm || packages.length === 0) return null;
|
|
3088
|
+
const list = packages.join(" ");
|
|
3089
|
+
const sudo = info.needsSudo ? "sudo " : "";
|
|
3090
|
+
switch (info.pm) {
|
|
3091
|
+
case "brew":
|
|
3092
|
+
return `brew install ${list}`;
|
|
3093
|
+
case "apt":
|
|
3094
|
+
return `${sudo}apt-get update && ${sudo}apt-get install -y ${list}`;
|
|
3095
|
+
case "dnf":
|
|
3096
|
+
return `${sudo}dnf install -y ${list}`;
|
|
3097
|
+
case "pacman":
|
|
3098
|
+
return `${sudo}pacman -S --needed --noconfirm ${list}`;
|
|
3099
|
+
case "zypper":
|
|
3100
|
+
return `${sudo}zypper install -y ${list}`;
|
|
3101
|
+
case "apk":
|
|
3102
|
+
return `${sudo}apk add ${list}`;
|
|
3103
|
+
case "winget":
|
|
3104
|
+
return `winget install --silent ${list}`;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
function detectGlobalPm(entry, io = {}) {
|
|
3108
|
+
const p = (entry ?? "").replace(/\\/g, "/");
|
|
3109
|
+
if (!p) return "unknown";
|
|
3110
|
+
if (p.endsWith(".ts") || /\/packages\/swarm-daemon\/(src|dist)\//.test(p)) return "sources";
|
|
3111
|
+
if (/\/\.bun\/install\/global\//.test(p) || /\/\.bun\/bin\//.test(p)) return "bun";
|
|
3112
|
+
if (/\/pnpm\//.test(p) || /\/\.pnpm-global\//.test(p) || /\/Library\/pnpm\//.test(p)) return "pnpm";
|
|
3113
|
+
if (/\/\.yarn\//.test(p) || /\/Yarn\/Data\/global\//.test(p) || /\/yarn\/global\//.test(p)) return "yarn";
|
|
3114
|
+
if (/\/node_modules\//.test(p)) return "npm";
|
|
3115
|
+
const env = io.env ?? process.env;
|
|
3116
|
+
const platform = io.platform ?? process.platform;
|
|
3117
|
+
const has = io.hasCommand ?? ((n) => hasCommandInPath(n, env, platform));
|
|
3118
|
+
return has("npm") ? "npm" : "unknown";
|
|
3119
|
+
}
|
|
3120
|
+
function globalInstallCommand(pm, spec) {
|
|
3121
|
+
switch (pm) {
|
|
3122
|
+
case "pnpm":
|
|
3123
|
+
return `pnpm add -g ${spec}`;
|
|
3124
|
+
case "bun":
|
|
3125
|
+
return `bun add -g ${spec}`;
|
|
3126
|
+
case "yarn":
|
|
3127
|
+
return `yarn global add ${spec}`;
|
|
3128
|
+
default:
|
|
3129
|
+
return `npm i -g ${spec}`;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
var LINUX_PMS, PLATFORM_LABEL;
|
|
3133
|
+
var init_platform = __esm({
|
|
3134
|
+
"src/platform.ts"() {
|
|
3135
|
+
"use strict";
|
|
3136
|
+
LINUX_PMS = [
|
|
3137
|
+
{ cmd: "apt-get", pm: "apt" },
|
|
3138
|
+
{ cmd: "dnf", pm: "dnf" },
|
|
3139
|
+
{ cmd: "pacman", pm: "pacman" },
|
|
3140
|
+
{ cmd: "zypper", pm: "zypper" },
|
|
3141
|
+
{ cmd: "apk", pm: "apk" }
|
|
3142
|
+
];
|
|
3143
|
+
PLATFORM_LABEL = {
|
|
3144
|
+
macos: "macOS",
|
|
3145
|
+
linux: "Linux",
|
|
3146
|
+
wsl: "WSL2 (Linux \u0432\u043D\u0443\u0442\u0440\u0438 Windows)",
|
|
3147
|
+
windows: "Windows (\u043D\u0430\u0442\u0438\u0432\u043D\u0430\u044F)"
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3152
|
+
// src/envGate.ts
|
|
3153
|
+
import { existsSync as existsSync12 } from "fs";
|
|
3154
|
+
import { join as join20 } from "path";
|
|
3155
|
+
import { createConnection } from "net";
|
|
3156
|
+
import { execa as execa3 } from "execa";
|
|
3157
|
+
function detectRequirements(repo, io = {}) {
|
|
3158
|
+
const exists = io.exists ?? existsSync12;
|
|
3159
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
|
|
3160
|
+
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
3161
|
+
}
|
|
3162
|
+
async function dockerAlive() {
|
|
3163
|
+
try {
|
|
3164
|
+
const res = await execa3("docker", ["version", "--format", "{{.Server.Version}}"], {
|
|
3165
|
+
timeout: PROBE_TIMEOUT_MS2,
|
|
3166
|
+
reject: false
|
|
3167
|
+
});
|
|
3168
|
+
return res.exitCode === 0;
|
|
3169
|
+
} catch {
|
|
3170
|
+
return false;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
var PROBE_TIMEOUT_MS2, COMPOSE_MARKERS;
|
|
3174
|
+
var init_envGate = __esm({
|
|
3175
|
+
"src/envGate.ts"() {
|
|
3176
|
+
"use strict";
|
|
3177
|
+
init_platform();
|
|
3178
|
+
PROBE_TIMEOUT_MS2 = 2500;
|
|
3179
|
+
COMPOSE_MARKERS = [
|
|
3180
|
+
"docker-compose.test.yml",
|
|
3181
|
+
"docker-compose.test.yaml",
|
|
3182
|
+
"docker-compose.yml",
|
|
3183
|
+
"docker-compose.yaml",
|
|
3184
|
+
"compose.yml",
|
|
3185
|
+
"compose.yaml"
|
|
3186
|
+
];
|
|
3187
|
+
}
|
|
3188
|
+
});
|
|
3189
|
+
|
|
3190
|
+
// src/release.ts
|
|
3191
|
+
async function fetchLatestVersion(opts = {}) {
|
|
3192
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
3193
|
+
const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
|
|
3194
|
+
const pkg = opts.pkg ?? SWARM_PACKAGE;
|
|
3195
|
+
const ctrl = new AbortController();
|
|
3196
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
3197
|
+
try {
|
|
3198
|
+
const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
|
|
3199
|
+
signal: ctrl.signal,
|
|
3200
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
|
|
3201
|
+
});
|
|
3202
|
+
if (!res.ok) return null;
|
|
3203
|
+
const body = await res.json();
|
|
3204
|
+
return typeof body.version === "string" && body.version ? body.version : null;
|
|
3205
|
+
} catch {
|
|
3206
|
+
return null;
|
|
3207
|
+
} finally {
|
|
3208
|
+
clearTimeout(timer);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
function isOutdated(current, latest) {
|
|
3212
|
+
if (!latest || !current) return false;
|
|
3213
|
+
return compareVersionDirs(latest, current) > 0;
|
|
3214
|
+
}
|
|
3215
|
+
var SWARM_PACKAGE, LATEST_TIMEOUT_MS;
|
|
3216
|
+
var init_release = __esm({
|
|
3217
|
+
"src/release.ts"() {
|
|
3218
|
+
"use strict";
|
|
3219
|
+
init_claudeBin();
|
|
3220
|
+
SWARM_PACKAGE = "@taskless-app/swarm";
|
|
3221
|
+
LATEST_TIMEOUT_MS = 4e3;
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3224
|
+
|
|
3225
|
+
// src/doctor.ts
|
|
3226
|
+
import { isAbsolute as isAbsolute4, join as join21 } from "path";
|
|
3227
|
+
function ok(id, title, detail) {
|
|
3228
|
+
return { id, title, status: "ok", detail, remedies: [], critical: false };
|
|
3229
|
+
}
|
|
3230
|
+
function nodeMajor(version) {
|
|
3231
|
+
const m = /^v?(\d+)/.exec(version.trim());
|
|
3232
|
+
return m ? Number(m[1]) : 0;
|
|
3233
|
+
}
|
|
3234
|
+
function checkNode(io) {
|
|
3235
|
+
const major = nodeMajor(io.nodeVersion);
|
|
3236
|
+
if (major >= MIN_NODE_MAJOR) return ok("node", "Node.js", `${io.nodeVersion} (\u043D\u0443\u0436\u043D\u043E \u2265 ${MIN_NODE_MAJOR})`);
|
|
3237
|
+
const command = io.platform.kind === "macos" ? `brew install node@${MIN_NODE_MAJOR}` : io.platform.kind === "windows" ? `winget install OpenJS.NodeJS.LTS` : `curl -fsSL https://fnm.vercel.app/install | bash && fnm install ${MIN_NODE_MAJOR}`;
|
|
3238
|
+
return {
|
|
3239
|
+
id: "node",
|
|
3240
|
+
title: "Node.js",
|
|
3241
|
+
status: "fail",
|
|
3242
|
+
detail: `${io.nodeVersion} \u2014 \u0441\u0442\u0430\u0440\u0435\u0435 ${MIN_NODE_MAJOR}. \u0420\u043E\u0439 \u043D\u0430 \u043D\u0451\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442`,
|
|
3243
|
+
remedies: [{ what: `Node ${MIN_NODE_MAJOR}+`, command, sudo: false, auto: false }],
|
|
3244
|
+
critical: true
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
function checkExecutor(io, executor = resolveExecutor()) {
|
|
3248
|
+
const bin = executor.resolveBin();
|
|
3249
|
+
const where = /[/\\]/.test(bin) ? `\u043D\u0430\u0439\u0434\u0435\u043D: ${bin}` : "\u043D\u0430\u0439\u0434\u0435\u043D \u0432 PATH";
|
|
3250
|
+
if (io.hasCommand(bin)) return ok("claude", executor.title, where);
|
|
3251
|
+
const remedies = [
|
|
3252
|
+
{
|
|
3253
|
+
what: executor.install.what,
|
|
3254
|
+
command: globalInstallCommand(io.globalPm === "sources" ? "npm" : io.globalPm, executor.install.pkg),
|
|
3255
|
+
sudo: false,
|
|
3256
|
+
auto: true
|
|
3257
|
+
}
|
|
3258
|
+
];
|
|
3259
|
+
const detail = io.platform.kind === "windows" ? `\u0432 PATH \u043D\u0435\u0442 \`${bin}\`. \u041D\u0430 Windows spawn \u0438\u0449\u0435\u0442 \u0444\u0430\u0439\u043B \u043F\u043E PATHEXT \u2014 bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0435\u043C\u0443 \u043D\u0435 \u0432\u0438\u0434\u043D\u0430; \u043D\u0435\u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u0430\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044F ${executor.binEnv}` : `\u0432 PATH \u043D\u0435\u0442 \`${bin}\` \u2014 \u0434\u0440\u043E\u043D\u0443 \u043D\u0435\u0447\u0435\u043C \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434`;
|
|
3260
|
+
return { id: "claude", title: executor.title, status: "fail", detail, remedies, critical: true };
|
|
3261
|
+
}
|
|
3262
|
+
async function checkGh(io) {
|
|
3263
|
+
if (!io.hasCommand("gh")) {
|
|
3264
|
+
const viaSystem = systemInstallCommand(io.platform, ["gh"]);
|
|
3265
|
+
return {
|
|
3266
|
+
id: "gh",
|
|
3267
|
+
title: "GitHub CLI (gh)",
|
|
3268
|
+
status: "fail",
|
|
3269
|
+
detail: "\u0432 PATH \u043D\u0435\u0442 `gh` \u2014 \u0434\u0440\u043E\u043D\u0443 \u043D\u0435\u0447\u0435\u043C \u043E\u0442\u043A\u0440\u044B\u0442\u044C pull request",
|
|
3270
|
+
remedies: [
|
|
3271
|
+
viaSystem ? { what: "GitHub CLI", command: viaSystem, sudo: io.platform.needsSudo, auto: true } : { what: "GitHub CLI \u2014 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C \u0441\u0432\u043E\u0435\u0439 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B", command: "\u0441\u043C. https://github.com/cli/cli#installation", sudo: false, auto: false },
|
|
3272
|
+
{ what: "\u0432\u0445\u043E\u0434 \u0432 GitHub (\u043F\u043E\u0441\u043B\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438)", command: "gh auth login", sudo: false, auto: false }
|
|
3273
|
+
],
|
|
3274
|
+
critical: true
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
const auth = await io.run("gh auth status");
|
|
3278
|
+
if (auth.exitCode === 0) return ok("gh", "GitHub CLI (gh)", "\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D, \u0432\u0445\u043E\u0434 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D");
|
|
3279
|
+
return {
|
|
3280
|
+
id: "gh",
|
|
3281
|
+
title: "GitHub CLI (gh)",
|
|
3282
|
+
status: "fail",
|
|
3283
|
+
detail: "`gh` \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D, \u043D\u043E \u0432\u0445\u043E\u0434 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u2014 `gh pr create` \u0443\u043F\u0430\u0434\u0451\u0442 \u0432 \u043A\u043E\u043D\u0446\u0435 \u0440\u0430\u0431\u043E\u0442\u044B \u0434\u0440\u043E\u043D\u0430",
|
|
3284
|
+
// Интерактивный вход за человека не сделаешь: он идёт через браузер и код подтверждения.
|
|
3285
|
+
remedies: [{ what: "\u0432\u0445\u043E\u0434 \u0432 GitHub", command: "gh auth login", sudo: false, auto: false }],
|
|
3286
|
+
critical: true
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
function checkSwarmPackage(io) {
|
|
3290
|
+
const globalBin = io.globalSwarmBin();
|
|
3291
|
+
if (io.globalPm === "sources") {
|
|
3292
|
+
return {
|
|
3293
|
+
id: "package",
|
|
3294
|
+
title: "\u041F\u0430\u043A\u0435\u0442 \u0440\u043E\u044F",
|
|
3295
|
+
status: "warn",
|
|
3296
|
+
detail: `\u0441\u0435\u0439\u0447\u0430\u0441 \u0442\u044B \u0437\u0430\u043F\u0443\u0449\u0435\u043D \u0418\u0417 \u0418\u0421\u0425\u041E\u0414\u041D\u0418\u041A\u041E\u0412 (${io.entry}), \u0430 \u043D\u0435 \u0438\u0437 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0430\u043A\u0435\u0442\u0430. \u042D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E \u0434\u043B\u044F \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u043A\u0438 \u0440\u043E\u044F, \u043D\u043E \u0434\u0435\u043C\u043E\u043D, \u043F\u043E\u0434\u043D\u044F\u0442\u044B\u0439 \u0442\u0430\u043A, \u0436\u0438\u0432\u0451\u0442 \u043D\u0430 \u043A\u043E\u0434\u0435 \u0440\u0430\u0431\u043E\u0447\u0435\u0433\u043E \u0434\u0435\u0440\u0435\u0432\u0430 \u2014 \u043D\u0435 \u043D\u0430 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438` + (globalBin ? `. \u0413\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0435\u0441\u0442\u044C: ${globalBin}` : ", \u0438 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u043F\u0430\u043A\u0435\u0442\u0430 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442"),
|
|
3297
|
+
remedies: globalBin ? [{ what: "\u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0442\u044C \u0440\u043E\u0439 \u0438\u0437 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0430\u043A\u0435\u0442\u0430", command: "swarm start", sudo: false, auto: false }] : [{ what: `${SWARM_PACKAGE} \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E`, command: globalInstallCommand("npm", SWARM_PACKAGE), sudo: false, auto: false }],
|
|
3298
|
+
critical: false
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
if (!globalBin) {
|
|
3302
|
+
return {
|
|
3303
|
+
id: "package",
|
|
3304
|
+
title: "\u041F\u0430\u043A\u0435\u0442 \u0440\u043E\u044F",
|
|
3305
|
+
status: "warn",
|
|
3306
|
+
detail: `\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \`swarm\` \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 PATH \u2014 \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0440\u0430\u0437\u043E\u0432\u044B\u0439 \u0437\u0430\u043F\u0443\u0441\u043A (${io.entry}). \u041F\u043E\u0441\u043B\u0435 \u043D\u0435\u0433\u043E \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F`,
|
|
3307
|
+
remedies: [{ what: `${SWARM_PACKAGE} \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E`, command: globalInstallCommand(io.globalPm, SWARM_PACKAGE), sudo: false, auto: true }],
|
|
3308
|
+
critical: false
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
return ok("package", "\u041F\u0430\u043A\u0435\u0442 \u0440\u043E\u044F", `v${io.packageVersion}, \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E: ${globalBin}`);
|
|
3312
|
+
}
|
|
3313
|
+
async function checkSwarmVersion(io) {
|
|
3314
|
+
const latest = await io.latestVersion();
|
|
3315
|
+
const cmp = latest === null ? 0 : compareVersionDirs(io.packageVersion, latest);
|
|
3316
|
+
if (latest === null) {
|
|
3317
|
+
return {
|
|
3318
|
+
id: "version",
|
|
3319
|
+
title: "\u0412\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F",
|
|
3320
|
+
status: "warn",
|
|
3321
|
+
detail: `v${io.packageVersion}; \u0434\u043E registry.npmjs.org \u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u2014 \u0441\u0432\u0435\u0440\u0438\u0442\u044C \u0441 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0439 \u043D\u0435\u0447\u0435\u043C`,
|
|
3322
|
+
remedies: [],
|
|
3323
|
+
critical: false
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
if (cmp >= 0) {
|
|
3327
|
+
return ok("version", "\u0412\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F", cmp > 0 ? `v${io.packageVersion} \u2014 \u043D\u043E\u0432\u0435\u0435 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0439 v${latest}` : `v${io.packageVersion} \u2014 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442 \u0441 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0439`);
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
id: "version",
|
|
3331
|
+
title: "\u0412\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F",
|
|
3332
|
+
status: "warn",
|
|
3333
|
+
detail: `v${io.packageVersion} \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest}`,
|
|
3334
|
+
// auto:false СОЗНАТЕЛЬНО: обновление своей же программы из своего же процесса — это спавн
|
|
3335
|
+
// `swarm update` изнутри `swarm install`, а перепроверить результат нечем (версия ЭТОГО
|
|
3336
|
+
// процесса после обновления пакета не меняется, он живёт на старом коде до перезапуска).
|
|
3337
|
+
// Раньше на этом цикл доводки крутил `swarm update` трижды и всё равно оставлял пункт жёлтым.
|
|
3338
|
+
remedies: [{ what: `\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C ${SWARM_PACKAGE} \u0434\u043E v${latest}`, command: "swarm update", sudo: false, auto: false }],
|
|
3339
|
+
critical: false
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
function chromiumRemedies(kind, io) {
|
|
3343
|
+
if (kind === "no-module") {
|
|
3344
|
+
const pm = io.globalPm === "sources" || io.globalPm === "unknown" ? "npm" : io.globalPm;
|
|
3345
|
+
return [
|
|
3346
|
+
{ what: "playwright-core \u0440\u044F\u0434\u043E\u043C \u0441 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u043C CLI", command: globalInstallCommand(pm, "playwright-core"), sudo: false, auto: true },
|
|
3347
|
+
{ what: "\u0441\u0430\u043C \u0431\u0440\u0430\u0443\u0437\u0435\u0440 Chromium", command: "npx playwright install chromium", sudo: false, auto: true }
|
|
3348
|
+
];
|
|
3349
|
+
}
|
|
3350
|
+
if (kind === "no-browser") {
|
|
3351
|
+
return [{ what: "\u0441\u0430\u043C \u0431\u0440\u0430\u0443\u0437\u0435\u0440 Chromium (~150 \u041C\u0411)", command: "npx playwright install chromium", sudo: false, auto: true }];
|
|
3352
|
+
}
|
|
3353
|
+
if (kind === "no-libs") {
|
|
3354
|
+
if (io.platform.pm === "apt") {
|
|
3355
|
+
return [
|
|
3356
|
+
{
|
|
3357
|
+
what: "\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0435 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 Chromium (libnss3, libatk, libgbm \u0438 \u0434\u0440.) \u2014 \u0442\u043E\u0447\u043D\u044B\u0439 \u0441\u043F\u0438\u0441\u043E\u043A \u0437\u043D\u0430\u0435\u0442 \u0441\u0430\u043C Playwright",
|
|
3358
|
+
command: `${io.platform.needsSudo ? "sudo " : ""}npx playwright install-deps chromium`,
|
|
3359
|
+
sudo: io.platform.needsSudo,
|
|
3360
|
+
auto: true
|
|
3361
|
+
}
|
|
3362
|
+
];
|
|
3363
|
+
}
|
|
3364
|
+
const libs = io.platform.pm ? CHROMIUM_LIBS[io.platform.pm] : void 0;
|
|
3365
|
+
const command = libs ? systemInstallCommand(io.platform, libs) : null;
|
|
3366
|
+
if (command) {
|
|
3367
|
+
return [
|
|
3368
|
+
{
|
|
3369
|
+
what: `\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0435 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 Chromium (\u0441\u043F\u0438\u0441\u043E\u043A \u0434\u043B\u044F ${io.platform.pm} \u2014 \u043D\u0430\u0448, \u0443 Playwright \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u043A\u0438 \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0434\u0438\u0441\u0442\u0440\u0438\u0431\u0443\u0442\u0438\u0432\u0430 \u043D\u0435\u0442; \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u043E\u043B\u043D\u044B\u043C)`,
|
|
3370
|
+
command,
|
|
3371
|
+
sudo: io.platform.needsSudo,
|
|
3372
|
+
auto: true
|
|
3373
|
+
}
|
|
3374
|
+
];
|
|
3375
|
+
}
|
|
3376
|
+
return [
|
|
3377
|
+
{
|
|
3378
|
+
what: "\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0435 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 Chromium \u2014 \u043F\u0430\u043A\u0435\u0442\u043D\u043E\u0433\u043E \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440\u0430 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435 \u043D\u0430\u0448\u043B\u0438, \u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430\u043C\u0438 \u0434\u0438\u0441\u0442\u0440\u0438\u0431\u0443\u0442\u0438\u0432\u0430",
|
|
3379
|
+
command: "nss nspr atk at-spi2-atk cups libdrm libXcomposite libXdamage libXfixes libXrandr libgbm alsa pango cairo",
|
|
3380
|
+
sudo: false,
|
|
3381
|
+
auto: false
|
|
3382
|
+
}
|
|
3383
|
+
];
|
|
3384
|
+
}
|
|
3385
|
+
return [];
|
|
3386
|
+
}
|
|
3387
|
+
async function checkScreenshots(io) {
|
|
3388
|
+
const probe = await io.probeChromium();
|
|
3389
|
+
if (probe.ok) return ok("screenshots", "\u0421\u043A\u0440\u0438\u043D\u0448\u043E\u0442\u044B (headless Chromium)", "Chromium \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u2014 `swarm shot` \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442");
|
|
3390
|
+
return {
|
|
3391
|
+
id: "screenshots",
|
|
3392
|
+
title: "\u0421\u043A\u0440\u0438\u043D\u0448\u043E\u0442\u044B (headless Chromium)",
|
|
3393
|
+
status: "fail",
|
|
3394
|
+
// Говорим ПОСЛЕДСТВИЕ, а не только причину: скрины — обязательная часть сдачи UI-задач,
|
|
3395
|
+
// и человек должен понимать, что именно он потеряет, если оставит пункт красным.
|
|
3396
|
+
detail: `Chromium \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u2014 \u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442\u044B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B, UI-\u0437\u0430\u0434\u0430\u0447\u0438 \u0431\u0443\u0434\u0443\u0442 \u0441\u0434\u0430\u0432\u0430\u0442\u044C\u0441\u044F \u0431\u0435\u0437 \u043D\u0438\u0445. ${probe.message}`,
|
|
3397
|
+
remedies: probe.kind ? chromiumRemedies(probe.kind, io) : [],
|
|
3398
|
+
critical: false
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
async function checkDocker(io, repoPaths) {
|
|
3402
|
+
const needed = repoPaths.filter((repo) => detectRequirements(repo, { exists: io.exists }).some((r) => r.kind === "docker"));
|
|
3403
|
+
if (needed.length === 0) {
|
|
3404
|
+
return ok("docker", "Docker", "\u043D\u0438 \u043E\u0434\u043D\u043E\u043C\u0443 \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E \u043D\u0435 \u043D\u0443\u0436\u0435\u043D (\u043D\u0435\u0442 compose-\u0444\u0430\u0439\u043B\u0430) \u2014 \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E \u0438 \u043D\u0435 \u0441\u0442\u0430\u0432\u043B\u044E");
|
|
3405
|
+
}
|
|
3406
|
+
if (await io.dockerAlive()) return ok("docker", "Docker", `\u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442; \u043D\u0443\u0436\u0435\u043D \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F\u043C: ${needed.join(", ")}`);
|
|
3407
|
+
const installed = io.hasCommand("docker");
|
|
3408
|
+
const remedies = installed ? [{ what: "\u0437\u0430\u043F\u0443\u0441\u0442\u0438 Docker (Desktop / `systemctl start docker`) \u2014 \u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442", command: io.platform.kind === "macos" ? "open -a Docker" : "sudo systemctl start docker", sudo: io.platform.kind !== "macos", auto: false }] : [
|
|
3409
|
+
io.platform.kind === "macos" ? { what: "Docker Desktop", command: "brew install --cask docker", sudo: false, auto: false } : { what: "Docker Engine \u2014 \u0441\u043F\u043E\u0441\u043E\u0431 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043E\u0442 \u0434\u0438\u0441\u0442\u0440\u0438\u0431\u0443\u0442\u0438\u0432\u0430", command: "\u0441\u043C. https://docs.docker.com/engine/install/", sudo: false, auto: false }
|
|
3410
|
+
];
|
|
3411
|
+
return {
|
|
3412
|
+
id: "docker",
|
|
3413
|
+
title: "Docker",
|
|
3414
|
+
status: "warn",
|
|
3415
|
+
detail: `${installed ? "\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D, \u043D\u043E \u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442" : "\u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D"}; \u043D\u0443\u0436\u0435\u043D \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F\u043C: ${needed.join(", ")} \u2014 \u0438\u0445 \u0434\u0440\u043E\u043D\u044B \u0431\u0443\u0434\u0443\u0442 \u043F\u0430\u0434\u0430\u0442\u044C \u043D\u0430 \u0442\u0435\u0441\u0442\u0430\u0445 \u0432 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0435`,
|
|
3416
|
+
remedies,
|
|
3417
|
+
critical: false
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
function checkRepos(io, snap) {
|
|
3421
|
+
const title = "\u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0438 SWARM_REPO";
|
|
3422
|
+
const env = snap.envRepo.trim();
|
|
3423
|
+
if (env && !isAbsolute4(env)) {
|
|
3424
|
+
return {
|
|
3425
|
+
id: "repos",
|
|
3426
|
+
title,
|
|
3427
|
+
status: "fail",
|
|
3428
|
+
detail: `SWARM_REPO \u0437\u0430\u0434\u0430\u043D \u041E\u0422\u041D\u041E\u0421\u0418\u0422\u0415\u041B\u042C\u041D\u042B\u041C \u043F\u0443\u0442\u0451\u043C \xAB${env}\xBB \u2014 \u043E\u043D \u0441\u043E\u0431\u0435\u0440\u0451\u0442\u0441\u044F \u043E\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430, \u0438 \u0434\u0440\u043E\u043D \u0443\u043C\u0440\u0451\u0442 \u043D\u0430 \`cannot change to \u2026\` (\u0442\u0430\u043A 09.08 \u043F\u043E\u0434\u0440\u044F\u0434 \u043F\u043E\u0433\u0438\u0431\u043B\u0438 \u0432\u043E\u0441\u0435\u043C\u044C \u0434\u0440\u043E\u043D\u043E\u0432)`,
|
|
3429
|
+
remedies: [{ what: "\u0443\u0431\u0440\u0430\u0442\u044C \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0443\u044E \u0432\u043E\u0432\u0441\u0435 \u2014 \u0440\u043E\u0439 \u043D\u0430\u0445\u043E\u0434\u0438\u0442 \u043A\u043B\u043E\u043D\u044B \u0441\u0430\u043C (TASWM-336) \u2014 \u043B\u0438\u0431\u043E \u0437\u0430\u0434\u0430\u0442\u044C \u0410\u0411\u0421\u041E\u041B\u042E\u0422\u041D\u042B\u0419 \u043F\u0443\u0442\u044C", command: "unset SWARM_REPO", sudo: false, auto: false }],
|
|
3430
|
+
critical: true
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
if (env && !io.exists(join21(env, ".git"))) {
|
|
3434
|
+
return {
|
|
3435
|
+
id: "repos",
|
|
3436
|
+
title,
|
|
3437
|
+
status: "fail",
|
|
3438
|
+
detail: `SWARM_REPO \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043D\u0430 \xAB${env}\xBB, \u0430 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u0442\u0430\u043C \u043D\u0435\u0442`,
|
|
3439
|
+
remedies: [{ what: "\u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0443\u0442\u044C \u0438\u043B\u0438 \u0443\u0431\u0440\u0430\u0442\u044C \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0443\u044E \u2014 \u0440\u043E\u0439 \u043D\u0430\u0439\u0434\u0451\u0442 \u043A\u043B\u043E\u043D\u044B \u0441\u0430\u043C", command: "unset SWARM_REPO", sudo: false, auto: false }],
|
|
3440
|
+
critical: true
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
const broken = snap.paths.filter((p) => !io.exists(join21(p, ".git")));
|
|
3444
|
+
if (broken.length) {
|
|
3445
|
+
return {
|
|
3446
|
+
id: "repos",
|
|
3447
|
+
title,
|
|
3448
|
+
status: "fail",
|
|
3449
|
+
detail: `\u0440\u043E\u0439 \u043F\u043E\u043C\u043D\u0438\u0442 \u043F\u0443\u0442\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u0443\u0436\u0435 \u043D\u0435\u0442: ${broken.join(", ")}`,
|
|
3450
|
+
remedies: [{ what: "\u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0443\u0442\u044C \u2014 \u043E\u043D \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u0441\u044F", command: "swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u043A\u043B\u043E\u043D\u0443>", sudo: false, auto: false }],
|
|
3451
|
+
critical: false
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
if (snap.missing.length) {
|
|
3455
|
+
return {
|
|
3456
|
+
id: "repos",
|
|
3457
|
+
title,
|
|
3458
|
+
status: "warn",
|
|
3459
|
+
// Рой сознательно НЕ клонирует сам (решение владельца 10.08: диск чужой).
|
|
3460
|
+
detail: `\u043A\u043B\u043E\u043D\u0430 \u043D\u0435\u0442 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435: ${snap.missing.join(", ")} \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u044D\u0442\u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0431\u0443\u0434\u0443\u0442 \u0441\u0442\u043E\u044F\u0442\u044C. \u0420\u043E\u0439 \u0441\u0430\u043C \u043D\u0435 \u043A\u043B\u043E\u043D\u0438\u0440\u0443\u0435\u0442`,
|
|
3461
|
+
remedies: [{ what: "\u0441\u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0443\u0436\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0432 \u0441\u0432\u043E\u0439 \u0440\u0430\u0431\u043E\u0447\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433", command: "git clone <url>", sudo: false, auto: false }],
|
|
3462
|
+
critical: false
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
if (snap.paths.length === 0) {
|
|
3466
|
+
return {
|
|
3467
|
+
id: "repos",
|
|
3468
|
+
title,
|
|
3469
|
+
status: "warn",
|
|
3470
|
+
detail: "\u0440\u043E\u0439 \u0435\u0449\u0451 \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u043B Taskless, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0433\u043E \u2014 \u0443\u0437\u043D\u0430\u0435\u0442 \u043F\u0440\u0438 \u043F\u0435\u0440\u0432\u043E\u043C `swarm start` \u0438 \u043D\u0430\u0439\u0434\u0451\u0442 \u0438\u0445 \u043A\u043B\u043E\u043D\u044B \u0441\u0430\u043C",
|
|
3471
|
+
remedies: [],
|
|
3472
|
+
critical: false
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
return ok("repos", title, `${snap.paths.length} \u043A\u043B\u043E\u043D(\u043E\u0432) \u043D\u0430 \u043C\u0435\u0441\u0442\u0435: ${snap.paths.join(", ")}${env ? ` (SWARM_REPO=${env})` : ""}`);
|
|
3476
|
+
}
|
|
3477
|
+
function checkToken(state) {
|
|
3478
|
+
const title = "\u0412\u0445\u043E\u0434 \u0432 Taskless";
|
|
3479
|
+
if (state === "ok") return ok("token", title, "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435 \u0438 \u043F\u0440\u0438\u043D\u044F\u0442 \u2014 \u043F\u0435\u0440\u0435\u043B\u043E\u0433\u0438\u043D \u043D\u0435 \u043D\u0443\u0436\u0435\u043D");
|
|
3480
|
+
if (state === "unreachable") {
|
|
3481
|
+
return { id: "token", title, status: "warn", detail: "\u0442\u043E\u043A\u0435\u043D \u0435\u0441\u0442\u044C, \u043D\u043E \u0434\u043E Taskless \u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0435\u0433\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0447\u0435\u043C", remedies: [], critical: false };
|
|
3482
|
+
}
|
|
3483
|
+
if (state === "revoked") {
|
|
3484
|
+
return {
|
|
3485
|
+
id: "token",
|
|
3486
|
+
title,
|
|
3487
|
+
status: "fail",
|
|
3488
|
+
detail: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u041D\u0415 \u043F\u0440\u0438\u043D\u044F\u0442 (401): \u0441\u0431\u0440\u043E\u0448\u0435\u043D \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0435\u043C \u0438\u043B\u0438 \u0440\u043E\u0439 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D \u043C\u0430\u0441\u0442\u0435\u0440-\u0442\u0443\u043C\u0431\u043B\u0435\u0440\u043E\u043C. \u041F\u043E\u043A\u0430 \u044D\u0442\u043E \u0442\u0430\u043A, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044F",
|
|
3489
|
+
remedies: [{ what: "\u0432\u043E\u0439\u0442\u0438 \u0437\u0430\u043D\u043E\u0432\u043E", command: "swarm login", sudo: false, auto: false }],
|
|
3490
|
+
critical: true
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
return {
|
|
3494
|
+
id: "token",
|
|
3495
|
+
title,
|
|
3496
|
+
status: "warn",
|
|
3497
|
+
detail: "\u0432\u0445\u043E\u0434 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F, \u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0443 \u0438\u0437 Taskless \u0431\u0440\u0430\u0442\u044C \u043D\u0435 \u0431\u0443\u0434\u0435\u0442",
|
|
3498
|
+
// Логин интерактивен (браузер + выбор воркспейса) — за человека его не сделаешь.
|
|
3499
|
+
remedies: [{ what: "\u0432\u043E\u0439\u0442\u0438 \u0432 \u0441\u0432\u043E\u0439 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441", command: "swarm login", sudo: false, auto: false }],
|
|
3500
|
+
critical: false
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
function checkDaemon(snap, opts) {
|
|
3504
|
+
if (!snap.alive) return ok("daemon", "\u0414\u0435\u043C\u043E\u043D \u0440\u043E\u044F", "\u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442 \u2014 \u043F\u043E\u0434\u043D\u044F\u0442\u044C: swarm start");
|
|
3505
|
+
const who = `v${snap.version ?? "?"}${snap.hostName ? `, \u043C\u0430\u0448\u0438\u043D\u0430 \xAB${snap.hostName}\xBB` : ""}`;
|
|
3506
|
+
if (snap.version && compareVersionDirs(opts.packageVersion, snap.version) > 0) {
|
|
3507
|
+
return {
|
|
3508
|
+
id: "daemon",
|
|
3509
|
+
title: "\u0414\u0435\u043C\u043E\u043D \u0440\u043E\u044F",
|
|
3510
|
+
status: "warn",
|
|
3511
|
+
detail: `\u043F\u043E\u0434\u043D\u044F\u0442 \u043D\u0430 \u0421\u0422\u0410\u0420\u041E\u041C \u043A\u043E\u0434\u0435 (${who}), \u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u0430\u043A\u0435\u0442 v${opts.packageVersion}. Node \u0434\u0435\u0440\u0436\u0438\u0442 \u043C\u043E\u0434\u0443\u043B\u044C-\u0433\u0440\u0430\u0444 \u0441 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u2014 \u0441\u0430\u043C \u043E\u043D \u043D\u0430 \u043D\u043E\u0432\u044B\u0439 \u043A\u043E\u0434 \u043D\u0435 \u043F\u0435\u0440\u0435\u0435\u0434\u0435\u0442`,
|
|
3512
|
+
// Перезапуск за человека не делаем: у демона могут быть живые дроны, и обрыв их работы —
|
|
3513
|
+
// его решение, а не наше (`swarm stop` сам покажет, кто работает).
|
|
3514
|
+
remedies: [{ what: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u043D\u043E\u0432\u043E\u043C \u043A\u043E\u0434\u0435", command: "swarm stop && swarm start", sudo: false, auto: false }],
|
|
3515
|
+
critical: false
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
return ok(
|
|
3519
|
+
"daemon",
|
|
3520
|
+
"\u0414\u0435\u043C\u043E\u043D \u0440\u043E\u044F",
|
|
3521
|
+
opts.releaseOutdated ? `\u0443\u0436\u0435 \u043F\u043E\u0434\u043D\u044F\u0442 (${who}) \u2014 \u0432\u0442\u043E\u0440\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u043D\u0430 \u0442\u043E\u0442 \u0436\u0435 \u043F\u043E\u0440\u0442 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E. \u041F\u043E\u0441\u043B\u0435 \`swarm update\` \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438: swarm stop && swarm start` : `\u0443\u0436\u0435 \u043F\u043E\u0434\u043D\u044F\u0442 (${who}) \u2014 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E. \u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A: swarm stop && swarm start`
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
async function runCheck(id, io) {
|
|
3525
|
+
switch (id) {
|
|
3526
|
+
case "node":
|
|
3527
|
+
return checkNode(io);
|
|
3528
|
+
case "package":
|
|
3529
|
+
return checkSwarmPackage(io);
|
|
3530
|
+
case "version":
|
|
3531
|
+
return checkSwarmVersion(io);
|
|
3532
|
+
case "claude":
|
|
3533
|
+
return checkExecutor(io);
|
|
3534
|
+
case "gh":
|
|
3535
|
+
return checkGh(io);
|
|
3536
|
+
case "screenshots":
|
|
3537
|
+
return checkScreenshots(io);
|
|
3538
|
+
case "docker":
|
|
3539
|
+
return checkDocker(io, (await io.repos()).paths);
|
|
3540
|
+
case "repos":
|
|
3541
|
+
return checkRepos(io, await io.repos());
|
|
3542
|
+
case "token":
|
|
3543
|
+
return checkToken(await io.token());
|
|
3544
|
+
default:
|
|
3545
|
+
return null;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
async function runDoctor(io) {
|
|
3549
|
+
const results = (await Promise.all(CHECK_ORDER.map((id) => runCheck(id, io)))).filter((r) => r !== null);
|
|
3550
|
+
results.push(
|
|
3551
|
+
checkDaemon(await io.daemon(), {
|
|
3552
|
+
packageVersion: io.packageVersion,
|
|
3553
|
+
// Спрашиваем ту же величину заново, а не ищем магическую строку в чужих remedies:
|
|
3554
|
+
// latestVersion мемоизирован, а переименование команды не должно молча ломать текст.
|
|
3555
|
+
releaseOutdated: isOutdated(io.packageVersion, await io.latestVersion())
|
|
3556
|
+
})
|
|
3557
|
+
);
|
|
3558
|
+
return results;
|
|
3559
|
+
}
|
|
3560
|
+
function renderReport(results) {
|
|
3561
|
+
const lines = [];
|
|
3562
|
+
for (const r of results) {
|
|
3563
|
+
lines.push(`${MARK[r.status]} ${r.title}: ${r.detail}`);
|
|
3564
|
+
if (r.status === "ok") continue;
|
|
3565
|
+
for (const rem of r.remedies) {
|
|
3566
|
+
lines.push(` ${rem.what}${rem.sudo ? " [\u043D\u0443\u0436\u0435\u043D sudo]" : ""}`);
|
|
3567
|
+
lines.push(` $ ${rem.command}`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
return lines;
|
|
3571
|
+
}
|
|
3572
|
+
function summarize(results) {
|
|
3573
|
+
const fails = results.filter((r) => r.status === "fail");
|
|
3574
|
+
const warns = results.filter((r) => r.status === "warn");
|
|
3575
|
+
if (fails.length === 0 && warns.length === 0) return { line: "\u0433\u043E\u0442\u043E\u0432\u043E: \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0432 \u0440\u0430\u0431\u043E\u0447\u0435\u043C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0438", exitCode: 0 };
|
|
3576
|
+
if (fails.length === 0) {
|
|
3577
|
+
return { line: `\u0440\u043E\u0439 \u043F\u043E\u0435\u0434\u0435\u0442, \u043D\u043E \u0435\u0441\u0442\u044C \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u044F (${warns.length}): ${warns.map((r) => r.title).join(", ")}`, exitCode: 0 };
|
|
3578
|
+
}
|
|
3579
|
+
const critical = fails.filter((r) => r.critical);
|
|
3580
|
+
return {
|
|
3581
|
+
line: critical.length ? `\u0440\u043E\u0439 \u041D\u0415 \u043F\u043E\u0435\u0434\u0435\u0442, \u043F\u043E\u043A\u0430 \u043D\u0435 \u043F\u043E\u0447\u0438\u043D\u0435\u043D\u043E: ${critical.map((r) => r.title).join(", ")}` + (fails.length > critical.length ? `; \u043A\u0440\u043E\u043C\u0435 \u0442\u043E\u0433\u043E \u0441\u043B\u043E\u043C\u0430\u043D\u043E: ${fails.filter((r) => !r.critical).map((r) => r.title).join(", ")}` : "") : `\u0440\u043E\u0439 \u043F\u043E\u0435\u0434\u0435\u0442, \u043D\u043E \u0441\u043B\u043E\u043C\u0430\u043D\u043E: ${fails.map((r) => r.title).join(", ")}`,
|
|
3582
|
+
exitCode: 1
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
var CHROMIUM_LIBS, CHECK_ORDER, MARK;
|
|
3586
|
+
var init_doctor = __esm({
|
|
3587
|
+
"src/doctor.ts"() {
|
|
3588
|
+
"use strict";
|
|
3589
|
+
init_claudeBin();
|
|
3590
|
+
init_executors();
|
|
3591
|
+
init_envGate();
|
|
3592
|
+
init_platform();
|
|
3593
|
+
init_release();
|
|
3594
|
+
init_version();
|
|
3595
|
+
CHROMIUM_LIBS = {
|
|
3596
|
+
dnf: ["nss", "nspr", "atk", "at-spi2-atk", "cups-libs", "libdrm", "libXcomposite", "libXdamage", "libXfixes", "libXrandr", "mesa-libgbm", "alsa-lib", "pango", "cairo"],
|
|
3597
|
+
pacman: ["nss", "nspr", "atk", "at-spi2-atk", "libcups", "libdrm", "libxcomposite", "libxdamage", "libxfixes", "libxrandr", "mesa", "alsa-lib", "pango", "cairo"],
|
|
3598
|
+
zypper: ["mozilla-nss", "mozilla-nspr", "libatk-1_0-0", "at-spi2-atk", "cups-libs", "libdrm2", "libXcomposite1", "libXdamage1", "libXfixes3", "libXrandr2", "libgbm1", "alsa", "pango", "cairo"],
|
|
3599
|
+
apk: ["nss", "nspr", "atk", "at-spi2-atk", "cups-libs", "libdrm", "libxcomposite", "libxdamage", "libxfixes", "libxrandr", "mesa-gbm", "alsa-lib", "pango", "cairo"]
|
|
3600
|
+
};
|
|
3601
|
+
CHECK_ORDER = ["node", "package", "version", "claude", "gh", "screenshots", "docker", "repos", "token"];
|
|
3602
|
+
MARK = { ok: "\u2713", warn: "\u26A0", fail: "\u2715" };
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
// src/repoMemory.ts
|
|
3607
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
|
|
3608
|
+
import { join as join22 } from "path";
|
|
3609
|
+
function loadRememberedRepos(home) {
|
|
3610
|
+
let raw;
|
|
3611
|
+
try {
|
|
3612
|
+
raw = readFileSync15(join22(home, FILE), "utf8");
|
|
3613
|
+
} catch {
|
|
3614
|
+
return [];
|
|
3615
|
+
}
|
|
3616
|
+
try {
|
|
3617
|
+
const parsed = JSON.parse(raw);
|
|
3618
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
3619
|
+
return parsed.paths.filter((p) => typeof p === "string" && p.trim().length > 0);
|
|
3620
|
+
} catch {
|
|
3621
|
+
return [];
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
var FILE;
|
|
3625
|
+
var init_repoMemory = __esm({
|
|
3626
|
+
"src/repoMemory.ts"() {
|
|
3627
|
+
"use strict";
|
|
3628
|
+
FILE = "repos.json";
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
// src/installCli.ts
|
|
3633
|
+
var installCli_exports = {};
|
|
3634
|
+
__export(installCli_exports, {
|
|
3635
|
+
CHROMIUM_PROBE_MS: () => CHROMIUM_PROBE_MS,
|
|
3636
|
+
MAX_FIX_ROUNDS: () => MAX_FIX_ROUNDS,
|
|
3637
|
+
classifyChromiumError: () => classifyChromiumError,
|
|
3638
|
+
defaultDoctorIO: () => defaultDoctorIO,
|
|
3639
|
+
nextSteps: () => nextSteps,
|
|
3640
|
+
probeChromium: () => probeChromium,
|
|
3641
|
+
readReposSnapshot: () => readReposSnapshot,
|
|
3642
|
+
readTokenState: () => readTokenState,
|
|
3643
|
+
runDoctorCli: () => runDoctorCli,
|
|
3644
|
+
runInstallCli: () => runInstallCli,
|
|
3645
|
+
shortenError: () => shortenError
|
|
3646
|
+
});
|
|
3647
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3648
|
+
function classifyChromiumError(message) {
|
|
3649
|
+
if (/playwright-core не установлен|Cannot find (module|package)/i.test(message)) return "no-module";
|
|
3650
|
+
if (/error while loading shared libraries|cannot open shared object file|missing dependencies|\blib[\w.+-]*\.so[.\d]*\b/i.test(message)) return "no-libs";
|
|
3651
|
+
if (/Executable doesn't exist|playwright install|browserType\.launch: Executable/i.test(message)) return "no-browser";
|
|
3652
|
+
return "other";
|
|
3653
|
+
}
|
|
3654
|
+
function shortenError(message, maxLines = 3) {
|
|
3655
|
+
return message.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, maxLines).join(" \xB7 ");
|
|
3656
|
+
}
|
|
3657
|
+
async function probeChromium(launch = () => launchChromium({ timeoutMs: CHROMIUM_PROBE_MS })) {
|
|
3658
|
+
try {
|
|
3659
|
+
const browser = await launch();
|
|
3660
|
+
await browser.close().catch(() => {
|
|
3661
|
+
});
|
|
3662
|
+
return { ok: true, kind: null, message: "" };
|
|
3663
|
+
} catch (e) {
|
|
3664
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
3665
|
+
return { ok: false, kind: classifyChromiumError(raw), message: shortenError(raw) };
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
function readReposSnapshot(st) {
|
|
3669
|
+
const envRepo = SWARM_REPO;
|
|
3670
|
+
if (st?.repos?.length) {
|
|
3671
|
+
return {
|
|
3672
|
+
envRepo,
|
|
3673
|
+
paths: st.repos.flatMap((r) => r.path ? [r.path] : []),
|
|
3674
|
+
missing: st.repos.filter((r) => !r.path).map((r) => r.repoFullName)
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
const cached = readRepoMapCache(REPO_MAP_PATH).map((e) => e.path);
|
|
3678
|
+
const paths = [];
|
|
3679
|
+
for (const p of [...SWARM_REPO_PATHS, ...cached, ...loadRememberedRepos(SWARM_HOME)]) {
|
|
3680
|
+
if (p && !paths.includes(p)) paths.push(p);
|
|
3681
|
+
}
|
|
3682
|
+
return { envRepo, paths, missing: [] };
|
|
3683
|
+
}
|
|
3684
|
+
async function readTokenState() {
|
|
3685
|
+
const store = loadAgentToken();
|
|
3686
|
+
if (!store) return "none";
|
|
3687
|
+
return probeAgentToken({ api: TASKLESS_API, token: store.token });
|
|
3688
|
+
}
|
|
3689
|
+
function defaultDoctorIO(packageVersion) {
|
|
3690
|
+
const platform = detectPlatform();
|
|
3691
|
+
const entry = process.argv[1] ?? "";
|
|
3692
|
+
let statusMemo = null;
|
|
3693
|
+
const status = () => statusMemo ??= daemonStatus2();
|
|
3694
|
+
let latestMemo = null;
|
|
3695
|
+
return {
|
|
3696
|
+
platform,
|
|
3697
|
+
globalPm: detectGlobalPm(entry),
|
|
3698
|
+
nodeVersion: process.version,
|
|
3699
|
+
packageVersion,
|
|
3700
|
+
entry,
|
|
3701
|
+
hasCommand: (name) => hasCommandInPath(name),
|
|
3702
|
+
exists: existsSync13,
|
|
3703
|
+
run: (command) => runQuiet(command),
|
|
3704
|
+
latestVersion: () => latestMemo ??= fetchLatestVersion(),
|
|
3705
|
+
probeChromium: () => probeChromium(),
|
|
3706
|
+
dockerAlive,
|
|
3707
|
+
// та же проба, что у гейта окружения демона (envGate) — и с тем же таймаутом
|
|
3708
|
+
repos: async () => readReposSnapshot(await status()),
|
|
3709
|
+
token: readTokenState,
|
|
3710
|
+
daemon: async () => {
|
|
3711
|
+
const st = await status();
|
|
3712
|
+
return st ? { alive: true, version: st.version, hostName: st.hostName ?? void 0 } : { alive: false };
|
|
3713
|
+
},
|
|
3714
|
+
// Нас самих в PATH может не быть (npx-запуск), и это отдельный факт: после разового
|
|
3715
|
+
// запуска на машине ничего не останется. Возвращаем ПУТЬ, а не «да/нет»: на машине с
|
|
3716
|
+
// историей их бывает несколько, и человек должен видеть, какой именно нашёлся.
|
|
3717
|
+
globalSwarmBin: () => resolveCommandPath("swarm")
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
function header(io) {
|
|
3721
|
+
const pm = io.platform.pm ? `\u043F\u0430\u043A\u0435\u0442\u043D\u044B\u0439 \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440: ${io.platform.pm}` : "\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u0433\u043E \u043F\u0430\u043A\u0435\u0442\u043D\u043E\u0433\u043E \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440\u0430 \u043D\u0435 \u043D\u0430\u0448\u043B\u0438";
|
|
3722
|
+
return `\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430: ${io.platform.label}; ${pm}${io.platform.needsSudo ? " (\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0435 \u043F\u0430\u043A\u0435\u0442\u044B \u2014 \u0447\u0435\u0440\u0435\u0437 sudo)" : ""}. Node ${io.nodeVersion}, \u0440\u043E\u0439 v${io.packageVersion}`;
|
|
3723
|
+
}
|
|
3724
|
+
function nextSteps(results, daemonAlive) {
|
|
3725
|
+
const steps = [];
|
|
3726
|
+
const token = results.find((r) => r.id === "token");
|
|
3727
|
+
if (token && token.status !== "ok") steps.push("\u0432\u043E\u0439\u0442\u0438 \u0432 \u0441\u0432\u043E\u0439 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441: swarm login");
|
|
3728
|
+
if (daemonAlive) steps.push("\u0434\u0435\u043C\u043E\u043D \u0443\u0436\u0435 \u043F\u043E\u0434\u043D\u044F\u0442; \u0447\u0442\u043E\u0431\u044B \u043E\u043D \u043F\u043E\u0435\u0445\u0430\u043B \u043D\u0430 \u043D\u043E\u0432\u043E\u043C \u043A\u043E\u0434\u0435: swarm stop && swarm start");
|
|
3729
|
+
else steps.push("\u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0440\u043E\u0439: swarm start");
|
|
3730
|
+
steps.push("\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043F\u043E\u0442\u043E\u043C: swarm status \xB7 \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443: swarm doctor");
|
|
3731
|
+
return steps;
|
|
3732
|
+
}
|
|
3733
|
+
async function runDoctorCli(_argv, deps = {}) {
|
|
3734
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
3735
|
+
const io = deps.io ?? defaultDoctorIO(deps.packageVersion ?? "0.0.0");
|
|
3736
|
+
out(header(io));
|
|
3737
|
+
out("");
|
|
3738
|
+
const results = await runDoctor(io);
|
|
3739
|
+
renderReport(results).forEach(out);
|
|
3740
|
+
const { line, exitCode } = summarize(results);
|
|
3741
|
+
out("");
|
|
3742
|
+
out(line);
|
|
3743
|
+
if (exitCode !== 0 || results.some((r) => r.status === "warn")) out("\u043F\u043E\u0447\u0438\u043D\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 (\u0442\u043E, \u0447\u0442\u043E \u043C\u043E\u0436\u043D\u043E): swarm install");
|
|
3744
|
+
return exitCode;
|
|
3745
|
+
}
|
|
3746
|
+
async function fixCheck(check, io, opts) {
|
|
3747
|
+
let current = check;
|
|
3748
|
+
const manual = [];
|
|
3749
|
+
for (let round = 0; round < MAX_FIX_ROUNDS && current.status !== "ok"; round++) {
|
|
3750
|
+
let ranAny = false;
|
|
3751
|
+
for (const rem of current.remedies) {
|
|
3752
|
+
if (!rem.auto) {
|
|
3753
|
+
manual.push(`${rem.what}: ${rem.command}`);
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
if (rem.sudo) {
|
|
3757
|
+
opts.out(` \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u0430 root: ${rem.what}`);
|
|
3758
|
+
opts.out(` $ ${rem.command}`);
|
|
3759
|
+
if (!opts.yes && !opts.confirm) {
|
|
3760
|
+
opts.out(" \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u0435 \u043D\u0435\u0433\u0434\u0435 (\u043D\u0435 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0437\u0430\u043F\u0443\u0441\u043A) \u2014 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u044E. \u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0437\u0430\u0440\u0430\u043D\u0435\u0435: swarm install --yes");
|
|
3761
|
+
manual.push(`${rem.what}: ${rem.command}`);
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
const agreed = opts.yes || await opts.confirm(" \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u044D\u0442\u0443 \u043A\u043E\u043C\u0430\u043D\u0434\u0443?");
|
|
3765
|
+
if (!agreed) {
|
|
3766
|
+
opts.out(" \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E \u043F\u043E \u0442\u0432\u043E\u0435\u043C\u0443 \u0440\u0435\u0448\u0435\u043D\u0438\u044E");
|
|
3767
|
+
manual.push(`${rem.what}: ${rem.command}`);
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
} else {
|
|
3771
|
+
opts.out(` $ ${rem.command}`);
|
|
3772
|
+
}
|
|
3773
|
+
const code = await opts.exec(rem.command);
|
|
3774
|
+
ranAny = true;
|
|
3775
|
+
if (code !== 0) opts.out(` \u21B3 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430 (\u043A\u043E\u0434 ${code}) \u2014 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E \u043F\u0443\u043D\u043A\u0442 \u043A\u0430\u043A \u0435\u0441\u0442\u044C`);
|
|
3776
|
+
}
|
|
3777
|
+
if (!ranAny) break;
|
|
3778
|
+
const rechecked = await runCheck(current.id, io);
|
|
3779
|
+
if (!rechecked) break;
|
|
3780
|
+
const stuck = rechecked.status === current.status && rechecked.detail === current.detail;
|
|
3781
|
+
current = rechecked;
|
|
3782
|
+
if (stuck) break;
|
|
3783
|
+
}
|
|
3784
|
+
return { result: current, manual: [...new Set(manual)] };
|
|
3785
|
+
}
|
|
3786
|
+
async function runInstallCli(argv, deps = {}) {
|
|
3787
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
3788
|
+
const yes = argv.includes("--yes") || argv.includes("-y");
|
|
3789
|
+
const io = deps.io ?? defaultDoctorIO(deps.packageVersion ?? "0.0.0");
|
|
3790
|
+
const confirm = deps.confirm !== void 0 ? deps.confirm : process.stdin.isTTY ? askYesNo : null;
|
|
3791
|
+
const exec = deps.exec ?? execLoud;
|
|
3792
|
+
out(header(io));
|
|
3793
|
+
if (io.platform.kind === "windows") {
|
|
3794
|
+
out("\u041D\u0430\u0442\u0438\u0432\u043D\u0430\u044F Windows: \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F WSL2 \u2014 \u0442\u0430\u043C \u0440\u043E\u0439 \u0432\u0435\u0434\u0451\u0442 \u0441\u0435\u0431\u044F \u0440\u043E\u0432\u043D\u043E \u043A\u0430\u043A \u043D\u0430 macOS/Linux.");
|
|
3795
|
+
out("\u0417\u0434\u0435\u0441\u044C \u0434\u0435\u043B\u0430\u044E \u0441\u0432\u043E\u044E \u0447\u0430\u0441\u0442\u044C (\u043F\u0430\u043A\u0435\u0442\u044B npm), \u043D\u043E \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0445 \u043F\u0430\u043A\u0435\u0442\u043E\u0432 \u043D\u0435 \u0441\u0442\u0430\u0432\u043B\u044E: \u043E\u0441\u043E\u0431\u0435\u043D\u043D\u043E\u0441\u0442\u0438 Windows \u2014 \u0432 README \u0434\u0435\u043C\u043E\u043D\u0430.");
|
|
3796
|
+
}
|
|
3797
|
+
out("");
|
|
3798
|
+
const initial = await runDoctor(io);
|
|
3799
|
+
renderReport(initial).forEach(out);
|
|
3800
|
+
const broken = initial.filter((r) => r.status !== "ok" && r.id !== "daemon");
|
|
3801
|
+
if (broken.length === 0) {
|
|
3802
|
+
out("");
|
|
3803
|
+
out("\u0433\u043E\u0442\u043E\u0432\u043E: \u0432\u0441\u0451 \u043D\u0430 \u043C\u0435\u0441\u0442\u0435, \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.");
|
|
3804
|
+
nextSteps(initial, (await io.daemon()).alive).forEach((s) => out(` ${s}`));
|
|
3805
|
+
return 0;
|
|
3806
|
+
}
|
|
3807
|
+
out("");
|
|
3808
|
+
out(`\u0434\u043E\u0432\u043E\u0436\u0443 \u0434\u043E \u0440\u0430\u0431\u043E\u0447\u0435\u0433\u043E \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F (${broken.length} \u043F\u0443\u043D\u043A\u0442(\u043E\u0432))\u2026`);
|
|
3809
|
+
const manual = [];
|
|
3810
|
+
const fixed = /* @__PURE__ */ new Map();
|
|
3811
|
+
for (const check of broken) {
|
|
3812
|
+
out("");
|
|
3813
|
+
out(`\u2014 ${check.title}`);
|
|
3814
|
+
const res = await fixCheck(check, io, { yes, out, confirm, exec });
|
|
3815
|
+
fixed.set(check.id, res.result);
|
|
3816
|
+
manual.push(...res.manual);
|
|
3817
|
+
}
|
|
3818
|
+
const final = initial.map((r) => fixed.get(r.id) ?? r);
|
|
3819
|
+
out("");
|
|
3820
|
+
out("\u2500\u2500 \u0438\u0442\u043E\u0433 \u2500\u2500");
|
|
3821
|
+
renderReport(final).forEach(out);
|
|
3822
|
+
const { line, exitCode } = summarize(final);
|
|
3823
|
+
out("");
|
|
3824
|
+
out(line);
|
|
3825
|
+
if (manual.length) {
|
|
3826
|
+
out("");
|
|
3827
|
+
out("\u0441\u0430\u043C \u044F \u044D\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u043D\u0435 \u043C\u043E\u0433\u0443 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 \u0440\u0443\u043A\u0430\u043C\u0438:");
|
|
3828
|
+
for (const m of manual) out(` \u2022 ${m}`);
|
|
3829
|
+
}
|
|
3830
|
+
out("");
|
|
3831
|
+
out("\u0434\u0430\u043B\u044C\u0448\u0435:");
|
|
3832
|
+
nextSteps(final, (await io.daemon()).alive).forEach((s) => out(` ${s}`));
|
|
3833
|
+
return exitCode;
|
|
3834
|
+
}
|
|
3835
|
+
var CHROMIUM_PROBE_MS, MAX_FIX_ROUNDS;
|
|
3836
|
+
var init_installCli = __esm({
|
|
3837
|
+
"src/installCli.ts"() {
|
|
3838
|
+
"use strict";
|
|
3839
|
+
init_doctor();
|
|
3840
|
+
init_cliIo();
|
|
3841
|
+
init_platform();
|
|
3842
|
+
init_release();
|
|
3843
|
+
init_screenshot();
|
|
3844
|
+
init_envGate();
|
|
3845
|
+
init_daemonLink();
|
|
3846
|
+
init_agentToken();
|
|
3847
|
+
init_repoMemory();
|
|
3848
|
+
init_repoDiscovery();
|
|
3849
|
+
init_config();
|
|
3850
|
+
CHROMIUM_PROBE_MS = 2e4;
|
|
3851
|
+
MAX_FIX_ROUNDS = 3;
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3854
|
+
|
|
3855
|
+
// src/updateCli.ts
|
|
3856
|
+
var updateCli_exports = {};
|
|
3857
|
+
__export(updateCli_exports, {
|
|
3858
|
+
runUpdateCli: () => runUpdateCli
|
|
3859
|
+
});
|
|
3860
|
+
async function runUpdateCli(argv, deps) {
|
|
3861
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
3862
|
+
const err = deps.err ?? ((l) => console.error(l));
|
|
3863
|
+
const pm = deps.globalPm ?? detectGlobalPm(process.argv[1]);
|
|
3864
|
+
const exec = deps.exec ?? execLoud;
|
|
3865
|
+
const latestVersion = deps.latestVersion ?? (() => fetchLatestVersion());
|
|
3866
|
+
const daemonAlive = deps.daemonAlive ?? (async () => await daemonStatus2() != null);
|
|
3867
|
+
const force = argv.includes("--force");
|
|
3868
|
+
if (pm === "sources") {
|
|
3869
|
+
err("\u0442\u044B \u0437\u0430\u043F\u0443\u0449\u0435\u043D \u0418\u0417 \u0418\u0421\u0425\u041E\u0414\u041D\u0418\u041A\u041E\u0412 \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u044B, \u0430 \u043D\u0435 \u0438\u0437 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0430\u043A\u0435\u0442\u0430 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C npm-\u043E\u043C \u0442\u0443\u0442 \u043D\u0435\u0447\u0435\u0433\u043E.");
|
|
3870
|
+
err(" \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F: git pull && pnpm install && pnpm --filter @taskless-app/swarm... build");
|
|
3871
|
+
err(` \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0430\u043A\u0435\u0442 \u0440\u044F\u0434\u043E\u043C: ${globalInstallCommand("npm", SWARM_PACKAGE)}`);
|
|
3872
|
+
return 1;
|
|
3873
|
+
}
|
|
3874
|
+
const latest = await latestVersion();
|
|
3875
|
+
if (latest === null) {
|
|
3876
|
+
err("\u0434\u043E registry.npmjs.org \u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u2014 \u043A\u0430\u043A\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430, \u0443\u0437\u043D\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C. \u041F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u0435\u0442\u044C \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438.");
|
|
3877
|
+
return 1;
|
|
3878
|
+
}
|
|
3879
|
+
if (!isOutdated(deps.packageVersion, latest) && !force) {
|
|
3880
|
+
out(`\u0443\u0436\u0435 \u0441\u0432\u0435\u0436\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F: v${deps.packageVersion} (\u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest}) \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.`);
|
|
3881
|
+
return 0;
|
|
3882
|
+
}
|
|
3883
|
+
const command = globalInstallCommand(pm, `${SWARM_PACKAGE}@${latest}`);
|
|
3884
|
+
out(`\u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E \u0440\u043E\u0439: v${deps.packageVersion} \u2192 v${latest}`);
|
|
3885
|
+
out(` $ ${command}`);
|
|
3886
|
+
const code = await exec(command);
|
|
3887
|
+
if (code !== 0) {
|
|
3888
|
+
err(`\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u043E (\u043A\u043E\u0434 ${code}). \u0421\u0442\u0440\u043E\u043A\u0430 \u0432\u044B\u0448\u0435 \u2014 \u0442\u0430 \u0441\u0430\u043C\u0430\u044F, \u0447\u0442\u043E \u043D\u0443\u0436\u043D\u0430; \u0432\u044B\u043F\u043E\u043B\u043D\u0438 \u0435\u0451 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u043D\u0430 \u0447\u0451\u043C \u0441\u043F\u043E\u0442\u044B\u043A\u0430\u0435\u0442\u0441\u044F.`);
|
|
3889
|
+
return code;
|
|
3890
|
+
}
|
|
3891
|
+
out(`\u0433\u043E\u0442\u043E\u0432\u043E: v${latest}`);
|
|
3892
|
+
if (await daemonAlive()) {
|
|
3893
|
+
out("\u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u043D\u044F\u0442 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043D\u0430 \u0421\u0422\u0410\u0420\u041E\u041C \u043A\u043E\u0434\u0435 \u2014 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438, \u0447\u0442\u043E\u0431\u044B \u043E\u043D \u043F\u043E\u0435\u0445\u0430\u043B \u043D\u0430 \u043D\u043E\u0432\u043E\u043C: swarm stop && swarm start");
|
|
3894
|
+
} else {
|
|
3895
|
+
out("\u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0440\u043E\u0439: swarm start");
|
|
3896
|
+
}
|
|
3897
|
+
return 0;
|
|
3898
|
+
}
|
|
3899
|
+
var init_updateCli = __esm({
|
|
3900
|
+
"src/updateCli.ts"() {
|
|
3901
|
+
"use strict";
|
|
3902
|
+
init_cliIo();
|
|
3903
|
+
init_platform();
|
|
3904
|
+
init_release();
|
|
3905
|
+
init_daemonLink();
|
|
3906
|
+
}
|
|
3907
|
+
});
|
|
3908
|
+
|
|
1618
3909
|
// src/cli.ts
|
|
3910
|
+
init_cliIo();
|
|
1619
3911
|
import { basename as basename4 } from "path";
|
|
1620
|
-
import { existsSync as
|
|
1621
|
-
import { createInterface } from "readline/promises";
|
|
3912
|
+
import { existsSync as existsSync14, readdirSync as readdirSync9, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
1622
3913
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
1623
3914
|
|
|
1624
3915
|
// src/swarmClient.ts
|
|
1625
3916
|
init_config();
|
|
1626
|
-
import { appendFileSync, existsSync as
|
|
3917
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1627
3918
|
import { dirname } from "path";
|
|
1628
3919
|
import { randomUUID } from "crypto";
|
|
1629
3920
|
|
|
@@ -1789,8 +4080,8 @@ init_names2();
|
|
|
1789
4080
|
init_storm();
|
|
1790
4081
|
|
|
1791
4082
|
// src/logTail.ts
|
|
1792
|
-
import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from "fs";
|
|
1793
|
-
import { join as
|
|
4083
|
+
import { closeSync, existsSync as existsSync3, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
4084
|
+
import { join as join5 } from "path";
|
|
1794
4085
|
var TAIL_LINES_FALLBACK = 200;
|
|
1795
4086
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
1796
4087
|
|
|
@@ -1858,6 +4149,15 @@ var SwarmClient = class _SwarmClient {
|
|
|
1858
4149
|
nextRunId = 1;
|
|
1859
4150
|
nextEventId = 1;
|
|
1860
4151
|
nextQuestionId = 1;
|
|
4152
|
+
// TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
|
|
4153
|
+
// первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
|
|
4154
|
+
// обрыва связи не превращается во второе взятие. См. claim().
|
|
4155
|
+
pendingClaimKey = null;
|
|
4156
|
+
// TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
|
|
4157
|
+
// заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
|
|
4158
|
+
// редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
|
|
4159
|
+
// всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
|
|
4160
|
+
lastClaimSkipLog = null;
|
|
1861
4161
|
// Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
|
|
1862
4162
|
// Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
|
|
1863
4163
|
// переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
|
|
@@ -1868,6 +4168,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
1868
4168
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
1869
4169
|
// каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
|
|
1870
4170
|
notifyLevelUnsupported = false;
|
|
4171
|
+
// TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
|
|
4172
|
+
// демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
|
|
4173
|
+
notifyResolveUnsupported = false;
|
|
1871
4174
|
// Корреляция с продом.
|
|
1872
4175
|
keyByLocal = /* @__PURE__ */ new Map();
|
|
1873
4176
|
// local run id → runKey
|
|
@@ -1910,24 +4213,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
1910
4213
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
1911
4214
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
1912
4215
|
this.host = opts.host ?? null;
|
|
1913
|
-
this.repoExists = opts.repoExists ??
|
|
4216
|
+
this.repoExists = opts.repoExists ?? existsSync4;
|
|
1914
4217
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
1915
4218
|
this.outboxPath = opts.outboxPath;
|
|
1916
4219
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
1917
4220
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
1918
|
-
|
|
4221
|
+
mkdirSync4(dirname(this.outboxPath), { recursive: true });
|
|
1919
4222
|
this.recover();
|
|
1920
4223
|
}
|
|
1921
4224
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
1922
4225
|
recover() {
|
|
1923
|
-
if (
|
|
4226
|
+
if (existsSync4(this.mapPath)) {
|
|
1924
4227
|
try {
|
|
1925
4228
|
const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
|
|
1926
4229
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
1927
4230
|
} catch {
|
|
1928
4231
|
}
|
|
1929
4232
|
}
|
|
1930
|
-
if (
|
|
4233
|
+
if (existsSync4(this.outboxPath)) {
|
|
1931
4234
|
const txt = readFileSync3(this.outboxPath, "utf8");
|
|
1932
4235
|
for (const line of txt.split("\n")) {
|
|
1933
4236
|
const s = line.trim();
|
|
@@ -1948,7 +4251,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1948
4251
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
1949
4252
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
1950
4253
|
replayGlobalEvents() {
|
|
1951
|
-
if (!
|
|
4254
|
+
if (!existsSync4(this.stormPath)) return;
|
|
1952
4255
|
let txt;
|
|
1953
4256
|
try {
|
|
1954
4257
|
txt = readFileSync3(this.stormPath, "utf8");
|
|
@@ -1988,6 +4291,18 @@ var SwarmClient = class _SwarmClient {
|
|
|
1988
4291
|
runKeyFor(localId) {
|
|
1989
4292
|
return this.keyByLocal.get(localId);
|
|
1990
4293
|
}
|
|
4294
|
+
/**
|
|
4295
|
+
* TASWM-336: заменить репо-реестр на лету. До этой задачи реестр собирался ОДИН РАЗ на
|
|
4296
|
+
* старте из окружения и жил до рестарта демона; теперь машина сама находит клоны своих
|
|
4297
|
+
* репозиториев и переспрашивает прод по TTL — у владельца появился новый swarm-проект,
|
|
4298
|
+
* и машина обязана подхватить его без рестарта. Меняем обе стороны разом: и список для
|
|
4299
|
+
* claim/heartbeat, и обратную карту `owner/repo` → путь (иначе прогон нового репо
|
|
4300
|
+
* пришёл бы именем, которое не во что перевести).
|
|
4301
|
+
*/
|
|
4302
|
+
setRepoRegistry(registry) {
|
|
4303
|
+
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
4304
|
+
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
4305
|
+
}
|
|
1991
4306
|
// cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
|
|
1992
4307
|
// Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
|
|
1993
4308
|
// прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
|
|
@@ -2005,8 +4320,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
2005
4320
|
this.scheduleFlush();
|
|
2006
4321
|
}
|
|
2007
4322
|
// Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
|
|
2008
|
-
// pid/worktree_path (рантайм),
|
|
2009
|
-
//
|
|
4323
|
+
// pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
|
|
4324
|
+
// и priority (локальная очередь ЭТОГО демона, TASWM-269)
|
|
2010
4325
|
// в прод НЕ уходят. kind → role (плоский enum). Self-ref
|
|
2011
4326
|
// прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
|
|
2012
4327
|
// supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
|
|
@@ -2017,6 +4332,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
2017
4332
|
if ("model" in patch) out.model = patch.model ?? null;
|
|
2018
4333
|
if ("name" in patch) out.droneName = patch.name ?? null;
|
|
2019
4334
|
if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
|
|
4335
|
+
if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
|
|
4336
|
+
if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
|
|
4337
|
+
if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
|
|
4338
|
+
if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
|
|
2020
4339
|
if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
|
|
2021
4340
|
if ("branch" in patch) out.branch = patch.branch ?? null;
|
|
2022
4341
|
if ("repo" in patch) out.repo = patch.repo ?? null;
|
|
@@ -2082,6 +4401,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
2082
4401
|
depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
|
|
2083
4402
|
model: input.model ?? null,
|
|
2084
4403
|
cost_usd: 0,
|
|
4404
|
+
tokens_in: 0,
|
|
4405
|
+
// копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
|
|
4406
|
+
tokens_out: 0,
|
|
4407
|
+
duration_ms: 0,
|
|
2085
4408
|
pr_url: null,
|
|
2086
4409
|
superseded_by: null,
|
|
2087
4410
|
decision: input.decision ?? null,
|
|
@@ -2204,6 +4527,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
2204
4527
|
if (!d) return 0;
|
|
2205
4528
|
d.respawns += 1;
|
|
2206
4529
|
d.updated_at = stamp();
|
|
4530
|
+
this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
|
|
2207
4531
|
return d.respawns;
|
|
2208
4532
|
}
|
|
2209
4533
|
cancel(id, reason) {
|
|
@@ -2619,7 +4943,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
2619
4943
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
2620
4944
|
*/
|
|
2621
4945
|
reconcileRuntime(opts) {
|
|
2622
|
-
const wtExists = opts.worktreeExists ??
|
|
4946
|
+
const wtExists = opts.worktreeExists ?? existsSync4;
|
|
2623
4947
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
2624
4948
|
try {
|
|
2625
4949
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -2707,23 +5031,69 @@ var SwarmClient = class _SwarmClient {
|
|
|
2707
5031
|
* шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
|
|
2708
5032
|
*/
|
|
2709
5033
|
async claim() {
|
|
5034
|
+
this.pendingClaimKey ??= randomUUID();
|
|
2710
5035
|
let res;
|
|
2711
5036
|
try {
|
|
2712
5037
|
res = await this.call("swarm_claim_work", {
|
|
2713
5038
|
...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
|
|
2714
5039
|
// TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
|
|
2715
5040
|
// прод помечает созданный прогон hostId, чужой демон его не увидит.
|
|
2716
|
-
...this.host ? { hostId: this.host.hostId } : {}
|
|
5041
|
+
...this.host ? { hostId: this.host.hostId } : {},
|
|
5042
|
+
idempotencyKey: this.pendingClaimKey
|
|
2717
5043
|
});
|
|
2718
5044
|
} catch {
|
|
2719
5045
|
return null;
|
|
2720
5046
|
}
|
|
2721
5047
|
if (res?.isError) return null;
|
|
5048
|
+
this.pendingClaimKey = null;
|
|
2722
5049
|
const sc = res.structuredContent ?? {};
|
|
5050
|
+
this.logClaimSkips(sc);
|
|
2723
5051
|
if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
|
|
2724
5052
|
const local = this.hydrateRun(sc.run);
|
|
2725
5053
|
return local ? this.mat(local) : null;
|
|
2726
5054
|
}
|
|
5055
|
+
/**
|
|
5056
|
+
* TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
|
|
5057
|
+
* незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
|
|
5058
|
+
* (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
|
|
5059
|
+
* живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
|
|
5060
|
+
* этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
|
|
5061
|
+
* гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
|
|
5062
|
+
*
|
|
5063
|
+
* Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
|
|
5064
|
+
* `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
|
|
5065
|
+
* полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
|
|
5066
|
+
* тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
|
|
5067
|
+
*/
|
|
5068
|
+
logClaimSkips(sc) {
|
|
5069
|
+
const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
|
|
5070
|
+
const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
|
|
5071
|
+
const lines = [];
|
|
5072
|
+
const parents = rows(sc.skippedParents).map(
|
|
5073
|
+
(p) => `${p.slug} (${typeof p.openChildren === "number" ? p.openChildren : "?"} \u043D\u0435\u0437\u0430\u043A\u0440\u044B\u0442\u044B\u0445 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447)`
|
|
5074
|
+
);
|
|
5075
|
+
if (parents.length > 0) {
|
|
5076
|
+
lines.push(
|
|
5077
|
+
`[swarm-client] claim \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0438-\u0440\u043E\u0434\u0438\u0442\u0435\u043B\u0438 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0432 \u043B\u0438\u0441\u0442\u044C\u044F\u0445, \u0434\u0440\u043E\u043D\u0430 \u043D\u0430 \u043D\u0438\u0445 \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u043C: ${parents.join(", ")}${others(sc.skippedParentsMore)}`
|
|
5078
|
+
);
|
|
5079
|
+
}
|
|
5080
|
+
const blocked = rows(sc.skippedBlocked).map(
|
|
5081
|
+
(b) => `${b.slug} \u0436\u0434\u0451\u0442 ${Array.isArray(b.blockers) && b.blockers.length > 0 ? b.blockers.join(", ") : "\u0431\u043B\u043E\u043A\u0435\u0440\u0430"}`
|
|
5082
|
+
);
|
|
5083
|
+
if (blocked.length > 0) {
|
|
5084
|
+
lines.push(
|
|
5085
|
+
`[swarm-client] claim \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0438 \u0441 \u043D\u0435\u0437\u0430\u043A\u0440\u044B\u0442\u044B\u043C \u0431\u043B\u043E\u043A\u0435\u0440\u043E\u043C \u2014 \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u0437\u0430\u0434\u0430\u043D \u0441\u0432\u044F\u0437\u044C\u044E \xAB\u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442\xBB, \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u0438\u0442\u044C \u043D\u0435 \u0431\u0443\u0434\u0443: ${blocked.join("; ")}${others(sc.skippedBlockedMore)}`
|
|
5086
|
+
);
|
|
5087
|
+
}
|
|
5088
|
+
const text = lines.join("\n");
|
|
5089
|
+
if (text.length === 0) {
|
|
5090
|
+
this.lastClaimSkipLog = null;
|
|
5091
|
+
return;
|
|
5092
|
+
}
|
|
5093
|
+
if (text === this.lastClaimSkipLog) return;
|
|
5094
|
+
this.lastClaimSkipLog = text;
|
|
5095
|
+
for (const line of lines) console.log(line);
|
|
5096
|
+
}
|
|
2727
5097
|
/**
|
|
2728
5098
|
* TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
|
|
2729
5099
|
* процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
|
|
@@ -2997,12 +5367,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
2997
5367
|
* Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
|
|
2998
5368
|
* означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
|
|
2999
5369
|
*
|
|
5370
|
+
* TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
|
|
5371
|
+
* дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
|
|
5372
|
+
* планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
|
|
5373
|
+
* число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
|
|
5374
|
+
* разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
|
|
5375
|
+
* трогает прошлое значение — та же логика, что у repoFullNames.
|
|
5376
|
+
*
|
|
3000
5377
|
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
3001
5378
|
*/
|
|
3002
|
-
async heartbeat(daemonVersion, pause) {
|
|
5379
|
+
async heartbeat(daemonVersion, pause, capacity) {
|
|
3003
5380
|
try {
|
|
3004
5381
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
3005
5382
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
5383
|
+
if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
|
|
3006
5384
|
if (pause !== void 0) {
|
|
3007
5385
|
args.paused = !!pause?.paused;
|
|
3008
5386
|
if (pause?.reason) args.pauseReason = pause.reason;
|
|
@@ -3108,6 +5486,35 @@ var SwarmClient = class _SwarmClient {
|
|
|
3108
5486
|
const res = await this.call("swarm_notify", base3);
|
|
3109
5487
|
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
3110
5488
|
}
|
|
5489
|
+
/**
|
|
5490
|
+
* TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
|
|
5491
|
+
* которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
|
|
5492
|
+
*
|
|
5493
|
+
* Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
|
|
5494
|
+
* ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
|
|
5495
|
+
* что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
|
|
5496
|
+
*
|
|
5497
|
+
* ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
|
|
5498
|
+
* «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
|
|
5499
|
+
* её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
|
|
5500
|
+
* процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
|
|
5501
|
+
* Тот же приём, что у notifyAnomaly с аргументом `level`.
|
|
5502
|
+
*/
|
|
5503
|
+
async resolveAnomalies(keys) {
|
|
5504
|
+
const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
|
|
5505
|
+
if (tags.length === 0 || this.notifyResolveUnsupported) return;
|
|
5506
|
+
const res = await this.call("swarm_notify_resolve", { tags });
|
|
5507
|
+
if (!res?.isError) return;
|
|
5508
|
+
const text = JSON.stringify(res);
|
|
5509
|
+
if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
|
|
5510
|
+
this.notifyResolveUnsupported = true;
|
|
5511
|
+
console.warn(
|
|
5512
|
+
"[swarm-client] \u043F\u0440\u043E\u0434 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 (\u043D\u0435\u0442 swarm_notify_resolve) \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0435\u0441\u044F \u0442\u0440\u0435\u0432\u043E\u0433\u0438 \u0440\u043E\u044F \u043F\u0440\u0438\u0434\u0451\u0442\u0441\u044F \u0433\u0430\u0441\u0438\u0442\u044C \u0432 \u0438\u043D\u0431\u043E\u043A\u0441\u0435 \u0440\u0443\u043A\u0430\u043C\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u043F\u0440\u043E\u0434 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0434\u0435\u043C\u043E\u043D."
|
|
5513
|
+
);
|
|
5514
|
+
return;
|
|
5515
|
+
}
|
|
5516
|
+
throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
|
|
5517
|
+
}
|
|
3111
5518
|
/**
|
|
3112
5519
|
* TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
|
|
3113
5520
|
* через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
|
|
@@ -3172,6 +5579,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
3172
5579
|
depends_on: [],
|
|
3173
5580
|
model: null,
|
|
3174
5581
|
cost_usd: 0,
|
|
5582
|
+
tokens_in: 0,
|
|
5583
|
+
// токены и время работы (TASWM-353) — прод-поля, заполняются ниже
|
|
5584
|
+
tokens_out: 0,
|
|
5585
|
+
duration_ms: 0,
|
|
3175
5586
|
pr_url: null,
|
|
3176
5587
|
superseded_by: null,
|
|
3177
5588
|
decision: null,
|
|
@@ -3182,9 +5593,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
3182
5593
|
review_verdict: null,
|
|
3183
5594
|
review_phase: null,
|
|
3184
5595
|
// вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
|
|
3185
|
-
// Локальная бухгалтерия (в прод-схему не
|
|
3186
|
-
//
|
|
3187
|
-
//
|
|
5596
|
+
// Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
|
|
5597
|
+
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
5598
|
+
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
3188
5599
|
review_cycle: 0,
|
|
3189
5600
|
task_title: null,
|
|
3190
5601
|
task_desc: null,
|
|
@@ -3207,8 +5618,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
3207
5618
|
this.runs.set(id, local);
|
|
3208
5619
|
this.keyByLocal.set(id, runKey);
|
|
3209
5620
|
}
|
|
3210
|
-
const
|
|
3211
|
-
local.task_slug =
|
|
5621
|
+
const prodSlug = pr.taskSlug ?? pr.task?.slug ?? null;
|
|
5622
|
+
if (prodSlug) local.task_slug = prodSlug;
|
|
5623
|
+
else if (!isTaskSlug(local.task_slug)) local.task_slug = pr.taskId || local.task_slug;
|
|
3212
5624
|
local.task_title = pr.taskTitle ?? local.task_title;
|
|
3213
5625
|
local.task_desc = pr.taskDesc ?? local.task_desc;
|
|
3214
5626
|
local.name = pr.droneName ?? local.name;
|
|
@@ -3235,6 +5647,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
3235
5647
|
local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
|
|
3236
5648
|
local.model = pr.model ?? null;
|
|
3237
5649
|
local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
|
|
5650
|
+
local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
|
|
5651
|
+
local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
|
|
5652
|
+
local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
|
|
3238
5653
|
local.pr_url = pr.prUrl ?? null;
|
|
3239
5654
|
local.stop_reason = pr.stopReason ?? null;
|
|
3240
5655
|
local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
|
|
@@ -3492,13 +5907,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
3492
5907
|
}
|
|
3493
5908
|
persistPending() {
|
|
3494
5909
|
try {
|
|
3495
|
-
|
|
5910
|
+
writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
3496
5911
|
} catch {
|
|
3497
5912
|
}
|
|
3498
5913
|
}
|
|
3499
5914
|
persistMap() {
|
|
3500
5915
|
try {
|
|
3501
|
-
|
|
5916
|
+
writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
3502
5917
|
} catch {
|
|
3503
5918
|
}
|
|
3504
5919
|
}
|
|
@@ -3523,11 +5938,102 @@ function parseDecision(v) {
|
|
|
3523
5938
|
}
|
|
3524
5939
|
}
|
|
3525
5940
|
|
|
5941
|
+
// src/cli.ts
|
|
5942
|
+
init_src();
|
|
5943
|
+
|
|
5944
|
+
// src/gitHistory.ts
|
|
5945
|
+
import { $ } from "execa";
|
|
5946
|
+
var REC = "";
|
|
5947
|
+
var FIELD = "";
|
|
5948
|
+
var LOG_FORMAT = `${REC}%H${FIELD}%s`;
|
|
5949
|
+
var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
|
|
5950
|
+
function slugFromSubject(subject) {
|
|
5951
|
+
return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
|
|
5952
|
+
}
|
|
5953
|
+
var SCOPE_IN_SUBJECT = /(?:^|·\s*)[a-z]+\(([^)]+)\):/g;
|
|
5954
|
+
function scopeFromText(text) {
|
|
5955
|
+
if (!text) return null;
|
|
5956
|
+
const votes = /* @__PURE__ */ new Map();
|
|
5957
|
+
for (const m of text.matchAll(SCOPE_IN_SUBJECT)) {
|
|
5958
|
+
const scope = m[1].trim().toLowerCase();
|
|
5959
|
+
if (scope) votes.set(scope, (votes.get(scope) ?? 0) + 1);
|
|
5960
|
+
}
|
|
5961
|
+
let best = null;
|
|
5962
|
+
let bestN = 0;
|
|
5963
|
+
for (const [scope, n] of votes) {
|
|
5964
|
+
if (n > bestN) {
|
|
5965
|
+
best = scope;
|
|
5966
|
+
bestN = n;
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
return best;
|
|
5970
|
+
}
|
|
5971
|
+
function unquotePath(raw) {
|
|
5972
|
+
const p = raw.trim();
|
|
5973
|
+
return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
|
|
5974
|
+
}
|
|
5975
|
+
function parseGitLog(out) {
|
|
5976
|
+
const commits = [];
|
|
5977
|
+
for (const chunk of out.split(REC)) {
|
|
5978
|
+
if (!chunk.trim()) continue;
|
|
5979
|
+
const [head, ...rest] = chunk.split("\n");
|
|
5980
|
+
const [sha, subject] = (head ?? "").split(FIELD);
|
|
5981
|
+
if (!sha) continue;
|
|
5982
|
+
const files = rest.map(unquotePath).filter(Boolean);
|
|
5983
|
+
commits.push({ sha, subject: subject ?? "", files });
|
|
5984
|
+
}
|
|
5985
|
+
return commits;
|
|
5986
|
+
}
|
|
5987
|
+
var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
|
|
5988
|
+
function isNoisePath(path) {
|
|
5989
|
+
return NOISE_RE.test(path);
|
|
5990
|
+
}
|
|
5991
|
+
var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
|
|
5992
|
+
function groupTaskHistory(commits, opts = {}) {
|
|
5993
|
+
const o = { ...GROUP_DEFAULTS, ...opts };
|
|
5994
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
5995
|
+
for (const c of commits) {
|
|
5996
|
+
const slug = slugFromSubject(c.subject);
|
|
5997
|
+
if (!slug) continue;
|
|
5998
|
+
if (c.files.length > o.maxFilesPerCommit) continue;
|
|
5999
|
+
const files = c.files.filter((f) => !o.ignore(f));
|
|
6000
|
+
if (!files.length) continue;
|
|
6001
|
+
let acc = bySlug.get(slug);
|
|
6002
|
+
if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
|
|
6003
|
+
for (const f of files) acc.files.add(f);
|
|
6004
|
+
acc.texts.push(c.subject);
|
|
6005
|
+
}
|
|
6006
|
+
return [...bySlug.entries()].map(([slug, acc]) => ({
|
|
6007
|
+
slug,
|
|
6008
|
+
files: [...acc.files],
|
|
6009
|
+
text: acc.texts.join(" \xB7 ")
|
|
6010
|
+
}));
|
|
6011
|
+
}
|
|
6012
|
+
async function readCommitLog(repo, opts = {}) {
|
|
6013
|
+
const max = opts.maxCommits ?? 1500;
|
|
6014
|
+
try {
|
|
6015
|
+
const { stdout } = await $({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
|
|
6016
|
+
return parseGitLog(stdout ?? "");
|
|
6017
|
+
} catch {
|
|
6018
|
+
return [];
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
async function readTaskHistory(repo, opts = {}) {
|
|
6022
|
+
return groupTaskHistory(await readCommitLog(repo, opts), opts);
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
// src/zones.ts
|
|
6026
|
+
init_src();
|
|
6027
|
+
var DEFAULT_TTL_MS = 10 * 6e4;
|
|
6028
|
+
function withManualAreas(history, areas) {
|
|
6029
|
+
return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
|
|
6030
|
+
}
|
|
6031
|
+
|
|
3526
6032
|
// src/cli.ts
|
|
3527
6033
|
init_config();
|
|
3528
6034
|
|
|
3529
6035
|
// src/daemonLaunch.ts
|
|
3530
|
-
import { openSync as openSync2, closeSync as closeSync2,
|
|
6036
|
+
import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync4 } from "fs";
|
|
3531
6037
|
import { spawn } from "child_process";
|
|
3532
6038
|
var FOREGROUND_FLAGS = /* @__PURE__ */ new Set(["--foreground", "-f", "--attach"]);
|
|
3533
6039
|
function parseStartMode(argv) {
|
|
@@ -3537,18 +6043,6 @@ function parseStartMode(argv) {
|
|
|
3537
6043
|
function childStartArgv(rest) {
|
|
3538
6044
|
return ["start", ...rest.filter((a) => !FOREGROUND_FLAGS.has(a)), "--foreground"];
|
|
3539
6045
|
}
|
|
3540
|
-
function rotateDaemonLog(path, maxBytes) {
|
|
3541
|
-
let size;
|
|
3542
|
-
try {
|
|
3543
|
-
size = statSync2(path).size;
|
|
3544
|
-
} catch {
|
|
3545
|
-
return "missing";
|
|
3546
|
-
}
|
|
3547
|
-
if (size <= maxBytes) return "kept";
|
|
3548
|
-
renameSync2(path, `${path}.1`);
|
|
3549
|
-
writeFileSync3(path, "");
|
|
3550
|
-
return "rotated";
|
|
3551
|
-
}
|
|
3552
6046
|
function tailText(text, lines) {
|
|
3553
6047
|
const rows = text.split("\n");
|
|
3554
6048
|
if (rows[rows.length - 1] === "") rows.pop();
|
|
@@ -3605,6 +6099,42 @@ function tailFile(path, lines) {
|
|
|
3605
6099
|
}
|
|
3606
6100
|
}
|
|
3607
6101
|
|
|
6102
|
+
// src/logRotate.ts
|
|
6103
|
+
import { copyFileSync, renameSync as renameSync2, statSync as statSync4, fstatSync, truncateSync, unlinkSync, existsSync as existsSync5 } from "fs";
|
|
6104
|
+
var realIo = {
|
|
6105
|
+
size: (path) => {
|
|
6106
|
+
try {
|
|
6107
|
+
return statSync4(path).size;
|
|
6108
|
+
} catch {
|
|
6109
|
+
return null;
|
|
6110
|
+
}
|
|
6111
|
+
},
|
|
6112
|
+
copy: copyFileSync,
|
|
6113
|
+
rename: renameSync2,
|
|
6114
|
+
truncate: (path) => truncateSync(path, 0),
|
|
6115
|
+
unlink: unlinkSync,
|
|
6116
|
+
exists: existsSync5
|
|
6117
|
+
};
|
|
6118
|
+
var STRAY_GENERATIONS = 8;
|
|
6119
|
+
function rotateDaemonLog(path, opts, io = realIo) {
|
|
6120
|
+
const size = io.size(path);
|
|
6121
|
+
if (size == null) return "missing";
|
|
6122
|
+
if (size <= opts.maxBytes) return "kept";
|
|
6123
|
+
const keep = Math.max(0, Math.trunc(opts.keep));
|
|
6124
|
+
const gen = (n) => `${path}.${n}`;
|
|
6125
|
+
for (let i = keep; i >= 1; i--) {
|
|
6126
|
+
if (!io.exists(gen(i))) continue;
|
|
6127
|
+
if (i === keep) io.unlink(gen(i));
|
|
6128
|
+
else io.rename(gen(i), gen(i + 1));
|
|
6129
|
+
}
|
|
6130
|
+
for (let i = keep + 1; i <= keep + STRAY_GENERATIONS; i++) {
|
|
6131
|
+
if (io.exists(gen(i))) io.unlink(gen(i));
|
|
6132
|
+
}
|
|
6133
|
+
if (keep >= 1) io.copy(path, gen(1));
|
|
6134
|
+
io.truncate(path);
|
|
6135
|
+
return "rotated";
|
|
6136
|
+
}
|
|
6137
|
+
|
|
3608
6138
|
// src/procList.ts
|
|
3609
6139
|
function parsePsRows(out) {
|
|
3610
6140
|
const rows = [];
|
|
@@ -3681,7 +6211,7 @@ function descendantPids(rows, roots) {
|
|
|
3681
6211
|
|
|
3682
6212
|
// src/procCleanup.ts
|
|
3683
6213
|
function sleep(ms) {
|
|
3684
|
-
return new Promise((
|
|
6214
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3685
6215
|
}
|
|
3686
6216
|
function parseLsofCwd(output) {
|
|
3687
6217
|
const rows = [];
|
|
@@ -3836,45 +6366,24 @@ async function cleanupWorktreeProcesses(worktree, opts = {}) {
|
|
|
3836
6366
|
}
|
|
3837
6367
|
}
|
|
3838
6368
|
|
|
3839
|
-
// src/
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
function parseRepoFullName(remoteUrl) {
|
|
3843
|
-
const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
|
|
3844
|
-
if (!m) return null;
|
|
3845
|
-
return `${m[1]}/${m[2]}`.toLowerCase();
|
|
3846
|
-
}
|
|
3847
|
-
async function resolveRepoFullName(path) {
|
|
3848
|
-
const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
|
|
3849
|
-
if (res.exitCode !== 0) return null;
|
|
3850
|
-
return parseRepoFullName(res.stdout);
|
|
3851
|
-
}
|
|
3852
|
-
async function buildRepoRegistry(paths) {
|
|
3853
|
-
const resolved = await Promise.all(
|
|
3854
|
-
paths.map(async (raw) => {
|
|
3855
|
-
const path = normalizeRepo(raw);
|
|
3856
|
-
if (!path) return null;
|
|
3857
|
-
const repoFullName = await resolveRepoFullName(path);
|
|
3858
|
-
return repoFullName ? { repoFullName, path } : null;
|
|
3859
|
-
})
|
|
3860
|
-
);
|
|
3861
|
-
return resolved.filter((e) => e !== null);
|
|
3862
|
-
}
|
|
6369
|
+
// src/cli.ts
|
|
6370
|
+
init_repoRegistry();
|
|
6371
|
+
init_repoDiscovery();
|
|
3863
6372
|
|
|
3864
6373
|
// src/hostIdentity.ts
|
|
3865
6374
|
import { hostname } from "os";
|
|
3866
|
-
import { existsSync as
|
|
3867
|
-
import { join as
|
|
6375
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
6376
|
+
import { join as join8 } from "path";
|
|
3868
6377
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3869
6378
|
function hostIdentityPath(home) {
|
|
3870
|
-
return
|
|
6379
|
+
return join8(home, "host.json");
|
|
3871
6380
|
}
|
|
3872
6381
|
function loadHostIdentity(deps) {
|
|
3873
|
-
const exists = deps.exists ??
|
|
3874
|
-
const readFile2 = deps.readFile ?? ((p) =>
|
|
6382
|
+
const exists = deps.exists ?? existsSync6;
|
|
6383
|
+
const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
|
|
3875
6384
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
3876
|
-
|
|
3877
|
-
|
|
6385
|
+
writeFileSync5(`${p}.tmp`, data);
|
|
6386
|
+
renameSync4(`${p}.tmp`, p);
|
|
3878
6387
|
});
|
|
3879
6388
|
const host = deps.hostname ?? hostname;
|
|
3880
6389
|
const newId = deps.newId ?? randomUUID2;
|
|
@@ -3985,9 +6494,9 @@ import { basename as basename2 } from "path";
|
|
|
3985
6494
|
import { serve as serve2 } from "@hono/node-server";
|
|
3986
6495
|
|
|
3987
6496
|
// src/diff.ts
|
|
3988
|
-
import { statSync as
|
|
3989
|
-
import { join as
|
|
3990
|
-
import { $ as $
|
|
6497
|
+
import { statSync as statSync6 } from "fs";
|
|
6498
|
+
import { join as join9 } from "path";
|
|
6499
|
+
import { $ as $3 } from "execa";
|
|
3991
6500
|
var MAX_FILES = 200;
|
|
3992
6501
|
var MAX_LINES_PER_FILE = 1500;
|
|
3993
6502
|
var MAX_UNTRACKED = 60;
|
|
@@ -4107,12 +6616,12 @@ function truncateFile(f) {
|
|
|
4107
6616
|
}
|
|
4108
6617
|
async function resolveBase(wt, baseBranch) {
|
|
4109
6618
|
const tryRef = async (ref) => {
|
|
4110
|
-
const sha = (await $
|
|
6619
|
+
const sha = (await $3({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
|
|
4111
6620
|
if (!sha) return null;
|
|
4112
|
-
const mb = (await $
|
|
6621
|
+
const mb = (await $3({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
|
|
4113
6622
|
return mb || null;
|
|
4114
6623
|
};
|
|
4115
|
-
const originHead = (await $
|
|
6624
|
+
const originHead = (await $3({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
4116
6625
|
const candidates = [
|
|
4117
6626
|
...baseBranch ? [`origin/${baseBranch}`, baseBranch] : [],
|
|
4118
6627
|
originHead,
|
|
@@ -4129,7 +6638,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
4129
6638
|
}
|
|
4130
6639
|
function mtime(path) {
|
|
4131
6640
|
try {
|
|
4132
|
-
return
|
|
6641
|
+
return statSync6(path).mtimeMs;
|
|
4133
6642
|
} catch {
|
|
4134
6643
|
return 0;
|
|
4135
6644
|
}
|
|
@@ -4138,23 +6647,23 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
4138
6647
|
const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
|
|
4139
6648
|
const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
|
|
4140
6649
|
if (!worktree) return stub(gone);
|
|
4141
|
-
const isWt = (await $
|
|
6650
|
+
const isWt = (await $3({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
4142
6651
|
if (isWt !== "true") return stub(gone);
|
|
4143
6652
|
const base3 = await resolveBase(worktree, opts.baseBranch);
|
|
4144
6653
|
const from = base3 ?? "HEAD";
|
|
4145
|
-
const trackedTxt = (await $
|
|
6654
|
+
const trackedTxt = (await $3({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
|
|
4146
6655
|
let files = parseUnifiedDiff(trackedTxt);
|
|
4147
|
-
const untrackedRaw = (await $
|
|
6656
|
+
const untrackedRaw = (await $3({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
4148
6657
|
const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
|
|
4149
6658
|
for (const rel of untracked) {
|
|
4150
|
-
const t = (await $
|
|
6659
|
+
const t = (await $3({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
|
|
4151
6660
|
for (const pf of parseUnifiedDiff(t)) {
|
|
4152
6661
|
pf.status = "added";
|
|
4153
6662
|
if (!pf.path || pf.path === "/dev/null") pf.path = rel;
|
|
4154
6663
|
files.push(pf);
|
|
4155
6664
|
}
|
|
4156
6665
|
}
|
|
4157
|
-
files.sort((a, b) => mtime(
|
|
6666
|
+
files.sort((a, b) => mtime(join9(worktree, b.path)) - mtime(join9(worktree, a.path)));
|
|
4158
6667
|
const truncatedFiles = files.length > MAX_FILES;
|
|
4159
6668
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
4160
6669
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -4170,70 +6679,16 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
4170
6679
|
};
|
|
4171
6680
|
}
|
|
4172
6681
|
|
|
4173
|
-
// package.json
|
|
4174
|
-
var package_default = {
|
|
4175
|
-
name: "@taskless-app/swarm",
|
|
4176
|
-
version: "0.4.10",
|
|
4177
|
-
type: "module",
|
|
4178
|
-
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
4179
|
-
keywords: [
|
|
4180
|
-
"ai-agents",
|
|
4181
|
-
"coding-agents",
|
|
4182
|
-
"swarm",
|
|
4183
|
-
"orchestration",
|
|
4184
|
-
"autonomous-agents",
|
|
4185
|
-
"mcp",
|
|
4186
|
-
"taskless",
|
|
4187
|
-
"cli"
|
|
4188
|
-
],
|
|
4189
|
-
license: "SEE LICENSE IN LICENSE",
|
|
4190
|
-
author: "Ilya Smurov <ilya@smurov.com>",
|
|
4191
|
-
homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
|
|
4192
|
-
repository: {
|
|
4193
|
-
type: "git",
|
|
4194
|
-
url: "git+https://github.com/ilyasmurov/taskless.git",
|
|
4195
|
-
directory: "packages/swarm-daemon"
|
|
4196
|
-
},
|
|
4197
|
-
engines: {
|
|
4198
|
-
node: ">=22"
|
|
4199
|
-
},
|
|
4200
|
-
bin: {
|
|
4201
|
-
swarm: "dist/cli.js"
|
|
4202
|
-
},
|
|
4203
|
-
files: [
|
|
4204
|
-
"dist",
|
|
4205
|
-
"docs/HERO.md",
|
|
4206
|
-
"roles"
|
|
4207
|
-
],
|
|
4208
|
-
scripts: {
|
|
4209
|
-
overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
|
|
4210
|
-
swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
|
|
4211
|
-
build: "tsup",
|
|
4212
|
-
start: "node dist/overmind.js",
|
|
4213
|
-
prepublishOnly: "pnpm run build",
|
|
4214
|
-
test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
|
|
4215
|
-
"test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
|
|
4216
|
-
"test:watch": "vitest",
|
|
4217
|
-
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
4218
|
-
lint: "echo 'no lint in swarm-daemon yet'"
|
|
4219
|
-
},
|
|
4220
|
-
dependencies: {
|
|
4221
|
-
"@hono/node-server": "^2.0.12",
|
|
4222
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4223
|
-
execa: "^10.0.0",
|
|
4224
|
-
zod: "3.25.76"
|
|
4225
|
-
}
|
|
4226
|
-
};
|
|
4227
|
-
|
|
4228
6682
|
// src/web.ts
|
|
6683
|
+
init_version();
|
|
4229
6684
|
init_storm();
|
|
4230
6685
|
|
|
4231
6686
|
// src/gh.ts
|
|
4232
6687
|
init_src();
|
|
4233
|
-
import { $ as $
|
|
6688
|
+
import { $ as $4 } from "execa";
|
|
4234
6689
|
var NO_CHECKS_STDERR = /no checks reported/i;
|
|
4235
6690
|
var realChecksRunner = async (pr) => {
|
|
4236
|
-
const r = await $
|
|
6691
|
+
const r = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
|
|
4237
6692
|
const stdout = r.stdout ?? "";
|
|
4238
6693
|
if (!stdout.trim() && NO_CHECKS_STDERR.test(r.stderr ?? "")) return "[]";
|
|
4239
6694
|
return stdout;
|
|
@@ -4277,64 +6732,19 @@ async function ghChecks(pr, run = realChecksRunner) {
|
|
|
4277
6732
|
|
|
4278
6733
|
// src/web.ts
|
|
4279
6734
|
init_config();
|
|
4280
|
-
|
|
4281
|
-
// src/droneChannel.ts
|
|
4282
|
-
init_src();
|
|
4283
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4284
|
-
import { chmodSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
4285
|
-
import { dirname as dirname2 } from "path";
|
|
4286
|
-
import { serve } from "@hono/node-server";
|
|
4287
|
-
function loadChannelSecret(path) {
|
|
4288
|
-
try {
|
|
4289
|
-
const s = readFileSync6(path, "utf8").trim();
|
|
4290
|
-
if (s.length >= 32) return s;
|
|
4291
|
-
} catch {
|
|
4292
|
-
}
|
|
4293
|
-
const secret = randomBytes(32).toString("hex");
|
|
4294
|
-
mkdirSync3(dirname2(path), { recursive: true });
|
|
4295
|
-
writeFileSync5(path, secret, { mode: 384 });
|
|
4296
|
-
try {
|
|
4297
|
-
chmodSync(path, 384);
|
|
4298
|
-
} catch {
|
|
4299
|
-
}
|
|
4300
|
-
return secret;
|
|
4301
|
-
}
|
|
4302
|
-
function sign(runKey, secret) {
|
|
4303
|
-
return createHmac("sha256", secret).update(runKey).digest("base64url");
|
|
4304
|
-
}
|
|
4305
|
-
function mintChannelToken(runKey, secret) {
|
|
4306
|
-
return `${Buffer.from(runKey, "utf8").toString("base64url")}.${sign(runKey, secret)}`;
|
|
4307
|
-
}
|
|
4308
|
-
function verifyChannelToken(token, secret) {
|
|
4309
|
-
const dot = token.lastIndexOf(".");
|
|
4310
|
-
if (dot <= 0) return null;
|
|
4311
|
-
const runKey = Buffer.from(token.slice(0, dot), "base64url").toString("utf8");
|
|
4312
|
-
if (!runKey) return null;
|
|
4313
|
-
const got = Buffer.from(token.slice(dot + 1));
|
|
4314
|
-
const want = Buffer.from(sign(runKey, secret));
|
|
4315
|
-
if (got.length !== want.length || !timingSafeEqual(got, want)) return null;
|
|
4316
|
-
return runKey;
|
|
4317
|
-
}
|
|
4318
|
-
var LOCAL_CLI_KEY = "cli";
|
|
4319
|
-
function mintLocalCliToken(secret) {
|
|
4320
|
-
return mintChannelToken(LOCAL_CLI_KEY, secret);
|
|
4321
|
-
}
|
|
4322
|
-
function verifyLocalCliToken(token, secret) {
|
|
4323
|
-
return verifyChannelToken(token, secret) === LOCAL_CLI_KEY;
|
|
4324
|
-
}
|
|
4325
|
-
var EVENT_KIND_SET = new Set(EVENT_KINDS);
|
|
6735
|
+
init_droneChannel();
|
|
4326
6736
|
|
|
4327
6737
|
// src/oauth.ts
|
|
4328
6738
|
init_config();
|
|
4329
6739
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
4330
|
-
import { readFileSync as
|
|
4331
|
-
import { join as
|
|
6740
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
6741
|
+
import { join as join10 } from "path";
|
|
4332
6742
|
var REGISTER = "/api/oauth/register";
|
|
4333
6743
|
var AUTHORIZE = "/api/oauth/authorize";
|
|
4334
6744
|
var TOKEN = "/api/oauth/token";
|
|
4335
6745
|
var SCOPE = "openid profile email taskless:full";
|
|
4336
|
-
var STORE_PATH =
|
|
4337
|
-
var CLIENT_PATH =
|
|
6746
|
+
var STORE_PATH = join10(SWARM_HOME, "oauth.json");
|
|
6747
|
+
var CLIENT_PATH = join10(SWARM_HOME, "oauth-client.json");
|
|
4338
6748
|
function makePkce() {
|
|
4339
6749
|
const verifier = randomBytes2(48).toString("base64url");
|
|
4340
6750
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
@@ -4419,23 +6829,23 @@ function toStore(clientId, t, now) {
|
|
|
4419
6829
|
}
|
|
4420
6830
|
function loadStore() {
|
|
4421
6831
|
try {
|
|
4422
|
-
return JSON.parse(
|
|
6832
|
+
return JSON.parse(readFileSync9(STORE_PATH, "utf8"));
|
|
4423
6833
|
} catch {
|
|
4424
6834
|
return null;
|
|
4425
6835
|
}
|
|
4426
6836
|
}
|
|
4427
6837
|
function saveStore(s) {
|
|
4428
|
-
|
|
6838
|
+
writeFileSync7(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
|
|
4429
6839
|
}
|
|
4430
6840
|
function clearStore() {
|
|
4431
6841
|
try {
|
|
4432
|
-
|
|
6842
|
+
unlinkSync2(STORE_PATH);
|
|
4433
6843
|
} catch {
|
|
4434
6844
|
}
|
|
4435
6845
|
}
|
|
4436
6846
|
function loadClientId() {
|
|
4437
6847
|
try {
|
|
4438
|
-
return JSON.parse(
|
|
6848
|
+
return JSON.parse(readFileSync9(CLIENT_PATH, "utf8")).clientId;
|
|
4439
6849
|
} catch {
|
|
4440
6850
|
return null;
|
|
4441
6851
|
}
|
|
@@ -4444,7 +6854,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
|
|
|
4444
6854
|
const existing = loadClientId();
|
|
4445
6855
|
if (existing) return existing;
|
|
4446
6856
|
const clientId = await registerClient({ api, redirectUri }, fetchFn);
|
|
4447
|
-
|
|
6857
|
+
writeFileSync7(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
|
|
4448
6858
|
return clientId;
|
|
4449
6859
|
}
|
|
4450
6860
|
async function ensureAccess(store, opts = {}) {
|
|
@@ -4463,7 +6873,6 @@ async function ensureAccess(store, opts = {}) {
|
|
|
4463
6873
|
}
|
|
4464
6874
|
|
|
4465
6875
|
// src/web.ts
|
|
4466
|
-
var PACKAGE_VERSION = package_default.version;
|
|
4467
6876
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
4468
6877
|
var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
4469
6878
|
var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
|
|
@@ -4519,6 +6928,10 @@ function roySnapshot(db2, now = Date.now(), overmindLog = [], paused = false, da
|
|
|
4519
6928
|
stopReason: r.stop_reason,
|
|
4520
6929
|
reason,
|
|
4521
6930
|
updated: r.updated_at,
|
|
6931
|
+
startedAt: r.created_at,
|
|
6932
|
+
// TASWM-351: возраст прогона на карточке
|
|
6933
|
+
costUsd: r.cost_usd || 0,
|
|
6934
|
+
// TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
|
|
4522
6935
|
step,
|
|
4523
6936
|
hasDecision: withQ.has(r.id),
|
|
4524
6937
|
rescued: evs.some((e) => e.kind === "rescued"),
|
|
@@ -5771,7 +8184,8 @@ function daemonStatus(deps) {
|
|
|
5771
8184
|
restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
|
|
5772
8185
|
counts,
|
|
5773
8186
|
live,
|
|
5774
|
-
version: PACKAGE_VERSION
|
|
8187
|
+
version: PACKAGE_VERSION,
|
|
8188
|
+
repos: deps.repoMap?.() ?? []
|
|
5775
8189
|
};
|
|
5776
8190
|
}
|
|
5777
8191
|
function eventsStream(db2, narratorLog, control, daemonVersion, restart, liveSessionIds, selfHost) {
|
|
@@ -6024,87 +8438,15 @@ import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDronePro
|
|
|
6024
8438
|
|
|
6025
8439
|
// src/launcher.ts
|
|
6026
8440
|
import { spawn as spawn2 } from "child_process";
|
|
6027
|
-
import { closeSync as closeSync3, existsSync as
|
|
8441
|
+
import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync7, openSync as openSync3, readdirSync as readdirSync7 } from "fs";
|
|
6028
8442
|
import { createRequire } from "module";
|
|
6029
|
-
import { homedir as
|
|
6030
|
-
import { dirname as dirname3, join as
|
|
8443
|
+
import { homedir as homedir4 } from "os";
|
|
8444
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
6031
8445
|
import { fileURLToPath } from "url";
|
|
6032
8446
|
|
|
6033
|
-
// src/claudeBin.ts
|
|
6034
|
-
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
6035
|
-
import { win32 as winPath } from "path";
|
|
6036
|
-
var CLAUDE_FALLBACK = "claude";
|
|
6037
|
-
function compareVersionDirs(a, b) {
|
|
6038
|
-
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
6039
|
-
const pa = parts(a);
|
|
6040
|
-
const pb = parts(b);
|
|
6041
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
6042
|
-
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
6043
|
-
if (d !== 0) return d;
|
|
6044
|
-
}
|
|
6045
|
-
return 0;
|
|
6046
|
-
}
|
|
6047
|
-
function realListDir(dir) {
|
|
6048
|
-
try {
|
|
6049
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
6050
|
-
} catch {
|
|
6051
|
-
return [];
|
|
6052
|
-
}
|
|
6053
|
-
}
|
|
6054
|
-
function winVersionRoots(env, listDir) {
|
|
6055
|
-
const roots = [];
|
|
6056
|
-
const appData = env.APPDATA;
|
|
6057
|
-
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
6058
|
-
const localAppData = env.LOCALAPPDATA;
|
|
6059
|
-
if (localAppData) {
|
|
6060
|
-
const packages = winPath.join(localAppData, "Packages");
|
|
6061
|
-
for (const name of listDir(packages)) {
|
|
6062
|
-
if (!name.startsWith("Claude")) continue;
|
|
6063
|
-
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
6064
|
-
}
|
|
6065
|
-
}
|
|
6066
|
-
return roots;
|
|
6067
|
-
}
|
|
6068
|
-
function findWindowsClaude(opts = {}) {
|
|
6069
|
-
const env = opts.env ?? process.env;
|
|
6070
|
-
const listDir = opts.listDir ?? realListDir;
|
|
6071
|
-
const fileExists = opts.fileExists ?? existsSync4;
|
|
6072
|
-
let best = null;
|
|
6073
|
-
for (const root of winVersionRoots(env, listDir)) {
|
|
6074
|
-
for (const version of listDir(root)) {
|
|
6075
|
-
const candidate = winPath.join(root, version, "claude.exe");
|
|
6076
|
-
if (!fileExists(candidate)) continue;
|
|
6077
|
-
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
6078
|
-
}
|
|
6079
|
-
}
|
|
6080
|
-
return best?.path ?? null;
|
|
6081
|
-
}
|
|
6082
|
-
var warnedNoWindowsClaude = false;
|
|
6083
|
-
var cachedWindowsBin = null;
|
|
6084
|
-
var windowsScanDone = false;
|
|
6085
|
-
function resolveClaudeBin(opts = {}) {
|
|
6086
|
-
const env = opts.env ?? process.env;
|
|
6087
|
-
const platform = opts.platform ?? process.platform;
|
|
6088
|
-
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
6089
|
-
if (explicit) return explicit;
|
|
6090
|
-
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
6091
|
-
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
6092
|
-
windowsScanDone = true;
|
|
6093
|
-
const found = findWindowsClaude({ ...opts, env });
|
|
6094
|
-
cachedWindowsBin = found;
|
|
6095
|
-
if (found) return found;
|
|
6096
|
-
if (!warnedNoWindowsClaude) {
|
|
6097
|
-
warnedNoWindowsClaude = true;
|
|
6098
|
-
console.warn(
|
|
6099
|
-
"[overmind] claude.exe \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0438 \u0432 %APPDATA%\\Claude\\claude-code, \u043D\u0438 \u0432 MSIX-\u043A\u043E\u043F\u0438\u0438 \u043F\u043E\u0434 %LOCALAPPDATA%\\Packages\\Claude* \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u0433\u043E\u043B\u043E\u0435 \xABclaude\xBB (\u043F\u043E\u0438\u0441\u043A \u043F\u043E PATH+PATHEXT). \u0423\u043F\u0430\u0434\u0451\u0442 ENOENT (bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0432 PATH \u043D\u0435 \u0432\u0438\u0434\u043D\u0430 spawn'\u0443) \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_CLAUDE_BIN \u043F\u0443\u0442\u0451\u043C \u043A \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u043C\u0443 claude.exe."
|
|
6100
|
-
);
|
|
6101
|
-
}
|
|
6102
|
-
return CLAUDE_FALLBACK;
|
|
6103
|
-
}
|
|
6104
|
-
|
|
6105
8447
|
// src/droneHome.ts
|
|
6106
|
-
import { lstatSync, mkdirSync as
|
|
6107
|
-
import { join as
|
|
8448
|
+
import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync6, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
|
|
8449
|
+
import { join as join11, relative, sep } from "path";
|
|
6108
8450
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
6109
8451
|
function droneHomeDeny(env = process.env) {
|
|
6110
8452
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -6122,34 +8464,34 @@ function buildDroneHome(opts) {
|
|
|
6122
8464
|
const { realHome, droneHome } = opts;
|
|
6123
8465
|
const deny = opts.deny ?? droneHomeDeny();
|
|
6124
8466
|
const platform = opts.platform ?? process.platform;
|
|
6125
|
-
|
|
6126
|
-
const wanted = new Set(mirroredEntries(
|
|
8467
|
+
mkdirSync6(droneHome, { recursive: true, mode: 448 });
|
|
8468
|
+
const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
|
|
6127
8469
|
const rel = relative(realHome, droneHome);
|
|
6128
8470
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
6129
|
-
for (const existing of
|
|
8471
|
+
for (const existing of readdirSync6(droneHome)) {
|
|
6130
8472
|
if (wanted.has(existing)) continue;
|
|
6131
|
-
const p =
|
|
8473
|
+
const p = join11(droneHome, existing);
|
|
6132
8474
|
try {
|
|
6133
8475
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
6134
8476
|
} catch {
|
|
6135
8477
|
}
|
|
6136
8478
|
}
|
|
6137
8479
|
for (const name of wanted) {
|
|
6138
|
-
const link =
|
|
8480
|
+
const link = join11(droneHome, name);
|
|
6139
8481
|
try {
|
|
6140
8482
|
lstatSync(link);
|
|
6141
8483
|
continue;
|
|
6142
8484
|
} catch {
|
|
6143
8485
|
}
|
|
6144
|
-
const target =
|
|
8486
|
+
const target = join11(realHome, name);
|
|
6145
8487
|
let isDir;
|
|
6146
8488
|
try {
|
|
6147
|
-
isDir =
|
|
8489
|
+
isDir = statSync7(target).isDirectory();
|
|
6148
8490
|
} catch {
|
|
6149
8491
|
continue;
|
|
6150
8492
|
}
|
|
6151
8493
|
try {
|
|
6152
|
-
|
|
8494
|
+
symlinkSync2(target, link, isDir ? "junction" : "file");
|
|
6153
8495
|
} catch (e) {
|
|
6154
8496
|
if (e.code !== "EEXIST") throw e;
|
|
6155
8497
|
}
|
|
@@ -6162,10 +8504,10 @@ var REAL_PATH_ENV_PINS = [
|
|
|
6162
8504
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
6163
8505
|
];
|
|
6164
8506
|
function realPathEnvPins(realHome) {
|
|
6165
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
8507
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
|
|
6166
8508
|
}
|
|
6167
|
-
function
|
|
6168
|
-
|
|
8509
|
+
function dronePrivatePaths(realHome, swarmHome) {
|
|
8510
|
+
return [
|
|
6169
8511
|
// НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
|
|
6170
8512
|
// (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
|
|
6171
8513
|
// дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
|
|
@@ -6186,11 +8528,6 @@ function droneDenyRules(realHome, swarmHome) {
|
|
|
6186
8528
|
`${realHome}/.claude/.credentials.json`,
|
|
6187
8529
|
`${realHome}/.npmrc`
|
|
6188
8530
|
];
|
|
6189
|
-
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
6190
|
-
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
6191
|
-
}
|
|
6192
|
-
function droneSettingsJson(realHome, swarmHome) {
|
|
6193
|
-
return JSON.stringify({ permissions: { deny: droneDenyRules(realHome, swarmHome) } });
|
|
6194
8531
|
}
|
|
6195
8532
|
|
|
6196
8533
|
// src/testForks.ts
|
|
@@ -6222,6 +8559,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
|
|
|
6222
8559
|
}
|
|
6223
8560
|
|
|
6224
8561
|
// src/launcher.ts
|
|
8562
|
+
init_executors();
|
|
6225
8563
|
var requireFromHere = createRequire(import.meta.url);
|
|
6226
8564
|
function resolveTsxCli() {
|
|
6227
8565
|
try {
|
|
@@ -6232,29 +8570,12 @@ function resolveTsxCli() {
|
|
|
6232
8570
|
);
|
|
6233
8571
|
}
|
|
6234
8572
|
}
|
|
6235
|
-
var
|
|
6236
|
-
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
6237
|
-
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
6238
|
-
const base3 = [
|
|
6239
|
-
"--dangerously-skip-permissions",
|
|
6240
|
-
...ISOLATION_FLAGS,
|
|
6241
|
-
"--mcp-config",
|
|
6242
|
-
cfgPath,
|
|
6243
|
-
...settings ? ["--settings", settings] : [],
|
|
6244
|
-
...pluginFlags,
|
|
6245
|
-
"--output-format",
|
|
6246
|
-
"json",
|
|
6247
|
-
"-p",
|
|
6248
|
-
prompt
|
|
6249
|
-
];
|
|
6250
|
-
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
6251
|
-
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
6252
|
-
}
|
|
8573
|
+
var MCP_SERVER_NAME = "overmind";
|
|
6253
8574
|
function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
6254
8575
|
const env = { ...base3 };
|
|
6255
8576
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
6256
8577
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
6257
|
-
env.TURBO_CACHE_DIR =
|
|
8578
|
+
env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
|
|
6258
8579
|
}
|
|
6259
8580
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
6260
8581
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -6282,20 +8603,20 @@ var DAEMON_STATE_ENV = [
|
|
|
6282
8603
|
"TASKLESS_TOKEN"
|
|
6283
8604
|
];
|
|
6284
8605
|
function bundledRoleDir(kind) {
|
|
6285
|
-
return
|
|
8606
|
+
return join12(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
6286
8607
|
}
|
|
6287
8608
|
function roleDir(worktree, kind) {
|
|
6288
|
-
return
|
|
8609
|
+
return join12(worktree, ".claude", "roles", kind);
|
|
6289
8610
|
}
|
|
6290
8611
|
function roleDirs(worktree, kind) {
|
|
6291
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
8612
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
|
|
6292
8613
|
}
|
|
6293
8614
|
function nextRunLogPath(runLogsDir, runId) {
|
|
6294
|
-
const dir =
|
|
6295
|
-
|
|
8615
|
+
const dir = join12(runLogsDir, String(runId));
|
|
8616
|
+
mkdirSync7(dir, { recursive: true });
|
|
6296
8617
|
let files = [];
|
|
6297
8618
|
try {
|
|
6298
|
-
files =
|
|
8619
|
+
files = readdirSync7(dir);
|
|
6299
8620
|
} catch {
|
|
6300
8621
|
}
|
|
6301
8622
|
let next = 0;
|
|
@@ -6303,7 +8624,7 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
6303
8624
|
const m = /^(\d+)\.json/.exec(f);
|
|
6304
8625
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
6305
8626
|
}
|
|
6306
|
-
return
|
|
8627
|
+
return join12(dir, `${next}.json`);
|
|
6307
8628
|
}
|
|
6308
8629
|
function openNextRunLog(runLogsDir, runId) {
|
|
6309
8630
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -6338,57 +8659,63 @@ function makeLauncher(cfg) {
|
|
|
6338
8659
|
);
|
|
6339
8660
|
}
|
|
6340
8661
|
const channelEnv = cfg.channel && channelToken ? { SWARM_DRONE_CHANNEL_URL: cfg.channel.url, SWARM_DRONE_CHANNEL_TOKEN: channelToken } : { SWARM_DB: cfg.dbPath };
|
|
6341
|
-
const
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
6376
|
-
}
|
|
6377
|
-
}
|
|
8662
|
+
const mcp = {
|
|
8663
|
+
name: MCP_SERVER_NAME,
|
|
8664
|
+
command: process.execPath,
|
|
8665
|
+
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
8666
|
+
// SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
|
|
8667
|
+
// Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
|
|
8668
|
+
// на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
|
|
8669
|
+
// SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
|
|
8670
|
+
// источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
|
|
8671
|
+
// найденной по номеру прогона).
|
|
8672
|
+
// SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
|
|
8673
|
+
// (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
|
|
8674
|
+
// ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
|
|
8675
|
+
// не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
|
|
8676
|
+
// SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
|
|
8677
|
+
// вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
|
|
8678
|
+
// и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
|
|
8679
|
+
// ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
|
|
8680
|
+
// (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
|
|
8681
|
+
// SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
|
|
8682
|
+
// нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
|
|
8683
|
+
// (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
|
|
8684
|
+
// ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
|
|
8685
|
+
// может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
|
|
8686
|
+
// SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
|
|
8687
|
+
env: {
|
|
8688
|
+
...channelEnv,
|
|
8689
|
+
...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
|
|
8690
|
+
SWARM_TASK_SLUG: run.task_slug,
|
|
8691
|
+
SWARM_RUN_KIND: run.kind,
|
|
8692
|
+
SWARM_RUN_NAME: run.name,
|
|
8693
|
+
...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
|
|
8694
|
+
...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
|
|
8695
|
+
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
6378
8696
|
}
|
|
6379
8697
|
};
|
|
6380
|
-
const
|
|
6381
|
-
|
|
6382
|
-
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
|
|
6383
|
-
const pluginDirs = roleDirs(worktree, run.kind);
|
|
6384
|
-
const realHome = cfg.droneIsolation?.realHome ?? homedir2();
|
|
8698
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8699
|
+
const realHome = cfg.droneIsolation?.realHome ?? homedir4();
|
|
6385
8700
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
6386
|
-
|
|
6387
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync5(join7(cfg.droneIsolation.swarmHome, "token"))) {
|
|
8701
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join12(cfg.droneIsolation.swarmHome, "token"))) {
|
|
6388
8702
|
console.error(
|
|
6389
|
-
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${
|
|
8703
|
+
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join12(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
|
|
6390
8704
|
);
|
|
6391
8705
|
}
|
|
8706
|
+
const executor = cfg.executor ?? resolveExecutor();
|
|
8707
|
+
const plan = executor.plan({
|
|
8708
|
+
worktree,
|
|
8709
|
+
prompt,
|
|
8710
|
+
continueSession: !!continueSession,
|
|
8711
|
+
model,
|
|
8712
|
+
mcp,
|
|
8713
|
+
skillDirs: roleDirs(worktree, run.kind),
|
|
8714
|
+
// Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
|
|
8715
|
+
// сидит в HOME демона, и запирать половину дверей — самообман.
|
|
8716
|
+
denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
|
|
8717
|
+
realHome
|
|
8718
|
+
});
|
|
6392
8719
|
let stdio = "ignore";
|
|
6393
8720
|
let outFd;
|
|
6394
8721
|
let errFd;
|
|
@@ -6398,15 +8725,16 @@ function makeLauncher(cfg) {
|
|
|
6398
8725
|
errFd = fds.err ?? void 0;
|
|
6399
8726
|
stdio = ["ignore", outFd, errFd ?? "ignore"];
|
|
6400
8727
|
}
|
|
6401
|
-
const child = spawn2(cfg.
|
|
8728
|
+
const child = spawn2(cfg.bin ?? plan.bin, plan.args, {
|
|
6402
8729
|
cwd: worktree,
|
|
6403
8730
|
detached: true,
|
|
6404
8731
|
stdio,
|
|
6405
8732
|
// Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
|
|
6406
8733
|
// worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
|
|
6407
8734
|
// демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
|
|
6408
|
-
// Остальной env демона наследуется, как раньше.
|
|
6409
|
-
|
|
8735
|
+
// Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
|
|
8736
|
+
// они про его собственное состояние, и перебивать их ройным нечем.
|
|
8737
|
+
env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
|
|
6410
8738
|
});
|
|
6411
8739
|
if (outFd !== void 0) closeSync3(outFd);
|
|
6412
8740
|
if (errFd !== void 0) closeSync3(errFd);
|
|
@@ -6416,99 +8744,58 @@ function makeLauncher(cfg) {
|
|
|
6416
8744
|
};
|
|
6417
8745
|
}
|
|
6418
8746
|
|
|
6419
|
-
// src/
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
import {
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
return false;
|
|
6429
|
-
}
|
|
6430
|
-
}
|
|
6431
|
-
async function detachWorktree(repo, worktree) {
|
|
6432
|
-
await $4({ reject: false })`git -C ${repo} worktree remove --force ${worktree}`;
|
|
6433
|
-
if (await ensureWorktreeDirGone(worktree)) {
|
|
6434
|
-
await $4({ reject: false })`git -C ${repo} worktree prune`;
|
|
6435
|
-
}
|
|
6436
|
-
}
|
|
6437
|
-
async function ensureWorktreeDirGone(worktree) {
|
|
6438
|
-
if (!existsSync6(worktree)) return false;
|
|
6439
|
-
if (!isLinkedWorktree(worktree)) {
|
|
6440
|
-
console.error(
|
|
6441
|
-
`[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
|
|
6442
|
-
);
|
|
6443
|
-
return false;
|
|
6444
|
-
}
|
|
6445
|
-
try {
|
|
6446
|
-
await rm(worktree, { recursive: true, force: true, maxRetries: 3 });
|
|
6447
|
-
} catch (e) {
|
|
6448
|
-
console.error(
|
|
6449
|
-
`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043D\u0435 \u0441\u043D\u0451\u0441\u0441\u044F \u043D\u0438 git'\u043E\u043C, \u043D\u0438 rm (${String(e)}). \u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u044D\u0442\u043E\u0433\u043E \u0441\u043B\u0430\u0433\u0430 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 WorktreeBusyError. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C <\u0440\u0435\u043F\u043E> worktree prune\`; \u043D\u0430 Windows \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0434\u043B\u0438\u043D\u043D\u044B\u0435 \u043F\u0443\u0442\u0438: \`git config --global core.longpaths true\`.`
|
|
6450
|
-
);
|
|
6451
|
-
return true;
|
|
6452
|
-
}
|
|
6453
|
-
if (existsSync6(worktree)) {
|
|
6454
|
-
console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
|
|
6455
|
-
}
|
|
6456
|
-
return true;
|
|
6457
|
-
}
|
|
6458
|
-
async function removeWorktree(repo, path) {
|
|
6459
|
-
const branch = (await $4({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
6460
|
-
await cleanupWorktreeProcesses(path);
|
|
6461
|
-
await detachWorktree(repo, path);
|
|
6462
|
-
if (branch) await $4({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
6463
|
-
}
|
|
6464
|
-
|
|
6465
|
-
// src/agentToken.ts
|
|
6466
|
-
init_config();
|
|
6467
|
-
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
6468
|
-
import { join as join9 } from "path";
|
|
6469
|
-
var STORE_PATH2 = join9(SWARM_HOME, "agent.json");
|
|
6470
|
-
var base2 = (api) => api.replace(/\/$/, "");
|
|
6471
|
-
function loadAgentToken() {
|
|
6472
|
-
try {
|
|
6473
|
-
const s = JSON.parse(readFileSync8(STORE_PATH2, "utf8"));
|
|
6474
|
-
return s.token ? s : null;
|
|
6475
|
-
} catch {
|
|
6476
|
-
return null;
|
|
6477
|
-
}
|
|
6478
|
-
}
|
|
6479
|
-
function saveAgentToken(s) {
|
|
6480
|
-
writeFileSync8(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
|
|
6481
|
-
}
|
|
6482
|
-
function clearAgentToken() {
|
|
6483
|
-
try {
|
|
6484
|
-
unlinkSync2(STORE_PATH2);
|
|
6485
|
-
} catch {
|
|
6486
|
-
}
|
|
6487
|
-
}
|
|
6488
|
-
async function mintAgentToken(args, fetchFn = fetch) {
|
|
6489
|
-
const res = await fetchFn(`${base2(args.api)}/api/swarm/agent/token`, {
|
|
6490
|
-
method: "POST",
|
|
6491
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${args.bearer}` },
|
|
6492
|
-
body: JSON.stringify(args.label ? { label: args.label } : {})
|
|
6493
|
-
});
|
|
6494
|
-
const text = await res.text();
|
|
6495
|
-
if (!res.ok) throw new Error(`agent token: HTTP ${res.status} ${text.slice(0, 200)}`);
|
|
6496
|
-
const j = JSON.parse(text);
|
|
6497
|
-
if (!j.token) throw new Error("agent token: \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 token");
|
|
6498
|
-
return { token: j.token, ...j.tokenId ? { tokenId: j.tokenId } : {} };
|
|
6499
|
-
}
|
|
6500
|
-
async function probeAgentToken(args, fetchFn = fetch) {
|
|
8747
|
+
// src/cli.ts
|
|
8748
|
+
init_droneChannel();
|
|
8749
|
+
|
|
8750
|
+
// src/git.ts
|
|
8751
|
+
import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
|
|
8752
|
+
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
8753
|
+
import { isAbsolute as isAbsolute2, join as join13 } from "path";
|
|
8754
|
+
import { $ as $5 } from "execa";
|
|
8755
|
+
function isLinkedWorktree(dir) {
|
|
6501
8756
|
try {
|
|
6502
|
-
|
|
6503
|
-
headers: { Authorization: `Bearer ${args.token}` }
|
|
6504
|
-
});
|
|
6505
|
-
if (res.status === 401 || res.status === 403) return "revoked";
|
|
6506
|
-
return res.ok ? "ok" : "unreachable";
|
|
8757
|
+
return statSync8(join13(dir, ".git")).isFile();
|
|
6507
8758
|
} catch {
|
|
6508
|
-
return
|
|
8759
|
+
return false;
|
|
8760
|
+
}
|
|
8761
|
+
}
|
|
8762
|
+
async function detachWorktree(repo, worktree) {
|
|
8763
|
+
await $5({ reject: false })`git -C ${repo} worktree remove --force ${worktree}`;
|
|
8764
|
+
if (await ensureWorktreeDirGone(worktree)) {
|
|
8765
|
+
await $5({ reject: false })`git -C ${repo} worktree prune`;
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
async function ensureWorktreeDirGone(worktree) {
|
|
8769
|
+
if (!existsSync8(worktree)) return false;
|
|
8770
|
+
if (!isLinkedWorktree(worktree)) {
|
|
8771
|
+
console.error(
|
|
8772
|
+
`[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
|
|
8773
|
+
);
|
|
8774
|
+
return false;
|
|
8775
|
+
}
|
|
8776
|
+
try {
|
|
8777
|
+
await rm(worktree, { recursive: true, force: true, maxRetries: 3 });
|
|
8778
|
+
} catch (e) {
|
|
8779
|
+
console.error(
|
|
8780
|
+
`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043D\u0435 \u0441\u043D\u0451\u0441\u0441\u044F \u043D\u0438 git'\u043E\u043C, \u043D\u0438 rm (${String(e)}). \u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u044D\u0442\u043E\u0433\u043E \u0441\u043B\u0430\u0433\u0430 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 WorktreeBusyError. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C <\u0440\u0435\u043F\u043E> worktree prune\`; \u043D\u0430 Windows \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0434\u043B\u0438\u043D\u043D\u044B\u0435 \u043F\u0443\u0442\u0438: \`git config --global core.longpaths true\`.`
|
|
8781
|
+
);
|
|
8782
|
+
return true;
|
|
8783
|
+
}
|
|
8784
|
+
if (existsSync8(worktree)) {
|
|
8785
|
+
console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
|
|
6509
8786
|
}
|
|
8787
|
+
return true;
|
|
8788
|
+
}
|
|
8789
|
+
async function removeWorktree(repo, path) {
|
|
8790
|
+
const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
8791
|
+
await cleanupWorktreeProcesses(path);
|
|
8792
|
+
await detachWorktree(repo, path);
|
|
8793
|
+
if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
6510
8794
|
}
|
|
6511
8795
|
|
|
8796
|
+
// src/cli.ts
|
|
8797
|
+
init_agentToken();
|
|
8798
|
+
|
|
6512
8799
|
// src/taskless.ts
|
|
6513
8800
|
init_src();
|
|
6514
8801
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -6516,7 +8803,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
6516
8803
|
|
|
6517
8804
|
// src/attachments.ts
|
|
6518
8805
|
import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
|
|
6519
|
-
import { join as
|
|
8806
|
+
import { join as join15 } from "path";
|
|
6520
8807
|
var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
|
|
6521
8808
|
var ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
6522
8809
|
var IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "heic"]);
|
|
@@ -6526,20 +8813,21 @@ var KNOWN_EXT = /* @__PURE__ */ new Set([...IMAGE_EXT, ...DOC_EXT, ...TEXT_EXT])
|
|
|
6526
8813
|
|
|
6527
8814
|
// src/shotUpload.ts
|
|
6528
8815
|
import { readFile as fsReadFile } from "fs/promises";
|
|
6529
|
-
import { basename as basename3, isAbsolute as
|
|
8816
|
+
import { basename as basename3, isAbsolute as isAbsolute3, join as join16 } from "path";
|
|
6530
8817
|
var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
6531
8818
|
|
|
6532
8819
|
// src/taskless.ts
|
|
6533
8820
|
init_config();
|
|
8821
|
+
init_agentToken();
|
|
6534
8822
|
|
|
6535
8823
|
// src/workspace.ts
|
|
6536
8824
|
init_config();
|
|
6537
|
-
import { readFileSync as
|
|
6538
|
-
import { join as
|
|
6539
|
-
var WORKSPACE_PATH =
|
|
8825
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4 } from "fs";
|
|
8826
|
+
import { join as join17 } from "path";
|
|
8827
|
+
var WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
|
|
6540
8828
|
function loadWorkspace() {
|
|
6541
8829
|
try {
|
|
6542
|
-
const w = JSON.parse(
|
|
8830
|
+
const w = JSON.parse(readFileSync11(WORKSPACE_PATH, "utf8"));
|
|
6543
8831
|
return w && typeof w.id === "string" && w.id ? w : null;
|
|
6544
8832
|
} catch {
|
|
6545
8833
|
return null;
|
|
@@ -6550,7 +8838,7 @@ function saveWorkspace(w) {
|
|
|
6550
8838
|
}
|
|
6551
8839
|
function clearWorkspace() {
|
|
6552
8840
|
try {
|
|
6553
|
-
|
|
8841
|
+
unlinkSync4(WORKSPACE_PATH);
|
|
6554
8842
|
} catch {
|
|
6555
8843
|
}
|
|
6556
8844
|
}
|
|
@@ -6695,7 +8983,7 @@ function resilientSwarmMcp(opts = {}) {
|
|
|
6695
8983
|
}
|
|
6696
8984
|
|
|
6697
8985
|
// src/login.ts
|
|
6698
|
-
import { execa } from "execa";
|
|
8986
|
+
import { execa as execa2 } from "execa";
|
|
6699
8987
|
async function waitForLogin(d) {
|
|
6700
8988
|
const deadline = d.now() + d.timeoutMs;
|
|
6701
8989
|
for (; ; ) {
|
|
@@ -6760,134 +9048,15 @@ function parseWhoAmI(res) {
|
|
|
6760
9048
|
async function openBrowser(url) {
|
|
6761
9049
|
const [cmd2, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
6762
9050
|
try {
|
|
6763
|
-
await
|
|
9051
|
+
await execa2(cmd2, args, { stdio: "ignore" });
|
|
6764
9052
|
return true;
|
|
6765
9053
|
} catch {
|
|
6766
9054
|
return false;
|
|
6767
9055
|
}
|
|
6768
9056
|
}
|
|
6769
9057
|
|
|
6770
|
-
// src/screenshot.ts
|
|
6771
|
-
var DESKTOP_VIEWPORT = { name: "desktop", width: 1360, height: 940 };
|
|
6772
|
-
var MOBILE_VIEWPORT = { name: "mobile", width: 390, height: 844 };
|
|
6773
|
-
var DEFAULT_VIEWPORTS = [DESKTOP_VIEWPORT, MOBILE_VIEWPORT];
|
|
6774
|
-
var DEFAULT_SHOT_TIMEOUT_MS = 2e4;
|
|
6775
|
-
var NETWORK_SETTLE_QUIET_MS = 500;
|
|
6776
|
-
var NETWORK_SETTLE_TIMEOUT_MS = 8e3;
|
|
6777
|
-
async function captureScreenshots(launchBrowser, opts) {
|
|
6778
|
-
const viewports = opts.viewports ?? DEFAULT_VIEWPORTS;
|
|
6779
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_SHOT_TIMEOUT_MS;
|
|
6780
|
-
const fullPage = opts.fullPage ?? true;
|
|
6781
|
-
const browser = await launchBrowser();
|
|
6782
|
-
try {
|
|
6783
|
-
const results = [];
|
|
6784
|
-
for (const vp of viewports) {
|
|
6785
|
-
const ctx = await browser.newContext({
|
|
6786
|
-
viewport: { width: vp.width, height: vp.height },
|
|
6787
|
-
...opts.storageStatePath ? { storageState: opts.storageStatePath } : {}
|
|
6788
|
-
});
|
|
6789
|
-
try {
|
|
6790
|
-
const page = await ctx.newPage();
|
|
6791
|
-
await page.goto(opts.url, { timeout: timeoutMs, waitUntil: "load" });
|
|
6792
|
-
await page.waitForNetworkSettle({
|
|
6793
|
-
quietMs: NETWORK_SETTLE_QUIET_MS,
|
|
6794
|
-
timeoutMs: Math.min(timeoutMs, NETWORK_SETTLE_TIMEOUT_MS)
|
|
6795
|
-
});
|
|
6796
|
-
const path = `${opts.outPrefix}-${vp.name}.png`;
|
|
6797
|
-
await page.screenshot({ path, fullPage, timeout: timeoutMs });
|
|
6798
|
-
results.push({ viewport: vp.name, path });
|
|
6799
|
-
} finally {
|
|
6800
|
-
await ctx.close();
|
|
6801
|
-
}
|
|
6802
|
-
}
|
|
6803
|
-
return results;
|
|
6804
|
-
} finally {
|
|
6805
|
-
await browser.close();
|
|
6806
|
-
}
|
|
6807
|
-
}
|
|
6808
|
-
function isEventStream(headers2) {
|
|
6809
|
-
for (const [k, v] of Object.entries(headers2)) {
|
|
6810
|
-
if (k.toLowerCase() === "content-type") return v.toLowerCase().startsWith("text/event-stream");
|
|
6811
|
-
}
|
|
6812
|
-
return false;
|
|
6813
|
-
}
|
|
6814
|
-
function createNetworkSettleTracker(target) {
|
|
6815
|
-
const pending2 = /* @__PURE__ */ new Set();
|
|
6816
|
-
const onChange = /* @__PURE__ */ new Set();
|
|
6817
|
-
const notify = () => {
|
|
6818
|
-
for (const cb of [...onChange]) cb();
|
|
6819
|
-
};
|
|
6820
|
-
target.on("request", ((req) => {
|
|
6821
|
-
pending2.add(req);
|
|
6822
|
-
notify();
|
|
6823
|
-
}));
|
|
6824
|
-
target.on("response", ((res) => {
|
|
6825
|
-
if (isEventStream(res.headers())) {
|
|
6826
|
-
pending2.delete(res.request());
|
|
6827
|
-
notify();
|
|
6828
|
-
}
|
|
6829
|
-
}));
|
|
6830
|
-
const onDone = ((req) => {
|
|
6831
|
-
pending2.delete(req);
|
|
6832
|
-
notify();
|
|
6833
|
-
});
|
|
6834
|
-
target.on("requestfinished", onDone);
|
|
6835
|
-
target.on("requestfailed", onDone);
|
|
6836
|
-
return {
|
|
6837
|
-
waitForSettle({ quietMs, timeoutMs }) {
|
|
6838
|
-
return new Promise((resolve3) => {
|
|
6839
|
-
let quietTimer;
|
|
6840
|
-
const hardTimer = setTimeout(finish, timeoutMs);
|
|
6841
|
-
function finish() {
|
|
6842
|
-
clearTimeout(quietTimer);
|
|
6843
|
-
clearTimeout(hardTimer);
|
|
6844
|
-
onChange.delete(check);
|
|
6845
|
-
resolve3();
|
|
6846
|
-
}
|
|
6847
|
-
function check() {
|
|
6848
|
-
clearTimeout(quietTimer);
|
|
6849
|
-
if (pending2.size === 0) quietTimer = setTimeout(finish, quietMs);
|
|
6850
|
-
}
|
|
6851
|
-
onChange.add(check);
|
|
6852
|
-
check();
|
|
6853
|
-
});
|
|
6854
|
-
}
|
|
6855
|
-
};
|
|
6856
|
-
}
|
|
6857
|
-
function wrapPage(page) {
|
|
6858
|
-
const tracker = createNetworkSettleTracker(page);
|
|
6859
|
-
return {
|
|
6860
|
-
goto: (url, gotoOpts) => page.goto(url, gotoOpts),
|
|
6861
|
-
screenshot: (shotOpts) => page.screenshot(shotOpts),
|
|
6862
|
-
waitForNetworkSettle: (settleOpts) => tracker.waitForSettle(settleOpts)
|
|
6863
|
-
};
|
|
6864
|
-
}
|
|
6865
|
-
function wrapContext(ctx) {
|
|
6866
|
-
return {
|
|
6867
|
-
newPage: async () => wrapPage(await ctx.newPage()),
|
|
6868
|
-
close: () => ctx.close()
|
|
6869
|
-
};
|
|
6870
|
-
}
|
|
6871
|
-
function wrapBrowser(browser) {
|
|
6872
|
-
return {
|
|
6873
|
-
newContext: async (ctxOpts) => wrapContext(await browser.newContext(ctxOpts)),
|
|
6874
|
-
close: () => browser.close()
|
|
6875
|
-
};
|
|
6876
|
-
}
|
|
6877
|
-
async function launchChromium(opts = {}) {
|
|
6878
|
-
let mod;
|
|
6879
|
-
try {
|
|
6880
|
-
mod = await import("playwright-core");
|
|
6881
|
-
} catch {
|
|
6882
|
-
throw new Error(
|
|
6883
|
-
"playwright-core \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438 \u043E\u0434\u0438\u043D \u0440\u0430\u0437: (\u0432 \u044D\u0442\u043E\u043C \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u043E) pnpm --filter @taskless-app/swarm add -D playwright-core; (\u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E) npm i -D playwright-core \u2014 \u0438 \u0432 \u043E\u0431\u043E\u0438\u0445 \u0441\u043B\u0443\u0447\u0430\u044F\u0445 npx playwright install chromium."
|
|
6884
|
-
);
|
|
6885
|
-
}
|
|
6886
|
-
const browser = await mod.chromium.launch({ headless: true, ...opts.timeoutMs !== void 0 ? { timeout: opts.timeoutMs } : {} });
|
|
6887
|
-
return wrapBrowser(browser);
|
|
6888
|
-
}
|
|
6889
|
-
|
|
6890
9058
|
// src/shotCli.ts
|
|
9059
|
+
init_screenshot();
|
|
6891
9060
|
init_config();
|
|
6892
9061
|
function isFlagValue(v) {
|
|
6893
9062
|
return v !== void 0 && !v.startsWith("--");
|
|
@@ -6962,82 +9131,19 @@ async function runShotCli(argv, deps = {}) {
|
|
|
6962
9131
|
}
|
|
6963
9132
|
}
|
|
6964
9133
|
|
|
6965
|
-
// src/
|
|
6966
|
-
|
|
6967
|
-
var TIMEOUT_MS = 5e3;
|
|
6968
|
-
var ACTION_TIMEOUT_MS = 2e4;
|
|
6969
|
-
var BASE = `http://127.0.0.1:${WEB_PORT}`;
|
|
6970
|
-
function headers(extra = {}) {
|
|
6971
|
-
return { "X-Swarm-Local": mintLocalCliToken(loadChannelSecret(DRONE_CHANNEL_SECRET_PATH)), ...extra };
|
|
6972
|
-
}
|
|
6973
|
-
async function daemonRuns(status) {
|
|
6974
|
-
try {
|
|
6975
|
-
const qs = status ? `?status=${encodeURIComponent(status)}` : "";
|
|
6976
|
-
const res = await fetch(`${BASE}/runs${qs}`, { headers: headers(), signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
6977
|
-
if (!res.ok) return null;
|
|
6978
|
-
const body = await res.json();
|
|
6979
|
-
if (!Array.isArray(body.runs)) return null;
|
|
6980
|
-
return { runs: body.runs, backend: String(body.backend ?? ""), hostId: body.hostId ?? null };
|
|
6981
|
-
} catch {
|
|
6982
|
-
return null;
|
|
6983
|
-
}
|
|
6984
|
-
}
|
|
6985
|
-
async function post(path, form) {
|
|
6986
|
-
try {
|
|
6987
|
-
const res = await fetch(`${BASE}${path}`, {
|
|
6988
|
-
method: "POST",
|
|
6989
|
-
headers: headers({ "Content-Type": "application/x-www-form-urlencoded" }),
|
|
6990
|
-
body: new URLSearchParams(form),
|
|
6991
|
-
signal: AbortSignal.timeout(ACTION_TIMEOUT_MS)
|
|
6992
|
-
});
|
|
6993
|
-
if (res.status === 503) return null;
|
|
6994
|
-
if (res.status === 401) return { ok: false, why: "\u043C\u043E\u0440\u0434\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B\u0430 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u043B\u044E\u0447 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C ~/.taskless-swarm/drone-channel.key" };
|
|
6995
|
-
return await res.json();
|
|
6996
|
-
} catch {
|
|
6997
|
-
return null;
|
|
6998
|
-
}
|
|
6999
|
-
}
|
|
7000
|
-
function daemonCancel(slug, reason) {
|
|
7001
|
-
return post("/cancel", { slug, reason });
|
|
7002
|
-
}
|
|
7003
|
-
function daemonWake(runId) {
|
|
7004
|
-
return post("/wake", { runId: String(runId) });
|
|
7005
|
-
}
|
|
7006
|
-
function daemonPause(reason) {
|
|
7007
|
-
return post("/pause", { source: "cli", ...reason ? { reason } : {} });
|
|
7008
|
-
}
|
|
7009
|
-
function daemonResume() {
|
|
7010
|
-
return post("/resume", { source: "cli" });
|
|
7011
|
-
}
|
|
7012
|
-
async function daemonStop(opts) {
|
|
7013
|
-
const res = await post("/shutdown", { force: opts.force ? "1" : "0", wait: opts.wait ? "1" : "0" });
|
|
7014
|
-
if (!res) return null;
|
|
7015
|
-
const raw = res;
|
|
7016
|
-
if (raw.stopping === void 0 && raw.waiting === void 0) return null;
|
|
7017
|
-
return { stopping: !!raw.stopping, waiting: !!raw.waiting, sessions: Array.isArray(raw.sessions) ? raw.sessions : [] };
|
|
7018
|
-
}
|
|
7019
|
-
async function daemonStatus2() {
|
|
7020
|
-
try {
|
|
7021
|
-
const res = await fetch(`${BASE}/status`, { headers: headers(), signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
7022
|
-
if (!res.ok) return null;
|
|
7023
|
-
const body = await res.json();
|
|
7024
|
-
return typeof body?.paused === "boolean" ? body : null;
|
|
7025
|
-
} catch {
|
|
7026
|
-
return null;
|
|
7027
|
-
}
|
|
7028
|
-
}
|
|
7029
|
-
var NO_DAEMON_NOTE = `\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT}) \u2014 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E \u041B\u041E\u041A\u0410\u041B\u042C\u041D\u042B\u0419 \u0441\u0442\u043E\u0440 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u044B, \u043E\u043D \u043C\u043E\u0436\u0435\u0442 \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u0442\u044C\u0441\u044F \u0441 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u043E\u043C. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0434\u0435\u043C\u043E\u043D\u0430: swarm start`;
|
|
9134
|
+
// src/cli.ts
|
|
9135
|
+
init_daemonLink();
|
|
7030
9136
|
|
|
7031
9137
|
// src/pauseState.ts
|
|
7032
|
-
import { existsSync as
|
|
7033
|
-
import { join as
|
|
9138
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
|
|
9139
|
+
import { join as join18 } from "path";
|
|
7034
9140
|
function pauseStatePath(home) {
|
|
7035
|
-
return
|
|
9141
|
+
return join18(home, "pause.json");
|
|
7036
9142
|
}
|
|
7037
9143
|
var ACTIVE = { paused: false, at: "", source: "cli" };
|
|
7038
9144
|
function loadPauseState(home, io = {}) {
|
|
7039
|
-
const exists = io.exists ??
|
|
7040
|
-
const readFile2 = io.readFile ?? ((p) =>
|
|
9145
|
+
const exists = io.exists ?? existsSync9;
|
|
9146
|
+
const readFile2 = io.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
7041
9147
|
const path = pauseStatePath(home);
|
|
7042
9148
|
if (!exists(path)) return ACTIVE;
|
|
7043
9149
|
try {
|
|
@@ -7056,13 +9162,13 @@ function loadPauseState(home, io = {}) {
|
|
|
7056
9162
|
function savePauseState(home, state, io = {}) {
|
|
7057
9163
|
const path = pauseStatePath(home);
|
|
7058
9164
|
if (!state.paused) {
|
|
7059
|
-
const remove = io.remove ?? ((p) =>
|
|
9165
|
+
const remove = io.remove ?? ((p) => existsSync9(p) && unlinkSync5(p));
|
|
7060
9166
|
remove(path);
|
|
7061
9167
|
return;
|
|
7062
9168
|
}
|
|
7063
9169
|
const writeFile2 = io.writeFile ?? ((p, data) => {
|
|
7064
9170
|
writeFileSync10(`${p}.tmp`, data);
|
|
7065
|
-
|
|
9171
|
+
renameSync5(`${p}.tmp`, p);
|
|
7066
9172
|
});
|
|
7067
9173
|
writeFile2(path, JSON.stringify(state, null, 2));
|
|
7068
9174
|
}
|
|
@@ -7091,10 +9197,10 @@ function sinceText(at, now) {
|
|
|
7091
9197
|
}
|
|
7092
9198
|
|
|
7093
9199
|
// src/heartbeat.ts
|
|
7094
|
-
import { readFileSync as
|
|
9200
|
+
import { readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
7095
9201
|
function readHeartbeat(path) {
|
|
7096
9202
|
try {
|
|
7097
|
-
const ms = Number(
|
|
9203
|
+
const ms = Number(readFileSync13(path, "utf8").trim());
|
|
7098
9204
|
return Number.isFinite(ms) ? ms : null;
|
|
7099
9205
|
} catch {
|
|
7100
9206
|
return null;
|
|
@@ -7103,7 +9209,7 @@ function readHeartbeat(path) {
|
|
|
7103
9209
|
|
|
7104
9210
|
// src/cli.ts
|
|
7105
9211
|
init_src();
|
|
7106
|
-
|
|
9212
|
+
init_version();
|
|
7107
9213
|
function takeRepo(argv) {
|
|
7108
9214
|
const i = argv.indexOf("--repo");
|
|
7109
9215
|
if (i === -1) return { repo: null, rest: argv };
|
|
@@ -7157,14 +9263,6 @@ function takeWorkspace(argv) {
|
|
|
7157
9263
|
const workspace = argv[i + 1] ?? null;
|
|
7158
9264
|
return { workspace, rest: [...argv.slice(0, i), ...argv.slice(i + 2)] };
|
|
7159
9265
|
}
|
|
7160
|
-
async function askTty(q) {
|
|
7161
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7162
|
-
try {
|
|
7163
|
-
return await rl.question(q);
|
|
7164
|
-
} finally {
|
|
7165
|
-
rl.close();
|
|
7166
|
-
}
|
|
7167
|
-
}
|
|
7168
9266
|
async function runLogin(db2, opts) {
|
|
7169
9267
|
const web = startWeb({ db: db2 });
|
|
7170
9268
|
try {
|
|
@@ -7283,7 +9381,6 @@ async function waitDaemonDown(opts) {
|
|
|
7283
9381
|
await sleep2(pollMs);
|
|
7284
9382
|
}
|
|
7285
9383
|
}
|
|
7286
|
-
var MAX_DAEMON_LOG_BYTES = 16 * 1024 * 1024;
|
|
7287
9384
|
var START_READY_TIMEOUT_MS = 45e3;
|
|
7288
9385
|
async function runStartDetached(args) {
|
|
7289
9386
|
const already = await daemonStatus2();
|
|
@@ -7292,7 +9389,7 @@ async function runStartDetached(args) {
|
|
|
7292
9389
|
console.log("\u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435: swarm status \xB7 \u043B\u043E\u0433: swarm logs -f \xB7 \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C: swarm stop");
|
|
7293
9390
|
return 0;
|
|
7294
9391
|
}
|
|
7295
|
-
const rotated = rotateDaemonLog(DAEMON_LOG_PATH,
|
|
9392
|
+
const rotated = rotateDaemonLog(DAEMON_LOG_PATH, { maxBytes: DAEMON_LOG_MAX_BYTES, keep: DAEMON_LOG_KEEP });
|
|
7296
9393
|
const entry = process.argv[1];
|
|
7297
9394
|
if (!entry) {
|
|
7298
9395
|
console.error("\u043D\u0435\u0447\u0435\u043C \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0441\u0435\u0431\u044F \u0444\u043E\u043D\u043E\u043C (\u043F\u0443\u0441\u0442\u043E\u0439 argv[1]) \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0438 \u0432 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435: swarm start --foreground");
|
|
@@ -7345,7 +9442,7 @@ async function runDaemonLogs(argv) {
|
|
|
7345
9442
|
console.error(`\u043B\u043E\u0433 \u041F\u0420\u041E\u0413\u041E\u041D\u0410 \u2014 \u044D\u0442\u043E \xABswarm log ${stray}\xBB (\u0431\u0435\u0437 s). \xABswarm logs\xBB \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430; \u0447\u0438\u0441\u043B\u043E \u0441\u0442\u0440\u043E\u043A \u2014 \xABswarm logs -n 200\xBB.`);
|
|
7346
9443
|
return 1;
|
|
7347
9444
|
}
|
|
7348
|
-
if (!
|
|
9445
|
+
if (!existsSync14(DAEMON_LOG_PATH)) {
|
|
7349
9446
|
console.error(`\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442: ${DAEMON_LOG_PATH}`);
|
|
7350
9447
|
console.error("\u043E\u043D \u043F\u0438\u0448\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u0444\u043E\u043D\u043E\u0432\u043E\u043C \u0437\u0430\u043F\u0443\u0441\u043A\u0435 (swarm start). \u0414\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u043D\u044F\u0442 \u0432 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435 (--foreground) \u2014 \u0432\u044B\u0432\u043E\u0434 \u0438\u0434\u0451\u0442 \u0442\u0443\u0434\u0430.");
|
|
7351
9448
|
return 1;
|
|
@@ -7353,12 +9450,12 @@ async function runDaemonLogs(argv) {
|
|
|
7353
9450
|
const head = tailFile(DAEMON_LOG_PATH, lines);
|
|
7354
9451
|
if (head) console.log(head);
|
|
7355
9452
|
if (!follow) return 0;
|
|
7356
|
-
let pos =
|
|
9453
|
+
let pos = statSync9(DAEMON_LOG_PATH).size;
|
|
7357
9454
|
for (; ; ) {
|
|
7358
9455
|
await sleep2(500);
|
|
7359
9456
|
let size;
|
|
7360
9457
|
try {
|
|
7361
|
-
size =
|
|
9458
|
+
size = statSync9(DAEMON_LOG_PATH).size;
|
|
7362
9459
|
} catch {
|
|
7363
9460
|
continue;
|
|
7364
9461
|
}
|
|
@@ -7519,21 +9616,33 @@ function printRuns(runs, view) {
|
|
|
7519
9616
|
}).forEach((l) => console.log(l));
|
|
7520
9617
|
}
|
|
7521
9618
|
function usage() {
|
|
7522
|
-
console.log(`swarm \u2014 \u0440\u043E\u0439 AI-\u0434\u0440\u043E\u043D\u043E\u0432 (v${
|
|
9619
|
+
console.log(`swarm \u2014 \u0440\u043E\u0439 AI-\u0434\u0440\u043E\u043D\u043E\u0432 (v${PACKAGE_VERSION})
|
|
7523
9620
|
|
|
7524
9621
|
\u041D\u0410\u0427\u0410\u0422\u042C
|
|
9622
|
+
swarm install [--yes] \u043F\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043C\u0430\u0448\u0438\u043D\u044B \u0432 \u0440\u0430\u0431\u043E\u0447\u0435\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435: Node, claude, gh,
|
|
9623
|
+
\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u0440\u043E\u044F, playwright-core \u0438 Chromium \u0434\u043B\u044F \u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442\u043E\u0432,
|
|
9624
|
+
Docker (\u0435\u0441\u043B\u0438 \u043D\u0443\u0436\u0435\u043D \u043F\u0440\u043E\u0435\u043A\u0442\u0443), \u043A\u043B\u043E\u043D \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F. \u041E\u0434\u0438\u043D\u0430\u043A\u043E\u0432\u043E \u043D\u0430
|
|
9625
|
+
macOS, Linux \u0438 WSL; sudo \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u043A\u0430\u0437\u0430\u0432 \u0441\u0442\u0440\u043E\u043A\u0443 \u0438 \u0441\u043F\u0440\u043E\u0441\u0438\u0432
|
|
9626
|
+
swarm doctor \u0442\u0430 \u0436\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0411\u0415\u0417 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438: \u0447\u0442\u043E \u043D\u0435 \u0442\u0430\u043A \u0438 \u043A\u0430\u043A \u0447\u0438\u043D\u0438\u0442\u044C
|
|
9627
|
+
swarm update \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0440\u043E\u0439 \u0434\u043E \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 (\u0442\u0435\u043C \u0436\u0435 \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440\u043E\u043C,
|
|
9628
|
+
\u043A\u0430\u043A\u0438\u043C \u043E\u043D \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D)
|
|
7525
9629
|
swarm login [--workspace <slug|id>] [--no-open]
|
|
7526
9630
|
\u0432\u043E\u0439\u0442\u0438 \u0432 Taskless \u0438 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441
|
|
7527
9631
|
swarm start [--repo <path>] [--foreground]
|
|
7528
9632
|
\u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0440\u043E\u0439 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u043E\u0447\u0435\u0440\u0435\u0434\u044C, \u0441\u043F\u0430\u0432\u043D\u0438\u0442 \u0438 \u0431\u0443\u0434\u0438\u0442 \u0434\u0440\u043E\u043D\u043E\u0432.
|
|
7529
9633
|
\u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0430\u0445\u043E\u0434\u0438\u0442 \u0441\u0430\u043C \u2014 \u043A\u043B\u043E\u043D\u044B \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 Taskless \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435;
|
|
7530
9634
|
--repo \u043D\u0443\u0436\u0435\u043D, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u043B\u0435\u0436\u0430\u0442 \u0432 \u043D\u0435\u043E\u0431\u044B\u0447\u043D\u043E\u043C \u043C\u0435\u0441\u0442\u0435
|
|
7531
|
-
swarm status \u0436\u0438\u0432 \u043B\u0438 \u0434\u0435\u043C\u043E\u043D, \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u043B\u0438 \u0440\u043E\u0439, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0434\u0440\u043E\u043D\u043E\u0432 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435
|
|
9635
|
+
swarm status \u0436\u0438\u0432 \u043B\u0438 \u0434\u0435\u043C\u043E\u043D, \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u043B\u0438 \u0440\u043E\u0439, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0434\u0440\u043E\u043D\u043E\u0432 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435,
|
|
9636
|
+
\u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F \u0438 \u0433\u0434\u0435 \u043D\u0430\u0448\u043B\u0438\u0441\u044C \u0438\u0445 \u043A\u043B\u043E\u043D\u044B
|
|
7532
9637
|
|
|
7533
9638
|
\u0414\u0410\u0422\u042C \u0420\u041E\u042E \u0420\u0410\u0411\u041E\u0422\u0423
|
|
7534
9639
|
swarm spawn <slug> [area] [\u0444\u043B\u0430\u0433\u0438 \u043D\u0438\u0436\u0435]
|
|
7535
9640
|
\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C
|
|
7536
9641
|
swarm ls [\u0441\u0442\u0430\u0442\u0443\u0441] \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0440\u043E\u0432\u043D\u043E \u0442\u0435, \u0447\u0442\u043E \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435
|
|
9642
|
+
swarm zones [check|suggest <\u0442\u0435\u043A\u0441\u0442|slug>]
|
|
9643
|
+
\u0437\u043E\u043D\u044B \u043A\u043E\u0434\u0430, \u0432\u044B\u0432\u0435\u0434\u0435\u043D\u043D\u044B\u0435 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E (\u043A\u0430\u043A\u0438\u0435 \u0444\u0430\u0439\u043B\u044B \u043C\u0435\u043D\u044F\u044E\u0442\u0441\u044F
|
|
9644
|
+
\u0432\u043C\u0435\u0441\u0442\u0435, \u0447\u0442\u043E \u0442\u0443\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442). check \u2014 \u0441\u0432\u0435\u0440\u043A\u0430 \u0441 \u0440\u0443\u0447\u043D\u043E\u0439
|
|
9645
|
+
\u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439 area:*; suggest \u2014 \u043A\u0430\u043A\u0443\u044E \u0437\u043E\u043D\u0443 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0435
|
|
7537
9646
|
swarm wake <runId> \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D (id \u0438\u0437 swarm ls); \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E \u2192 \u043D\u0430\u0447\u043D\u0451\u0442 \u0441 \u043D\u0443\u043B\u044F
|
|
7538
9647
|
swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026>
|
|
7539
9648
|
\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430: \u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\xBB \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439, \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F
|
|
@@ -7586,10 +9695,10 @@ function effectiveWorktreesDir() {
|
|
|
7586
9695
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
7587
9696
|
targetDir: WORKTREES_DIR,
|
|
7588
9697
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
7589
|
-
exists: (d) =>
|
|
9698
|
+
exists: (d) => existsSync14(d),
|
|
7590
9699
|
isNonEmptyDir: (d) => {
|
|
7591
9700
|
try {
|
|
7592
|
-
return
|
|
9701
|
+
return readdirSync9(d).length > 0;
|
|
7593
9702
|
} catch {
|
|
7594
9703
|
return false;
|
|
7595
9704
|
}
|
|
@@ -7614,6 +9723,80 @@ async function makeCliStore(repo) {
|
|
|
7614
9723
|
client.reconcileRuntime({ worktreesDir: effectiveWorktreesDir(), listProcesses: listDroneProcesses });
|
|
7615
9724
|
return client;
|
|
7616
9725
|
}
|
|
9726
|
+
async function runZones(db2, repo, args) {
|
|
9727
|
+
const manual = new Map(db2.listRuns().map((r) => [r.task_slug, r.area]));
|
|
9728
|
+
const raw = await readTaskHistory(repo);
|
|
9729
|
+
let fromScope = 0;
|
|
9730
|
+
for (const e of raw) {
|
|
9731
|
+
if (manual.get(e.slug)) continue;
|
|
9732
|
+
const scope = scopeFromText(e.text);
|
|
9733
|
+
if (scope) {
|
|
9734
|
+
manual.set(e.slug, scope);
|
|
9735
|
+
fromScope++;
|
|
9736
|
+
}
|
|
9737
|
+
}
|
|
9738
|
+
const history = withManualAreas(raw, manual);
|
|
9739
|
+
const model = buildZoneModel(history);
|
|
9740
|
+
if (!hasZoneModel(model)) {
|
|
9741
|
+
console.log(`\u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043C\u0430\u043B\u043E (${history.length} \u0437\u0430\u0434\u0430\u0447 \u0441\u043E \u0441\u043B\u0430\u0433\u043E\u043C \u0432 \u043A\u043E\u043C\u043C\u0438\u0442\u0430\u0445) \u2014 \u0437\u043E\u043D\u044B \u043D\u0435 \u0432\u044B\u0432\u043E\u0434\u044F\u0442\u0441\u044F, \u0440\u0430\u0437\u0434\u0430\u0447\u0430 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E \u043B\u0435\u0439\u0431\u043B\u0430\u043C area:*`);
|
|
9742
|
+
return;
|
|
9743
|
+
}
|
|
9744
|
+
const sub = args[0];
|
|
9745
|
+
if (sub === "check") {
|
|
9746
|
+
const a = checkZoneAgreement(model);
|
|
9747
|
+
if (!a.checked) {
|
|
9748
|
+
console.log("\u043D\u0438 \u0443 \u043E\u0434\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u0440\u0443\u0447\u043D\u043E\u0439 \u0437\u043E\u043D\u044B \u2014 \u0441\u0432\u0435\u0440\u044F\u0442\u044C \u043D\u0435 \u0441 \u0447\u0435\u043C (\u043F\u0440\u043E\u0441\u0442\u0430\u0432\u044C area:* \u0445\u043E\u0442\u044F \u0431\u044B \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u043C)");
|
|
9749
|
+
return;
|
|
9750
|
+
}
|
|
9751
|
+
console.log(
|
|
9752
|
+
`\u0441\u0432\u0435\u0440\u043A\u0430 \u0441 \u0440\u0443\u0447\u043D\u043E\u0439 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439: \u0442\u043E\u0447\u043D\u043E ${a.matched}/${a.checked} (${Math.round(a.rate * 100)}%), \u043D\u0435 \u043F\u0440\u043E\u0442\u0438\u0432\u043E\u0440\u0435\u0447\u0438\u0442 ${Math.round(a.looseRate * 100)}% (\u0440\u0443\u0447\u043D\u043E\u0439 \u043B\u0435\u0439\u0431\u043B \u0448\u0438\u0440\u0435 \u0432\u044B\u0432\u0435\u0434\u0435\u043D\u043D\u043E\u0439 \u0437\u043E\u043D\u044B)`
|
|
9753
|
+
);
|
|
9754
|
+
if (fromScope) {
|
|
9755
|
+
console.log(
|
|
9756
|
+
` \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0438: area:* \u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0440\u043E\u044F, \u0430 \u0433\u0434\u0435 \u0435\u0451 \u043D\u0435\u0442 (${fromScope} \u0437\u0430\u0434\u0430\u0447) \u2014 scope \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 (\`feat(calls):\`). \u0421\u043B\u043E\u0432\u0430\u0440\u044C scope \u0448\u0438\u0440\u0435 \u0437\u043E\u043D: \xABci\xBB, \xABapi,web\xBB \u0437\u043E\u043D\u0430\u043C\u0438 \u043A\u043E\u0434\u0430 \u043D\u0435 \u044F\u0432\u043B\u044F\u044E\u0442\u0441\u044F \u0432\u043E\u0432\u0441\u0435.`
|
|
9757
|
+
);
|
|
9758
|
+
}
|
|
9759
|
+
const bad = a.rows.filter((r) => r.verdict === "mismatch");
|
|
9760
|
+
if (bad.length) {
|
|
9761
|
+
console.log(`
|
|
9762
|
+
\u0440\u0430\u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043F\u043E \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443 (${bad.length}) \u2014 \u0437\u0434\u0435\u0441\u044C \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0430 \u0438 \u0444\u0430\u0439\u043B\u044B \u0433\u043E\u0432\u043E\u0440\u044F\u0442 \u0440\u0430\u0437\u043D\u043E\u0435:`);
|
|
9763
|
+
for (const r of bad.slice(0, 30)) console.log(` ${r.slug.padEnd(12)} \u0440\u0430\u0437\u043C\u0435\u0447\u0435\u043D\u043E ${r.manual} \u2192 \u043F\u043E \u0444\u0430\u0439\u043B\u0430\u043C ${r.suggested}`);
|
|
9764
|
+
}
|
|
9765
|
+
return;
|
|
9766
|
+
}
|
|
9767
|
+
if (sub === "suggest") {
|
|
9768
|
+
const query = args.slice(1).join(" ").trim();
|
|
9769
|
+
if (!query) {
|
|
9770
|
+
console.error('\u043D\u0443\u0436\u0435\u043D \u0442\u0435\u043A\u0441\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u043B\u0438 \u0435\u0451 \u0441\u043B\u0430\u0433: swarm zones suggest "\u0437\u0432\u043E\u043D\u043A\u0438: \u0442\u0430\u0439\u043C\u043B\u0430\u0439\u043D \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430"');
|
|
9771
|
+
process.exit(1);
|
|
9772
|
+
}
|
|
9773
|
+
const bySlug = db2.listRuns().find((r) => r.task_slug === query);
|
|
9774
|
+
const text = bySlug ? [bySlug.task_title ?? "", bySlug.task_desc ?? ""].join(" ").trim() || query : query;
|
|
9775
|
+
const guesses = suggestZonesFromText(model, text);
|
|
9776
|
+
if (!guesses.length) {
|
|
9777
|
+
console.log("\u043F\u043E \u044D\u0442\u043E\u043C\u0443 \u0442\u0435\u043A\u0441\u0442\u0443 \u0437\u043E\u043D\u0430 \u043D\u0435 \u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u2014 \u0438\u0441\u0442\u043E\u0440\u0438\u044F \u043D\u0438\u0447\u0435\u0433\u043E \u043F\u043E\u0445\u043E\u0436\u0435\u0433\u043E \u043D\u0435 \u043F\u043E\u043C\u043D\u0438\u0442");
|
|
9778
|
+
return;
|
|
9779
|
+
}
|
|
9780
|
+
for (const g of guesses) console.log(`${g.zone.padEnd(20)} ${String(Math.round(g.confidence * 100)).padStart(3)}% ${g.why}`);
|
|
9781
|
+
const files = predictFiles(model, { text });
|
|
9782
|
+
if (files.length) {
|
|
9783
|
+
console.log("\n\u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E \u0442\u0440\u043E\u043D\u0435\u0442:");
|
|
9784
|
+
for (const f of files.slice(0, 10)) console.log(` ${f.path} (${f.why})`);
|
|
9785
|
+
}
|
|
9786
|
+
return;
|
|
9787
|
+
}
|
|
9788
|
+
console.log(`\u0437\u043E\u043D\u044B \u043F\u043E \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E (${model.tasks} \u0437\u0430\u0434\u0430\u0447):
|
|
9789
|
+
`);
|
|
9790
|
+
for (const z of model.zones.filter((x) => x.zone !== SPINE_ZONE)) {
|
|
9791
|
+
console.log(`${z.zone.padEnd(20)} \u0437\u0430\u0434\u0430\u0447 ${String(z.tasks).padStart(4)} \u0444\u0430\u0439\u043B\u043E\u0432 ${String(z.files.length).padStart(4)} \u043D\u0430\u043F\u0440.: ${z.files.slice(0, 2).join(", ")}`);
|
|
9792
|
+
}
|
|
9793
|
+
if (model.spine.length) {
|
|
9794
|
+
console.log(`
|
|
9795
|
+
\u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${model.spine.length} \u0444\u0430\u0439\u043B\u043E\u0432) \u2014 \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u0440\u043E \u043D\u0435\u0433\u043E \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B:`);
|
|
9796
|
+
for (const f of model.spine.slice(0, 15)) console.log(` ${f}`);
|
|
9797
|
+
}
|
|
9798
|
+
console.log("\n\u0441\u0432\u0435\u0440\u0438\u0442\u044C \u0441 \u0440\u0443\u0447\u043D\u043E\u0439 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439: swarm zones check");
|
|
9799
|
+
}
|
|
7617
9800
|
async function finishClient(db2) {
|
|
7618
9801
|
if (!(db2 instanceof SwarmClient)) return;
|
|
7619
9802
|
await db2.flush();
|
|
@@ -7622,7 +9805,7 @@ async function finishClient(db2) {
|
|
|
7622
9805
|
}
|
|
7623
9806
|
var [cmd, ...rawArgs] = process.argv.slice(2);
|
|
7624
9807
|
if (cmd === "--version" || cmd === "-v") {
|
|
7625
|
-
console.log(
|
|
9808
|
+
console.log(PACKAGE_VERSION);
|
|
7626
9809
|
process.exit(0);
|
|
7627
9810
|
}
|
|
7628
9811
|
if (cmd === "logout") {
|
|
@@ -7744,6 +9927,7 @@ if (cmd === "status") {
|
|
|
7744
9927
|
const counts = Object.entries(st.counts).filter(([, n]) => n > 0);
|
|
7745
9928
|
console.log(counts.length ? `\u043F\u0440\u043E\u0433\u043E\u043D\u044B: ${counts.map(([s, n]) => `${s} ${n}`).join(", ")}` : "\u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u043D\u0435\u0442");
|
|
7746
9929
|
console.log(st.live.length ? `\u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u0441\u0435\u0439\u0447\u0430\u0441: ${st.live.length} (#${st.live.join(", #")})` : "\u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442");
|
|
9930
|
+
for (const line of repoMapLines(st.repos)) console.log(line);
|
|
7747
9931
|
process.exit(0);
|
|
7748
9932
|
}
|
|
7749
9933
|
if (cmd === "pause" || cmd === "resume") {
|
|
@@ -7774,6 +9958,15 @@ if (cmd === "remove") {
|
|
|
7774
9958
|
const code = await runRemove(rawArgs);
|
|
7775
9959
|
process.exit(code);
|
|
7776
9960
|
}
|
|
9961
|
+
if (cmd === "install" || cmd === "doctor") {
|
|
9962
|
+
const { runInstallCli: runInstallCli2, runDoctorCli: runDoctorCli2 } = await Promise.resolve().then(() => (init_installCli(), installCli_exports));
|
|
9963
|
+
const run = cmd === "install" ? runInstallCli2 : runDoctorCli2;
|
|
9964
|
+
process.exit(await run(rawArgs, { packageVersion: PACKAGE_VERSION }));
|
|
9965
|
+
}
|
|
9966
|
+
if (cmd === "update") {
|
|
9967
|
+
const { runUpdateCli: runUpdateCli2 } = await Promise.resolve().then(() => (init_updateCli(), updateCli_exports));
|
|
9968
|
+
process.exit(await runUpdateCli2(rawArgs, { packageVersion: PACKAGE_VERSION }));
|
|
9969
|
+
}
|
|
7777
9970
|
if (cmd === "shot") {
|
|
7778
9971
|
const code = await runShotCli(rawArgs);
|
|
7779
9972
|
process.exit(code);
|
|
@@ -7903,6 +10096,9 @@ switch (cmd) {
|
|
|
7903
10096
|
overmindEntry: OVERMIND_ENTRY,
|
|
7904
10097
|
dbPath: DB_PATH,
|
|
7905
10098
|
runLogsDir: RUN_LOGS_DIR,
|
|
10099
|
+
// Тот же исполнитель, что у демона (TASWM-349): `swarm wake` при мёртвом демоне не
|
|
10100
|
+
// должен внезапно поднимать дрона другим харнессом.
|
|
10101
|
+
executor: EXECUTOR,
|
|
7906
10102
|
droneIsolation: DRONE_ISOLATION,
|
|
7907
10103
|
...DRONE_CHANNEL_ENABLED ? {
|
|
7908
10104
|
channel: {
|
|
@@ -7998,6 +10194,20 @@ switch (cmd) {
|
|
|
7998
10194
|
db.close();
|
|
7999
10195
|
process.exit(code);
|
|
8000
10196
|
}
|
|
10197
|
+
// TASWM-348: зоны кода, выведенные из истории репозитория.
|
|
10198
|
+
// swarm zones — какие зоны видит рой и что считает общим хребтом
|
|
10199
|
+
// swarm zones check — сверка с ручной разметкой area:* на прошлых задачах
|
|
10200
|
+
// swarm zones suggest <текст|slug> — какую зону предложить задаче ПРИ ПОСТАНОВКЕ
|
|
10201
|
+
case "zones": {
|
|
10202
|
+
const { repo: repoFlag, rest } = takeRepo(rawArgs);
|
|
10203
|
+
const repo = repoFlag ?? SWARM_REPO;
|
|
10204
|
+
if (!repo) {
|
|
10205
|
+
console.error("\u043D\u0435\u0442 \u0440\u0435\u043F\u043E: \u0437\u0430\u0434\u0430\u0439 --repo <path> \u0438\u043B\u0438 SWARM_REPO");
|
|
10206
|
+
process.exit(1);
|
|
10207
|
+
}
|
|
10208
|
+
await runZones(db, repo, rest);
|
|
10209
|
+
break;
|
|
10210
|
+
}
|
|
8001
10211
|
case "web": {
|
|
8002
10212
|
const web = startWeb({ db });
|
|
8003
10213
|
console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
|