@apinggithub/mydsh-auto-model-router 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 +6 -4
- package/README.zh.md +31 -5
- package/config.mjs +8 -8
- package/examples/README.md +6 -1
- package/index.mjs +26 -15
- package/package.json +3 -1
- package/scripts/check-patch.mjs +186 -0
package/README.md
CHANGED
|
@@ -63,10 +63,12 @@ LLM dispatch
|
|
|
63
63
|
| `L2` | writing code, review, refactoring | strong |
|
|
64
64
|
| `L3` | complex bug fixes, multi-step tasks, reports | most powerful |
|
|
65
65
|
|
|
66
|
-
Each level is a user-assigned provider/model route with
|
|
67
|
-
`reasoningEffort
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
Each level is a user-assigned provider/model route with an **optional**
|
|
67
|
+
`reasoningEffort`: omitting it (or writing `null`) sends no effort at all —
|
|
68
|
+
required for models without a thinking mode (GLM/Qwen flash), which the host
|
|
69
|
+
otherwise rejects; `off` explicitly disables thinking; `high`/`max` set its
|
|
70
|
+
intensity. Levels may be left unconfigured; the router then falls to the nearest
|
|
71
|
+
configured level (direction depends on the cost
|
|
70
72
|
mode).
|
|
71
73
|
|
|
72
74
|
## Auto mode in the model selector
|
package/README.zh.md
CHANGED
|
@@ -52,9 +52,10 @@ LLM 分发
|
|
|
52
52
|
| `L2` | 写代码、审查、重构 | 较强 |
|
|
53
53
|
| `L3` | 复杂 BUG 修复、多步任务、汇报 | 最强 |
|
|
54
54
|
|
|
55
|
-
每个层级是一个用户自行指定的 provider/model
|
|
56
|
-
`reasoningEffort
|
|
57
|
-
|
|
55
|
+
每个层级是一个用户自行指定的 provider/model 路由,**可选**附带
|
|
56
|
+
`reasoningEffort`:不写(或写 `null`)= 不发送该字段——GLM/Qwen flash 这类没有
|
|
57
|
+
思考模式的模型**必须**这样,否则宿主会拒绝请求;`off` = 显式关闭思考;`high`/`max`
|
|
58
|
+
= 思考强度。层级可以留空不配置;路由会落到最近的已配置层级(方向由成本模式决定)。
|
|
58
59
|
|
|
59
60
|
## 模型选择框中的 Auto 模式
|
|
60
61
|
|
|
@@ -389,14 +390,39 @@ contextGuard: { enabled: true, headroom: 0.85 }
|
|
|
389
390
|
|
|
390
391
|
```powershell
|
|
391
392
|
# 1. 从 npm 装(推荐;peer 范围已放宽为宿主自适应,不需要 allow-version 豁免)
|
|
392
|
-
dsh plugin --profile desktop add '@apinggithub/mydsh-auto-model-router@0.3.
|
|
393
|
+
dsh plugin --profile desktop add '@apinggithub/mydsh-auto-model-router@0.3.3'
|
|
393
394
|
|
|
394
395
|
# 2. 可选:按档位拆账
|
|
395
396
|
dsh plugin --profile desktop add '@apinggithub/dsh-tier-cost@0.1.0'
|
|
396
397
|
|
|
397
|
-
# 3.
|
|
398
|
+
# 3. 改完 cordis.patch.yml 后先校验(见下),再重启
|
|
399
|
+
node scripts/check-patch.mjs --profile desktop
|
|
400
|
+
|
|
401
|
+
# 4. 重启 DSH(bundle 只在启动时组合;配置改动用热加载即可)
|
|
398
402
|
```
|
|
399
403
|
|
|
404
|
+
### 改配置前必读:先校验,再重启
|
|
405
|
+
|
|
406
|
+
`cordis.patch.yml` 里一个少的换行就能让宿主**启动失败**:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
DesktopHostFatalError: failed to parse overlay … YAMLException: bad indentation (454:44)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
更麻烦的是崩溃对话框里有 **"Disable third-party plugins"** 按钮——点它会把所有第三方
|
|
413
|
+
bundle 从 profile 里摘掉,插件(含 Auto 入口)随之消失,需要手工恢复。
|
|
414
|
+
|
|
415
|
+
所以每次改完都跑一次(会用宿主自己的解析器校验,并清理临时 profile):
|
|
416
|
+
|
|
417
|
+
```powershell
|
|
418
|
+
node scripts/check-patch.mjs --profile desktop
|
|
419
|
+
# PASS profile "desktop" composes (194 top-level entries)
|
|
420
|
+
# FAIL … 会打印宿主的原始错误
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
参数:`--home <DSH_HOME>`、`--profile <name>`、`--file <patch>`(只做静态检查)、`--keep`(保留临时 profile 便于排查)。
|
|
424
|
+
|
|
425
|
+
|
|
400
426
|
也可以从源码装:`git clone …` 后 `dsh plugin --profile desktop add ./mydsh-auto-model-router`。
|
|
401
427
|
|
|
402
428
|
### 每台机器必须自带档位块
|
package/config.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// fallbackChain: [route, ...] (model-level, appended with the default)
|
|
23
23
|
// maxRetries: per-route retry budget on request-error
|
|
24
24
|
|
|
25
|
-
export const PLUGIN_VERSION = '0.3.
|
|
25
|
+
export const PLUGIN_VERSION = '0.3.4'
|
|
26
26
|
|
|
27
27
|
export const LEVEL_IDS = Object.freeze(['L0', 'L1', 'L2', 'L3'])
|
|
28
28
|
export const COST_MODES = Object.freeze(['cost-first', 'quality-first', 'balanced'])
|
|
@@ -382,13 +382,14 @@ function normalizeLevels(levels) {
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
function normalizeLevelRoute(route, where) {
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
385
|
+
// A level states a reasoning effort only when the config states one.
|
|
386
|
+
//
|
|
387
|
+
// This used to default to 'off', which broke twice over: providers whose
|
|
388
|
+
// models have no thinking mode (GLM/Qwen flash) reject *any* effort with
|
|
389
|
+
// UNSUPPORTED_REASONING_EFFORT, and `apply()` normalises the config a second
|
|
390
|
+
// time, so an omitted field came back as 'off' even when the config had said
|
|
391
|
+
// `null`. Absent now stays absent, which makes normalisation idempotent.
|
|
390
392
|
const normalized = normalizeRoute(route, where)
|
|
391
|
-
// Every other level carries an explicit effort; absent → 'off' (no thinking).
|
|
392
393
|
const contextWindow = route.contextWindow === undefined || route.contextWindow === null
|
|
393
394
|
? undefined
|
|
394
395
|
: Number(route.contextWindow)
|
|
@@ -397,7 +398,6 @@ function normalizeLevelRoute(route, where) {
|
|
|
397
398
|
}
|
|
398
399
|
return {
|
|
399
400
|
...normalized,
|
|
400
|
-
...omitEffort ? {} : { reasoningEffort: normalized.reasoningEffort ?? 'off' },
|
|
401
401
|
// A level may declare that its route is a free-quota route. The flag rides
|
|
402
402
|
// along with the route so the router can prefer/demote it.
|
|
403
403
|
...route.freeQuota === true ? { freeQuota: true } : {},
|
package/examples/README.md
CHANGED
|
@@ -26,7 +26,12 @@ dsh plugin --profile desktop add '@apinggithub/mydsh-auto-model-router@0.3.1'
|
|
|
26
26
|
# 2. append the levels block to the profile patch
|
|
27
27
|
notepad $env:DSH_HOME\profiles\desktop\cordis.patch.yml
|
|
28
28
|
|
|
29
|
-
# 3.
|
|
29
|
+
# 3. ALWAYS validate it before restarting — a YAML typo here stops DSH from
|
|
30
|
+
# starting, and the crash dialog's "disable third-party plugins" button then
|
|
31
|
+
# strips every bundle (the router disappears from the model picker)
|
|
32
|
+
node scripts/check-patch.mjs --profile desktop
|
|
33
|
+
|
|
34
|
+
# 4. restart DSH once (bundles compose at boot; later edits hot-reload)
|
|
30
35
|
```
|
|
31
36
|
|
|
32
37
|
Check what the plugin sees at any time:
|
package/index.mjs
CHANGED
|
@@ -294,8 +294,29 @@ export function apply(ctx, input = {}) {
|
|
|
294
294
|
}
|
|
295
295
|
})
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Rewrite a host proposal onto one routed model.
|
|
299
|
+
*
|
|
300
|
+
* The incoming proposal still carries the *session's* reasoning effort — the
|
|
301
|
+
* host's model-selection listener re-applies it on every step — and that
|
|
302
|
+
* value belongs to the virtual `auto` model, not to the model we route to.
|
|
303
|
+
* Spreading it through sends a thinking parameter to a model that has no
|
|
304
|
+
* thinking mode, and the host rejects the request with
|
|
305
|
+
* `provider "zhipu" model "glm-4.7-flash" does not support reasoning effort "off"`.
|
|
306
|
+
* So the inherited effort is dropped unless the level itself declares one.
|
|
307
|
+
*
|
|
308
|
+
* @param {object} proposed - config from the host's own waterfall
|
|
309
|
+
* @param {object} route - the level's route
|
|
310
|
+
* @returns {object} the rewritten config
|
|
311
|
+
*/
|
|
312
|
+
const withRoute = (proposed, route) => {
|
|
313
|
+
const next = { ...proposed, provider: route.provider, model: route.model }
|
|
314
|
+
delete next.reasoningEffort
|
|
315
|
+
if (route.reasoningEffort !== undefined) next.reasoningEffort = route.reasoningEffort
|
|
316
|
+
return next
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── ★ agent/request: reuse the per-input decision ────────────────────── // Only a NEW user input triggers a fresh decision (and only when the
|
|
299
320
|
// session picked the virtual auto provider). Steps within the same input
|
|
300
321
|
// reuse state.current — no per-step re-judging, no per-step classifier.
|
|
301
322
|
ctx.on('agent/request', async (payload, next) => {
|
|
@@ -333,15 +354,13 @@ export function apply(ctx, input = {}) {
|
|
|
333
354
|
}
|
|
334
355
|
// fallback: user dismissed → stay in heuristic mode.
|
|
335
356
|
const route = router.current(sessionId)
|
|
336
|
-
return
|
|
337
|
-
...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort } }
|
|
357
|
+
return withRoute(proposed, route)
|
|
338
358
|
}
|
|
339
359
|
if (decision.needsAsk === 'fixed-reelect') {
|
|
340
360
|
const picked = await askFixedLevel(ctx, agentOf(payload), router, sessionId)
|
|
341
361
|
if (picked) router.setMode(sessionId, 'fixed', { fixedLevel: picked })
|
|
342
362
|
const route = router.current(sessionId)
|
|
343
|
-
return
|
|
344
|
-
...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort } }
|
|
363
|
+
return withRoute(proposed, route)
|
|
345
364
|
}
|
|
346
365
|
}
|
|
347
366
|
|
|
@@ -363,15 +382,7 @@ export function apply(ctx, input = {}) {
|
|
|
363
382
|
if (demotion && config.freeQuota.notify) {
|
|
364
383
|
await notifyFreeQuotaFallback(agentOf(payload), sessionId, demotion, resolveLocale(ctx))
|
|
365
384
|
}
|
|
366
|
-
return
|
|
367
|
-
...proposed,
|
|
368
|
-
provider: route.provider,
|
|
369
|
-
model: route.model,
|
|
370
|
-
// A level that omits `reasoningEffort` (configured as `null`) has no
|
|
371
|
-
// thinking mode: sending an effort to a model without reasoning
|
|
372
|
-
// metadata would be rejected with UNSUPPORTED_REASONING_EFFORT.
|
|
373
|
-
...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort },
|
|
374
|
-
}
|
|
385
|
+
return withRoute(proposed, route)
|
|
375
386
|
} catch (error) {
|
|
376
387
|
// The router must never be able to fail a user request: hand the host's
|
|
377
388
|
// own proposal back untouched and surface the reason once.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apinggithub/mydsh-auto-model-router",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Hybrid model router for DeepSeek Harness: declarative rules, user-configurable cost control (cost-first / quality-first / balanced), optional LLM task classifier, and failure fallback chains.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"client.js",
|
|
27
27
|
"cordis.patch.yml",
|
|
28
28
|
"examples",
|
|
29
|
+
"scripts",
|
|
29
30
|
"README.md",
|
|
30
31
|
"README.zh.md"
|
|
31
32
|
],
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"scripts": {
|
|
42
43
|
"test": "node --test *.test.mjs",
|
|
43
44
|
"build:client": "node build-client.mjs",
|
|
45
|
+
"check:patch": "node scripts/check-patch.mjs",
|
|
44
46
|
"publish:all": "npm publish --access public && cd tier-cost && npm publish --access public"
|
|
45
47
|
},
|
|
46
48
|
"peerDependencies": {
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Validate a profile's cordis.patch.yml with the host's own parser.
|
|
3
|
+
//
|
|
4
|
+
// This exists because a single missing newline in the patch (a `- { … }` entry
|
|
5
|
+
// glued to the next key) makes DSH fail at startup with
|
|
6
|
+
// DesktopHostFatalError: failed to parse overlay … YAMLException
|
|
7
|
+
// and the desktop app offers to "disable third-party plugins", which strips
|
|
8
|
+
// every bundle. Catching it here costs two seconds.
|
|
9
|
+
//
|
|
10
|
+
// The check is authoritative rather than re-implemented: it copies the profile
|
|
11
|
+
// into a throw-away sibling profile and runs `dsh --dump-config`, so the real
|
|
12
|
+
// loader parses the real overlay with the real merge rules.
|
|
13
|
+
//
|
|
14
|
+
// Usage:
|
|
15
|
+
// node scripts/check-patch.mjs # profile "desktop", $DSH_HOME
|
|
16
|
+
// node scripts/check-patch.mjs --profile web
|
|
17
|
+
// node scripts/check-patch.mjs --home E:\dsh --profile desktop
|
|
18
|
+
// node scripts/check-patch.mjs --file path\to\cordis.patch.yml # syntax only
|
|
19
|
+
//
|
|
20
|
+
// Exit code 0 = the host parsed and composed the profile; 1 = it would not boot.
|
|
21
|
+
import { execFileSync } from 'node:child_process'
|
|
22
|
+
import {
|
|
23
|
+
copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, unlinkSync,
|
|
24
|
+
} from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import { join, resolve } from 'node:path'
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const out = { profile: 'desktop', home: undefined, file: undefined, keep: false }
|
|
30
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
+
const arg = argv[i]
|
|
32
|
+
if (arg === '--profile') out.profile = argv[++i]
|
|
33
|
+
else if (arg === '--home') out.home = argv[++i]
|
|
34
|
+
else if (arg === '--file') out.file = argv[++i]
|
|
35
|
+
else if (arg === '--keep') out.keep = true
|
|
36
|
+
else if (arg === '--help' || arg === '-h') out.help = true
|
|
37
|
+
else throw new Error(`unknown argument: ${arg}`)
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Cheap structural checks that catch the mistakes people actually make. */
|
|
43
|
+
function staticChecks(text) {
|
|
44
|
+
const problems = []
|
|
45
|
+
if (text.trim() === '') {
|
|
46
|
+
problems.push('the file is empty (an empty overlay must still be `[]`)')
|
|
47
|
+
return problems
|
|
48
|
+
}
|
|
49
|
+
if (text.includes('\t')) problems.push('contains a TAB character; YAML forbids tabs for indentation')
|
|
50
|
+
if (text.charCodeAt(0) === 0xfeff) problems.push('starts with a UTF-8 BOM; write it without one')
|
|
51
|
+
|
|
52
|
+
const lines = text.split(/\r?\n/)
|
|
53
|
+
lines.forEach((line, index) => {
|
|
54
|
+
// A `}` followed by more content on the same line is the classic
|
|
55
|
+
// "missing newline between a flow mapping and the next key" breakage.
|
|
56
|
+
if (/}[ \t]+[A-Za-z_][\w-]*:/.test(line)) {
|
|
57
|
+
problems.push(`line ${index + 1}: a flow mapping is glued to the next key — missing newline`)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
return problems
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Windows needs a junction for node_modules; POSIX can use a symlink. */
|
|
64
|
+
function linkModules(target, linkPath) {
|
|
65
|
+
if (process.platform === 'win32') {
|
|
66
|
+
execFileSync('cmd', ['/c', 'mklink', '/J', linkPath, target], { stdio: 'pipe' })
|
|
67
|
+
} else {
|
|
68
|
+
execFileSync('ln', ['-s', target, linkPath], { stdio: 'pipe' })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function unlinkModules(linkPath) {
|
|
73
|
+
if (!existsSync(linkPath)) return
|
|
74
|
+
const stat = lstatSync(linkPath)
|
|
75
|
+
if (process.platform === 'win32' && stat.isSymbolicLink()) {
|
|
76
|
+
execFileSync('cmd', ['/c', 'rmdir', linkPath], { stdio: 'pipe' })
|
|
77
|
+
} else if (stat.isSymbolicLink()) {
|
|
78
|
+
unlinkSync(linkPath)
|
|
79
|
+
} else {
|
|
80
|
+
rmSync(linkPath, { recursive: true, force: true })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function main() {
|
|
85
|
+
const args = parseArgs(process.argv.slice(2))
|
|
86
|
+
if (args.help) {
|
|
87
|
+
console.log(readFileSync(new URL(import.meta.url), 'utf8').split('\n').slice(1, 22).join('\n').replace(/^\/\/ ?/gm, ''))
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const home = args.home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
92
|
+
|
|
93
|
+
if (args.file !== undefined) {
|
|
94
|
+
const patchPath = resolve(args.file)
|
|
95
|
+
if (!existsSync(patchPath)) {
|
|
96
|
+
console.error(`FAIL ${patchPath} does not exist`)
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
const problems = staticChecks(readFileSync(patchPath, 'utf8'))
|
|
100
|
+
if (problems.length > 0) {
|
|
101
|
+
console.error(`FAIL ${patchPath}`)
|
|
102
|
+
for (const problem of problems) console.error(` - ${problem}`)
|
|
103
|
+
return 1
|
|
104
|
+
}
|
|
105
|
+
console.log(`PASS ${patchPath} (static checks only; run without --file to compose the profile)`)
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const profileDir = join(home, 'profiles', args.profile)
|
|
110
|
+
const patchPath = join(profileDir, 'cordis.patch.yml')
|
|
111
|
+
if (!existsSync(patchPath)) {
|
|
112
|
+
console.error(`FAIL no profile patch at ${patchPath}`)
|
|
113
|
+
return 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const problems = staticChecks(readFileSync(patchPath, 'utf8'))
|
|
117
|
+
if (problems.length > 0) {
|
|
118
|
+
console.error(`FAIL static checks on ${patchPath}`)
|
|
119
|
+
for (const problem of problems) console.error(` - ${problem}`)
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const scratchName = `${args.profile}-check-${process.pid}`
|
|
124
|
+
const scratchDir = join(home, 'profiles', scratchName)
|
|
125
|
+
const modulesLink = join(scratchDir, 'node_modules')
|
|
126
|
+
const realModules = join(profileDir, 'node_modules')
|
|
127
|
+
|
|
128
|
+
const cleanup = () => {
|
|
129
|
+
if (args.keep) {
|
|
130
|
+
console.log(`kept ${scratchDir}`)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
try { unlinkModules(modulesLink) } catch { /* already gone */ }
|
|
134
|
+
try { rmSync(scratchDir, { recursive: true, force: true }) } catch { /* best effort */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(scratchDir, { recursive: true })
|
|
139
|
+
for (const name of ['package.json', 'cordis.patch.yml', 'cordis.yml']) {
|
|
140
|
+
const from = join(profileDir, name)
|
|
141
|
+
if (existsSync(from)) copyFileSync(from, join(scratchDir, name))
|
|
142
|
+
}
|
|
143
|
+
if (existsSync(realModules)) linkModules(realModules, modulesLink)
|
|
144
|
+
|
|
145
|
+
let output = ''
|
|
146
|
+
let ok = true
|
|
147
|
+
try {
|
|
148
|
+
// `dsh` is a .cmd shim on Windows, which execFileSync cannot spawn
|
|
149
|
+
// directly: go through cmd /c there.
|
|
150
|
+
const command = process.platform === 'win32' ? 'cmd' : 'dsh'
|
|
151
|
+
const argv = process.platform === 'win32'
|
|
152
|
+
? ['/c', 'dsh', '--profile', scratchName, '--dump-config']
|
|
153
|
+
: ['--profile', scratchName, '--dump-config']
|
|
154
|
+
output = execFileSync(command, argv, {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
157
|
+
env: { ...process.env, DSH_HOME: home },
|
|
158
|
+
timeout: 180_000,
|
|
159
|
+
})
|
|
160
|
+
} catch (error) {
|
|
161
|
+
ok = false
|
|
162
|
+
output = `${error.stdout ?? ''}${error.stderr ?? ''}` || String(error.message ?? error)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!ok) {
|
|
166
|
+
console.error(`FAIL the host cannot compose profile "${args.profile}"`)
|
|
167
|
+
const detail = output.split('\n').filter((line) => line.trim() !== '').slice(-25).join('\n')
|
|
168
|
+
console.error(detail)
|
|
169
|
+
return 1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const entries = (output.match(/^- id: /gm) ?? []).length
|
|
173
|
+
console.log(`PASS profile "${args.profile}" composes (${entries} top-level entries)`)
|
|
174
|
+
console.log(` ${patchPath}`)
|
|
175
|
+
return 0
|
|
176
|
+
} finally {
|
|
177
|
+
cleanup()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
process.exitCode = main()
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(`FAIL ${error?.message ?? error}`)
|
|
185
|
+
process.exitCode = 1
|
|
186
|
+
}
|