@owdproject/cli 0.0.3 → 0.1.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/LICENSE +20 -0
- package/README.md +50 -16
- package/bin/bridge.js +127 -0
- package/bin/cli.js +433 -0
- package/bin/cp.js +3523 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +501 -0
- package/bin/lib/config.js +130 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/desktopConfig.js +8 -0
- package/bin/lib/generateTemplate.js +426 -0
- package/bin/lib/install.js +525 -0
- package/bin/lib/installWizardFlow.js +726 -0
- package/bin/lib/migrateModulePlayground.config.mjs +187 -0
- package/bin/lib/migrateModulePlayground.mjs +318 -0
- package/bin/lib/npmVersions.js +124 -0
- package/bin/lib/packageSources.js +365 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +317 -0
- package/bin/lib/validateModule.js +868 -0
- package/bin/lib/workspace.js +222 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +200 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +26 -0
- package/go/go.sum +37 -0
- package/go/main.go +30 -0
- package/go/tui/tui.go +3014 -0
- package/package.json +33 -22
- package/src/commands/create.js +0 -11
- package/src/commands/generate.js +0 -12
- package/src/commands/installApp.js +0 -9
- package/src/commands/installTheme.js +0 -9
- package/src/commands/start.js +0 -13
- package/src/utils/cloneRepo.js +0 -25
- package/src/utils/installPackage.js +0 -15
- package/src/utils/logging.js +0 -15
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import {
|
|
17
|
+
fetchLatestVersions,
|
|
18
|
+
isOwdScopedPackage,
|
|
19
|
+
resolveLatestVersions,
|
|
20
|
+
} from './npmVersions.js'
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const BLUEPRINT_ROOT = resolve(__dirname, '../../template-blueprint')
|
|
24
|
+
|
|
25
|
+
export const STARTER_DESKTOP_DEPS = [
|
|
26
|
+
'@owdproject/core',
|
|
27
|
+
'@owdproject/theme-nova',
|
|
28
|
+
'@owdproject/app-about',
|
|
29
|
+
'h3',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/** Used when a starter package is not on npm yet (before first publish). */
|
|
33
|
+
export const TEMPLATE_NPM_FALLBACK_VERSIONS = {
|
|
34
|
+
'@owdproject/core': '3.4.0',
|
|
35
|
+
'@owdproject/theme-nova': '0.0.1',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DESKTOP_COPY_FILES = [
|
|
39
|
+
'tsconfig.json',
|
|
40
|
+
'tsconfig.app.json',
|
|
41
|
+
'tsconfig.spec.json',
|
|
42
|
+
'.npmrc',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const ROOT_COPY_FROM_MONOREPO = [
|
|
46
|
+
'nx.json',
|
|
47
|
+
'tsconfig.base.json',
|
|
48
|
+
'tsconfig.json',
|
|
49
|
+
'.editorconfig',
|
|
50
|
+
'.npmrc',
|
|
51
|
+
'.prettierrc',
|
|
52
|
+
'.prettierignore',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const DESKTOP_SKIP_NAMES = new Set([
|
|
56
|
+
'node_modules',
|
|
57
|
+
'.nuxt',
|
|
58
|
+
'.output',
|
|
59
|
+
'dist',
|
|
60
|
+
'.env',
|
|
61
|
+
'.data',
|
|
62
|
+
'vitest.config.ts',
|
|
63
|
+
'tsconfig.tsbuildinfo',
|
|
64
|
+
'package.json',
|
|
65
|
+
'desktop.config.ts',
|
|
66
|
+
'nuxt.config.ts',
|
|
67
|
+
'app',
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
const BLUEPRINT_SKIP_NAMES = new Set(['root-package.json'])
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} blueprintRoot
|
|
74
|
+
*/
|
|
75
|
+
export function resolveBlueprintRoot(blueprintRoot = BLUEPRINT_ROOT) {
|
|
76
|
+
if (!existsSync(join(blueprintRoot, 'pnpm-workspace.yaml'))) {
|
|
77
|
+
throw new Error(`Template blueprint not found at ${blueprintRoot}`)
|
|
78
|
+
}
|
|
79
|
+
return blueprintRoot
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} src
|
|
84
|
+
* @param {string} dest
|
|
85
|
+
* @param {Set<string>} [skipNames]
|
|
86
|
+
*/
|
|
87
|
+
function copyTree(src, dest, skipNames = new Set()) {
|
|
88
|
+
if (!existsSync(src)) return
|
|
89
|
+
const stat = statSync(src)
|
|
90
|
+
if (stat.isFile()) {
|
|
91
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
92
|
+
cpSync(src, dest)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mkdirSync(dest, { recursive: true })
|
|
97
|
+
for (const name of readdirSync(src)) {
|
|
98
|
+
if (skipNames.has(name)) continue
|
|
99
|
+
copyTree(join(src, name), join(dest, name), skipNames)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} workspaceRoot
|
|
105
|
+
*/
|
|
106
|
+
function readMonorepoPackageJson(workspaceRoot) {
|
|
107
|
+
const path = join(workspaceRoot, 'package.json')
|
|
108
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveCatalogVersion(workspaceRoot, name, specifier) {
|
|
112
|
+
if (specifier !== 'catalog:') return specifier
|
|
113
|
+
|
|
114
|
+
const yamlPath = join(workspaceRoot, 'pnpm-workspace.yaml')
|
|
115
|
+
if (!existsSync(yamlPath)) return specifier
|
|
116
|
+
|
|
117
|
+
const content = readFileSync(yamlPath, 'utf8')
|
|
118
|
+
const lines = content.split('\n')
|
|
119
|
+
let insideCatalog = false
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const trimmed = line.trim()
|
|
122
|
+
if (trimmed === 'catalog:') {
|
|
123
|
+
insideCatalog = true
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (insideCatalog) {
|
|
127
|
+
if (line.match(/^\s+/)) {
|
|
128
|
+
const match = trimmed.match(/^([^:]+):\s*(.+)$/)
|
|
129
|
+
if (match && match[1].trim() === name) {
|
|
130
|
+
return match[2].trim()
|
|
131
|
+
}
|
|
132
|
+
} else if (trimmed) {
|
|
133
|
+
insideCatalog = false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return specifier
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} workspaceRoot
|
|
143
|
+
* @param {{ useCache?: boolean }} [options]
|
|
144
|
+
*/
|
|
145
|
+
export async function resolveTemplateVersions(workspaceRoot, options = {}) {
|
|
146
|
+
const monorepo = readMonorepoPackageJson(workspaceRoot)
|
|
147
|
+
const monorepoDevDeps = monorepo.devDependencies ?? {}
|
|
148
|
+
|
|
149
|
+
const owdPackages = [
|
|
150
|
+
...new Set([
|
|
151
|
+
...STARTER_DESKTOP_DEPS,
|
|
152
|
+
...Object.keys(monorepoDevDeps).filter(isOwdScopedPackage),
|
|
153
|
+
]),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
const npmLatest = await resolveLatestVersions(workspaceRoot, owdPackages, options)
|
|
157
|
+
|
|
158
|
+
/** @type {Record<string, string>} */
|
|
159
|
+
const versions = { ...monorepoDevDeps, ...npmLatest }
|
|
160
|
+
for (const name of Object.keys(versions)) {
|
|
161
|
+
if (versions[name] === 'catalog:') {
|
|
162
|
+
versions[name] = resolveCatalogVersion(workspaceRoot, name, versions[name])
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const name of STARTER_DESKTOP_DEPS) {
|
|
167
|
+
versions[name] =
|
|
168
|
+
npmLatest[name] ?? TEMPLATE_NPM_FALLBACK_VERSIONS[name] ?? monorepoDevDeps[name]
|
|
169
|
+
if (!versions[name]) {
|
|
170
|
+
throw new Error(`Could not resolve version for ${name}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return versions
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {Record<string, string>} versions
|
|
179
|
+
*/
|
|
180
|
+
function buildTemplateRootPackageJson(versions) {
|
|
181
|
+
const base = JSON.parse(readFileSync(join(BLUEPRINT_ROOT, 'root-package.json'), 'utf8'))
|
|
182
|
+
/** @type {Record<string, string>} */
|
|
183
|
+
const devDependencies = {}
|
|
184
|
+
for (const [name, range] of Object.entries(versions)) {
|
|
185
|
+
if (name === 'h3') continue
|
|
186
|
+
devDependencies[name] = range
|
|
187
|
+
}
|
|
188
|
+
base.devDependencies = Object.fromEntries(
|
|
189
|
+
Object.entries(devDependencies).sort(([a], [b]) => a.localeCompare(b)),
|
|
190
|
+
)
|
|
191
|
+
return `${JSON.stringify(base, null, 2)}\n`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {Record<string, string>} versions
|
|
196
|
+
*/
|
|
197
|
+
function buildTemplateDesktopPackageJson(versions) {
|
|
198
|
+
/** @type {Record<string, string>} */
|
|
199
|
+
const dependencies = {}
|
|
200
|
+
for (const name of STARTER_DESKTOP_DEPS) {
|
|
201
|
+
dependencies[name] = versions[name]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const pkg = {
|
|
205
|
+
name: '@owdproject/client',
|
|
206
|
+
private: true,
|
|
207
|
+
nx: { name: 'desktop' },
|
|
208
|
+
scripts: {
|
|
209
|
+
build: 'nuxt generate',
|
|
210
|
+
dev: 'nuxt dev --host',
|
|
211
|
+
generate: 'nuxt generate --dev',
|
|
212
|
+
postinstall: 'nuxt prepare',
|
|
213
|
+
preview: 'nuxt preview',
|
|
214
|
+
},
|
|
215
|
+
dependencies: Object.fromEntries(
|
|
216
|
+
Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)),
|
|
217
|
+
),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return `${JSON.stringify(pkg, null, 2)}\n`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} srcPath
|
|
225
|
+
*/
|
|
226
|
+
function readProjectJson(srcPath) {
|
|
227
|
+
const raw = readFileSync(srcPath, 'utf8')
|
|
228
|
+
const data = JSON.parse(raw)
|
|
229
|
+
if (data.name === 'desktopx') data.name = 'desktop'
|
|
230
|
+
return `${JSON.stringify(data, null, 2)}\n`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {string} workspaceRoot
|
|
235
|
+
* @param {string} outputRoot
|
|
236
|
+
* @param {{ versions: Record<string, string> }} ctx
|
|
237
|
+
*/
|
|
238
|
+
function writeGeneratedFiles(workspaceRoot, outputRoot, ctx) {
|
|
239
|
+
const { versions } = ctx
|
|
240
|
+
const desktopSrc = join(workspaceRoot, 'desktop')
|
|
241
|
+
const desktopOut = join(outputRoot, 'desktop')
|
|
242
|
+
const blueprint = resolveBlueprintRoot()
|
|
243
|
+
|
|
244
|
+
mkdirSync(outputRoot, { recursive: true })
|
|
245
|
+
|
|
246
|
+
copyTree(blueprint, outputRoot, BLUEPRINT_SKIP_NAMES)
|
|
247
|
+
|
|
248
|
+
writeFileSync(join(outputRoot, 'package.json'), buildTemplateRootPackageJson(versions))
|
|
249
|
+
|
|
250
|
+
mkdirSync(desktopOut, { recursive: true })
|
|
251
|
+
for (const file of DESKTOP_COPY_FILES) {
|
|
252
|
+
const src = join(desktopSrc, file)
|
|
253
|
+
if (existsSync(src)) {
|
|
254
|
+
cpSync(src, join(desktopOut, file))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const i18nBlueprint = join(blueprint, 'desktop/i18n')
|
|
259
|
+
if (existsSync(i18nBlueprint)) {
|
|
260
|
+
copyTree(i18nBlueprint, join(desktopOut, 'i18n'))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const projectJsonSrc = join(desktopSrc, 'project.json')
|
|
264
|
+
if (existsSync(projectJsonSrc)) {
|
|
265
|
+
writeFileSync(join(desktopOut, 'project.json'), readProjectJson(projectJsonSrc))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const file of ROOT_COPY_FROM_MONOREPO) {
|
|
269
|
+
const src = join(workspaceRoot, file)
|
|
270
|
+
if (existsSync(src)) {
|
|
271
|
+
cpSync(src, join(outputRoot, file))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
writeFileSync(join(desktopOut, 'package.json'), buildTemplateDesktopPackageJson(versions))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {string} dir
|
|
280
|
+
* @returns {string[]}
|
|
281
|
+
*/
|
|
282
|
+
function listFilesRecursive(dir) {
|
|
283
|
+
if (!existsSync(dir)) return []
|
|
284
|
+
/** @type {string[]} */
|
|
285
|
+
const files = []
|
|
286
|
+
for (const name of readdirSync(dir)) {
|
|
287
|
+
const path = join(dir, name)
|
|
288
|
+
const stat = statSync(path)
|
|
289
|
+
if (stat.isDirectory()) {
|
|
290
|
+
if (name === 'node_modules' || name === '.git') continue
|
|
291
|
+
files.push(...listFilesRecursive(path))
|
|
292
|
+
} else {
|
|
293
|
+
files.push(path)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return files
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} aRoot
|
|
301
|
+
* @param {string} bRoot
|
|
302
|
+
*/
|
|
303
|
+
export function diffTemplateTrees(aRoot, bRoot) {
|
|
304
|
+
const aFiles = new Set(listFilesRecursive(aRoot).map((p) => relative(aRoot, p)))
|
|
305
|
+
const bFiles = new Set(listFilesRecursive(bRoot).map((p) => relative(bRoot, p)))
|
|
306
|
+
/** @type {{ path: string, type: 'missing' | 'extra' | 'changed' }[]} */
|
|
307
|
+
const diffs = []
|
|
308
|
+
|
|
309
|
+
for (const rel of aFiles) {
|
|
310
|
+
if (!bFiles.has(rel)) {
|
|
311
|
+
diffs.push({ path: rel, type: 'missing' })
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
const a = readFileSync(join(aRoot, rel))
|
|
315
|
+
const b = readFileSync(join(bRoot, rel))
|
|
316
|
+
if (!a.equals(b)) diffs.push({ path: rel, type: 'changed' })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const rel of bFiles) {
|
|
320
|
+
if (!aFiles.has(rel)) diffs.push({ path: rel, type: 'extra' })
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return diffs.sort((x, y) => x.path.localeCompare(y.path))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {string} workspaceRoot
|
|
328
|
+
* @param {{ dryRun?: boolean, check?: boolean, useCache?: boolean }} [options]
|
|
329
|
+
*/
|
|
330
|
+
export async function generateTemplate(workspaceRoot, options = {}) {
|
|
331
|
+
const { dryRun = false, check = false, useCache = true } = options
|
|
332
|
+
const templateOut = join(workspaceRoot, 'template')
|
|
333
|
+
const desktopPath = join(workspaceRoot, 'desktop')
|
|
334
|
+
|
|
335
|
+
if (!existsSync(desktopPath)) {
|
|
336
|
+
throw new Error(`Monorepo desktop/ not found at ${desktopPath}`)
|
|
337
|
+
}
|
|
338
|
+
resolveBlueprintRoot()
|
|
339
|
+
|
|
340
|
+
const versions = await resolveTemplateVersions(workspaceRoot, { useCache })
|
|
341
|
+
|
|
342
|
+
if (dryRun) {
|
|
343
|
+
console.log('\nTemplate generation plan (dry run)\n')
|
|
344
|
+
console.log('Output:', templateOut)
|
|
345
|
+
console.log('\nStarter desktop dependencies:')
|
|
346
|
+
for (const name of STARTER_DESKTOP_DEPS) {
|
|
347
|
+
console.log(` ${name} ${versions[name]}`)
|
|
348
|
+
}
|
|
349
|
+
console.log('\nRoot devDependencies (@owdproject/* from npm):')
|
|
350
|
+
for (const [name, range] of Object.entries(versions).sort(([a], [b]) => a.localeCompare(b))) {
|
|
351
|
+
if (isOwdScopedPackage(name)) console.log(` ${name} ${range}`)
|
|
352
|
+
}
|
|
353
|
+
console.log('\nSources:')
|
|
354
|
+
console.log(` blueprint ${BLUEPRINT_ROOT}`)
|
|
355
|
+
console.log(` desktop/ ${desktopPath}`)
|
|
356
|
+
console.log(` monorepo ${workspaceRoot}`)
|
|
357
|
+
console.log('')
|
|
358
|
+
return { versions, outputRoot: templateOut }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const outputRoot = check ? mkdtempSync(join(tmpdir(), 'owd-template-check-')) : templateOut
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
if (!check && existsSync(templateOut)) {
|
|
365
|
+
rmSync(templateOut, { recursive: true, force: true })
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
writeGeneratedFiles(workspaceRoot, outputRoot, { versions })
|
|
369
|
+
|
|
370
|
+
if (check) {
|
|
371
|
+
const diffs = diffTemplateTrees(templateOut, outputRoot)
|
|
372
|
+
if (diffs.length === 0) {
|
|
373
|
+
console.log('\n✓ template/ is up to date\n')
|
|
374
|
+
return { ok: true, versions }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.error('\n✗ template/ is out of date\n')
|
|
378
|
+
for (const d of diffs.slice(0, 40)) {
|
|
379
|
+
const label =
|
|
380
|
+
d.type === 'missing'
|
|
381
|
+
? 'only in committed template'
|
|
382
|
+
: d.type === 'extra'
|
|
383
|
+
? 'missing from committed template'
|
|
384
|
+
: 'changed'
|
|
385
|
+
console.error(` ${label}: ${d.path}`)
|
|
386
|
+
}
|
|
387
|
+
if (diffs.length > 40) {
|
|
388
|
+
console.error(` … and ${diffs.length - 40} more`)
|
|
389
|
+
}
|
|
390
|
+
console.error('\nRun: pnpm desktop template\n')
|
|
391
|
+
return { ok: false, versions, diffs }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log('\n✓ Generated template/\n')
|
|
395
|
+
console.log('Starter packages:')
|
|
396
|
+
for (const name of STARTER_DESKTOP_DEPS) {
|
|
397
|
+
console.log(` ${name} ${versions[name]}`)
|
|
398
|
+
}
|
|
399
|
+
console.log('\nCommit template/ when ready.\n')
|
|
400
|
+
return { ok: true, versions, outputRoot }
|
|
401
|
+
} finally {
|
|
402
|
+
if (check && existsSync(outputRoot)) {
|
|
403
|
+
rmSync(outputRoot, { recursive: true, force: true })
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @param {{ workspaceRoot: string, dryRun?: boolean, check?: boolean }} options
|
|
410
|
+
*/
|
|
411
|
+
export async function runGenerateTemplateCli(options) {
|
|
412
|
+
const { workspaceRoot, dryRun = false, check = false } = options
|
|
413
|
+
|
|
414
|
+
if (!workspaceRoot) {
|
|
415
|
+
console.error('\n✗ Not inside an OWD workspace.\n')
|
|
416
|
+
process.exit(1)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const result = await generateTemplate(workspaceRoot, { dryRun, check })
|
|
421
|
+
if (check && result.ok === false) process.exit(1)
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error(`\n✗ ${error.message ?? String(error)}\n`)
|
|
424
|
+
process.exit(1)
|
|
425
|
+
}
|
|
426
|
+
}
|