@gordon.gan/specflow 1.3.1-beta → 1.3.2-beta

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
@@ -124,7 +124,7 @@ npm install -g @gordon.gan/specflow
124
124
  npm install -g github:Gordon-Gan-Jiang/specflow
125
125
 
126
126
  # 验证
127
- specflow --version # 以 npm / package.json 为准(当前 1.3.1-beta)
127
+ specflow --version # 以 npm / package.json 为准(当前 1.3.2-beta)
128
128
  specflow --help
129
129
  ```
130
130
 
@@ -5,14 +5,15 @@ export interface OpenerSpawnPlan {
5
5
  readonly args: string[];
6
6
  readonly options: SpawnSyncOptions;
7
7
  }
8
+ export declare function selectWindowsOpenerPath(output: string): string | null;
8
9
  /**
9
10
  * Build spawn argv for launching a workspace-file opener.
10
11
  *
11
- * On Windows, Cursor/VS Code ship as `*.cmd` shims. Node's spawn with
12
- * `shell: false` cannot execute those directly (ENOENT). Follow Node's
13
- * recommended pattern: `cmd.exe /d /s /c` with a single quoted command line
14
- * and `windowsVerbatimArguments`, without enabling shell interpolation.
12
+ * On Windows, Cursor/VS Code normally ship as `*.cmd` shims. Resolve the shim
13
+ * to an absolute path first: invoking a bare command while cwd is a project
14
+ * can make the shim resolve its own relative Cursor.exe path from the project.
15
+ * Batch shims run through cmd.exe; native executables run directly.
15
16
  */
16
- export declare function buildOpenerSpawn(opener: OpenerDefinition, workspacePath: string, primaryPath: string, platform?: NodeJS.Platform, comSpec?: string): OpenerSpawnPlan;
17
+ export declare function buildOpenerSpawn(opener: OpenerDefinition, workspacePath: string, primaryPath: string, platform?: NodeJS.Platform, comSpec?: string, resolvedPath?: string): OpenerSpawnPlan;
17
18
  export declare function isOpenerAvailable(opener: OpenerDefinition): boolean;
18
19
  export declare function launchOpener(opener: OpenerDefinition, workspacePath: string, primaryPath: string): void;
@@ -1,13 +1,36 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ export function selectWindowsOpenerPath(output) {
4
+ return (output
5
+ .split(/\r?\n/)
6
+ .map((candidate) => candidate.trim())
7
+ .find((candidate) => /\.(?:cmd|bat|exe)$/i.test(candidate)) ?? null);
8
+ }
9
+ function resolveWindowsOpenerPath(opener) {
10
+ for (const suffix of ['.cmd', '.bat', '.exe']) {
11
+ const result = spawnSync('where.exe', [`${opener.command}${suffix}`], {
12
+ encoding: 'utf8',
13
+ shell: false,
14
+ stdio: ['ignore', 'pipe', 'ignore'],
15
+ });
16
+ if (result.status === 0) {
17
+ const executablePath = selectWindowsOpenerPath(result.stdout ?? '');
18
+ if (executablePath) {
19
+ return executablePath;
20
+ }
21
+ }
22
+ }
23
+ return null;
24
+ }
2
25
  /**
3
26
  * Build spawn argv for launching a workspace-file opener.
4
27
  *
5
- * On Windows, Cursor/VS Code ship as `*.cmd` shims. Node's spawn with
6
- * `shell: false` cannot execute those directly (ENOENT). Follow Node's
7
- * recommended pattern: `cmd.exe /d /s /c` with a single quoted command line
8
- * and `windowsVerbatimArguments`, without enabling shell interpolation.
28
+ * On Windows, Cursor/VS Code normally ship as `*.cmd` shims. Resolve the shim
29
+ * to an absolute path first: invoking a bare command while cwd is a project
30
+ * can make the shim resolve its own relative Cursor.exe path from the project.
31
+ * Batch shims run through cmd.exe; native executables run directly.
9
32
  */
10
- export function buildOpenerSpawn(opener, workspacePath, primaryPath, platform = process.platform, comSpec = process.env.ComSpec || 'cmd.exe') {
33
+ export function buildOpenerSpawn(opener, workspacePath, primaryPath, platform = process.platform, comSpec = process.env.ComSpec || 'cmd.exe', resolvedPath) {
11
34
  const baseOptions = {
12
35
  cwd: primaryPath,
13
36
  shell: false,
@@ -20,27 +43,48 @@ export function buildOpenerSpawn(opener, workspacePath, primaryPath, platform =
20
43
  options: baseOptions,
21
44
  };
22
45
  }
23
- // Escape " for cmd.exe by doubling; wrap each token so spaces are preserved.
24
- const quote = (value) => `"${value.replace(/"/g, '""')}"`;
46
+ if (!resolvedPath) {
47
+ throw new Error(`Could not resolve '${opener.command}.cmd' or '${opener.command}.exe' on PATH.`);
48
+ }
49
+ const executablePath = path.win32.normalize(resolvedPath);
50
+ const extension = path.win32.extname(executablePath).toLowerCase();
51
+ const shimOptions = {
52
+ ...baseOptions,
53
+ // Cursor's .cmd shim derives Cursor.exe from its own directory.
54
+ cwd: path.win32.dirname(executablePath),
55
+ };
56
+ if (extension === '.exe') {
57
+ return {
58
+ command: executablePath,
59
+ args: [workspacePath],
60
+ options: shimOptions,
61
+ };
62
+ }
63
+ // cmd.exe's /s removes the outer pair of quotes. Supply that pair explicitly
64
+ // so the batch-file path and workspace path retain their independent quotes.
65
+ const commandLine = `""${executablePath.replace(/"/g, '""')}" "${workspacePath.replace(/"/g, '""')}""`;
25
66
  return {
26
67
  command: comSpec,
27
- args: ['/d', '/s', '/c', `${quote(opener.command)} ${quote(workspacePath)}`],
68
+ args: ['/d', '/s', '/c', commandLine],
28
69
  options: {
29
- ...baseOptions,
70
+ ...shimOptions,
30
71
  windowsVerbatimArguments: true,
31
72
  },
32
73
  };
33
74
  }
34
75
  export function isOpenerAvailable(opener) {
35
- // `where.exe` resolves .cmd/.bat shims the same way cmd does.
36
- const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [opener.command], {
76
+ if (process.platform === 'win32') {
77
+ return resolveWindowsOpenerPath(opener) !== null;
78
+ }
79
+ const result = spawnSync('which', [opener.command], {
37
80
  stdio: 'ignore',
38
81
  shell: false,
39
82
  });
40
83
  return result.status === 0;
41
84
  }
42
85
  export function launchOpener(opener, workspacePath, primaryPath) {
43
- const plan = buildOpenerSpawn(opener, workspacePath, primaryPath);
86
+ const resolvedPath = process.platform === 'win32' ? resolveWindowsOpenerPath(opener) : undefined;
87
+ const plan = buildOpenerSpawn(opener, workspacePath, primaryPath, process.platform, process.env.ComSpec || 'cmd.exe', resolvedPath ?? undefined);
44
88
  const child = spawnSync(plan.command, plan.args, plan.options);
45
89
  if (child.error) {
46
90
  throw child.error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gordon.gan/specflow",
3
- "version": "1.3.1-beta",
3
+ "version": "1.3.2-beta",
4
4
  "type": "module",
5
5
  "description": "SpecFlow — unified spec-driven development: OpenSpec planning + Superpowers execution in one CLI and cross-IDE workflow",
6
6
  "keywords": [