@shidesheng0218/dsh-plugin-lint 0.1.0 → 0.4.0

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.
Files changed (66) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +515 -81
  3. package/bin/dsh-plugin-analyze.mjs +378 -0
  4. package/bin/dsh-plugin-lint.mjs +315 -27
  5. package/bin/dsh-plugin-matrix.mjs +78 -0
  6. package/bin/dsh-plugin-upgrade-plan.mjs +180 -0
  7. package/package.json +12 -4
  8. package/schema/dsh-manifest.schema.json +134 -0
  9. package/scripts/lib/probe.mjs +28 -385
  10. package/scripts/probe-boot.mjs +103 -37
  11. package/src/composition.js +1 -0
  12. package/src/core/baseline.js +98 -0
  13. package/src/core/changed.js +41 -0
  14. package/src/core/load.js +145 -0
  15. package/src/core/mask.js +207 -0
  16. package/src/core/pack.js +95 -0
  17. package/src/core/packlist.js +186 -0
  18. package/src/core/peer.js +39 -0
  19. package/src/core/profile-scan.js +121 -0
  20. package/src/core/report.js +174 -0
  21. package/src/core/rules.js +581 -0
  22. package/src/core/semver.js +192 -0
  23. package/src/core/tarball.js +90 -0
  24. package/src/data/event-types.json +111 -9
  25. package/src/experimental/conflicts.js +315 -0
  26. package/src/experimental/index.js +4 -0
  27. package/src/experimental/performance.js +515 -0
  28. package/src/experimental/security.js +462 -0
  29. package/src/index.js +15 -17
  30. package/src/load.js +1 -116
  31. package/src/mask.js +1 -156
  32. package/src/matrix/cache.js +19 -0
  33. package/src/matrix/cell.js +79 -0
  34. package/src/matrix/cells.js +58 -0
  35. package/src/matrix/conflicts.js +210 -0
  36. package/src/matrix/export.js +155 -0
  37. package/src/matrix/index.js +71 -0
  38. package/src/matrix/manifest.js +17 -0
  39. package/src/matrix/migrate.js +41 -0
  40. package/src/matrix/run.js +241 -0
  41. package/src/matrix/sanitize.js +42 -0
  42. package/src/matrix/schema.js +178 -0
  43. package/src/matrix/site.js +61 -0
  44. package/src/matrix/sync.js +72 -0
  45. package/src/matrix/verify.js +152 -0
  46. package/src/performance.js +1 -0
  47. package/src/probe/artifact-resolver.js +161 -0
  48. package/src/probe/classify.js +8 -0
  49. package/src/probe/dsh-adapter.js +212 -0
  50. package/src/probe/environment/docker.js +270 -0
  51. package/src/probe/environment/host-unsafe.js +39 -0
  52. package/src/probe/environment/materialize.js +22 -0
  53. package/src/probe/environment.js +50 -0
  54. package/src/probe/installed-artifact.js +17 -0
  55. package/src/probe/probe.js +278 -0
  56. package/src/report.js +1 -79
  57. package/src/rules.js +1 -379
  58. package/src/security.js +1 -0
  59. package/src/shared/errors.js +7 -0
  60. package/src/shared/hash.js +31 -0
  61. package/src/shared/status.js +12 -0
  62. package/src/shared/validate.js +14 -0
  63. package/src/shared/version.js +4 -0
  64. package/src/upgrade/compatibility.js +14 -0
  65. package/src/upgrade/planner.js +119 -0
  66. package/src/upgrade/profile.js +82 -0
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-plugin-analyze — experimental static analysis for DeepSeek Harness plugins.
4
+ *
5
+ * conflicts <dir> <dir> [dir ...] composition clashes between plugins
6
+ * performance <dir> bundle size, complexity, DOM and network indicators
7
+ * security <dir> heuristic security review
8
+ * full <dir> lintPackage + analyzePerformance + analyzeSecurityDeep
9
+ *
10
+ * The analyses are static heuristics, not runtime measurements and not a
11
+ * security verdict; the reports carry their own limits. --json keeps stdout
12
+ * machine-readable: human text is suppressed and errors go to stderr.
13
+ */
14
+
15
+ import { loadPackage } from '../src/load.js';
16
+ import { lintPackage } from '../src/index.js';
17
+ import { detectConflicts, analyzePerformance, analyzeSecurityDeep, EXPERIMENTAL_SCHEMA_VERSION } from '../src/experimental/index.js';
18
+
19
+ const USAGE = [
20
+ 'dsh-plugin-analyze — experimental static analysis for DeepSeek Harness plugins',
21
+ '',
22
+ 'Usage',
23
+ ' dsh-plugin-analyze conflicts <dir> <dir> [dir ...] [--json] [--exit-code]',
24
+ ' dsh-plugin-analyze performance <dir> [--json] [--exit-code]',
25
+ ' dsh-plugin-analyze security <dir> [--json] [--exit-code]',
26
+ ' dsh-plugin-analyze full <dir> [--json] [--exit-code]',
27
+ '',
28
+ 'Commands',
29
+ ' conflicts UI slot, tool name, route, config-key and waterfall clashes',
30
+ ' performance bundle size, code complexity, DOM and network indicators',
31
+ ' security heuristic security review; findings cite file and line',
32
+ ' full conformance lint + performance + security for one plugin',
33
+ '',
34
+ 'Options',
35
+ ' --json print only the JSON report on stdout (errors go to stderr)',
36
+ ' --exit-code gate the exit status (see below); without it a completed run exits 0',
37
+ ' -h, --help show this message',
38
+ '',
39
+ 'Exit codes',
40
+ ' 0 the analysis completed (with --exit-code: the gate did not trip)',
41
+ ' 1 --exit-code gate tripped: conflicts found (conflicts), warnings present',
42
+ ' (security), a lint error or a security warning is present (full);',
43
+ ' performance and info-level findings never gate',
44
+ ' 2 usage error, missing directory, or a directory without package.json',
45
+ '',
46
+ 'These analyses are experimental static heuristics, not a verdict.',
47
+ ].join('\n');
48
+
49
+ const CONFLICT_SECTIONS = [
50
+ ['slots', 'UI SLOT COLLISIONS', 'Slot'],
51
+ ['toolNames', 'TOOL NAME COLLISIONS', 'Tool'],
52
+ ['routes', 'ROUTE CONFLICTS', 'Route'],
53
+ ['configKeys', 'CONFIG KEY CONFLICTS', 'Key'],
54
+ ['eventListeners', 'WATERFALL LISTENER ISSUES', 'Event'],
55
+ ];
56
+
57
+ const RULE = '-'.repeat(70);
58
+ const BANNER = '='.repeat(70);
59
+
60
+ function fail(message) {
61
+ console.error('dsh-plugin-analyze: ' + message);
62
+ process.exit(2);
63
+ }
64
+
65
+ function parseArgs(argv) {
66
+ const options = { json: false, exitCode: false, help: false, positional: [] };
67
+ for (let i = 0; i < argv.length; i++) {
68
+ const arg = argv[i];
69
+ if (arg === '-h' || arg === '--help') options.help = true;
70
+ else if (arg === '--json') options.json = true;
71
+ else if (arg === '--exit-code') options.exitCode = true;
72
+ else if (arg === '--') { options.positional.push(...argv.slice(i + 1)); break; }
73
+ else if (arg.startsWith('-') && arg !== '-') fail('unknown option: ' + arg + '\nRun with --help for usage.');
74
+ else options.positional.push(arg);
75
+ }
76
+ return options;
77
+ }
78
+
79
+ function loadTarget(dir) {
80
+ try {
81
+ return loadPackage(dir);
82
+ } catch (error) {
83
+ fail('cannot load ' + JSON.stringify(dir) + ': ' + error.message);
84
+ }
85
+ }
86
+
87
+ function targetOf(pkg, dir) {
88
+ return {
89
+ name: pkg.manifest.name || dir,
90
+ version: pkg.manifest.version,
91
+ path: dir,
92
+ };
93
+ }
94
+
95
+ function analyzeConflicts(dirs) {
96
+ const packages = dirs.map((dir) => {
97
+ const pkg = loadTarget(dir);
98
+ return {
99
+ name: pkg.manifest.name || dir,
100
+ dir,
101
+ manifest: pkg.manifest,
102
+ jsFiles: pkg.jsFiles,
103
+ entries: pkg.entries,
104
+ };
105
+ });
106
+ const conflicts = detectConflicts(packages);
107
+ return {
108
+ targets: packages.map((pkg) => ({ name: pkg.name, path: pkg.dir })),
109
+ ...conflicts,
110
+ };
111
+ }
112
+
113
+ function analyzePerformanceTarget(dir) {
114
+ const pkg = loadTarget(dir);
115
+ return {
116
+ target: targetOf(pkg, dir),
117
+ ...analyzePerformance(pkg),
118
+ };
119
+ }
120
+
121
+ function analyzeSecurityTarget(dir) {
122
+ const pkg = loadTarget(dir);
123
+ return {
124
+ target: targetOf(pkg, dir),
125
+ ...analyzeSecurityDeep(pkg),
126
+ };
127
+ }
128
+
129
+ function analyzeFullTarget(dir) {
130
+ const pkg = loadTarget(dir);
131
+ const lint = lintPackage(dir);
132
+ return {
133
+ schemaVersion: EXPERIMENTAL_SCHEMA_VERSION,
134
+ target: targetOf(pkg, dir),
135
+ lint,
136
+ performance: analyzePerformance(pkg),
137
+ security: analyzeSecurityDeep(pkg),
138
+ experimental: true,
139
+ };
140
+ }
141
+
142
+ function riskIcon(risk) {
143
+ return risk === 'high' ? '🔴' : risk === 'medium' ? '🟡' : '🟢';
144
+ }
145
+
146
+ function printConflicts(report) {
147
+ console.log('\n🔍 Composition conflicts across ' + report.targets.length + ' plugin(s)');
148
+ console.log(' ' + report.targets.map((target) => target.name).join(', ') + '\n');
149
+ console.log(BANNER);
150
+ console.log('COMPOSITION CONFLICT REPORT');
151
+ console.log(BANNER + '\n');
152
+ console.log('Summary: ' + report.summary.total + ' conflict(s) found');
153
+ console.log(' 🔴 High risk: ' + report.summary.high);
154
+ console.log(' 🟡 Medium risk: ' + report.summary.medium);
155
+ console.log(' 🟢 Low risk: ' + report.summary.low + '\n');
156
+ if (report.summary.total === 0) {
157
+ console.log('✅ No conflicts detected. (A static scan resolves literal names only.)\n');
158
+ return;
159
+ }
160
+ for (const [key, heading, label] of CONFLICT_SECTIONS) {
161
+ const entries = report[key] || [];
162
+ if (entries.length === 0) continue;
163
+ console.log('\n' + RULE);
164
+ console.log(heading + ' (' + entries.length + ')');
165
+ console.log(RULE);
166
+ for (const conflict of entries) {
167
+ console.log('\n' + riskIcon(conflict.risk) + ' ' + label + ': ' + conflict.resource + ' (' + conflict.risk + ' risk)');
168
+ console.log(' Plugins: ' + conflict.plugins.join(', '));
169
+ console.log(' Resolution: ' + conflict.resolution);
170
+ for (const claim of conflict.details || []) {
171
+ const where = claim.file ? ' (' + claim.file + ':' + claim.line + ')' : '';
172
+ console.log(' - ' + claim.plugin + where);
173
+ }
174
+ }
175
+ }
176
+ console.log('\n' + BANNER + '\n');
177
+ }
178
+
179
+ function printPerformance(target, metrics) {
180
+ console.log('\n⚡ Performance analysis: ' + target.name + '\n');
181
+ console.log(BANNER);
182
+ console.log('PERFORMANCE REPORT: ' + target.name);
183
+ console.log(BANNER + '\n');
184
+
185
+ const scoreIcon = metrics.score >= 90 ? '🟢'
186
+ : metrics.score >= 75 ? '🟡'
187
+ : metrics.score >= 60 ? '🟠' : '🔴';
188
+ console.log(scoreIcon + ' Overall Score: ' + metrics.score + '/100 (' + metrics.rating + ')\n');
189
+
190
+ console.log(RULE);
191
+ console.log('BUNDLE SIZE');
192
+ console.log(RULE);
193
+ console.log(' Total: ' + metrics.bundleSize.totalMB + ' MB (' + metrics.bundleSize.fileCount + ' files)');
194
+ console.log(' JavaScript: ' + metrics.bundleSize.jsMB + ' MB');
195
+ if (metrics.bundleSize.largest.length > 0) {
196
+ console.log('\n Largest files:');
197
+ for (const file of metrics.bundleSize.largest) {
198
+ console.log(' - ' + file.path + ': ' + file.kb + ' KB');
199
+ }
200
+ }
201
+
202
+ console.log('\n' + RULE);
203
+ console.log('CODE COMPLEXITY');
204
+ console.log(RULE);
205
+ console.log(' Total lines: ' + metrics.complexity.totalLines);
206
+ console.log(' Avg lines/file: ' + metrics.complexity.avgLinesPerFile);
207
+ console.log(' Total functions: ' + metrics.complexity.totalFunctions);
208
+ console.log(' Deep nesting (4+ levels): ' + metrics.complexity.deepNesting + ' file(s)');
209
+ console.log(' Long functions (large bodies): ' + metrics.complexity.longFunctions);
210
+ console.log(' Maintainability: ' + metrics.complexity.maintainability);
211
+ if (metrics.complexity.complexFiles.length > 0) {
212
+ console.log('\n Most complex files:');
213
+ for (const file of metrics.complexity.complexFiles) {
214
+ console.log(' - ' + file.path + ': complexity ' + file.complexity + ', ' + file.lines + ' lines');
215
+ }
216
+ }
217
+
218
+ if (metrics.domImpact.hasClient) {
219
+ console.log('\n' + RULE);
220
+ console.log('DOM IMPACT');
221
+ console.log(RULE);
222
+ if (metrics.domImpact.analyzed) {
223
+ console.log(' ' + riskIcon(metrics.domImpact.risk) + ' Risk level: ' + metrics.domImpact.risk);
224
+ console.log(' DOM mutations: ' + metrics.domImpact.domMutations);
225
+ console.log(' Direct body access: ' + metrics.domImpact.directBodyAccess);
226
+ console.log(' Heavy selectors: ' + metrics.domImpact.heavySelectors);
227
+ } else {
228
+ console.log(' ⚠️ Client entry declared but not analyzed');
229
+ }
230
+ }
231
+
232
+ console.log('\n' + RULE);
233
+ console.log('NETWORK ACTIVITY');
234
+ console.log(RULE);
235
+ console.log(' ' + riskIcon(metrics.networkCalls.risk) + ' Risk level: ' + metrics.networkCalls.risk);
236
+ console.log(' Total network calls: ' + metrics.networkCalls.total);
237
+ console.log(' External domains: ' + metrics.networkCalls.externalDomains.length);
238
+ if (metrics.networkCalls.externalDomains.length > 0) {
239
+ console.log('\n Domains contacted:');
240
+ for (const domain of metrics.networkCalls.externalDomains) {
241
+ console.log(' - ' + domain);
242
+ }
243
+ }
244
+
245
+ if (metrics.heavyDependencies.length > 0) {
246
+ console.log('\n' + RULE);
247
+ console.log('PERFORMANCE-HEAVY PATTERNS');
248
+ console.log(RULE);
249
+ for (const issue of metrics.heavyDependencies) {
250
+ console.log(' ' + riskIcon(issue.severity) + ' ' + issue.type + ' in ' + issue.file);
251
+ }
252
+ }
253
+
254
+ if (metrics.recommendations.length > 0) {
255
+ console.log('\n' + RULE);
256
+ console.log('RECOMMENDATIONS');
257
+ console.log(RULE);
258
+ for (const rec of metrics.recommendations) {
259
+ console.log('\n ' + riskIcon(rec.impact) + ' ' + rec.issue);
260
+ console.log(' → ' + rec.suggestion);
261
+ }
262
+ }
263
+ console.log('\n');
264
+ }
265
+
266
+ function printLint(lint) {
267
+ console.log(BANNER);
268
+ console.log('CONFORMANCE REPORT: ' + (lint.target?.name || 'unknown'));
269
+ console.log(BANNER + '\n');
270
+ console.log(' ' + lint.summary.error + ' error(s) · ' + lint.summary.warn + ' warn(s) · ' + lint.summary.info + ' note(s)');
271
+ if (lint.findings.length === 0) {
272
+ console.log(' No findings.\n');
273
+ return;
274
+ }
275
+ for (const finding of lint.findings) {
276
+ console.log(' [' + finding.level + '] ' + finding.rule + ' ' + finding.file + ':' + finding.line + ' — ' + finding.message);
277
+ }
278
+ console.log('');
279
+ }
280
+
281
+ function printSecurity(target, result) {
282
+ console.log('\n🔐 Security analysis: ' + target.name + '\n');
283
+ console.log(BANNER);
284
+ console.log('SECURITY REPORT: ' + target.name);
285
+ console.log(BANNER + '\n');
286
+ console.log('Risk level: ' + result.riskLevel + ' (' + result.summary.warn + ' warning(s), ' + result.summary.info + ' note(s))\n');
287
+ if (result.findings.length === 0) {
288
+ console.log(' No findings.\n');
289
+ return;
290
+ }
291
+ for (const [category, findings] of Object.entries(result.byCategory || {})) {
292
+ console.log(' ' + category + ':');
293
+ for (const finding of findings) {
294
+ console.log(' [' + finding.rule + ' ' + finding.level + '] ' + finding.title);
295
+ console.log(' ' + finding.file + ':' + finding.line + ' ' + finding.message);
296
+ if (finding.evidence) console.log(' evidence: ' + finding.evidence);
297
+ }
298
+ console.log('');
299
+ }
300
+ console.log(' These are heuristic findings, not a security verdict. Each cites the file and line so it can be checked by hand.\n');
301
+ }
302
+
303
+ function printFull(report) {
304
+ printLint(report.lint);
305
+ console.log(BANNER + '\n');
306
+ printPerformance(report.target, report.performance);
307
+ printSecurity(report.target, report.security);
308
+ console.log('These analyses are experimental static heuristics; the plugin is read, not run.\n');
309
+ console.log('💡 To check composition conflicts with other plugins, run:');
310
+ console.log(' dsh-plugin-analyze conflicts ' + report.target.path + ' <other-plugin-dir>\n');
311
+ }
312
+
313
+ function gateExitCode(command, report, enabled) {
314
+ if (!enabled) return 0;
315
+ if (command === 'conflicts') return report.summary.total > 0 ? 1 : 0;
316
+ if (command === 'performance') return 0;
317
+ // Info findings are an inventory (for example "this plugin calls example.com"),
318
+ // not a problem to gate CI on; only warnings and errors fail the run.
319
+ const securityWarn = () => (report.findings || []).some((f) => f.level === 'warn' || f.level === 'error');
320
+ if (command === 'security') return securityWarn() ? 1 : 0;
321
+ if (command === 'full') {
322
+ const lintErrors = report.lint?.summary?.error ?? 0;
323
+ const fullSecurityWarn = (report.security?.findings || []).some((f) => f.level === 'warn' || f.level === 'error');
324
+ return lintErrors > 0 || fullSecurityWarn ? 1 : 0;
325
+ }
326
+ return 0;
327
+ }
328
+
329
+ const COMMANDS = {
330
+ conflicts: 'conflicts',
331
+ performance: 'performance',
332
+ perf: 'performance',
333
+ security: 'security',
334
+ sec: 'security',
335
+ full: 'full',
336
+ all: 'full',
337
+ };
338
+
339
+ const options = parseArgs(process.argv.slice(2));
340
+ if (options.help) {
341
+ console.log(USAGE);
342
+ process.exit(0);
343
+ }
344
+
345
+ const [rawCommand, ...targets] = options.positional;
346
+ if (rawCommand === undefined) fail('missing command\n\n' + USAGE);
347
+ const command = COMMANDS[rawCommand];
348
+ if (command === undefined) fail('unknown command: ' + rawCommand + '\nCommands: conflicts, performance, security, full');
349
+
350
+ if (command === 'conflicts') {
351
+ if (targets.length < 2) fail('conflicts needs at least two plugin directories (got ' + targets.length + ')');
352
+ } else if (targets.length !== 1) {
353
+ fail(command + ' needs exactly one plugin directory (got ' + targets.length + ')');
354
+ }
355
+
356
+ let report;
357
+ try {
358
+ if (command === 'conflicts') report = analyzeConflicts(targets);
359
+ else if (command === 'performance') report = analyzePerformanceTarget(targets[0]);
360
+ else if (command === 'security') report = analyzeSecurityTarget(targets[0]);
361
+ else report = analyzeFullTarget(targets[0]);
362
+ } catch (error) {
363
+ fail('analysis failed: ' + error.message);
364
+ }
365
+
366
+ if (options.json) {
367
+ console.log(JSON.stringify(report, null, 2));
368
+ } else if (command === 'conflicts') {
369
+ printConflicts(report);
370
+ } else if (command === 'performance') {
371
+ printPerformance(report.target, report);
372
+ } else if (command === 'security') {
373
+ printSecurity(report.target, report);
374
+ } else {
375
+ printFull(report);
376
+ }
377
+
378
+ process.exit(gateExitCode(command, report, options.exitCode));