agent-takkub 1.0.82 → 1.0.85

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.
@@ -2,12 +2,25 @@
2
2
  'use strict';
3
3
  // Launches the cockpit GUI from the provisioned (isolated) venv.
4
4
  const { spawnSync } = require('child_process');
5
- const { venvPythonIfExists } = require('../scripts/lib');
5
+ const {
6
+ venvPythonIfExists,
7
+ pythonLooksExecutable,
8
+ brokenInterpreterMessage,
9
+ agentTakkubHome,
10
+ } = require('../scripts/lib');
6
11
 
7
12
  const py = venvPythonIfExists();
8
13
  if (!py) {
9
14
  console.error('agent-takkub is not provisioned. Run: npm install -g agent-takkub');
10
15
  process.exit(1);
11
16
  }
17
+ if (!pythonLooksExecutable(py)) {
18
+ console.error(brokenInterpreterMessage(py, agentTakkubHome()));
19
+ process.exit(1);
20
+ }
12
21
  const r = spawnSync(py, ['-m', 'agent_takkub', ...process.argv.slice(2)], { stdio: 'inherit' });
22
+ if (r.error) {
23
+ console.error(`[agent-takkub] failed to launch the cockpit interpreter: ${r.error.message}`);
24
+ process.exit(1);
25
+ }
13
26
  process.exit(r.status == null ? 1 : r.status);
package/npm/bin/takkub.js CHANGED
@@ -2,12 +2,28 @@
2
2
  'use strict';
3
3
  // The `takkub` CLI, run from the provisioned (isolated) venv.
4
4
  const { spawnSync } = require('child_process');
5
- const { venvPythonIfExists } = require('../scripts/lib');
5
+ const {
6
+ venvPythonIfExists,
7
+ pythonLooksExecutable,
8
+ brokenInterpreterMessage,
9
+ agentTakkubHome,
10
+ } = require('../scripts/lib');
6
11
 
7
12
  const py = venvPythonIfExists();
8
13
  if (!py) {
9
14
  console.error('takkub is not provisioned. Run: npm install -g agent-takkub');
10
15
  process.exit(1);
11
16
  }
17
+ if (!pythonLooksExecutable(py)) {
18
+ console.error(brokenInterpreterMessage(py, agentTakkubHome()));
19
+ process.exit(1);
20
+ }
12
21
  const r = spawnSync(py, ['-m', 'agent_takkub.cli', ...process.argv.slice(2)], { stdio: 'inherit' });
22
+ if (r.error) {
23
+ // spawnSync itself failed (e.g. the interpreter passed validation above
24
+ // but the OS still refused to launch it) — never let this read as a
25
+ // quiet success (#341).
26
+ console.error(`[takkub] failed to launch the cockpit interpreter: ${r.error.message}`);
27
+ process.exit(1);
28
+ }
13
29
  process.exit(r.status == null ? 1 : r.status);
@@ -31,6 +31,71 @@ function venvPythonIfExists() {
31
31
  return fs.existsSync(p) ? p : null;
32
32
  }
33
33
 
