@ran-sh/dsh-crew 2.0.13 → 2.0.15

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync, rmSync, statSync, lstatSync, opendirSync } from 'node:fs';
6
6
  import { createHash } from 'node:crypto';
7
- import { createRequire } from 'node:module';
7
+ import { createRequire, isBuiltin } from 'node:module';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import { dirname, join, resolve, relative } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
@@ -36,6 +36,79 @@ const CLAUDE_SNAPSHOT_POLL_MS = 5_000;
36
36
  // record stand in for a missing user-scope snapshot, so a failed install read as
37
37
  // a current one.
38
38
  const CLAUDE_PLUGIN_SCOPE = 'user';
39
+ // What a module specifier can look like: a relative or absolute path, a package
40
+ // name, or a scoped one. Anything else the scan produces is the source text
41
+ // between two quotes, not a dependency.
42
+ const SPECIFIER_SHAPE = /^(?:(?:\.{1,2}\/|\/|[A-Za-z]:[\\/])[^\s"']*|(?:@[^/\s]+\/[^/\s]+|[A-Za-z][\w.-]*)(?:\/[^\s"']*)?)$/;
43
+
44
+ // A `/` starts a regular expression, not a division, when the last significant
45
+ // token could not end an expression. Getting this wrong desynchronises the scan:
46
+ // the quotes inside a regex were read as string delimiters, and everything after
47
+ // them — including real imports — was swallowed into a phantom literal.
48
+ const REGEX_MAY_FOLLOW = new Set([
49
+ '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '=>',
50
+ 'return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'instanceof', 'do', 'else', 'yield', 'await',
51
+ ]);
52
+
53
+ /**
54
+ * Split module source into tokens: comments dropped, string and template literals
55
+ * kept whole, regular-expression literals kept whole, identifiers single, and
56
+ * everything else one character. Specifiers are then read from tokens rather than
57
+ * from the raw text, so a keyword inside a comment or a string is not a
58
+ * declaration and a quote inside a regex is not a string.
59
+ */
60
+ export function tokenizeModuleSource(source) {
61
+ const tokens = [];
62
+ const text = String(source ?? '');
63
+ let i = 0;
64
+ let previous = null;
65
+ const push = (token) => { tokens.push(token); previous = token; };
66
+ const scanQuoted = (quote) => {
67
+ let j = i + 1;
68
+ while (j < text.length) {
69
+ if (text[j] === '\\') { j += 2; continue; }
70
+ if (text[j] === quote) { j += 1; break; }
71
+ if (quote !== '`' && text[j] === '\n') break;
72
+ j += 1;
73
+ }
74
+ push(text.slice(i, j));
75
+ i = j;
76
+ };
77
+ const scanRegex = () => {
78
+ let j = i + 1;
79
+ let inClass = false;
80
+ while (j < text.length) {
81
+ const c = text[j];
82
+ if (c === '\\') { j += 2; continue; }
83
+ if (c === '\n') break;
84
+ if (inClass) { if (c === ']') inClass = false; }
85
+ else if (c === '[') inClass = true;
86
+ else if (c === '/') { j += 1; break; }
87
+ j += 1;
88
+ }
89
+ while (j < text.length && /[a-z]/i.test(text[j])) j += 1;
90
+ push(text.slice(i, j));
91
+ i = j;
92
+ };
93
+ while (i < text.length) {
94
+ const ch = text[i];
95
+ if (/\s/.test(ch)) { i += 1; continue; }
96
+ if (ch === '/' && text[i + 1] === '/') { const end = text.indexOf('\n', i); i = end === -1 ? text.length : end; continue; }
97
+ if (ch === '/' && text[i + 1] === '*') { const end = text.indexOf('*/', i + 2); i = end === -1 ? text.length : end + 2; continue; }
98
+ if (ch === '"' || ch === "'" || ch === '`') { scanQuoted(ch); continue; }
99
+ if (ch === '/' && (previous === null || REGEX_MAY_FOLLOW.has(previous))) { scanRegex(); continue; }
100
+ if (/[A-Za-z_$]/.test(ch)) {
101
+ let j = i + 1;
102
+ while (j < text.length && /[\w$]/.test(text[j])) j += 1;
103
+ push(text.slice(i, j));
104
+ i = j;
105
+ continue;
106
+ }
107
+ push(ch);
108
+ i += 1;
109
+ }
110
+ return tokens;
111
+ }
39
112
  const POLICY_START = '<!-- DSH CREW MANAGED POLICY:START -->';
40
113
  const POLICY_END = '<!-- DSH CREW MANAGED POLICY:END -->';
41
114
 
@@ -593,25 +666,65 @@ export function claudeSnapshotSettleMs(err) {
593
666
  * present is what *it* imports. The manifest is not a substitute: it declares 28
594
667
  * dependencies, including meta-packages the server never imports from here, and
595
668
  * requiring all of them to resolve read a working install as broken. Resolution
596
- * is asked from the entry itself, and subpath specifiers are kept as they are —
669
+ * follows local modules from the entry, and subpath specifiers are kept as they are —
597
670
  * `@modelcontextprotocol/sdk/server/mcp.js` resolves through its package's
598
671
  * `exports`, which the bare package name does not.
599
672
  */
600
673
  function claudeSnapshotResolvable(snapshotRoot) {
601
674
  if (typeof snapshotRoot !== 'string' || !snapshotRoot.trim()) return false;
602
- const entry = join(snapshotRoot, 'src', 'server.mjs');
603
- if (!existsSync(entry)) return false;
604
- let source;
605
- try { source = readFileSync(entry, 'utf8'); } catch { return false; }
606
- const specifiers = [...source.matchAll(/(?:from|import)\s*\(?\s*['"]([^'"]+)['"]/g)]
607
- .map((match) => match[1])
608
- .filter((specifier) => !specifier.startsWith('.') && !specifier.startsWith('node:'));
609
- if (!specifiers.length) return true;
610
- let fromEntry;
611
- try { fromEntry = createRequire(entry); } catch { return false; }
612
- return specifiers.every((specifier) => {
613
- try { return existsSync(fromEntry.resolve(specifier)); } catch { return false; }
614
- });
675
+ const root = resolve(snapshotRoot);
676
+ const visited = new Set();
677
+ let bytes = 0;
678
+ const inspect = (entry) => {
679
+ if (visited.has(entry)) return true;
680
+ if (visited.size >= 512) return false;
681
+ const local = relative(root, entry);
682
+ if (local.startsWith('..') || resolve(root, local) !== entry) return false;
683
+ visited.add(entry);
684
+ try {
685
+ const info = lstatSync(entry);
686
+ if (!info.isFile() || info.isSymbolicLink() || bytes + info.size > 8 * 1024 * 1024) return false;
687
+ bytes += info.size;
688
+ if (!/\.[cm]?js$/i.test(entry)) return true;
689
+ const source = readFileSync(entry, 'utf8');
690
+ const fromEntry = createRequire(entry);
691
+ // Consume comments and literals as complete tokens before recognizing declarations.
692
+ const tokens = tokenizeModuleSource(source);
693
+ const specifiers = [];
694
+ const literal = (token) => token && /^['"]/.test(token);
695
+ for (let i = 0; i < tokens.length; i += 1) {
696
+ if (!['import', 'export', 'require'].includes(tokens[i])) continue;
697
+ if (tokens[i - 1] === '.') continue;
698
+ let next = i + 1;
699
+ while (tokens[next]?.startsWith('/*') || tokens[next]?.startsWith('//')) next += 1;
700
+ if (['import', 'require'].includes(tokens[i]) && tokens[next] === '(') {
701
+ if (literal(tokens[next + 1])) specifiers.push(tokens[next + 1].slice(1, -1));
702
+ continue;
703
+ }
704
+ if (tokens[i] === 'require') continue;
705
+ if (literal(tokens[next])) { specifiers.push(tokens[next].slice(1, -1)); continue; }
706
+ // Only import/export declarations can introduce a `from` clause.
707
+ if (tokens[next] === '.' || tokens[next] === '(' || ['const', 'let', 'var', 'function', 'class', 'default', 'async'].includes(tokens[next])) continue;
708
+ for (let j = next; j < tokens.length && tokens[j] !== ';'; j += 1) {
709
+ if (tokens[j] === 'from' && literal(tokens[j + 1])) { specifiers.push(tokens[j + 1].slice(1, -1)); break; }
710
+ }
711
+ }
712
+ // The scan is lexical, and a regex literal holding a quote — `/^['"]/`, or
713
+ // the TOML matchers above — desynchronises it: the phantom string swallows
714
+ // text up to the next quote in the file, which can expose a keyword as a
715
+ // bare token and hand back the source between two of them as a "specifier".
716
+ // Those are not module specifiers, and resolving them read a working install
717
+ // as broken, so anything not shaped like a path or a package name is dropped.
718
+ const shaped = specifiers.filter((specifier) => isBuiltin(specifier) || SPECIFIER_SHAPE.test(specifier));
719
+ return shaped.every((specifier) => {
720
+ if (isBuiltin(specifier)) return true;
721
+ const target = fromEntry.resolve(specifier);
722
+ if (!existsSync(target)) return false;
723
+ return specifier.startsWith('.') ? inspect(target) : true;
724
+ });
725
+ } catch { return false; }
726
+ };
727
+ return inspect(join(root, 'src', 'server.mjs'));
615
728
  }
616
729
 
617
730
  /**
@@ -1181,9 +1181,15 @@ export function defaultPayloadSmoke(dir, { nodePath = process.execPath, runner =
1181
1181
  // Keyword boundaries reject identifiers containing the keywords
1182
1182
  // ('legacy-import') and member calls (.from); captures stay on one line and
1183
1183
  // bounded — real specifiers never span lines.
1184
- const IMPORT_SPECIFIER_RE = /(?<![\w.\-])(?:from|import|require)\b\s*\(?\s*["']([^"'\n]{1,200})["']/g;
1185
-
1186
- function collectExternalSpecifiers(dir) {
1184
+ //
1185
+ // A quote before the keyword is rejected too. Writing the keyword as a string —
1186
+ // `['import', 'export', 'require'].includes(word)` — is ordinary JavaScript, and
1187
+ // without this the closing quote of `'import'` was taken for the opening quote of
1188
+ // a specifier: it captured the text between two keywords and the payload failed to
1189
+ // stage on code that imports nothing at all.
1190
+ const IMPORT_SPECIFIER_RE = /(?<![\w.\-'"`])(?:from|import|require)\b\s*\(?\s*["']([^"'\n]{1,200})["']/g;
1191
+
1192
+ export function collectExternalSpecifiers(dir) {
1187
1193
  const specifiers = new Set();
1188
1194
  const walk = (root) => {
1189
1195
  if (!existsSync(root)) return;
@@ -36,7 +36,7 @@ export {
36
36
  // included in the identity contract.
37
37
  const RUNTIME_ID = randomUUID();
38
38
 
39
- export const RUNTIME_VERSION = '2.0.13';
39
+ export const RUNTIME_VERSION = '2.0.15';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([