promptmic 0.1.0 → 0.1.2

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,85 @@
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
+ }
53
+ function readShebang(filePath) {
54
+ try {
55
+ const contents = fs.readFileSync(filePath, 'utf8');
56
+ const firstLine = contents.split(/\r?\n/, 1)[0];
57
+ return firstLine.startsWith('#!') ? firstLine : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function resolveDirectSpawnTarget(command, args, env, platform) {
64
+ const executable = resolveDirectExecutable(command, env.PATH, platform, env.PATHEXT) ?? command;
65
+ try {
66
+ const realPath = fs.realpathSync(executable);
67
+ const shebang = readShebang(realPath)?.toLowerCase() ?? '';
68
+ if (shebang.includes('node')) {
69
+ return {
70
+ file: process.execPath,
71
+ args: [realPath, ...args],
72
+ };
73
+ }
74
+ }
75
+ catch {
76
+ // Fall back to the resolved executable path when realpath/read fails.
77
+ }
78
+ return {
79
+ file: executable,
80
+ args,
81
+ };
82
+ }
4
83
  export function expandTilde(value) {
5
84
  return value.startsWith('~/') ? path.join(HOME_DIR, value.slice(2)) : value;
6
85
  }
@@ -10,11 +89,13 @@ function quoteShellArg(value) {
10
89
  return `'${value.replace(/'/g, `'\\''`)}'`;
11
90
  }
12
91
  function formatSpawnError(entry, error) {
92
+ const errorMessage = error instanceof Error ? error.message : String(error ?? '');
93
+ const errorCode = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
13
94
  if (entry.executionMode === 'direct' &&
14
- error &&
15
- typeof error === 'object' &&
16
- 'code' in error &&
17
- error.code === 'ENOENT') {
95
+ (errorCode === 'ENOENT' ||
96
+ /posix_spawnp failed/i.test(errorMessage) ||
97
+ /spawn .* ENOENT/i.test(errorMessage) ||
98
+ /not found/i.test(errorMessage))) {
18
99
  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
100
  }
20
101
  if (error instanceof Error) {
@@ -50,19 +131,24 @@ export function createPtySpawner(options = {}) {
50
131
  const baseEnv = Object.fromEntries(Object.entries(options.env ?? process.env).filter((entry) => typeof entry[1] === 'string'));
51
132
  const spawn = options.spawn ?? pty.spawn;
52
133
  const shell = options.shell ?? process.env.SHELL ?? '/bin/bash';
134
+ const platform = options.platform ?? process.platform;
53
135
  return ({ provider, cwd, config }) => {
54
136
  const entry = config.providers[provider];
55
137
  const command = resolveProviderCommand(config, provider, shell);
138
+ const env = {
139
+ ...baseEnv,
140
+ ...command.envOverrides,
141
+ };
142
+ const spawnTarget = command.executionMode === 'direct'
143
+ ? resolveDirectSpawnTarget(command.file, command.args, env, platform)
144
+ : { file: command.file, args: command.args };
56
145
  try {
57
- return spawn(command.file, command.args, {
146
+ return spawn(spawnTarget.file, spawnTarget.args, {
58
147
  name: 'xterm-256color',
59
148
  cols: 120,
60
149
  rows: 30,
61
150
  cwd,
62
- env: {
63
- ...baseEnv,
64
- ...command.envOverrides,
65
- },
151
+ env,
66
152
  });
67
153
  }
68
154
  catch (error) {
@@ -70,3 +156,4 @@ export function createPtySpawner(options = {}) {
70
156
  }
71
157
  };
72
158
  }
159
+ export { resolveDirectExecutable, resolveDirectSpawnTarget };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptmic",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Voice-first local UI for terminal-based AI coding assistants.",
5
5
  "license": "MIT",
6
6
  "keywords": [