dsh-graft-plugin 0.3.3 → 0.3.4

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": "dsh-graft-plugin",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Shows which graft index a dsh session is serving and whether it still matches the code, and gives the model graft_ask, graft_grep, graft_callers, graft_skeleton and graft_map as native tools.",
5
5
  "icon": "assets/icon.png",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { join } from 'node:path'
1
+ import { delimiter, join } from 'node:path'
2
2
  import { resolveGraftCommand, savedTokensIn } from '../src/graft-tools.js'
3
3
  import { createAutoSync } from '../src/auto-sync.js'
4
4
  import { totalSaved } from '../src/index.js'
@@ -178,5 +178,40 @@ check('or via USERPROFILE/.bun on Windows', viaUserprofile.prefix[0] === join('C
178
178
  const bare = resolveGraftCommand({ PATH: '', HOME: '/home/x' }, fakeExists(new Set()))
179
179
  check('nothing installed falls back to the bare command', bare.command === 'graft' && bare.prefix.length === 0)
180
180
 
181
+ // The dsh DESKTOP app runs on Electron, where process.execPath is
182
+ // "DeepSeek Harness.exe". Spawning that still RUNS the script — but the child
183
+ // keeps process.versions.electron, and commander 15 then reads argv from 1
184
+ // instead of 2, so graft's own cli.js path is taken as the subcommand:
185
+ // error: unknown command '<...>\graft\dist\cli.js'
186
+ // which is what Build and every query tool returned there. The npm shim's own
187
+ // rule — the node beside the shims — is the interpreter to use instead.
188
+ const NODE_EXE = process.platform === 'win32' ? 'node.exe' : 'node'
189
+ const npmDir = join('C:\\Users\\x', 'AppData', 'Local', 'hermes', 'node')
190
+ const npmCli = join(npmDir, 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
191
+ const appExe = join('C:\\Application', 'DeepSeek Harness', 'DeepSeek Harness.exe')
192
+ const onElectron = resolveGraftCommand({ PATH: npmDir }, fakeExists(new Set([npmCli, join(npmDir, NODE_EXE)])), appExe, true)
193
+ check('an Electron host runs graft with the node beside it, not the app binary', onElectron.command === join(npmDir, NODE_EXE) && onElectron.shell === false, onElectron.command)
194
+ // A graft whose prefix has no node of its own (bun, a hand-made layout): the
195
+ // PATH search is the fallback, so the Electron binary is still not used.
196
+ const elsewhere = join('C:\\Program Files', 'nodejs')
197
+ const viaPath = resolveGraftCommand({ PATH: [npmDir, elsewhere].join(delimiter) }, fakeExists(new Set([npmCli, join(elsewhere, NODE_EXE)])), appExe, true)
198
+ check('a node elsewhere on PATH is preferred over the app binary', viaPath.command === join(elsewhere, NODE_EXE), viaPath.command)
199
+ const noNode = resolveGraftCommand({ PATH: npmDir }, fakeExists(new Set([npmCli])), appExe, true)
200
+ check('with no node anywhere it still returns the cli, not nothing', noNode.prefix[0] === npmCli && noNode.command === appExe, noNode.command)
201
+ // A plain Node host is untouched by all of this: its execPath IS the runtime.
202
+ const onNode = resolveGraftCommand({ PATH: npmDir }, fakeExists(new Set([npmCli])), join('C:\\node', 'node.exe'), false)
203
+ check('a Node host keeps using its own process.execPath', onNode.command === join('C:\\node', 'node.exe'), onNode.command)
204
+
205
+ // And the rebuild must be handed that interpreter, because sync-run rebuilds
206
+ // through execFileSync(process.execPath, ...) — the grandchild inherits whatever
207
+ // binary started sync-run.
208
+ spawned = []
209
+ state.stats = { dirty: true }
210
+ state.lock = true
211
+ now += 60_000
212
+ const withNode = make({ nodePath: 'C:/fake/node.exe' })
213
+ check('the rebuild spawns sync-run', (await withNode.rebuild('C:/interp')) === 'started')
214
+ check('with the resolved interpreter, not process.execPath', spawned.at(-1)?.[0] === 'C:/fake/node.exe', String(spawned.at(-1)?.[0]))
215
+
181
216
  console.log(failures === 0 ? '\nALL CHECKS PASSED' : `\n${failures} CHECK(S) FAILED`)
182
217
  process.exit(failures === 0 ? 0 : 1)
package/src/auto-sync.js CHANGED
@@ -92,6 +92,7 @@ async function loadGraftState(cliPath) {
92
92
 
93
93
  /**
94
94
  * @param deps.cliPath - graft's `dist/cli.js`, from which its `claude/` dir is found.
95
+ * @param deps.nodePath - the interpreter to run it with; defaults to execPath.
95
96
  * @param deps.minIntervalMs - floor between rebuilds of the same repo.
96
97
  */
97
98
  export function createAutoSync(deps = {}) {
@@ -108,6 +109,20 @@ export function createAutoSync(deps = {}) {
108
109
  */
109
110
  const minIntervalMs = Number.isInteger(deps.minIntervalMs) ? deps.minIntervalMs : 5_000
110
111
  const spawnFn = deps.spawn ?? spawn
112
+ /**
113
+ * The interpreter for sync-run.js — the one the plugin resolved for graft's
114
+ * CLI, which is NOT necessarily `process.execPath`.
115
+ *
116
+ * sync-run rebuilds through `execFileSync(process.execPath, [cli, 'build',
117
+ * '.'])`, i.e. it re-spawns whatever binary started it. So handing it the
118
+ * plugin's node is enough to fix the grandchild as well, and spawning this
119
+ * host's execPath is not: in the dsh DESKTOP app that is
120
+ * `DeepSeek Harness.exe`, and Electron's `process.versions.electron` makes
121
+ * graft's commander read the cli.js path as the subcommand. That failure is
122
+ * invisible from here — sync-run's build call discards stdio, so the rebuild
123
+ * simply reported success over an unchanged graph.
124
+ */
125
+ const nodePath = deps.nodePath ?? process.execPath
111
126
  const now = deps.now ?? (() => Date.now())
112
127
 
113
128
  /**
@@ -271,7 +286,7 @@ export function createAutoSync(deps = {}) {
271
286
  * and `sync-run` clears `syncing` and releases the lock on its own way
272
287
  * out either way.
273
288
  */
274
- const child = spawnFn(process.execPath, [g.syncRun, root], {
289
+ const child = spawnFn(nodePath, [g.syncRun, root], {
275
290
  stdio: 'ignore',
276
291
  windowsHide: true,
277
292
  })
@@ -73,21 +73,68 @@ const MAX_CHARS = 60_000
73
73
  *
74
74
  * The bare-name fallback keeps a differently-installed graft working; it is
75
75
  * only reached when the entry point cannot be found.
76
+ *
77
+ * WHY THE INTERPRETER IS NOT ALWAYS `process.execPath`
78
+ *
79
+ * execPath is Node only when the host is Node. The dsh DESKTOP app is Electron
80
+ * (`DeepSeek Harness.exe`) running with ELECTRON_RUN_AS_NODE=1, so execPath is
81
+ * that app binary: the script still starts, and the breakage happens one layer
82
+ * in, inside graft's own argument parsing. The child keeps
83
+ * `process.versions.electron`, and commander 15 picks its `electron` argv
84
+ * convention from exactly that (`if (process.versions?.electron) from =
85
+ * 'electron'`). That branch then keys on `process.defaultApp`, which is
86
+ * undefined when the JS arrived as a script path rather than an app directory,
87
+ * and slices argv from 1 instead of 2 — so the cli.js path lands where the
88
+ * subcommand should be:
89
+ *
90
+ * error: unknown command '<...>\@nanonets\graft\dist\cli.js'
91
+ *
92
+ * which is what the Build button and all five query tools used to return in the
93
+ * desktop app. A real node avoids the Electron binary altogether.
76
94
  */
77
- export function resolveGraftCommand(env = process.env, exists = existsSync, execPath = process.execPath) {
78
- const path = String(env.PATH ?? env.Path ?? '')
79
- for (const dir of path.split(delimiter)) {
80
- if (dir === '') continue
95
+ const NODE_EXE = process.platform === 'win32' ? 'node.exe' : 'node'
96
+
97
+ /**
98
+ * Which binary to run graft's cli.js with.
99
+ *
100
+ * The npm shim's own rule is the first choice — `%dp0%\node.exe`, the node that
101
+ * was installed beside the shims — so this agrees with what typing `graft` in a
102
+ * terminal would do. Only an Electron host needs the PATH search after it,
103
+ * because `graft` and its node need not share a prefix. A Node host keeps using
104
+ * its own execPath, unchanged from before.
105
+ */
106
+ function interpreterFor(installDir, dirs, exists, execPath, electron) {
107
+ const beside = join(installDir, NODE_EXE)
108
+ if (exists(beside)) return beside
109
+ if (!electron) return execPath
110
+ for (const dir of dirs) {
111
+ const candidate = join(dir, NODE_EXE)
112
+ if (exists(candidate)) return candidate
113
+ }
114
+ // Nothing better to hand: execPath at least runs the script, and graft's own
115
+ // stderr says what happened — the same answer as before this existed.
116
+ return execPath
117
+ }
118
+
119
+ export function resolveGraftCommand(
120
+ env = process.env,
121
+ exists = existsSync,
122
+ execPath = process.execPath,
123
+ electron = process.versions?.electron !== undefined,
124
+ ) {
125
+ const dirs = String(env.PATH ?? env.Path ?? '').split(delimiter).filter((dir) => dir !== '')
126
+ for (const dir of dirs) {
81
127
  const cli = join(dir, 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
82
- if (exists(cli)) return { command: execPath, prefix: [cli], shell: false }
128
+ if (exists(cli)) return { command: interpreterFor(dir, dirs, exists, execPath, electron), prefix: [cli], shell: false }
83
129
  }
84
130
  // A bun global install puts only the shim on PATH; the package lives under
85
131
  // $BUN_INSTALL/install/global. Missing it left prefix empty, which auto-sync
86
132
  // used to turn into a fatal dirname(undefined).
87
133
  const bunRoot = env.BUN_INSTALL ?? join(env.HOME ?? env.USERPROFILE ?? '.', '.bun')
88
134
  if (typeof bunRoot === 'string' && bunRoot !== '') {
89
- const bunCli = join(bunRoot, 'install', 'global', 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
90
- if (exists(bunCli)) return { command: execPath, prefix: [bunCli], shell: false }
135
+ const bunInstall = join(bunRoot, 'install', 'global')
136
+ const bunCli = join(bunInstall, 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
137
+ if (exists(bunCli)) return { command: interpreterFor(bunInstall, dirs, exists, execPath, electron), prefix: [bunCli], shell: false }
91
138
  }
92
139
  return { command: 'graft', prefix: [], shell: process.platform === 'win32' }
93
140
  }
package/src/index.js CHANGED
@@ -561,8 +561,12 @@ export function apply(ctx, config = {}) {
561
561
  * Build button. Hoisted out of the `autoBuild` block on purpose: the button
562
562
  * must rebuild even in a profile that turned the automatic hooks off.
563
563
  */
564
+ // Resolved once: the rebuild needs graft's cli.js AND the interpreter that
565
+ // may run it, and re-resolving per call could pick a different node mid-session.
566
+ const graftCommand = resolveGraftCommand()
564
567
  const autoSync = createAutoSync({
565
- cliPath: resolveGraftCommand().prefix[0],
568
+ cliPath: graftCommand.prefix[0],
569
+ nodePath: graftCommand.command,
566
570
  minIntervalMs: config.autoBuildMinIntervalMs,
567
571
  writeTools: config.autoBuildTools,
568
572
  })