dsh-plugin-upgrade 0.1.3 → 2.0.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.
- package/AGENTS.md +146 -47
- package/CHANGELOG.md +92 -32
- package/README-es.md +241 -0
- package/README-hi.md +241 -0
- package/README-pt.md +241 -0
- package/README-zh.md +241 -0
- package/README.md +115 -46
- package/SECURITY.md +1 -1
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +6 -6
- package/docs/EVIDENCE.md +354 -0
- package/index.mjs +21 -17
- package/lib/route.mjs +112 -0
- package/lib/scan-0.1.6.mjs +310 -0
- package/lib/scan.mjs +264 -31
- package/package.json +55 -10
- package/scripts/changelog-section.mjs +1 -0
- package/scripts/check-readme-sync.mjs +4 -1
- package/scripts/scan-0.1.5.mjs +20 -6
- package/scripts/sweep-all.mjs +6 -2
- package/scripts/verify-artifacts.mjs +44 -14
- package/scripts/verify-self-contained.mjs +13 -3
- package/skills/plugin-upgrade/SKILL.md +88 -0
- package/skills/plugin-upgrade/references/v0.1.3-alpha.1-to-v0.1.5-rc.1.md +421 -0
- package/skills/plugin-upgrade/references/v0.1.5-rc.2-to-v0.1.6-alpha.2.md +75 -0
- package/skills/plugin-upgrade/scripts/scan-0.1.5.mjs +29 -0
- package/types.d.ts +39 -7
- package/README.es.md +0 -172
- package/README.hi.md +0 -172
- package/README.pt.md +0 -172
- package/README.zh.md +0 -172
- package/skills/plugin-upgrade-015/SKILL.md +0 -38
- package/skills/plugin-upgrade-015/references/v0.1.3-alpha.1-to-v0.1.5-alpha.1.md +0 -90
- package/skills/plugin-upgrade-015/scripts/scan-0.1.5.mjs +0 -14
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* scan-0.1.6.mjs — zero-dependency detector for the DSH `0.1.5-rc.2 →
|
|
5
|
+
* 0.1.6-alpha.2` plugin-adaptation seams.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: this span's breakage is mostly *silent*. Four examples that
|
|
8
|
+
* motivate the whole catalog:
|
|
9
|
+
* - the `settings.plugin.item` slot was deleted with no alias, and
|
|
10
|
+
* `ctx.slots.inject()` only runs its callback when the declaration exists —
|
|
11
|
+
* so a settings card that still targets it stops mounting without an error,
|
|
12
|
+
* a log line or a failed build (`E3`);
|
|
13
|
+
* - `sessions.open`/`openSubagent`/`clear` were removed from `ISessions`
|
|
14
|
+
* (replaced by `retain`/`using`/`retainInfo`), so navigation code dies at
|
|
15
|
+
* click time with a loud TypeError or a swallowed error string (`E4`);
|
|
16
|
+
* - `SessionListState.current` was deleted, so every reader of "the current
|
|
17
|
+
* session" silently degrades (cast-typed readers pass tsc anyway) (`E3`);
|
|
18
|
+
* - the default model catalog shrank 4 → 2 and uncatalogued ids like
|
|
19
|
+
* `deepseek-v4-flash` route as text-only, so a vision tier that names one
|
|
20
|
+
* throws UNSUPPORTED_CONTENT on the first image request (`E5`).
|
|
21
|
+
* A green local gate is therefore NOT evidence of adaptation: the published
|
|
22
|
+
* type line hides the deletions entirely, and the two remaining breakages are
|
|
23
|
+
* runtime races (`E1`, `E2`) that no typecheck can see.
|
|
24
|
+
*
|
|
25
|
+
* This scanner is the CLOSED catalog for the whole `0.1.5-rc.2 → 0.1.6-alpha.2`
|
|
26
|
+
* corridor. It supersedes nothing and shares nothing: the previous span's
|
|
27
|
+
* package (dsh-plugin-upgrade-015) owns its own twenty seams, and a corridor
|
|
28
|
+
* never widens — this package's five seams have zero overlap with that one.
|
|
29
|
+
*
|
|
30
|
+
* Born hardened (the previous scanner's known defects are absorbed here, not
|
|
31
|
+
* back-patched there):
|
|
32
|
+
* - `SKIP_DIRS` does NOT contain `lib`: a repo with a committed build (like
|
|
33
|
+
* the family's `lib/client.js`) must be scanned, because committed build
|
|
34
|
+
* artifacts ship the old seam to the user.
|
|
35
|
+
* - Every line-level behavior (context filter, file filter, downgrade rule)
|
|
36
|
+
* is a per-seam catalog field applied uniformly by one loop — no seam gets
|
|
37
|
+
* a special-case branch — and the render order derives from the catalog
|
|
38
|
+
* itself.
|
|
39
|
+
*
|
|
40
|
+
* The catalog below is the single source of truth shared by the version card,
|
|
41
|
+
* the packaged skill and this CLI. `test/card.test.mjs` fails when the card and
|
|
42
|
+
* this catalog disagree about the seam ids.
|
|
43
|
+
*
|
|
44
|
+
* Usage:
|
|
45
|
+
* node scan-0.1.6.mjs [--repo <path>] [--json <out.json>] [--seams E1,E3] [--quiet]
|
|
46
|
+
*
|
|
47
|
+
* Exit codes: 0 = no error-severity hit, 1 = at least one error-severity hit,
|
|
48
|
+
* 2 = usage/scan failure.
|
|
49
|
+
*
|
|
50
|
+
* Provenance: every upstream fact behind a seam was re-read from the harness
|
|
51
|
+
* checkout at tag `dsh-v0.1.6-alpha.2` (ddefc45) and recorded with `path:line`
|
|
52
|
+
* in `docs/EVIDENCE.md` and in the version card
|
|
53
|
+
* (`skills/plugin-upgrade/references/v0.1.5-rc.2-to-v0.1.6-alpha.2.md`).
|
|
54
|
+
* This scanner ships so a plugin author can re-measure their own repository; it
|
|
55
|
+
* imports nothing outside Node's standard library and never writes inside the
|
|
56
|
+
* scanned tree.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import fs from 'node:fs'
|
|
60
|
+
import path from 'node:path'
|
|
61
|
+
|
|
62
|
+
// Hardened: `lib` is deliberately absent — committed build artifacts must be
|
|
63
|
+
// scanned (a rebuilt `lib/client.js` still carrying the old slot key ships the
|
|
64
|
+
// breakage to every user).
|
|
65
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.tmp', 'coverage', '_scratch', '_archive', 'downloads', 'upstream', 'dev'])
|
|
66
|
+
const SCAN_EXT = /\.(ts|tsx|mts|cts|mjs|cjs|js|jsx|json|yml|yaml)$/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {object} Seam
|
|
70
|
+
* @property {string} id
|
|
71
|
+
* @property {string} title
|
|
72
|
+
* @property {'error'} severity
|
|
73
|
+
* @property {string} action
|
|
74
|
+
* @property {RegExp|null} test null = structured, detected by a dedicated file-level check
|
|
75
|
+
* @property {(line: string) => boolean} [lineFilter]
|
|
76
|
+
* @property {(lines: string[], i: number) => boolean} [windowFilter]
|
|
77
|
+
* @property {(text: string, file: string) => boolean} [fileCheck]
|
|
78
|
+
* @property {(text: string, file: string) => boolean} [downgradeIf] file-level guard recognition
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/** @type {Seam[]} */
|
|
82
|
+
export const SEAMS = [
|
|
83
|
+
// ---------------------------------------------------------------- leg C
|
|
84
|
+
// 0.1.5-rc.2 → 0.1.6-alpha.2
|
|
85
|
+
{
|
|
86
|
+
id: 'E1',
|
|
87
|
+
title: 'agent/created 监听器抛错或异步拖延会阻断 agent 创建(A1 串行链)',
|
|
88
|
+
severity: 'error',
|
|
89
|
+
action: '宿主在 agent 创建链上串行派发 `agent/created`:监听器抛错直接阻断创建,异步工作拖延首次消息。把监听器改写成「绝不抛错 + 快速返回」:同步工作包 try/catch,重工作 queueMicrotask/setImmediate 或投递到自己的队列,副作用失败只记日志。',
|
|
90
|
+
test: /agent\/created/,
|
|
91
|
+
// Registration contexts only: `.on(`, `.oneline(`, `addListener(`, listener maps.
|
|
92
|
+
windowFilter: (lines, i) => {
|
|
93
|
+
const win = lines.slice(Math.max(0, i - 2), i + 1).join('\n')
|
|
94
|
+
return /\.on\s*\(|\.oneline\s*\(|addListener\s*\(|listener\s*[:=]\s*\{/.test(win)
|
|
95
|
+
},
|
|
96
|
+
// A file that guards its listener (try/catch, .catch, deferred work) is
|
|
97
|
+
// adapted in spirit: report as a review lead instead of a blocker.
|
|
98
|
+
downgradeIf: text => /try\s*\{|\.catch\s*\(|queueMicrotask|setImmediate|\.push\s*\(/.test(text),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'E2',
|
|
102
|
+
title: '异步 apply 竞态:首个 await 之后才注册 ⇒ 卸载窗口内 INACTIVE_EFFECT(A02 全类)',
|
|
103
|
+
severity: 'error',
|
|
104
|
+
action: '`export async function apply(ctx)` 里,一切注册(ctx.effect/ctx.on/ctx.provide/ctx.plugin/*.register)必须在第一个 `await` 之前完成;`register()` 的返回值必须交给 `ctx.effect()` 持有。首个 await 之后的注册在卸载窗口内必抛 INACTIVE_EFFECT,旧闭包继续生效。',
|
|
105
|
+
test: null, // structured detector: checkAsyncApply
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'E3',
|
|
109
|
+
title: '被删的槽/状态键:settings.plugin.item 与 SessionListState.current(静默消失)',
|
|
110
|
+
severity: 'error',
|
|
111
|
+
action: '`settings.plugin.item` 已删(新契约 `plugins.item`,keyed→list,条目带 `id`/`order`/`label`、props `{view:\'summary\'|\'page\'}`,不传 priority);`SessionListState.current` 已删——「当前会话」从槽标准 props `sessionId`/`useSessionStatus`/`retainedBy.mainView` 推导(上游 ui-session 是模式),保留 undefined 守卫。两者都是静默失效:inject 回调不执行、cast 结构面 tsc 抓不到。',
|
|
112
|
+
test: /settings\.plugin\.item|(?:list|snapshot|sessions\.list)\.current\b|getSnapshot\s*\(\s*\)\s*\.current/,
|
|
113
|
+
// Slot/session consumption contexts only, never prose about the migration.
|
|
114
|
+
lineFilter: line => /slots\.inject|register\s*\(\s*\{|name\s*:|sessions|snapshot|current|SessionListState|list\b/.test(line),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'E4',
|
|
118
|
+
title: '被删的客户端 API:sessions.open / openSubagent / clear(点击路径 TypeError/吞错)',
|
|
119
|
+
severity: 'error',
|
|
120
|
+
action: '`ISessions` 删除了 `open`/`openSubagent`/`clear`,替代面是 `retain(id, { source })`(返回 Disposable,面板持有时存下、卸载时 dispose)/`using`/`retainInfo`。跳转代码改走 retain;特性探测(typeof 检查 + 回退)是保留旧 peer 带期间的合法写法。',
|
|
121
|
+
test: /sessions\.(open|openSubagent|clear)\b/,
|
|
122
|
+
// A file that also names the new surface (or probes before calling) is
|
|
123
|
+
// handling the transition on purpose: report as a review lead.
|
|
124
|
+
downgradeIf: text => /sessions\.retain|sessions\.using|typeof\s+\w*\.?\s*sessions|feature[-\s]?detect|hasOwnProperty\(['"]open['"]\)/i.test(text),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'E5',
|
|
128
|
+
title: '被删的模型字面量:deepseek-v4-flash* / deepseek-v4-vision-exp(未编目 id 透传 text-only)',
|
|
129
|
+
severity: 'error',
|
|
130
|
+
action: '默认模型目录 4→2,`deepseek-v4-flash*` 与 `deepseek-v4-vision-exp` 已不在目录:未编目 id 透传为 text-only 路由,带图请求(vision 档)响亮抛 UNSUPPORTED_CONTENT、cheap 档静默降级。把配置默认值与测试硬编码换成仍在册的模型 id,README 同步。',
|
|
131
|
+
test: /['"](deepseek-v4-flash[\w.-]*|deepseek-v4-vision-exp[\w.-]*)['"]|(?:default|model)\s*:\s*(deepseek-v4-flash[\w.-]*|deepseek-v4-vision-exp[\w.-]*)\s*$/,
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
/** Seam ids in card order; the version card must name exactly this set. */
|
|
136
|
+
export const SEAM_IDS = SEAMS.map(s => s.id)
|
|
137
|
+
|
|
138
|
+
/** Seams implemented structurally (not by regex), like `E2`. */
|
|
139
|
+
const STRUCTURED = new Set(['E2'])
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Seams that are documented on the card and id-parity checked, but deliberately
|
|
143
|
+
* have no automatic detection. This corridor has none: every seam ships a
|
|
144
|
+
* detector.
|
|
145
|
+
*/
|
|
146
|
+
export const CARD_ONLY = SEAMS.filter(s => s.test === null && !STRUCTURED.has(s.id)).map(s => s.id)
|
|
147
|
+
|
|
148
|
+
function* walk(dir, depth = 0) {
|
|
149
|
+
if (depth > 8) return
|
|
150
|
+
let ents
|
|
151
|
+
try { ents = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
152
|
+
for (const e of ents) {
|
|
153
|
+
if (e.isDirectory()) {
|
|
154
|
+
if (SKIP_DIRS.has(e.name)) continue
|
|
155
|
+
yield* walk(path.join(dir, e.name), depth + 1)
|
|
156
|
+
} else if (SCAN_EXT.test(e.name)) {
|
|
157
|
+
yield path.join(dir, e.name)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 1-based line number of the first line containing `needle`, or 1. */
|
|
163
|
+
function lineOf(text, needle) {
|
|
164
|
+
const lines = text.split(/\r?\n/)
|
|
165
|
+
const i = lines.findIndex(l => l.includes(needle))
|
|
166
|
+
return i < 0 ? 1 : i + 1
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* E2 — the async-apply race, structured. Locate `export async function apply`,
|
|
171
|
+
* walk its body with brace counting, find the first `await`, and flag every
|
|
172
|
+
* registration call after it. `register()` return values are flagged regardless
|
|
173
|
+
* of whether they reach `ctx.effect()`: the card instructs the author to hand
|
|
174
|
+
* them to the effect.
|
|
175
|
+
*/
|
|
176
|
+
function checkAsyncApply(text, file) {
|
|
177
|
+
const lines = text.split(/\r?\n/)
|
|
178
|
+
const hits = []
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
if (!/export\s+async\s+function\s+apply\b/.test(lines[i])) continue
|
|
181
|
+
// First line from the signature onward that carries a brace. The signature
|
|
182
|
+
// line itself may hold a `config = {}` default: counting starts there and
|
|
183
|
+
// only a close to depth 0 on a LATER line ends the body.
|
|
184
|
+
let j = i
|
|
185
|
+
while (j < lines.length && !lines[j].includes('{')) j++
|
|
186
|
+
if (j >= lines.length) continue
|
|
187
|
+
let depth = 0
|
|
188
|
+
let firstAwait = -1
|
|
189
|
+
let bodyLine = 0
|
|
190
|
+
for (let k = j; k < lines.length; k++) {
|
|
191
|
+
const line = lines[k]
|
|
192
|
+
let inString = null
|
|
193
|
+
for (let c = 0; c < line.length; c++) {
|
|
194
|
+
const ch = line[c]
|
|
195
|
+
if (inString) {
|
|
196
|
+
if (ch === inString && line[c - 1] !== '\\') inString = null
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
if (ch === '"' || ch === "'" || ch === '`') { inString = ch; continue }
|
|
200
|
+
if (ch === '{') depth++
|
|
201
|
+
if (ch === '}') { depth--; if (depth === 0 && k > j) { bodyLine = k; break } }
|
|
202
|
+
}
|
|
203
|
+
if (bodyLine) break
|
|
204
|
+
if (firstAwait < 0 && k > j && /\bawait\s+/.test(line)) firstAwait = k
|
|
205
|
+
}
|
|
206
|
+
if (bodyLine === 0 || firstAwait < 0) continue
|
|
207
|
+
for (let k = firstAwait + 1; k <= bodyLine; k++) {
|
|
208
|
+
const line = lines[k]
|
|
209
|
+
if (/^\s*(?:\/\/|\/\*|\*|#)/.test(line.trim())) continue
|
|
210
|
+
if (/ctx\.(effect|on|provide|plugin)\s*\(|\b[a-zA-Z_$][\w.$]*\.register\s*\(/.test(line)) {
|
|
211
|
+
hits.push({
|
|
212
|
+
seam: 'E2', severity: 'error', file, line: k + 1,
|
|
213
|
+
snippet: line.trim().slice(0, 200),
|
|
214
|
+
detail: `registration after the first await (line ${firstAwait + 1}) of an async apply → INACTIVE_EFFECT in the unload window; move it before any await and hand register() results to ctx.effect()`,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
i = bodyLine
|
|
219
|
+
}
|
|
220
|
+
return hits
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Scan one repo.
|
|
225
|
+
* @param {string} repoDir
|
|
226
|
+
* @param {{ seams?: string[] }} [options]
|
|
227
|
+
* @returns {{ repo: string, scannedAt: string, files: number, hits: any[], bySeam: Record<string, number> }}
|
|
228
|
+
*/
|
|
229
|
+
export function scanRepo(repoDir, options = {}) {
|
|
230
|
+
const wanted = options.seams && options.seams.length ? new Set(options.seams) : null
|
|
231
|
+
const hits = []
|
|
232
|
+
let files = 0
|
|
233
|
+
for (const file of walk(repoDir)) {
|
|
234
|
+
files++
|
|
235
|
+
let text
|
|
236
|
+
try { text = fs.readFileSync(file, 'utf8') } catch { continue }
|
|
237
|
+
const lines = text.split(/\r?\n/)
|
|
238
|
+
for (const seam of SEAMS) {
|
|
239
|
+
if (wanted && !wanted.has(seam.id)) continue
|
|
240
|
+
if (seam.test === null) continue // structured seam, handled below
|
|
241
|
+
if (STRUCTURED.has(seam.id)) continue
|
|
242
|
+
if (seam.fileCheck && !seam.fileCheck(text, file)) continue
|
|
243
|
+
const downgraded = seam.downgradeIf ? seam.downgradeIf(text) : false
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
const trimmed = lines[i].trim()
|
|
246
|
+
// Comment-only lines carry prose, not code: never a seam hit.
|
|
247
|
+
if (/^(?:\/\/|\/\*|\*|#)/.test(trimmed)) continue
|
|
248
|
+
if (!seam.test.test(lines[i])) continue
|
|
249
|
+
if (seam.lineFilter && !seam.lineFilter(lines[i])) continue
|
|
250
|
+
if (seam.windowFilter && !seam.windowFilter(lines, i)) continue
|
|
251
|
+
hits.push({
|
|
252
|
+
seam: seam.id, severity: downgraded ? 'warn' : seam.severity, file, line: i + 1,
|
|
253
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
254
|
+
detail: downgraded ? `${seam.title} (guarded/transitional form — verify it is intentional)` : seam.title,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!wanted || wanted.has('E2')) hits.push(...checkAsyncApply(text, file))
|
|
259
|
+
}
|
|
260
|
+
const bySeam = {}
|
|
261
|
+
for (const h of hits) bySeam[h.seam] = (bySeam[h.seam] || 0) + 1
|
|
262
|
+
return { repo: repoDir, scannedAt: new Date().toISOString(), files, hits, bySeam }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Human-readable rendering. Order derives from the catalog, never a copy. */
|
|
266
|
+
export function render(report) {
|
|
267
|
+
const L = []
|
|
268
|
+
L.push(`# scan-0.1.6 · ${report.repo}`)
|
|
269
|
+
L.push(`files scanned: ${report.files} · hits: ${report.hits.length}`)
|
|
270
|
+
if (!report.hits.length) {
|
|
271
|
+
L.push('no seam hits — still verify with a real-host smoke AND a real browser assertion for the client half')
|
|
272
|
+
L.push('(this scanner is necessary, not sufficient: the breakage this corridor covers is silent)')
|
|
273
|
+
}
|
|
274
|
+
for (const id of SEAM_IDS) {
|
|
275
|
+
const group = report.hits.filter(h => h.seam === id)
|
|
276
|
+
if (!group.length) continue
|
|
277
|
+
const seam = SEAMS.find(s => s.id === id)
|
|
278
|
+
L.push('')
|
|
279
|
+
L.push(`## ${id} [${seam.severity}] ${seam.title} — ${group.length} hit(s)`)
|
|
280
|
+
L.push(` action: ${seam.action}`)
|
|
281
|
+
for (const h of group.slice(0, 12)) L.push(` ${path.relative(process.cwd(), h.file)}:${h.line} ${h.snippet}`)
|
|
282
|
+
if (group.length > 12) L.push(` ... ${group.length - 12} more`)
|
|
283
|
+
}
|
|
284
|
+
L.push('')
|
|
285
|
+
L.push('every seam in this corridor ships a detector; CARD_ONLY = []')
|
|
286
|
+
return L.join('\n')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function main(argv) {
|
|
290
|
+
const args = { repo: process.cwd(), json: null, seams: null, quiet: false }
|
|
291
|
+
for (let i = 0; i < argv.length; i++) {
|
|
292
|
+
const a = argv[i]
|
|
293
|
+
if (a === '--repo') args.repo = argv[++i]
|
|
294
|
+
else if (a === '--json') args.json = argv[++i]
|
|
295
|
+
else if (a === '--seams') args.seams = String(argv[++i]).split(',').map(s => s.trim()).filter(Boolean)
|
|
296
|
+
else if (a === '--quiet') args.quiet = true
|
|
297
|
+
else if (a === '--help' || a === '-h') { console.log('usage: node scan-0.1.6.mjs [--repo <path>] [--json <out.json>] [--seams E1,E3] [--quiet]'); return 0 }
|
|
298
|
+
else { console.error(`unknown argument: ${a}`); return 2 }
|
|
299
|
+
}
|
|
300
|
+
const repoDir = path.resolve(args.repo)
|
|
301
|
+
if (!fs.existsSync(repoDir)) { console.error(`repo not found: ${repoDir}`); return 2 }
|
|
302
|
+
const report = scanRepo(repoDir, { seams: args.seams })
|
|
303
|
+
if (!args.quiet) console.log(render(report))
|
|
304
|
+
if (args.json) fs.writeFileSync(path.resolve(args.json), JSON.stringify(report, null, 1), 'utf8')
|
|
305
|
+
return report.hits.some(h => h.severity === 'error') ? 1 : 0
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (process.argv[1]?.endsWith('scan-0.1.6.mjs')) {
|
|
309
|
+
process.exit(main(process.argv.slice(2)))
|
|
310
|
+
}
|