agent-takkub 1.0.6 → 1.0.7

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.
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+ // Ensure the npm global bin dir stays on the user's persistent PATH.
3
+ //
4
+ // Field incident 2026-07-04: a Node update dropped %APPDATA%\npm from the user
5
+ // PATH — `claude`, `takkub`, and `agent-takkub` all became "command not found"
6
+ // and the user had to hand-edit the registry. This module makes the install
7
+ // self-healing:
8
+ // • win32 — appends the dir to HKCU\Environment Path via .NET RegistryKey,
9
+ // PRESERVING the value kind (REG_SZ vs REG_EXPAND_SZ, read with
10
+ // DoNotExpandEnvironmentNames so %VAR% entries survive verbatim),
11
+ // then broadcasts WM_SETTINGCHANGE so new shells see it.
12
+ // • darwin/linux — appends a marker-guarded export block to ~/.zshrc (and
13
+ // ~/.bashrc when present). Idempotent via the marker.
14
+ // Best-effort by design: any failure only prints a manual-fix hint — it never
15
+ // fails the install.
16
+
17
+ const { execFileSync, spawnSync } = require('child_process');
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+
22
+ const MARKER = '# >>> agent-takkub PATH >>>';
23
+
24
+ function npmGlobalBinDir() {
25
+ // Node ≥18 refuses to spawn .cmd shims without a shell (CVE-2024-27980
26
+ // hardening) — route through cmd.exe explicitly on Windows.
27
+ const r =
28
+ process.platform === 'win32'
29
+ ? spawnSync('cmd.exe', ['/d', '/s', '/c', 'npm prefix -g'], {
30
+ encoding: 'utf8',
31
+ timeout: 30000,
32
+ windowsHide: true,
33
+ })
34
+ : spawnSync('npm', ['prefix', '-g'], { encoding: 'utf8', timeout: 30000 });
35
+ const prefix = (r.stdout || '').trim();
36
+ if (r.status !== 0 || !prefix) return null;
37
+ return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
38
+ }
39
+
40
+ function normalize(p) {
41
+ return path.normalize(p.trim()).replace(/[\\/]+$/, '').toLowerCase();
42
+ }
43
+
44
+ function dirOnPath(dir, pathValue) {
45
+ const want = normalize(dir);
46
+ return pathValue
47
+ .split(path.delimiter)
48
+ .filter(Boolean)
49
+ .some((p) => normalize(p) === want);
50
+ }
51
+
52
+ // PowerShell one-shot: read raw user Path (unexpanded), append if missing with
53
+ // the same value kind, broadcast WM_SETTINGCHANGE. Prints ADDED/PRESENT.
54
+ function winEnsure(binDir) {
55
+ const script = `
56
+ $bin = ${JSON.stringify(binDir)}
57
+ $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
58
+ $raw = ''
59
+ $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
60
+ if ($key.GetValueNames() -contains 'Path') {
61
+ $raw = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
62
+ $kind = $key.GetValueKind('Path')
63
+ }
64
+ $parts = @($raw -split ';' | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\\').ToLower() })
65
+ $expanded = @($parts | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_) })
66
+ $want = $bin.TrimEnd('\\').ToLower()
67
+ if (($parts -contains $want) -or ($expanded -contains $want)) {
68
+ Write-Output 'PRESENT'
69
+ } else {
70
+ $new = if ($raw) { $raw.TrimEnd(';') + ';' + $bin } else { $bin }
71
+ $key.SetValue('Path', $new, $kind)
72
+ $sig = '[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'
73
+ $w32 = Add-Type -MemberDefinition $sig -Name 'PathBroadcast' -Namespace 'Win32' -PassThru
74
+ [UIntPtr]$res = [UIntPtr]::Zero
75
+ $w32::SendMessageTimeout([IntPtr]0xFFFF, 0x1A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$res) | Out-Null
76
+ Write-Output 'ADDED'
77
+ }
78
+ $key.Close()
79
+ `;
80
+ const out = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
81
+ encoding: 'utf8',
82
+ timeout: 30000,
83
+ }).trim();
84
+ return out.includes('ADDED');
85
+ }
86
+
87
+ function posixEnsure(binDir) {
88
+ if (dirOnPath(binDir, process.env.PATH || '')) return false;
89
+ const block = `\n${MARKER}\nexport PATH="$PATH:${binDir}"\n# <<< agent-takkub PATH <<<\n`;
90
+ const rcs = [path.join(os.homedir(), '.zshrc')];
91
+ const bashrc = path.join(os.homedir(), '.bashrc');
92
+ if (fs.existsSync(bashrc)) rcs.push(bashrc);
93
+ let added = false;
94
+ for (const rc of rcs) {
95
+ const existing = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
96
+ if (existing.includes(MARKER)) continue;
97
+ fs.writeFileSync(rc, existing + block);
98
+ added = true;
99
+ }
100
+ return added;
101
+ }
102
+
103
+ // Returns true when it CHANGED something (caller prints the restart hint).
104
+ function ensureGlobalBinOnPath() {
105
+ try {
106
+ const binDir = npmGlobalBinDir();
107
+ if (!binDir) return false;
108
+ if (process.platform === 'win32') return winEnsure(binDir);
109
+ return posixEnsure(binDir);
110
+ } catch (e) {
111
+ console.log(
112
+ `[agent-takkub] PATH check skipped (${e && e.message}) — if 'takkub' is not found later, run: takkub doctor --fix`
113
+ );
114
+ return false;
115
+ }
116
+ }
117
+
118
+ module.exports = { ensureGlobalBinOnPath, npmGlobalBinDir, dirOnPath };
@@ -89,6 +89,21 @@ function main() {
89
89
 
90
90
  const claudeOk = ensureClaudeCli(env.claudeCli.present);
91
91
 
92
+ // Keep the npm global bin dir on the persistent PATH — otherwise a broken
93
+ // PATH makes claude/takkub/agent-takkub "command not found" in new shells
94
+ // (field incident 2026-07-04). Best-effort; never fails the install.
95
+ let pathAdded = false;
96
+ try {
97
+ pathAdded = require('./pathfix').ensureGlobalBinOnPath();
98
+ if (pathAdded) {
99
+ console.log(
100
+ '[agent-takkub] ✓ npm global bin dir added to your PATH (open a NEW terminal to use takkub/claude).'
101
+ );
102
+ }
103
+ } catch (_e) {
104
+ /* pathfix already printed its own hint */
105
+ }
106
+
92
107
  console.log(`\n[agent-takkub] ✓ cockpit ready (isolated in ${agentTakkubHome()}).`);
93
108
  try {
94
109
  const sc = require('./shortcut').create();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-takkub",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Desktop cockpit for orchestrating a team of Claude Code agents (Windows + macOS)",
5
5
  "keywords": [
6
6
  "claude",