@harness-mix/cli 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +469 -467
  3. package/output/native-build/desktop-controller.mjs +1 -1
  4. package/output/native-build/renderer-extension.js +23 -4
  5. package/package.json +16 -9
  6. package/scripts/antigravity-adapter-test.cjs +647 -626
  7. package/scripts/codex-adapter-test.cjs +162 -127
  8. package/scripts/collaboration-test.cjs +274 -262
  9. package/scripts/core-review-test.cjs +68 -0
  10. package/scripts/delegation-await-test.cjs +76 -0
  11. package/scripts/jsonl-stdin-test.cjs +40 -0
  12. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  13. package/scripts/native-acp-depth-test.cjs +30 -5
  14. package/scripts/native-protocol-test.cjs +14 -1
  15. package/scripts/native-update-apply-test.cjs +269 -215
  16. package/scripts/native-update.cjs +78 -0
  17. package/scripts/native-vendor-adapters-test.cjs +196 -154
  18. package/scripts/salvage-rollout-writes.cjs +72 -0
  19. package/scripts/send-cancel-race-test.cjs +80 -0
  20. package/scripts/send-pre-turn-cancel-test.cjs +100 -0
  21. package/scripts/stuck-turn-test.cjs +6 -1
  22. package/scripts/zcode-adapter-test.cjs +329 -0
  23. package/scripts/zcode-live-probe.cjs +66 -0
  24. package/src/main/adapters/antigravity.js +1428 -1415
  25. package/src/main/adapters/codex.js +656 -649
  26. package/src/main/adapters/native-acp-command.js +51 -48
  27. package/src/main/adapters/native-acp.js +47 -12
  28. package/src/main/adapters/qoder.js +12 -8
  29. package/src/main/adapters/zcode.js +921 -10
  30. package/src/main/harness-adapter/event-normalizer.js +5 -2
  31. package/src/main/host/collaboration.js +723 -715
  32. package/src/main/host/jsonl.js +130 -116
  33. package/src/main/host/runtime.js +30 -14
  34. package/src/main/native/config.js +9 -9
  35. package/src/main/native/host.js +2 -0
  36. package/src/main/native/launcher.js +252 -237
  37. package/src/main/native/process-utils.js +157 -57
  38. package/src/main/native/protocol.js +1221 -1177
  39. package/src/main/native/secure-store.js +2 -0
  40. package/src/main/native/update-state.js +123 -110
  41. package/src/main/native/updater.js +460 -394
  42. package/src/main/workspace/core-review.js +13 -5
  43. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  44. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
  45. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
@@ -33,6 +33,8 @@ function run(args, input) {
33
33
  }
34
34
  resolve({ missing: false, stdout: Buffer.from(stdout || '') });
35
35
  });
36
+ // helper 进程提前退出时 stdin.end 会异步抛 EPIPE,无监听即 crash 宿主;真实错误由 execFile 回调覆盖
37
+ if (child.stdin) child.stdin.on('error', () => {});
36
38
  if (input !== undefined && child.stdin) child.stdin.end(Buffer.from(input));
37
39
  });
38
40
  }