34
+ // Pick the wheel matching an exact version out of a dist/ dir — never the
35
+ // lexically-last filename. A string sort puts "1.0.9" after "1.0.10" (#340),
36
+ // and dist/ is gitignored so a stale wheel from a previous release can sit
37
+ // there unnoticed. `version` is the SemVer string as read from package.json;
38
+ // the filename convention (setuptools normalizes the project name's `-` to
39
+ // `_`) is `agent_takkub-{version}-py3-none-any.whl`.
40
+ function findWheelForVersion(distDir, version) {
41
+ if (!fs.existsSync(distDir)) return null;
42
+ const prefix = `agent_takkub-${version}-`;
43
+ const match = fs.readdirSync(distDir).find((f) => f.endsWith('.whl') && f.startsWith(prefix));
44
+ return match ? path.join(distDir, match) : null;
45
+ }
46
+
47
+ // A venv python that EXISTS but was overwritten/truncated by something
48
+ // outside agent-takkub (#341 — a stray tool once clobbered a live
49
+ // ~/.agent-takkub/venv/Scripts/python.exe with a 29-byte text file) must
50
+ // never be handed to spawnSync: every command built on top of it then fails
51
+ // to even start, with EMPTY stdout/stderr and no diagnostic — the cockpit
52
+ // "dies silently". This check is deliberately static (stat + a couple of
53
+ // header bytes, no attempt to execute the file) so a broken interpreter is
54
+ // never spawned at all — spawning it is exactly the risky step that can
55
+ // leave the file locked/hard to overwrite during recovery.
56
+ const MIN_PYTHON_EXE_BYTES = 40 * 1024; // a real venv python.exe/python is comfortably >90KB
57
+
58
+ function pythonLooksExecutable(py) {
59
+ let stat;
60
+ try {
61
+ stat = fs.statSync(py);
62
+ } catch (_e) {
63
+ return false;
64
+ }
65
+ if (!stat.isFile() || stat.size < MIN_PYTHON_EXE_BYTES) return false;
66
+ if (process.platform === 'win32') {
67
+ // Windows PE executables always start with the 'MZ' DOS-header magic.
68
+ let fd;
69
+ try {
70
+ fd = fs.openSync(py, 'r');
71
+ const buf = Buffer.alloc(2);
72
+ fs.readSync(fd, buf, 0, 2, 0);
73
+ return buf[0] === 0x4d && buf[1] === 0x5a; // 'M', 'Z'
74
+ } catch (_e) {
75
+ return false;
76
+ } finally {
77
+ if (fd !== undefined) fs.closeSync(fd);
78
+ }
79
+ }
80
+ return true;
81
+ }
82
+
83
+ function brokenInterpreterMessage(py, home) {
84
+ const venvDirPath = path.join(home, 'venv');
85
+ return (
86
+ `[agent-takkub] the cockpit interpreter looks broken (exists but is not a real executable):\n` +
87
+ ` ${py}\n` +
88
+ ' Something outside agent-takkub overwrote or truncated this file — the cockpit\n' +
89
+ ' did not do this to itself. Fix:\n' +
90
+ ' 1) close every running cockpit/takkub window and process for this install\n' +
91
+ ' 2) run: npm install -g agent-takkub --force (reprovisions the venv in place)\n' +
92
+ ` 3) if step 2 still can't overwrite it, delete this folder by hand:\n` +
93
+ ` ${venvDirPath}\n` +
94
+ ' then re-run npm install -g agent-takkub\n' +
95
+ ' `takkub doctor` (once the interpreter itself works again) also checks this.'
96
+ );
97
+ }
98
+
34
99
  // Windowless python for GUI launchers (Windows: pythonw.exe = no console
35
100
  // window pops up behind the cockpit). macOS has no separate pythonw.
