dsh-graft-plugin 0.3.2 → 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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/scripts/test-autosync.mjs +36 -1
- package/src/auto-sync.js +16 -1
- package/src/client.js +8 -1
- package/src/graft-tools.js +54 -7
- package/src/index.js +5 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<picture>
|
|
2
|
-
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png">
|
|
3
|
-
<img alt="graft" src="assets/banner-light.png" width="1012">
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png?v=2">
|
|
3
|
+
<img alt="graft" src="assets/banner-light.png?v=2" width="1012">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
6
|
<br>
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<picture>
|
|
2
|
-
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png">
|
|
3
|
-
<img alt="graft" src="assets/banner-light.png" width="1012">
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png?v=2">
|
|
3
|
+
<img alt="graft" src="assets/banner-light.png?v=2" width="1012">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
6
|
<br>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-graft-plugin",
|
|
3
|
-
"version": "0.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(
|
|
289
|
+
const child = spawnFn(nodePath, [g.syncRun, root], {
|
|
275
290
|
stdio: 'ignore',
|
|
276
291
|
windowsHide: true,
|
|
277
292
|
})
|
package/src/client.js
CHANGED
|
@@ -185,6 +185,13 @@ window.__ModuleLoader__.load({
|
|
|
185
185
|
"cursor:pointer;--accent:var(--dsh-graft-accent,#e5484d)}",
|
|
186
186
|
".gs-viz:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14))}",
|
|
187
187
|
".gs-viz:disabled{opacity:.5;cursor:progress}",
|
|
188
|
+
// No graph (or a status not yet resolved): the mark greys out with the
|
|
189
|
+
// chip. The red accent is the "this repo IS grafted" signal — wearing
|
|
190
|
+
// it on a workspace with no index advertised a graph the tools would
|
|
191
|
+
// then refuse with "No graft index". currentColor folds the leaf and
|
|
192
|
+
// the node into the mark's own label colour; the opacity matches
|
|
193
|
+
// .gs-chip.gs-muted so the two read as one disabled pair.
|
|
194
|
+
".gs-viz.gs-muted{--accent:currentColor;opacity:.6}",
|
|
188
195
|
".gs-viz.gs-err{color:var(--dsw-alias-state-error-primary,#e5484d)}",
|
|
189
196
|
".gs-tabwrap{display:flex;flex-direction:column;height:100%;min-height:0}",
|
|
190
197
|
".gs-tabbar{display:flex;align-items:center;gap:6px;padding:0 6px 0 10px;min-height:32px;",
|
|
@@ -1172,7 +1179,7 @@ window.__ModuleLoader__.load({
|
|
|
1172
1179
|
"button",
|
|
1173
1180
|
{
|
|
1174
1181
|
type: "button",
|
|
1175
|
-
className: "gs-viz" + (failed ? " gs-err" : ""),
|
|
1182
|
+
className: "gs-viz" + (failed ? " gs-err" : "") + (ok ? "" : " gs-muted"),
|
|
1176
1183
|
onClick: openViz,
|
|
1177
1184
|
disabled: busy,
|
|
1178
1185
|
title: busy
|
package/src/graft-tools.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
90
|
-
|
|
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:
|
|
568
|
+
cliPath: graftCommand.prefix[0],
|
|
569
|
+
nodePath: graftCommand.command,
|
|
566
570
|
minIntervalMs: config.autoBuildMinIntervalMs,
|
|
567
571
|
writeTools: config.autoBuildTools,
|
|
568
572
|
})
|