promptmic 0.1.0 → 0.1.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/README.md CHANGED
@@ -57,7 +57,7 @@ Installed executable:
57
57
  promptmic
58
58
  ```
59
59
 
60
- After the package is published, the default user flow will be:
60
+ The default user flow is:
61
61
 
62
62
  ```bash
63
63
  npx promptmic
@@ -1,6 +1,55 @@
1
1
  import pty from 'node-pty';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { HOME_DIR } from './runtime-config.js';
5
+ function isExecutable(filePath, platform) {
6
+ try {
7
+ fs.accessSync(filePath, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function resolveDirectExecutable(command, envPath, platform = process.platform, pathExt = process.env.PATHEXT) {
15
+ const expandedCommand = expandTilde(command);
16
+ const hasPathSeparator = expandedCommand.includes('/') || expandedCommand.includes('\\');
17
+ const pathDelimiter = platform === 'win32' ? ';' : path.delimiter;
18
+ const windowsExtensions = platform === 'win32'
19
+ ? (pathExt ?? '.COM;.EXE;.BAT;.CMD')
20
+ .split(';')
21
+ .map((entry) => entry.trim())
22
+ .map((entry) => entry.toLowerCase())
23
+ .filter(Boolean)
24
+ : [];
25
+ const hasWindowsExtension = platform === 'win32' &&
26
+ windowsExtensions.some((extension) => expandedCommand.toLowerCase().endsWith(extension.toLowerCase()));
27
+ const candidatesFor = (baseCommand) => {
28
+ if (platform !== 'win32')
29
+ return [baseCommand];
30
+ if (hasWindowsExtension)
31
+ return [baseCommand];
32
+ return [baseCommand, ...windowsExtensions.map((extension) => `${baseCommand}${extension}`)];
33
+ };
34
+ if (hasPathSeparator) {
35
+ for (const candidate of candidatesFor(expandedCommand)) {
36
+ if (isExecutable(candidate, platform))
37
+ return candidate;
38
+ }
39
+ return null;
40
+ }
41
+ if (!envPath)
42
+ return null;
43
+ for (const dir of envPath.split(pathDelimiter)) {
44
+ if (!dir)
45
+ continue;
46
+ for (const candidate of candidatesFor(path.join(dir, expandedCommand))) {
47
+ if (isExecutable(candidate, platform))
48
+ return candidate;
49
+ }
50
+ }
51
+ return null;
52
+ }
4
53
  export function expandTilde(value) {
5
54
  return value.startsWith('~/') ? path.join(HOME_DIR, value.slice(2)) : value;
6
55
  }
@@ -10,11 +59,13 @@ function quoteShellArg(value) {
10
59
  return `'${value.replace(/'/g, `'\\''`)}'`;
11
60
  }
12
61
  function formatSpawnError(entry, error) {
62
+ const errorMessage = error instanceof Error ? error.message : String(error ?? '');
63
+ const errorCode = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
13
64
  if (entry.executionMode === 'direct' &&
14
- error &&
15
- typeof error === 'object' &&
16
- 'code' in error &&
17
- error.code === 'ENOENT') {
65
+ (errorCode === 'ENOENT' ||
66
+ /posix_spawnp failed/i.test(errorMessage) ||
67
+ /spawn .* ENOENT/i.test(errorMessage) ||
68
+ /not found/i.test(errorMessage))) {
18
69
  return new Error(`Could not start "${entry.label}". Command "${entry.command}" was not found in PATH. Install it or switch this provider to Shell command if it depends on a shell wrapper.`);
19
70
  }
20
71
  if (error instanceof Error) {
@@ -50,19 +101,24 @@ export function createPtySpawner(options = {}) {
50
101
  const baseEnv = Object.fromEntries(Object.entries(options.env ?? process.env).filter((entry) => typeof entry[1] === 'string'));
51
102
  const spawn = options.spawn ?? pty.spawn;
52
103
  const shell = options.shell ?? process.env.SHELL ?? '/bin/bash';
104
+ const platform = options.platform ?? process.platform;
53
105
  return ({ provider, cwd, config }) => {
54
106
  const entry = config.providers[provider];
55
107
  const command = resolveProviderCommand(config, provider, shell);
108
+ const env = {
109
+ ...baseEnv,
110
+ ...command.envOverrides,
111
+ };
112
+ const file = command.executionMode === 'direct'
113
+ ? resolveDirectExecutable(command.file, env.PATH, platform, env.PATHEXT) ?? command.file
114
+ : command.file;
56
115
  try {
57
- return spawn(command.file, command.args, {
116
+ return spawn(file, command.args, {
58
117
  name: 'xterm-256color',
59
118
  cols: 120,
60
119
  rows: 30,
61
120
  cwd,
62
- env: {
63
- ...baseEnv,
64
- ...command.envOverrides,
65
- },
121
+ env,
66
122
  });
67
123
  }
68
124
  catch (error) {
@@ -70,3 +126,4 @@ export function createPtySpawner(options = {}) {
70
126
  }
71
127
  };
72
128
  }
129
+ export { resolveDirectExecutable };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptmic",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Voice-first local UI for terminal-based AI coding assistants.",
5
5
  "license": "MIT",
6
6
  "keywords": [