@@ -1,110 +1,123 @@
1
- // Update bookkeeping for updater.js: channel detection, version compare,
2
- // the update state file and its lock. Pure Node built-ins; `dataDir` is
3
- // injected so tests can use temp directories.
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
-
7
- const STATE_FILE = 'update-state.json';
8
- const LOCK_FILE = 'update.lock';
9
- const LOCK_STALE_MS = 30 * 60 * 1000;
10
-
11
- function defaultState() {
12
- return {
13
- schema: 1,
14
- channel: null,
15
- phase: 'idle',
16
- appliedVersion: null,
17
- prevVersion: null,
18
- appliedAt: 0,
19
- attempts: 0,
20
- lastBootOkAt: 0,
21
- pendingVersion: null,
22
- lastCheckAt: 0,
23
- preUpdateHead: null,
24
- rolledBackAt: 0,
25
- };
26
- }
27
-
28
- function readState(dataDir) {
29
- try {
30
- const parsed = JSON.parse(fs.readFileSync(path.join(dataDir, STATE_FILE), 'utf8'));
31
- return { ...defaultState(), ...(parsed && typeof parsed === 'object' ? parsed : {}) };
32
- } catch {
33
- return defaultState(); // missing or corrupt: rebuild, never block launch
34
- }
35
- }
36
-
37
- function writeState(dataDir, state) {
38
- fs.mkdirSync(dataDir, { recursive: true });
39
- const file = path.join(dataDir, STATE_FILE);
40
- fs.writeFileSync(`${file}.tmp`, JSON.stringify(state, null, 2));
41
- fs.renameSync(`${file}.tmp`, file);
42
- return state;
43
- }
44
-
45
- function updateState(dataDir, patch) {
46
- return writeState(dataDir, { ...readState(dataDir), ...patch });
47
- }
48
-
49
- // Called by the launcher once the desktop is up and the controller survived long
50
- // enough to count as a healthy boot of the freshly applied version.
51
- function markBootOk(dataDir) {
52
- const state = readState(dataDir);
53
- return writeState(dataDir, { ...state, lastBootOkAt: Date.now(), attempts: 0 });
54
- }
55
-
56
- function pidAlive(pid) {
57
- if (!pid || typeof pid !== 'number') return false;
58
- try { process.kill(pid, 0); return true; }
59
- catch (error) { return error.code === 'EPERM'; }
60
- }
61
-
62
- // Mutating update operations (apply / rollback / repair) take this lock so two
63
- // launchers cannot fight over the same install. Stale locks are reaped.
64
- function acquireLock(dataDir, op) {
65
- fs.mkdirSync(dataDir, { recursive: true });
66
- const file = path.join(dataDir, LOCK_FILE);
67
- let existing = null;
68
- try { existing = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* no lock yet */ }
69
- if (existing && pidAlive(existing.pid) && Date.now() - Number(existing.ts || 0) < LOCK_STALE_MS) {
70
- throw new Error(`更新锁被进程 ${existing.pid} 持有(${existing.op || 'update'})`);
71
- }
72
- fs.writeFileSync(file, JSON.stringify({ pid: process.pid, ts: Date.now(), op }));
73
- let released = false;
74
- return function release() {
75
- if (released) return;
76
- released = true;
77
- try {
78
- const current = JSON.parse(fs.readFileSync(file, 'utf8'));
79
- if (current.pid === process.pid) fs.unlinkSync(file);
80
- } catch { /* already gone */ }
81
- };
82
- }
83
-
84
- function compareVersions(a, b) {
85
- const parse = value => String(value || '0').split('-')[0].split('.').map(part => parseInt(part, 10) || 0);
86
- const left = parse(a);
87
- const right = parse(b);
88
- for (let i = 0; i < 3; i += 1) {
89
- const x = left[i] || 0;
90
- const y = right[i] || 0;
91
- if (x !== y) return x > y ? 1 : -1;
92
- }
93
- const preA = String(a || '').includes('-');
94
- const preB = String(b || '').includes('-');
95
- if (preA !== preB) return preA ? -1 : 1; // release beats prerelease
96
- if (String(a) === String(b)) return 0;
97
- return String(a) > String(b) ? 1 : -1; // prerelease ordering is lexical (good enough)
98
- }
99
-
100
- function detectChannel(root) {
101
- if (fs.existsSync(path.join(root, '.git'))) return 'git';
102
- if (path.basename(path.dirname(path.resolve(root))) === 'node_modules') return 'npm';
103
- return 'portable';
104
- }
105
-
106
- module.exports = {
107
- STATE_FILE, LOCK_FILE, LOCK_STALE_MS,
108
- defaultState, readState, writeState, updateState, markBootOk,
109
- acquireLock, pidAlive, compareVersions, detectChannel,
110
- };
1
+ // Update bookkeeping for updater.js: channel detection, version compare,
2
+ // the update state file and its lock. Pure Node built-ins; `dataDir` is
3
+ // injected so tests can use temp directories.
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const STATE_FILE = 'update-state.json';
8
+ const LOCK_FILE = 'update.lock';
9
+ const LOCK_STALE_MS = 30 * 60 * 1000;
10
+
11
+ function defaultState() {
12
+ return {
13
+ schema: 1,
14
+ channel: null,
15
+ phase: 'idle',
16
+ appliedVersion: null,
17
+ prevVersion: null,
18
+ appliedAt: 0,
19
+ attempts: 0,
20
+ lastBootOkAt: 0,
21
+ pendingVersion: null,
22
+ pendingBuild: null,
23
+ lastCheckAt: 0,
24
+ preUpdateHead: null,
25
+ rolledBackAt: 0,
26
+ };
27
+ }
28
+
29
+ function readState(dataDir) {
30
+ try {
31
+ const parsed = JSON.parse(fs.readFileSync(path.join(dataDir, STATE_FILE), 'utf8'));
32
+ return { ...defaultState(), ...(parsed && typeof parsed === 'object' ? parsed : {}) };
33
+ } catch {
34
+ return defaultState(); // missing or corrupt: rebuild, never block launch
35
+ }
36
+ }
37
+
38
+ function writeState(dataDir, state) {
39
+ fs.mkdirSync(dataDir, { recursive: true });
40
+ const file = path.join(dataDir, STATE_FILE);
41
+ fs.writeFileSync(`${file}.tmp`, JSON.stringify(state, null, 2));
42
+ fs.renameSync(`${file}.tmp`, file);
43
+ return state;
44
+ }
45
+
46
+ function updateState(dataDir, patch) {
47
+ return writeState(dataDir, { ...readState(dataDir), ...patch });
48
+ }
49
+
50
+ // Called by the launcher once the desktop is up and the controller survived long
51
+ // enough to count as a healthy boot of the freshly applied version.
52
+ function markBootOk(dataDir) {
53
+ const state = readState(dataDir);
54
+ return writeState(dataDir, { ...state, lastBootOkAt: Date.now(), attempts: 0 });
55
+ }
56
+
57
+ function pidAlive(pid) {
58
+ if (!pid || typeof pid !== 'number') return false;
59
+ try { process.kill(pid, 0); return true; }
60
+ catch (error) { return error.code === 'EPERM'; }
61
+ }
62
+
63
+ // Mutating update operations (apply / rollback / repair) take this lock so two
64
+ // updaters the launcher at boot and the desktop-triggered helper — cannot
65
+ // fight over the same install. Creation is exclusive (O_EXCL via 'wx'); stale
66
+ // locks are reaped and the create is retried, so a plain read-check-write race
67
+ // between two processes cannot both "win".
68
+ function acquireLock(dataDir, op) {
69
+ fs.mkdirSync(dataDir, { recursive: true });
70
+ const file = path.join(dataDir, LOCK_FILE);
71
+ for (let attempt = 0; ; attempt += 1) {
72
+ try {
73
+ fs.writeFileSync(file, JSON.stringify({ pid: process.pid, ts: Date.now(), op }), { flag: 'wx' });
74
+ break;
75
+ } catch (error) {
76
+ if (error.code !== 'EEXIST') throw error;
77
+ let existing = null;
78
+ try { existing = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* corrupt lock */ }
79
+ if (existing && pidAlive(existing.pid) && Date.now() - Number(existing.ts || 0) < LOCK_STALE_MS) {
80
+ throw new Error(`更新锁被进程 ${existing.pid} 持有(${existing.op || 'update'})`);
81
+ }
82
+ if (attempt >= 20) throw new Error('更新锁冲突:过期的更新锁无法清除');
83
+ try { fs.unlinkSync(file); } catch { /* another process reaped it first; retry the create */ }
84
+ }
85
+ }
86
+ let released = false;
87
+ return function release() {
88
+ if (released) return;
89
+ released = true;
90
+ try {
91
+ const current = JSON.parse(fs.readFileSync(file, 'utf8'));
92
+ if (current.pid === process.pid) fs.unlinkSync(file);
93
+ } catch { /* already gone */ }
94
+ };
95
+ }
96
+
97
+ function compareVersions(a, b) {
98
+ const parse = value => String(value || '0').split('-')[0].split('.').map(part => parseInt(part, 10) || 0);
99
+ const left = parse(a);
100
+ const right = parse(b);
101
+ for (let i = 0; i < 3; i += 1) {
102
+ const x = left[i] || 0;
103
+ const y = right[i] || 0;
104
+ if (x !== y) return x > y ? 1 : -1;
105
+ }
106
+ const preA = String(a || '').includes('-');
107
+ const preB = String(b || '').includes('-');
108
+ if (preA !== preB) return preA ? -1 : 1; // release beats prerelease
109
+ if (String(a) === String(b)) return 0;
110
+ return String(a) > String(b) ? 1 : -1; // prerelease ordering is lexical (good enough)
111
+ }
112
+
113
+ function detectChannel(root) {
114
+ if (fs.existsSync(path.join(root, '.git'))) return 'git';
115
+ if (path.basename(path.dirname(path.resolve(root))) === 'node_modules') return 'npm';
116
+ return 'portable';
117
+ }
118
+
119
+ module.exports = {
120
+ STATE_FILE, LOCK_FILE, LOCK_STALE_MS,
121
+ defaultState, readState, writeState, updateState, markBootOk,
122
+ acquireLock, pidAlive, compareVersions, detectChannel,
123
+ };