36
101
  function venvPythonw() {
@@ -40,4 +105,13 @@ function venvPythonw() {
40
105
  : path.join(dir, 'bin', 'python');
41
106
  }
42
107
 
43
- module.exports = { agentTakkubHome, venvDir, venvPython, venvPythonIfExists, venvPythonw };
108
+ module.exports = {
109
+ agentTakkubHome,
110
+ venvDir,
111
+ venvPython,
112
+ venvPythonIfExists,
113
+ venvPythonw,
114
+ findWheelForVersion,
115
+ pythonLooksExecutable,
116
+ brokenInterpreterMessage,
117
+ };
@@ -15,15 +15,18 @@
15
15
  const { spawnSync } = require('child_process');
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
- const { agentTakkubHome, venvDir, venvPython } = require('./lib');
18
+ const { agentTakkubHome, venvDir, venvPython, findWheelForVersion } = require('./lib');
19
19
  const preflight = require('./preflight');
20
20
 
21
+ // The wheel matching THIS install's declared version (#340) — not the
22
+ // lexically-last filename in dist/. A string sort put "1.0.9" after
23
+ // "1.0.10", and a stale wheel from a previous release (dist/ is gitignored,
24
+ // so nothing flags it) got installed silently under a newer version number.
21
25
  function findWheel() {
22
26
  // Shipped alongside this package under dist/ (see package.json `files`).
23
27
  const dist = path.join(__dirname, '..', '..', 'dist');
24
- if (!fs.existsSync(dist)) return null;
25
- const whl = fs.readdirSync(dist).filter((f) => f.endsWith('.whl')).sort();
26
- return whl.length ? path.join(dist, whl[whl.length - 1]) : null;
28
+ const version = require('../../package.json').version;
29
+ return findWheelForVersion(dist, version);
27
30
  }
28
31
 
29
32
  function run(cmd, args) {
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+ // `npm publish` preflight guard (#340) — wired as `prepublishOnly` so it runs
3
+ // automatically. dist/ is gitignored, so a wheel left over from a previous
4
+ // `takkub release` sits there unnoticed; without this check `npm publish`
5
+ // happily tars up that stale wheel under the NEW version number (v1.0.84
6
+ // shipped v1.0.82's code, see issue #340). Dies loudly instead of warning —
7
+ // a silent old-code publish is worse than a blocked one.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { findWheelForVersion } = require('./lib');
12
+
13
+ function checkDistWheelMatchesVersion(distDir, version) {
14
+ if (!fs.existsSync(distDir)) {
15
+ return {
16
+ ok: false,
17
+ reason: `dist/ not found — build first: python -m build --wheel`,
18
+ };
19
+ }
20
+ const whls = fs.readdirSync(distDir).filter((f) => f.endsWith('.whl'));
21
+ if (whls.length === 0) {
22
+ return {
23
+ ok: false,
24
+ reason: `dist/ has no wheel — build first: python -m build --wheel`,
25
+ };
26
+ }
27
+ if (whls.length > 1) {
28
+ return {
29
+ ok: false,
30
+ reason:
31
+ `dist/ has ${whls.length} wheels (expected exactly one): ${whls.join(', ')} — ` +
32
+ `rebuild clean: rm -f dist/*.whl && python -m build --wheel`,
33
+ };
34
+ }
35
+ const wheel = findWheelForVersion(distDir, version);
36
+ if (!wheel) {
37
+ return {
38
+ ok: false,
39
+ reason:
40
+ `dist/${whls[0]} does not match package.json version ${version} — ` +
41
+ `rebuild: rm -f dist/*.whl && python -m build --wheel`,
42
+ };
43
+ }
44
+ return { ok: true, wheel: path.basename(wheel) };
45
+ }
46
+
47
+ function main() {
48
+ const pkg = require('../../package.json');
49
+ const dist = path.join(__dirname, '..', '..', 'dist');
50
+ const result = checkDistWheelMatchesVersion(dist, pkg.version);
51
+ if (!result.ok) {
52
+ console.error(`[agent-takkub] publish preflight FAILED: ${result.reason}`);
53
+ process.exit(1);
54
+ }
55
+ console.log(`[agent-takkub] publish preflight OK — dist/${result.wheel} matches package.json ${pkg.version}`);
56
+ }
57
+
58
+ module.exports = { checkDistWheelMatchesVersion };
59
+
60
+ if (require.main === module) {
61
+ main();
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-takkub",
3
- "version": "1.0.82",
3
+ "version": "1.0.85",
4
4
  "description": "Desktop cockpit for orchestrating a team of Claude Code agents (Windows + macOS). Install globally: npm i -g agent-takkub",
5
5
  "keywords": [
6
6
  "claude",
@@ -26,10 +26,13 @@
26
26
  "takkub": "npm/bin/takkub.js"
27
27
  },
28
28
  "scripts": {
29
- "postinstall": "node npm/scripts/postinstall.js"
29
+ "postinstall": "node npm/scripts/postinstall.js",
30
+ "prepublishOnly": "node npm/scripts/preflight-publish.js",
31
+ "test": "node --test npm/scripts/*.test.js"
30
32
  },
31
33
  "files": [
32
34
  "npm/",
35
+ "!npm/scripts/*.test.js",
33
36
  "dist/*.whl",
34
37
  "assets/icon.ico",
35
38
  "assets/icon.png",