hmgr 0.0.28 → 0.0.30

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.
@@ -9,6 +9,7 @@ import { LokalerZugriff } from "./dateisystem.js";
9
9
  import { gitignoreEintragen, istGitRepo } from "./gitignore.js";
10
10
  import { arbeitsverzeichnisVerbinden, heimVerbinden, nurHome } from "./heime.js";
11
11
  import { tiefGleich } from "./vergleich.js";
12
+ import { mitZeitlimit } from "./zeitlimit.js";
12
13
  const CLAUDE_VERZEICHNIS = '.claude';
13
14
  const SKILLS_ORDNER = 'skills';
14
15
  /**
@@ -34,8 +35,8 @@ const GITIGNORE_ZEILE = '.claude/';
34
35
  /**
35
36
  * Standard-Ausführer: `claude` vom PATH. Ein Fehlschlag wird zum Code, nicht zur Ausnahme.
36
37
  * stdin ist zu, sonst hängt eine CLI, die auf Eingabe wartet, den ganzen Ablauf; der Timeout
37
- * fängt alles Übrige (beobachtet: 13 Minuten ohne Rückkehr in der GUI). SIGKILL, weil die
38
- * CLI SIGTERM überleben kann.
38
+ * fängt alles Übrige (beobachtet: 13 Minuten ohne Rückkehr in der GUI), selbst gesetzt statt
39
+ * über die spawn-Option (E98, siehe `mitZeitlimit`).
39
40
  *
40
41
  * `spawn` kommt von cross-spawn, nicht aus node:child_process (E65/B7): unter Windows ist die
41
42
  * npm-installierte `claude` ein `.cmd`-Shim, und Node startet `.cmd`/`.bat` seit dem Fix zu
@@ -48,9 +49,8 @@ export const claudeAusfuehrer = (args, cwd) => new Promise((aufloesen) => {
48
49
  const kind = spawn('claude', args, {
49
50
  ...(cwd === undefined ? {} : { cwd }),
50
51
  stdio: ['ignore', 'pipe', 'pipe'],
51
- timeout: 120_000,
52
- killSignal: 'SIGKILL',
53
52
  });
53
+ mitZeitlimit(kind, 120_000);
54
54
  let stdout = '';
55
55
  let stderr = '';
56
56
  // `!`, weil cross-spawn nur den allgemeinen ChildProcess-Typ mit nullbaren Strömen kennt;
@@ -121,10 +121,39 @@ async function dateienAufPlatte(wurzel, unter = '') {
121
121
  }
122
122
  return dateien;
123
123
  }
124
+ /**
125
+ * Wartezeiten zwischen den Versuchen bei belegter git-Sperre (E99): 5 Versuche, zusammen rund 3 s.
126
+ * Tests übergeben kürzere über den Konstruktor.
127
+ */
128
+ export const SPERRE_WARTEZEITEN_MS = [200, 400, 800, 1600];
129
+ /** Meldung, mit der git eine belegte Sperrdatei anzeigt (E99). */
130
+ const SPERRE_BELEGT = /Unable to create '[^']+\.lock': File exists/;
124
131
  export class GitQuellen {
125
132
  wurzel;
126
- constructor(wurzel) {
133
+ sperreWartezeitenMs;
134
+ constructor(wurzel, optionen = {}) {
127
135
  this.wurzel = wurzel;
136
+ this.sperreWartezeitenMs = optionen.sperreWartezeitenMs ?? SPERRE_WARTEZEITEN_MS;
137
+ }
138
+ /**
139
+ * Wiederholt einen git-Aufruf, wenn eine fremde Sperre ihn blockiert (E99): GUI-Aktionen und der
140
+ * Start-Lauf rufen git parallel auf, und git bricht bei `….lock: File exists` sofort ab. Nur
141
+ * diese Meldung wird wiederholt, jeder andere Fehler gilt sofort. Nach dem letzten Versuch bleibt
142
+ * die bisherige Fehlermeldung. hmgr löscht fremde Sperren nie, auch keine verwaisten.
143
+ */
144
+ async mitSperrWiederholung(aufruf) {
145
+ for (let versuch = 0;; versuch++) {
146
+ try {
147
+ return await aufruf();
148
+ }
149
+ catch (fehler) {
150
+ const wartezeit = this.sperreWartezeitenMs[versuch];
151
+ const meldung = fehler instanceof Error ? fehler.message : String(fehler);
152
+ if (wartezeit === undefined || !SPERRE_BELEGT.test(meldung))
153
+ throw fehler;
154
+ await new Promise((fertig) => setTimeout(fertig, wartezeit));
155
+ }
156
+ }
128
157
  }
129
158
  /**
130
159
  * stdin ist zu und git darf nicht nach Zugangsdaten fragen, sonst hängt ein wartender Aufruf
@@ -132,6 +161,9 @@ export class GitQuellen {
132
161
  * als dort, ein erster Clone über ein langsames Netz braucht Zeit.
133
162
  */
134
163
  async git(args, cwd) {
164
+ return await this.mitSperrWiederholung(() => this.gitEinmal(args, cwd));
165
+ }
166
+ async gitEinmal(args, cwd) {
135
167
  return await new Promise((erfuellen, ablehnen) => {
136
168
  const kind = spawn('git', args, {
137
169
  cwd,
@@ -155,6 +187,9 @@ export class GitQuellen {
155
187
  }
156
188
  /** Wie `git`, aber mit Text auf stdin und Bytes als Ergebnis: `execFile` kennt keine Eingabe. */
157
189
  async gitMitEingabe(args, cwd, eingabe) {
190
+ return await this.mitSperrWiederholung(() => this.gitMitEingabeEinmal(args, cwd, eingabe));
191
+ }
192
+ async gitMitEingabeEinmal(args, cwd, eingabe) {
158
193
  return await new Promise((erfuellen, ablehnen) => {
159
194
  const kind = spawn('git', args, { cwd, timeout: 300_000, killSignal: 'SIGKILL' });
160
195
  const stuecke = [];
@@ -4,13 +4,15 @@
4
4
  import spawn from 'cross-spawn';
5
5
  import path from 'node:path';
6
6
  import { WslBrueckeFehlgeschlagen } from "../kern/modell.js";
7
+ import { mitZeitlimit } from "./zeitlimit.js";
7
8
  /**
8
9
  * Standard-Ausführer: `wsl.exe` vom PATH, über cross-spawn wie die claude-CLI (E65/B7). stdin ist
9
- * zu, sonst hängt eine Distro, die auf Eingabe wartet; der Timeout fängt alles Übrige. Ein
10
+ * zu, sonst hängt eine Distro, die auf Eingabe wartet; der Timeout (E98, `mitZeitlimit`) fängt alles Übrige. Ein
10
11
  * Fehlschlag wird zum Code, nicht zur Ausnahme.
11
12
  */
12
13
  export const wslAusfuehrer = (args) => new Promise((aufloesen) => {
13
- const kind = spawn('wsl.exe', args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, killSignal: 'SIGKILL' });
14
+ const kind = spawn('wsl.exe', args, { stdio: ['ignore', 'pipe', 'pipe'] });
15
+ mitZeitlimit(kind, 60_000);
14
16
  const teile = [];
15
17
  const fehlerTeile = [];
16
18
  kind.stdout.on('data', (d) => teile.push(d));
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Zeitlimit für einen mit cross-spawn gestarteten Prozess (E98). Statt der spawn-Option `timeout`
3
+ * setzen wir den Wecker selbst: Node räumt dessen Timer im `exit`-Handler ab, cross-spawn ersetzt
4
+ * unter Windows aber das `exit` eines nicht gefundenen Programms durch ein `error`. Der Timer
5
+ * bliebe stehen und hielte den Prozess bis zum Ablauf am Leben (gemessen: 120 s ohne `claude`).
6
+ * Abgeräumt wird deshalb bei `close` und bei `error`. Bei Ablauf SIGKILL, weil CLIs SIGTERM
7
+ * überleben können; der Aufrufer sieht dann wie bisher `close` mit Code `null`.
8
+ */
9
+ export function mitZeitlimit(kind, ms) {
10
+ const wecker = setTimeout(() => kind.kill('SIGKILL'), ms);
11
+ const abraeumen = () => clearTimeout(wecker);
12
+ kind.once('close', abraeumen);
13
+ kind.once('error', abraeumen);
14
+ }
@@ -4,6 +4,7 @@
4
4
  import { homedir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import spawn from 'cross-spawn';
7
+ import { mitZeitlimit } from "../adapter/zeitlimit.js";
7
8
  import { versionLesen } from "../kern/version.js";
8
9
  /**
9
10
  * Zeigt der aufgelöste Treffer auf die laufende GUI selbst (E97)? Deren Electron-exe heißt ebenfalls
@@ -25,8 +26,6 @@ export function istEigeneGui(aufgeloest, execPath, plattform) {
25
26
  export function cliSpawnOptionen(heim = homedir()) {
26
27
  return {
27
28
  stdio: ['ignore', 'pipe', 'ignore'],
28
- timeout: 10_000,
29
- killSignal: 'SIGKILL',
30
29
  cwd: heim,
31
30
  };
32
31
  }
@@ -45,7 +44,7 @@ function aufgeloesterPfad(optionen) {
45
44
  *
46
45
  * `spawn` von cross-spawn wie im claude-Adapter (E65/B7): unter Windows ist die npm-installierte
47
46
  * `hmgr` ein `.cmd`-Shim, das Node seit dem Fix zu CVE-2024-27980 ohne Umweg über cmd.exe nicht
48
- * mehr startet. stdin zu und ein Timeout, damit nichts hängen bleibt — ein Timeout liefert `fehlt`
47
+ * mehr startet. stdin zu und ein Timeout (E98, `mitZeitlimit`), damit nichts hängen bleibt — ein Timeout liefert `fehlt`
49
48
  * (cross-spawn beendet den Prozess dann mit `close` und `null`-Code samt Signal, kein `error`).
50
49
  */
51
50
  export function hmgrCliVersion() {
@@ -63,6 +62,7 @@ export function hmgrCliVersion() {
63
62
  return melden({ art: 'fehlt' });
64
63
  }
65
64
  const kind = spawn('hmgr', ['--version'], optionen);
65
+ mitZeitlimit(kind, 10_000);
66
66
  let stdout = '';
67
67
  kind.stdout.on('data', (stueck) => (stdout += stueck));
68
68
  kind.on('error', () => melden({ art: 'fehlt' }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",