@kisuke/cli 1.1.76-dev.85.1 → 1.1.84-dev.93.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisuke/cli",
3
- "version": "1.1.76-dev.85.1",
3
+ "version": "1.1.84-dev.93.1",
4
4
  "private": false,
5
5
  "description": "Kisuke CLI (dev) installer",
6
6
  "type": "module",
@@ -26,6 +26,50 @@ const executable = process.platform === 'win32'
26
26
  ? path.join(bundleDir, 'kisuke.cmd')
27
27
  : path.join(bundleDir, 'kisuke');
28
28
 
29
+ // ---------------------------------------------------------------------------
30
+ // Windows: kill the daemon BEFORE calling `kisuke uninstall`, so that by the
31
+ // time the uninstall command spawns its own (short-lived) node.exe the daemon
32
+ // is already gone and we don't fight two lock sources at once.
33
+ //
34
+ // Process tree started by Task Scheduler:
35
+ // wscript.exe → cmd.exe (daemon-task-runner.cmd) → node.exe (daemon.js)
36
+ //
37
+ // We need to kill the entire tree, not just node.exe.
38
+ // ---------------------------------------------------------------------------
39
+ if (process.platform === 'win32') {
40
+ const bundledNodeExe = path.join(bundleDir, 'bin', 'node.exe');
41
+
42
+ // Kill daemon by PID from pid file (fastest path)
43
+ try {
44
+ const kisukeDir = path.join(process.env.USERPROFILE || '', '.kisuke');
45
+ const pidFile = path.join(kisukeDir, 'kisuke.pid');
46
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
47
+ if (pid > 0) {
48
+ // taskkill /T kills the entire process tree rooted at the PID
49
+ spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], {
50
+ stdio: 'ignore',
51
+ timeout: 5_000,
52
+ });
53
+ try { fs.unlinkSync(pidFile); } catch {}
54
+ }
55
+ } catch {
56
+ // No PID file or process already gone — fine
57
+ }
58
+
59
+ // Kill any remaining processes using the bundled node.exe.
60
+ // Catches orphaned daemon processes not tracked by the PID file.
61
+ try {
62
+ const escaped = bundledNodeExe.replace(/'/g, "''");
63
+ spawnSync('powershell', [
64
+ '-NoProfile', '-NonInteractive', '-Command',
65
+ `Get-Process | Where-Object { $_.Path -eq '${escaped}' } | Stop-Process -Force -ErrorAction SilentlyContinue`,
66
+ ], { stdio: 'ignore', timeout: 10_000 });
67
+ } catch {}
68
+ }
69
+
70
+ // Run `kisuke uninstall` to remove the scheduled task / launchd plist /
71
+ // systemd unit. On Windows the daemon is already dead at this point, so
72
+ // the only node.exe invocation here is this short-lived CLI command.
29
73
  if (fs.existsSync(executable)) {
30
74
  const result = spawnSync(executable, ['uninstall'], {
31
75
  stdio: 'pipe',
@@ -41,61 +85,41 @@ if (fs.existsSync(executable)) {
41
85
  console.warn(`kisuke uninstall warning: ${stderr}`);
42
86
  }
43
87
  }
88
+ }
44
89
 
45
- // On Windows, if the daemon is still alive after service removal,
46
- // kill it directly so npm can delete the bundled node.exe.
47
- // We must also wait for Windows to fully release the file lock —
48
- // the kernel holds it briefly after process termination while
49
- // cleaning up handles and memory-mapped image sections.
50
- if (process.platform === 'win32') {
51
- const bundledNodeExe = path.join(bundleDir, 'bin', 'node.exe');
52
-
53
- // Stage 1: Kill daemon by PID from pid file
54
- try {
55
- const kisukeDir = path.join(process.env.USERPROFILE || '', '.kisuke');
56
- const pidFile = path.join(kisukeDir, 'kisuke.pid');
57
- const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
58
- if (pid > 0) {
59
- try { process.kill(pid, 'SIGTERM'); } catch {}
60
- // Always follow up with taskkill /F — process.kill on Windows
61
- // can succeed without the process fully terminating yet
62
- spawnSync('taskkill', ['/F', '/PID', String(pid)], {
63
- stdio: 'ignore',
64
- timeout: 5_000,
65
- });
66
- try { fs.unlinkSync(pidFile); } catch {}
67
- }
68
- } catch {
69
- // No PID file or process already gone — fine
70
- }
71
-
72
- // Stage 2: Kill any remaining processes using the bundled node.exe.
73
- // Catches orphaned daemon processes not tracked by the PID file.
74
- try {
75
- const escaped = bundledNodeExe.replace(/'/g, "''");
76
- spawnSync('powershell', [
77
- '-NoProfile', '-NonInteractive', '-Command',
78
- `Get-Process | Where-Object { $_.Path -eq '${escaped}' } | Stop-Process -Force -ErrorAction SilentlyContinue`,
79
- ], { stdio: 'ignore', timeout: 10_000 });
80
- } catch {}
90
+ // ---------------------------------------------------------------------------
91
+ // Windows: proactively remove node.exe so npm's cleanup phase can't fail on
92
+ // it. After the CLI command above, the only lock source is the brief kernel
93
+ // hold while Windows tears down the image mapping (and possibly Defender).
94
+ // ---------------------------------------------------------------------------
95
+ if (process.platform === 'win32') {
96
+ const bundledNodeExe = path.join(bundleDir, 'bin', 'node.exe');
81
97
 
82
- // Stage 3: Wait for Windows to release the file lock on node.exe.
83
- // Without this, npm's rename-then-delete fails with EBUSY / EPERM.
84
- if (fs.existsSync(bundledNodeExe)) {
85
- const deadline = Date.now() + 5_000;
86
- while (Date.now() < deadline) {
87
- try {
88
- // Opening the executable for writing fails while it's mapped
89
- const fd = fs.openSync(bundledNodeExe, 'r+');
90
- fs.closeSync(fd);
98
+ if (fs.existsSync(bundledNodeExe)) {
99
+ let removed = false;
100
+ const deadline = Date.now() + 10_000;
101
+ while (Date.now() < deadline) {
102
+ try {
103
+ fs.unlinkSync(bundledNodeExe);
104
+ removed = true;
105
+ break;
106
+ } catch (e) {
107
+ if (e.code !== 'EBUSY' && e.code !== 'EPERM' && e.code !== 'EACCES') {
91
108
  break;
92
- } catch (e) {
93
- if (e.code !== 'EBUSY' && e.code !== 'EPERM' && e.code !== 'EACCES') {
94
- break; // Unexpected error — don't keep retrying
95
- }
96
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
97
109
  }
110
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
98
111
  }
99
112
  }
113
+ // Last resort: rename into %TEMP% — rename often succeeds even while
114
+ // the file is locked because most AV locks use FILE_SHARE_DELETE.
115
+ if (!removed && fs.existsSync(bundledNodeExe)) {
116
+ try {
117
+ const tmp = path.join(
118
+ process.env.TEMP || process.env.TMP || '.',
119
+ `kisuke-node-${Date.now()}.exe.old`,
120
+ );
121
+ fs.renameSync(bundledNodeExe, tmp);
122
+ } catch {}
123
+ }
100
124
  }
101
125
  }