dsh-background-by-model 0.4.0 → 0.4.2
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 +37 -1
- package/README.zh.md +37 -1
- package/lib/client.js +1795 -96
- package/lib/client.js.map +1 -1
- package/lib/host-scan-BDh0upIQ.js +1160 -0
- package/lib/host-scan.js +2 -0
- package/lib/index.js +26 -1
- package/lib/testing.js +560 -0
- package/package.json +128 -122
- package/scripts/host-contract-scan.mjs +182 -0
- package/test/host-check.test.mjs +296 -0
package/package.json
CHANGED
|
@@ -1,122 +1,128 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur
|
|
5
|
-
"keywords": [
|
|
6
|
-
"deepseek-harness",
|
|
7
|
-
"dsh",
|
|
8
|
-
"plugin",
|
|
9
|
-
"dsh-plugin",
|
|
10
|
-
"theme",
|
|
11
|
-
"wallpaper",
|
|
12
|
-
"web-ui"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/HarmlessFunny/dsh-background-by-model#readme",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/HarmlessFunny/dsh-background-by-model/issues"
|
|
17
|
-
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/HarmlessFunny/dsh-background-by-model.git"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"author": "Tkingxiao",
|
|
24
|
-
"type": "module",
|
|
25
|
-
"engines": {
|
|
26
|
-
"dsh": ">=0.1.
|
|
27
|
-
},
|
|
28
|
-
"exports": {
|
|
29
|
-
".": {
|
|
30
|
-
"default": "./lib/index.js"
|
|
31
|
-
},
|
|
32
|
-
"./invariant": {
|
|
33
|
-
"default": "./lib/invariant.js"
|
|
34
|
-
},
|
|
35
|
-
"./client": {
|
|
36
|
-
"default": "./lib/client.js"
|
|
37
|
-
},
|
|
38
|
-
"./package.json": "./package.json"
|
|
39
|
-
},
|
|
40
|
-
"main": "lib/index.js",
|
|
41
|
-
"directories": {
|
|
42
|
-
"lib": "lib"
|
|
43
|
-
},
|
|
44
|
-
"files": [
|
|
45
|
-
"lib/index.js",
|
|
46
|
-
"lib/invariant.js",
|
|
47
|
-
"lib/client.js",
|
|
48
|
-
"lib/client.js.map",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"@
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"@deepseek-ai/dsh-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"@deepseek-ai/
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
"optional": true
|
|
88
|
-
},
|
|
89
|
-
"@deepseek-ai/
|
|
90
|
-
"optional": true
|
|
91
|
-
},
|
|
92
|
-
"@deepseek-ai/dsh-client-
|
|
93
|
-
"optional": true
|
|
94
|
-
},
|
|
95
|
-
"@deepseek-ai/dsh-client-
|
|
96
|
-
"optional": true
|
|
97
|
-
},
|
|
98
|
-
"@deepseek-ai/dsh-client-
|
|
99
|
-
"optional": true
|
|
100
|
-
},
|
|
101
|
-
"@deepseek-ai/dsh-
|
|
102
|
-
"optional": true
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-background-by-model",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur 鈥?the background follows the model you switch to.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"plugin",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"theme",
|
|
11
|
+
"wallpaper",
|
|
12
|
+
"web-ui"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/HarmlessFunny/dsh-background-by-model#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/HarmlessFunny/dsh-background-by-model/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/HarmlessFunny/dsh-background-by-model.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Tkingxiao",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"dsh": ">=0.1.7-rc.2"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"default": "./lib/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./invariant": {
|
|
33
|
+
"default": "./lib/invariant.js"
|
|
34
|
+
},
|
|
35
|
+
"./client": {
|
|
36
|
+
"default": "./lib/client.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"main": "lib/index.js",
|
|
41
|
+
"directories": {
|
|
42
|
+
"lib": "lib"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"lib/index.js",
|
|
46
|
+
"lib/invariant.js",
|
|
47
|
+
"lib/client.js",
|
|
48
|
+
"lib/client.js.map",
|
|
49
|
+
"lib/host-scan.js",
|
|
50
|
+
"lib/host-scan-*.js",
|
|
51
|
+
"lib/testing.js",
|
|
52
|
+
"scripts/host-contract-scan.mjs",
|
|
53
|
+
"test/host-check.test.mjs",
|
|
54
|
+
"cordis.patch.yml",
|
|
55
|
+
"example_img"
|
|
56
|
+
], "scripts": {
|
|
57
|
+
"bundle": "tsdown",
|
|
58
|
+
"watch": "tsdown --watch",
|
|
59
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
60
|
+
"scan": "node scripts/host-contract-scan.mjs",
|
|
61
|
+
"test": "tsdown && node --test test/host-check.test.mjs"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|
|
65
|
+
"@types/node": "^26.2.0",
|
|
66
|
+
"@types/react": "^19.2.18",
|
|
67
|
+
"@types/react-dom": "^19.2.4",
|
|
68
|
+
"tsdown": "^0.22.2",
|
|
69
|
+
"typescript": "^7.0.2",
|
|
70
|
+
"unrun": "^0.3.1"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6"
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-store": ">=0.1.7-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.7-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-theme": ">=0.1.7-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-invariants": ">=0.1.7-rc.2",
|
|
83
|
+
"react": "^18.2.0"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"react": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@deepseek-ai/cordis": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@deepseek-ai/dsh-client-store": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-settings": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-theme": {
|
|
102
|
+
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
105
|
+
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"@deepseek-ai/dsh-invariants": {
|
|
108
|
+
"optional": true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"dsh": {
|
|
112
|
+
"bundle": {
|
|
113
|
+
"patch": "./cordis.patch.yml"
|
|
114
|
+
},
|
|
115
|
+
"client": {
|
|
116
|
+
"platform": "web",
|
|
117
|
+
"inject": [
|
|
118
|
+
"@deepseek-ai/dsh-client-store",
|
|
119
|
+
"@deepseek-ai/dsh-client-locale",
|
|
120
|
+
"@deepseek-ai/dsh-client-connection",
|
|
121
|
+
"@deepseek-ai/dsh-client-ui-theme",
|
|
122
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
123
|
+
],
|
|
124
|
+
"immediately": true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
|
|
128
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-contract scan — run this after a dsh upgrade.
|
|
3
|
+
*
|
|
4
|
+
* pnpm bundle && node scripts/host-contract-scan.mjs [--host <dir>] [--json]
|
|
5
|
+
*
|
|
6
|
+
* The settings page's "Host check" tab answers the same question against the
|
|
7
|
+
* RUNNING host, from the DOM, the host stylesheets and the Cordis services. This
|
|
8
|
+
* answers it with no browser at all, straight from the installed files — which is
|
|
9
|
+
* what makes it runnable in CI, or right after `npm i -g @deepseek-ai/dsh@latest`
|
|
10
|
+
* and before anyone looks at the interface.
|
|
11
|
+
*
|
|
12
|
+
* It exits non-zero the moment a contract target is gone, so a host rename turns
|
|
13
|
+
* into a red build instead of a support report. `src/host-contracts.ts` is the
|
|
14
|
+
* single source of truth for WHAT is checked — this script only drives it, which
|
|
15
|
+
* is why a newly added contract point cannot be forgotten here.
|
|
16
|
+
*
|
|
17
|
+
* It reads the built `lib/host-scan.js` (a node entry of its own) rather than the
|
|
18
|
+
* TypeScript sources: that is the code that actually ships, and it keeps this
|
|
19
|
+
* script runnable by plain `node` with no flags and no loader.
|
|
20
|
+
*/
|
|
21
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
22
|
+
import { dirname, join, resolve } from 'node:path'
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
const pluginDir = resolve(here, '..')
|
|
27
|
+
|
|
28
|
+
const exists = async (p) => { try { await readFile(p); return true } catch { return false } }
|
|
29
|
+
|
|
30
|
+
const scanPath = join(pluginDir, 'lib', 'host-scan.js')
|
|
31
|
+
if (!(await exists(scanPath))) {
|
|
32
|
+
console.error(`contract scan: ${scanPath} is missing — run "pnpm bundle" first`)
|
|
33
|
+
process.exit(2)
|
|
34
|
+
}
|
|
35
|
+
const { HOST_CONTRACTS, DSH_FLOOR, checkHostOnDisk, resolveHostRoots, meetsFloor, versionOf } =
|
|
36
|
+
await import(pathToFileURL(scanPath).href)
|
|
37
|
+
|
|
38
|
+
const args = process.argv.slice(2)
|
|
39
|
+
const json = args.includes('--json')
|
|
40
|
+
const hostFlag = args.indexOf('--host')
|
|
41
|
+
if (hostFlag >= 0 && args[hostFlag + 1] !== undefined) {
|
|
42
|
+
process.env.DAB_HOST_ROOT = args[hostFlag + 1]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every installation of the host this machine can reach.
|
|
47
|
+
*
|
|
48
|
+
* A contract holds when ANY of them still contains the literal, because the dsh
|
|
49
|
+
* that is running may not be the one a dev checkout resolves first. The roots the
|
|
50
|
+
* plugin itself would pick come first, then the two layouts this project ships
|
|
51
|
+
* against (a global npm install, and a dsh profile's own node_modules).
|
|
52
|
+
*/
|
|
53
|
+
async function hostCandidates() {
|
|
54
|
+
const found = []
|
|
55
|
+
const push = (base, origin) => {
|
|
56
|
+
base = resolve(base)
|
|
57
|
+
if (!found.some(f => f.base === base)) found.push({ base, origin })
|
|
58
|
+
}
|
|
59
|
+
const resolved = await resolveHostRoots(pluginDir)
|
|
60
|
+
if (resolved.roots !== null) push(resolved.roots.packages, 'resolved')
|
|
61
|
+
// Two shipped layouts: a global npm install keeps its own `@deepseek-ai` scope
|
|
62
|
+
// (`dsh` moves to its latest release each time), and the dsh version the profile
|
|
63
|
+
// actually loads lives in that package's nested node_modules.
|
|
64
|
+
const roots = []
|
|
65
|
+
const appData = process.env.APPDATA
|
|
66
|
+
if (typeof appData === 'string' && appData !== '') roots.push(join(appData, 'npm', 'node_modules', '@deepseek-ai'))
|
|
67
|
+
const profileDir = process.env.DSH_PROFILE_DIR
|
|
68
|
+
if (typeof profileDir === 'string' && profileDir !== '') roots.push(join(profileDir, 'node_modules', '@deepseek-ai'))
|
|
69
|
+
const home = process.env.USERPROFILE ?? process.env.HOME
|
|
70
|
+
if (typeof home === 'string' && home !== '') roots.push(join(home, '.dsh', 'profiles'))
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
push(root, 'npm scope')
|
|
73
|
+
push(join(root, 'dsh', 'node_modules', '@deepseek-ai'), 'dsh nested')
|
|
74
|
+
}
|
|
75
|
+
return found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const usable = []
|
|
79
|
+
for (const candidate of await hostCandidates()) {
|
|
80
|
+
// A scope counts only when it holds the UI plugin packages the contracts name.
|
|
81
|
+
// The runtime package itself may be a sibling (`<scope>/dsh`) or absent — on a
|
|
82
|
+
// global npm install the version the profile actually loads sits one level down,
|
|
83
|
+
// in `<scope>/dsh/node_modules/@deepseek-ai`, where `dsh` is NOT repeated — so
|
|
84
|
+
// requiring `<scope>/dsh/package.json` would reject the only real host on this
|
|
85
|
+
// machine and report every contract as missing.
|
|
86
|
+
if (!(await exists(join(candidate.base, 'dsh-client-ui-theme', 'lib', 'client.js')))) continue
|
|
87
|
+
const named = join(candidate.base, 'dsh', 'package.json')
|
|
88
|
+
const nested = join(candidate.base, 'dsh-app-boot', 'package.json')
|
|
89
|
+
const manifest = (await exists(named)) ? named : ((await exists(nested)) ? nested : '')
|
|
90
|
+
// The served frontend is NOT necessarily a sibling of the package scope: on a
|
|
91
|
+
// global install the scope lives inside the `dsh` package, so its web frontend
|
|
92
|
+
// is at `<sourceDir>/dsh-web-frontend`. Prefer wherever that package really is.
|
|
93
|
+
let frontend = join(candidate.base, 'dsh-web-frontend', 'dist', 'assets')
|
|
94
|
+
for (const guess of [join(dirname(candidate.base), 'dsh-web-frontend'), join(candidate.base, 'dsh', 'node_modules', '@deepseek-ai', 'dsh-web-frontend')]) {
|
|
95
|
+
if (await exists(join(guess, 'dist', 'assets'))) { frontend = join(guess, 'dist', 'assets'); break }
|
|
96
|
+
}
|
|
97
|
+
usable.push({ ...candidate, manifest, frontend })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (usable.length === 0) {
|
|
101
|
+
const report = await checkHostOnDisk(pluginDir, 'en')
|
|
102
|
+
console.error('contract scan: no installed dsh was found.')
|
|
103
|
+
if (report.host.note !== '') console.error(` ${report.host.note}`)
|
|
104
|
+
console.error(' pass --host <dir containing @deepseek-ai/dsh-app-boot> or set DAB_HOST_ROOT')
|
|
105
|
+
process.exit(2)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** How one contract's sources fare in one candidate installation. */
|
|
109
|
+
async function checkOne(contract, candidate) {
|
|
110
|
+
const bad = []
|
|
111
|
+
for (const source of contract.sources) {
|
|
112
|
+
const root = source.root === 'frontend' ? candidate.frontend : candidate.base
|
|
113
|
+
// `.` names the root directory: the literal is searched across the script and
|
|
114
|
+
// stylesheet files in it, for targets whose FILE name is not stable.
|
|
115
|
+
if (source.path === '.') {
|
|
116
|
+
let names = []
|
|
117
|
+
try { names = await readdir(root) } catch { /* missing */ }
|
|
118
|
+
let hit = ''
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
if (!/\.(?:js|mjs|cjs|css|html|json)$/i.test(name)) continue
|
|
121
|
+
try {
|
|
122
|
+
if ((await readFile(join(root, name), 'utf8')).includes(source.literal)) { hit = name; break }
|
|
123
|
+
} catch { /* unreadable entry */ }
|
|
124
|
+
}
|
|
125
|
+
if (hit === '') bad.push(`${root} — no ${JSON.stringify(source.literal)} in any asset`)
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
const file = source.path.startsWith('src/') ? join(pluginDir, source.path) : join(root, source.path)
|
|
129
|
+
let text = null
|
|
130
|
+
try { text = await readFile(file, 'utf8') } catch { /* missing */ }
|
|
131
|
+
if (text === null) bad.push(`${source.path} — file missing`)
|
|
132
|
+
else if (!text.includes(source.literal)) bad.push(`${source.path} — no ${JSON.stringify(source.literal)}`)
|
|
133
|
+
}
|
|
134
|
+
return bad
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const rows = []
|
|
138
|
+
for (const contract of HOST_CONTRACTS) {
|
|
139
|
+
let best = null
|
|
140
|
+
for (const candidate of usable) {
|
|
141
|
+
const bad = await checkOne(contract, candidate)
|
|
142
|
+
if (bad.length === 0) { best = { candidate, bad: [] }; break }
|
|
143
|
+
if (best === null || bad.length < best.bad.length) best = { candidate, bad }
|
|
144
|
+
}
|
|
145
|
+
rows.push({
|
|
146
|
+
id: contract.id,
|
|
147
|
+
group: contract.group,
|
|
148
|
+
criticality: contract.criticality,
|
|
149
|
+
target: contract.target,
|
|
150
|
+
ok: best !== null && best.bad.length === 0,
|
|
151
|
+
why: best === null ? ['no host found'] : best.bad,
|
|
152
|
+
where: best === null ? '' : best.candidate.origin,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const hosts = []
|
|
157
|
+
for (const candidate of usable) {
|
|
158
|
+
const version = candidate.manifest === '' ? '' : await versionOf(dirname(candidate.manifest))
|
|
159
|
+
hosts.push({ origin: candidate.origin, path: candidate.base, version, meetsFloor: meetsFloor(version, DSH_FLOOR) })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (json) {
|
|
163
|
+
console.log(JSON.stringify({ floor: DSH_FLOOR, hosts, contracts: rows }, null, 2))
|
|
164
|
+
} else {
|
|
165
|
+
console.log('dsh-background-by-model — host contract scan')
|
|
166
|
+
console.log(`floor: dsh >=${DSH_FLOOR}`)
|
|
167
|
+
for (const h of hosts) {
|
|
168
|
+
const verdict = h.meetsFloor === null ? 'unknown' : h.meetsFloor ? 'ok' : 'BELOW FLOOR'
|
|
169
|
+
console.log(`host : ${h.version !== '' ? h.version : 'unreadable'} [${verdict}] ${h.path}`)
|
|
170
|
+
}
|
|
171
|
+
console.log('')
|
|
172
|
+
let failed = 0
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
if (!row.ok) failed++
|
|
175
|
+
console.log(`${row.ok ? 'PASS' : 'FAIL'} ${row.id.padEnd(30)} ${row.target}`)
|
|
176
|
+
for (const why of row.why) console.log(` > ${why}`)
|
|
177
|
+
}
|
|
178
|
+
console.log('')
|
|
179
|
+
console.log(`${rows.length - failed}/${rows.length} contracts hold`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process.exit(rows.some(r => !r.ok) ? 1 : 0)
|