subconscious-cli 5.0.3 → 6.0.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.
@@ -1,14 +1,16 @@
1
1
  // Windows process handling. Unix callers keep using their existing launch paths.
2
+
3
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
2
4
  import fs from 'node:fs';
3
5
  import os from 'node:os';
4
6
  import path from 'node:path';
5
- import { spawn, spawnSync, execFileSync } from 'node:child_process';
6
7
 
7
8
  export function windowsEnv(...sources) {
8
9
  const result = {};
9
10
  for (const source of sources) {
10
11
  for (const [key, value] of Object.entries(source || {})) {
11
- if (value !== undefined && value !== null) result[key.toUpperCase()] = String(value);
12
+ if (value !== undefined && value !== null)
13
+ result[key.toUpperCase()] = String(value);
12
14
  }
13
15
  }
14
16
  return result;
@@ -16,42 +18,88 @@ export function windowsEnv(...sources) {
16
18
 
17
19
  export function windowsBinDirs(environment = process.env, home = os.homedir()) {
18
20
  const env = windowsEnv(environment);
19
- return [...new Set([
20
- env.APPDATA && path.win32.join(env.APPDATA, 'npm'),
21
- env.USERPROFILE && path.win32.join(env.USERPROFILE, '.local', 'bin'),
22
- home && path.win32.join(home, '.local', 'bin'),
23
- env.LOCALAPPDATA && path.win32.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links'),
24
- env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32', 'config', 'systemprofile', '.local', 'bin'),
25
- env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32'),
26
- env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32', 'WindowsPowerShell', 'v1.0'),
27
- path.dirname(process.execPath),
28
- ].filter(Boolean))];
21
+ return [
22
+ ...new Set(
23
+ [
24
+ env.APPDATA && path.win32.join(env.APPDATA, 'npm'),
25
+ env.USERPROFILE && path.win32.join(env.USERPROFILE, '.local', 'bin'),
26
+ home && path.win32.join(home, '.local', 'bin'),
27
+ env.LOCALAPPDATA &&
28
+ path.win32.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links'),
29
+ env.SYSTEMROOT &&
30
+ path.win32.join(
31
+ env.SYSTEMROOT,
32
+ 'System32',
33
+ 'config',
34
+ 'systemprofile',
35
+ '.local',
36
+ 'bin',
37
+ ),
38
+ env.SYSTEMROOT && path.win32.join(env.SYSTEMROOT, 'System32'),
39
+ env.SYSTEMROOT &&
40
+ path.win32.join(
41
+ env.SYSTEMROOT,
42
+ 'System32',
43
+ 'WindowsPowerShell',
44
+ 'v1.0',
45
+ ),
46
+ path.dirname(process.execPath),
47
+ ].filter(Boolean),
48
+ ),
49
+ ];
29
50
  }
30
51
 
31
52
  export function windowsPath(extraDirs, currentPath = '') {
32
53
  const seen = new Set();
33
- return [...extraDirs, ...currentPath.split(';')].filter(dir => {
34
- const key = dir.replace(/^"|"$/g, '').replace(/[\\/]+$/, '').toLowerCase();
35
- if (!key || seen.has(key)) return false;
36
- seen.add(key);
37
- return true;
38
- }).map(dir => dir.replace(/^"|"$/g, '')).join(';');
54
+ return [...extraDirs, ...currentPath.split(';')]
55
+ .filter((dir) => {
56
+ const key = dir
57
+ .replace(/^"|"$/g, '')
58
+ .replace(/[\\/]+$/, '')
59
+ .toLowerCase();
60
+ if (!key || seen.has(key)) return false;
61
+ seen.add(key);
62
+ return true;
63
+ })
64
+ .map((dir) => dir.replace(/^"|"$/g, ''))
65
+ .join(';');
39
66
  }
40
67
 
41
68
  export function resolveWindowsExecutable(command, options = {}) {
42
69
  const env = windowsEnv(options.env || process.env);
43
70
  const pathApi = options.pathApi || path.win32;
44
- const isFile = options.isFile || (file => { try { return fs.statSync(file).isFile(); } catch { return false; } });
45
- const extensions = [...new Set(['.exe', '.com', ...(env.PATHEXT || '.CMD;.BAT').split(';').map(x => x.toLowerCase()), ''])];
46
- const hasExtension = extensions.some(ext => ext && command.toLowerCase().endsWith(ext));
47
- const dirs = /[\\/]/.test(command) ? [''] : [
48
- ...(options.preferredDirs || []),
49
- ...(env.PATH || '').split(';').filter(Boolean),
50
- ...windowsBinDirs(env, options.home),
71
+ const isFile =
72
+ options.isFile ||
73
+ ((file) => {
74
+ try {
75
+ return fs.statSync(file).isFile();
76
+ } catch {
77
+ return false;
78
+ }
79
+ });
80
+ const extensions = [
81
+ ...new Set([
82
+ '.exe',
83
+ '.com',
84
+ ...(env.PATHEXT || '.CMD;.BAT').split(';').map((x) => x.toLowerCase()),
85
+ '',
86
+ ]),
51
87
  ];
88
+ const hasExtension = extensions.some(
89
+ (ext) => ext && command.toLowerCase().endsWith(ext),
90
+ );
91
+ const dirs = /[\\/]/.test(command)
92
+ ? ['']
93
+ : [
94
+ ...(options.preferredDirs || []),
95
+ ...(env.PATH || '').split(';').filter(Boolean),
96
+ ...windowsBinDirs(env, options.home),
97
+ ];
52
98
  for (const dir of dirs) {
53
99
  for (const ext of hasExtension ? [''] : extensions) {
54
- const candidate = dir ? pathApi.join(dir.replace(/^"|"$/g, ''), command + ext) : command + ext;
100
+ const candidate = dir
101
+ ? pathApi.join(dir.replace(/^"|"$/g, ''), command + ext)
102
+ : command + ext;
55
103
  if (isFile(candidate)) return candidate;
56
104
  }
57
105
  }
@@ -61,53 +109,113 @@ export function resolveWindowsExecutable(command, options = {}) {
61
109
  // npm shims pass %* through cmd.exe, which reinterprets quotes, JSON, %, &,
62
110
  // newlines, etc. Resolve their quoted entry point and run it with Node directly.
63
111
  // This also supports npm/npx and VS Code's code.cmd without invoking a shell.
64
- export function windowsInvocation(command, args = [], environment = process.env, options = {}) {
112
+ export function windowsInvocation(
113
+ command,
114
+ args = [],
115
+ environment = process.env,
116
+ options = {},
117
+ ) {
65
118
  const env = windowsEnv(environment);
66
119
  const resolve = options.resolve || resolveWindowsExecutable;
67
120
  const executable = resolve(command, { env, ...options });
68
- if (!executable) throw new Error(`Could not find ${command} on the Windows PATH.`);
69
- if (!/\.(cmd|bat)$/i.test(executable)) return { command: executable, args, env };
70
- const readFile = options.readFile || (file => fs.readFileSync(file, 'utf8'));
121
+ if (!executable)
122
+ throw new Error(`Could not find ${command} on the Windows PATH.`);
123
+ if (!/\.(cmd|bat)$/i.test(executable))
124
+ return { command: executable, args, env };
125
+ const readFile =
126
+ options.readFile || ((file) => fs.readFileSync(file, 'utf8'));
71
127
  const pathApi = options.pathApi || path.win32;
72
128
  const contents = readFile(executable);
73
- const candidates = [...contents.matchAll(/%(?:dp0%|~dp0)[\\/]?([^"\r\n]+)"/gi)];
74
- const name = pathApi.basename(executable).replace(/\.(cmd|bat)$/i, '').toLowerCase();
129
+ const candidates = [
130
+ ...contents.matchAll(/%(?:dp0%|~dp0)[\\/]?([^"\r\n]+)"/gi),
131
+ ];
132
+ const name = pathApi
133
+ .basename(executable)
134
+ .replace(/\.(cmd|bat)$/i, '')
135
+ .toLowerCase();
75
136
  let electron;
76
137
  if (/ELECTRON_RUN_AS_NODE\s*=\s*1/i.test(contents)) {
77
- electron = candidates.map(match => pathApi.resolve(pathApi.dirname(executable), match[1])).find(target => /\.exe$/i.test(target) && !/node\.exe$/i.test(target) && fs.existsSync(target));
138
+ electron = candidates
139
+ .map((match) => pathApi.resolve(pathApi.dirname(executable), match[1]))
140
+ .find(
141
+ (target) =>
142
+ /\.exe$/i.test(target) &&
143
+ !/node\.exe$/i.test(target) &&
144
+ fs.existsSync(target),
145
+ );
78
146
  }
79
147
  for (const match of candidates) {
80
148
  const target = pathApi.resolve(pathApi.dirname(executable), match[1]);
81
149
  // A shim's first executable is often node.exe, not the agent entry point.
82
150
  if (/\.(exe|com)$/i.test(target)) {
83
- if (!electron && !/node\.exe$/i.test(target) && fs.existsSync(target)) return { command: target, args, env };
151
+ if (!electron && !/node\.exe$/i.test(target) && fs.existsSync(target))
152
+ return { command: target, args, env };
84
153
  continue;
85
154
  }
86
- if (['npm', 'npx'].includes(name) && pathApi.basename(target) !== `${name}-cli.js`) continue;
155
+ if (
156
+ ['npm', 'npx'].includes(name) &&
157
+ pathApi.basename(target) !== `${name}-cli.js`
158
+ )
159
+ continue;
87
160
  let source;
88
- try { source = readFile(target); } catch { continue; }
89
- if (!/\.[cm]?js$/i.test(target) && !/^#![^\r\n]*\bnode\b/.test(source)) continue;
90
- return { command: electron || process.execPath, args: [target, ...args], env: electron ? { ...env, ELECTRON_RUN_AS_NODE: '1' } : env };
161
+ try {
162
+ source = readFile(target);
163
+ } catch {
164
+ continue;
165
+ }
166
+ if (!/\.[cm]?js$/i.test(target) && !/^#![^\r\n]*\bnode\b/.test(source))
167
+ continue;
168
+ return {
169
+ command: electron || process.execPath,
170
+ args: [target, ...args],
171
+ env: electron ? { ...env, ELECTRON_RUN_AS_NODE: '1' } : env,
172
+ };
91
173
  }
92
- throw new Error(`Cannot safely launch the custom batch wrapper ${executable}. Install the native executable or standard npm package for ${command}.`);
174
+ throw new Error(
175
+ `Cannot safely launch the custom batch wrapper ${executable}. Install the native executable or standard npm package for ${command}.`,
176
+ );
93
177
  }
94
178
 
95
179
  export function spawnWindows(command, args, options = {}) {
96
- const invocation = windowsInvocation(command, args, options.env || process.env);
97
- return spawn(invocation.command, invocation.args, { ...options, env: invocation.env, shell: false });
180
+ const invocation = windowsInvocation(
181
+ command,
182
+ args,
183
+ options.env || process.env,
184
+ );
185
+ return spawn(invocation.command, invocation.args, {
186
+ ...options,
187
+ env: invocation.env,
188
+ shell: false,
189
+ });
98
190
  }
99
191
 
100
192
  export function spawnWindowsSync(command, args, options = {}) {
101
193
  try {
102
- const invocation = windowsInvocation(command, args, options.env || process.env);
103
- return spawnSync(invocation.command, invocation.args, { ...options, env: invocation.env, shell: false });
104
- } catch (error) { return { error, status: null, stdout: '', stderr: '' }; }
194
+ const invocation = windowsInvocation(
195
+ command,
196
+ args,
197
+ options.env || process.env,
198
+ );
199
+ return spawnSync(invocation.command, invocation.args, {
200
+ ...options,
201
+ env: invocation.env,
202
+ shell: false,
203
+ });
204
+ } catch (error) {
205
+ return { error, status: null, stdout: '', stderr: '' };
206
+ }
105
207
  }
106
208
 
107
209
  export function runWindows(command, args, options = {}) {
108
210
  return new Promise((resolve, reject) => {
109
211
  const child = spawnWindows(command, args, { stdio: 'inherit', ...options });
110
- const forward = signal => { try { child.kill(signal); } catch { /* already exited */ } };
212
+ const forward = (signal) => {
213
+ try {
214
+ child.kill(signal);
215
+ } catch {
216
+ /* already exited */
217
+ }
218
+ };
111
219
  const interrupt = () => forward('SIGINT');
112
220
  const terminate = () => forward('SIGTERM');
113
221
  process.on('SIGINT', interrupt);
@@ -116,7 +224,10 @@ export function runWindows(command, args, options = {}) {
116
224
  process.off('SIGINT', interrupt);
117
225
  process.off('SIGTERM', terminate);
118
226
  };
119
- child.once('error', error => { cleanup(); reject(error); });
227
+ child.once('error', (error) => {
228
+ cleanup();
229
+ reject(error);
230
+ });
120
231
  child.once('close', (code, signal) => {
121
232
  cleanup();
122
233
  resolve(code ?? (signal === 'SIGINT' ? 130 : 143));
@@ -128,30 +239,61 @@ export function readWindowsVersion(command, env) {
128
239
  try {
129
240
  const invocation = windowsInvocation(command, ['--version'], env);
130
241
  return execFileSync(invocation.command, invocation.args, {
131
- env: invocation.env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'],
242
+ env: invocation.env,
243
+ encoding: 'utf8',
244
+ timeout: 3000,
245
+ stdio: ['ignore', 'pipe', 'ignore'],
132
246
  });
133
- } catch { return ''; }
247
+ } catch {
248
+ return '';
249
+ }
134
250
  }
135
251
 
136
252
  export function powershellCommand(source, env = process.env) {
137
253
  // Only fixed application source is encoded here. Dynamic data uses env vars.
138
- return { command: 'powershell.exe', args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')], env: windowsEnv(env) };
254
+ return {
255
+ command: 'powershell.exe',
256
+ args: [
257
+ '-NoLogo',
258
+ '-NoProfile',
259
+ '-NonInteractive',
260
+ '-EncodedCommand',
261
+ Buffer.from(source, 'utf16le').toString('base64'),
262
+ ],
263
+ env: windowsEnv(env),
264
+ };
139
265
  }
140
266
 
141
267
  export function openWindowsBrowser(url) {
142
- const invocation = powershellCommand('Start-Process -FilePath $env:SUBC_BROWSER_URL', { ...process.env, SUBC_BROWSER_URL: url });
143
- return runWindows(invocation.command, invocation.args, { env: invocation.env, stdio: 'ignore' });
268
+ const invocation = powershellCommand(
269
+ 'Start-Process -FilePath $env:SUBC_BROWSER_URL',
270
+ { ...process.env, SUBC_BROWSER_URL: url },
271
+ );
272
+ return runWindows(invocation.command, invocation.args, {
273
+ env: invocation.env,
274
+ stdio: 'ignore',
275
+ });
144
276
  }
145
277
 
146
278
  export function windowsEditor(requested, environment = process.env) {
147
279
  const env = windowsEnv(environment);
148
- const text = requested || env.VISUAL?.trim() || env.EDITOR?.trim() || 'notepad.exe';
280
+ const text =
281
+ requested || env.VISUAL?.trim() || env.EDITOR?.trim() || 'notepad.exe';
149
282
  // An existing full path may contain spaces without quotes. Otherwise accept
150
283
  // double-quoted argv, preserving Windows backslashes and rejecting shell use.
151
- const parts = fs.existsSync(text) ? [text] : text.match(/"[^"]*"|[^\s"]+/g) || [];
152
- if ((text.match(/"/g) || []).length % 2) throw new Error('Unmatched quote in VISUAL / EDITOR');
153
- const [command, ...args] = parts.map(part => part.replace(/^"|"$/g, ''));
284
+ const parts = fs.existsSync(text)
285
+ ? [text]
286
+ : text.match(/"[^"]*"|[^\s"]+/g) || [];
287
+ if ((text.match(/"/g) || []).length % 2)
288
+ throw new Error('Unmatched quote in VISUAL / EDITOR');
289
+ const [command, ...args] = parts.map((part) => part.replace(/^"|"$/g, ''));
154
290
  if (!command) throw new Error('Editor is empty');
155
- if (/^(code|code-insiders)(\.cmd|\.exe)?$/i.test(path.win32.basename(command)) && !args.includes('--wait')) args.unshift('--wait');
291
+ if (
292
+ /^(code|code-insiders)(\.cmd|\.exe)?$/i.test(
293
+ path.win32.basename(command),
294
+ ) &&
295
+ !args.includes('--wait')
296
+ )
297
+ args.unshift('--wait');
156
298
  return { command, args };
157
299
  }