@twinsearth/fang-dsh-plugin 0.0.0-stage → 3.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/CLA.md +79 -0
- package/LICENSE-COMMERCIAL +78 -0
- package/LICENSE-CORE +661 -0
- package/LICENSE-SDK +21 -0
- package/LICENSES/cordis-MIT.txt +39 -0
- package/NOTICE +95 -0
- package/README.md +158 -2
- package/client.js +7287 -0
- package/cordis.patch.yml +163 -0
- package/defaults/umbrella-rules.yaml +113 -0
- package/docs/CONTRACT-INTEROP.md +77 -0
- package/docs/CRASH-SAFETY.md +69 -0
- package/docs/DECISION-RELAY.md +46 -0
- package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
- package/docs/FORK-SEED.md +64 -0
- package/docs/MAIN-CITY-GATE.md +58 -0
- package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
- package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
- package/docs/PM-DECISION-POLICY.md +61 -0
- package/docs/WORKFLOW-UI.md +86 -0
- package/icon.svg +8 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -4
- package/sdk/README.md +65 -0
- package/sdk/examples/status-badge/README.md +16 -0
- package/sdk/examples/status-badge/client.js +60 -0
- package/sdk/examples/status-badge/cordis.patch.yml +7 -0
- package/sdk/examples/status-badge/index.js +12 -0
- package/sdk/examples/status-badge/package.json +24 -0
- package/sdk/templates/plugin/README.md +36 -0
- package/sdk/templates/plugin/client.js +35 -0
- package/sdk/templates/plugin/cordis.patch.yml +8 -0
- package/sdk/templates/plugin/icon.svg +5 -0
- package/sdk/templates/plugin/index.js +23 -0
- package/sdk/templates/plugin/locale/en.json +6 -0
- package/sdk/templates/plugin/package.json +36 -0
- package/src/compose.mjs +67 -0
- package/src/contract/VENDOR.json +16 -0
- package/src/contract/client.js +93 -0
- package/src/contract/constants.js +94 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +20 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/relay.js +223 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.mjs +11 -0
- package/src/kernel/core.mjs +100 -0
- package/src/kernel/extensions.mjs +61 -0
- package/src/kernel/fsx.mjs +349 -0
- package/src/kernel/index.mjs +21 -0
- package/src/kernel/ledger.mjs +94 -0
- package/src/kernel/paths.mjs +182 -0
- package/src/kernel/role.mjs +68 -0
- package/src/kernel/rounds.mjs +172 -0
- package/src/kernel/schema.mjs +5 -0
- package/src/kernel/types.mjs +266 -0
- package/src/kernel/vendor/cosmokit.mjs +421 -0
- package/src/kernel/vendor/schemastery.mjs +638 -0
- package/src/lib/adapter-client.mjs +383 -0
- package/src/lib/adapter-protocol.mjs +243 -0
- package/src/lib/ai-secret-store.mjs +202 -0
- package/src/lib/boot-guard.mjs +186 -0
- package/src/lib/branch-store.mjs +84 -0
- package/src/lib/branch.mjs +74 -0
- package/src/lib/bridge-client.mjs +133 -0
- package/src/lib/broker-adapter.mjs +67 -0
- package/src/lib/broker.mjs +45 -0
- package/src/lib/business-producers.mjs +109 -0
- package/src/lib/command-cache.mjs +209 -0
- package/src/lib/compat.mjs +170 -0
- package/src/lib/composer-journal.mjs +128 -0
- package/src/lib/container/index.mjs +806 -0
- package/src/lib/decision-events.mjs +129 -0
- package/src/lib/decision-ingest-client.mjs +41 -0
- package/src/lib/decision-outbox.mjs +129 -0
- package/src/lib/decision-policy.mjs +52 -0
- package/src/lib/default-essence.mjs +61 -0
- package/src/lib/desktop-launch.mjs +138 -0
- package/src/lib/essence.mjs +1211 -0
- package/src/lib/fork-client.mjs +410 -0
- package/src/lib/fork-config-lock.mjs +166 -0
- package/src/lib/fork-seed.mjs +304 -0
- package/src/lib/fork-sessions.mjs +183 -0
- package/src/lib/fork-supervision-settings.mjs +58 -0
- package/src/lib/fork-supervisor.mjs +122 -0
- package/src/lib/fs-guard.mjs +28 -0
- package/src/lib/http-auth.mjs +162 -0
- package/src/lib/idle-audit.mjs +178 -0
- package/src/lib/interaction-poke.mjs +61 -0
- package/src/lib/main-gate.mjs +579 -0
- package/src/lib/mirror-etag.mjs +92 -0
- package/src/lib/mirror-official-source.mjs +93 -0
- package/src/lib/mirror.mjs +478 -0
- package/src/lib/mother-link.mjs +156 -0
- package/src/lib/mutex.mjs +31 -0
- package/src/lib/permissions.mjs +115 -0
- package/src/lib/pnpm-store.mjs +189 -0
- package/src/lib/preferences.mjs +212 -0
- package/src/lib/producer-contract.mjs +162 -0
- package/src/lib/reflow.mjs +41 -0
- package/src/lib/remote.mjs +408 -0
- package/src/lib/restart-contract.mjs +752 -0
- package/src/lib/roots.mjs +30 -0
- package/src/lib/rule-registry.mjs +1127 -0
- package/src/lib/runtime-capture.mjs +307 -0
- package/src/lib/semaphore.mjs +34 -0
- package/src/lib/session-bundle.mjs +138 -0
- package/src/lib/session-format.mjs +672 -0
- package/src/lib/sse-parser.mjs +102 -0
- package/src/lib/ssrf-guard.mjs +170 -0
- package/src/lib/status-store.mjs +257 -0
- package/src/lib/task-handle.mjs +622 -0
- package/src/lib/ui-actions.mjs +55 -0
- package/src/lib/umbrella.mjs +1661 -0
- package/src/lib/upgrade-coordinator.mjs +107 -0
- package/src/lib/upgrade-status.mjs +43 -0
- package/src/plugins/adapter.mjs +1468 -0
- package/src/plugins/approver.mjs +1210 -0
- package/src/plugins/auditor.mjs +330 -0
- package/src/plugins/buffer-keeper.mjs +522 -0
- package/src/plugins/decision-center.mjs +2581 -0
- package/src/plugins/forker.mjs +1610 -0
- package/src/plugins/healer.mjs +702 -0
- package/src/plugins/hot-reloader.mjs +274 -0
- package/src/plugins/imports.mjs +247 -0
- package/src/plugins/orchestrator.mjs +3938 -0
- package/src/plugins/restart-guard.mjs +1022 -0
- package/src/plugins/router.mjs +714 -0
- package/src/plugins/scorer.mjs +661 -0
- package/src/plugins/session-bridge.mjs +166 -0
- package/src/plugins/session-catalog.mjs +771 -0
- package/src/plugins/session-router.mjs +125 -0
- package/src/plugins/session-sync.mjs +171 -0
- package/src/plugins/umbrella.mjs +419 -0
- package/tools/boot-guard.mjs +95 -0
- package/tools/decision-ingest.mjs +53 -0
- package/tools/desktop-launch.mjs +71 -0
- package/tools/fangctl.mjs +408 -0
- package/tools/gate/main-city.mjs +204 -0
- package/tools/mcp/server.mjs +121 -0
- package/tools/mcp/tools.mjs +65 -0
- package/tools/recover-queue.mjs +133 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2026 dsh-Fang contributors — see LICENSE-SDK.
|
|
4
|
+
// fangctl — dsh-Fang 离线运维 CLI。
|
|
5
|
+
// node tools/fangctl.mjs status [--profile <name>] [--data-root <dir>]
|
|
6
|
+
// node tools/fangctl.mjs forks
|
|
7
|
+
// node tools/fangctl.mjs verify # 校验审计哈希链
|
|
8
|
+
// node tools/fangctl.mjs tail [n] # 最近 n 条审计
|
|
9
|
+
// node tools/fangctl.mjs doctor # 环境自检(不修改任何东西)
|
|
10
|
+
// node tools/fangctl.mjs restart-status # 只读:重启守卫状态 + 授权清单摘要(--json 可机读;--url 读运行中宿主)
|
|
11
|
+
// node tools/fangctl.mjs approvals [--json] --url <host> [--token <t>]
|
|
12
|
+
// node tools/fangctl.mjs approve <source:id> --url <host> [--token <t>] [--note text]
|
|
13
|
+
// node tools/fangctl.mjs deny <source:id> --url <host> [--token <t>] [--note text]
|
|
14
|
+
//
|
|
15
|
+
// 离线只读:fork/reap/promote 由宿主内的 orchestrator 服务与 /fang 路由完成,
|
|
16
|
+
// 这里不模拟它们,避免"CLI 说成功、宿主没执行"的假象。
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
resolveFangLayout, resolveProfileDir, resolveDshHome, parseWebUrl,
|
|
22
|
+
exists, readJson, readJsonl, isLink, verifyChain, FANG_VERSION,
|
|
23
|
+
} from '../src/kernel/index.mjs'
|
|
24
|
+
import { readRestartStatus, clearCrashLoopSafeMode } from '../src/lib/restart-contract.mjs'
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const args = { _: [] }
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i]
|
|
30
|
+
if (a === '--profile' && argv[i + 1]) { args.profile = argv[++i]; continue }
|
|
31
|
+
if (a.startsWith('--profile=')) { args.profile = a.slice(10); continue }
|
|
32
|
+
if (a === '--data-root' && argv[i + 1]) { args.dataRoot = argv[++i]; continue }
|
|
33
|
+
if (a.startsWith('--data-root=')) { args.dataRoot = a.slice(12); continue }
|
|
34
|
+
if (a === '--url' && argv[i + 1]) { args.url = argv[++i]; continue }
|
|
35
|
+
if (a.startsWith('--url=')) { args.url = a.slice(6); continue }
|
|
36
|
+
if (a === '--token' && argv[i + 1]) { args.token = argv[++i]; continue }
|
|
37
|
+
if (a.startsWith('--token=')) { args.token = a.slice(8); continue }
|
|
38
|
+
if (a === '--note' && argv[i + 1]) { args.note = argv[++i]; continue }
|
|
39
|
+
if (a.startsWith('--note=')) { args.note = a.slice(7); continue }
|
|
40
|
+
if (a === '--by' && argv[i + 1]) { args.by = argv[++i]; continue }
|
|
41
|
+
if (a.startsWith('--by=')) { args.by = a.slice(5); continue }
|
|
42
|
+
if (a === '--json') { args.json = true; continue }
|
|
43
|
+
args._.push(a)
|
|
44
|
+
}
|
|
45
|
+
return args
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function layoutOf(args) {
|
|
49
|
+
const argv = args.profile ? ['node', 'fangctl', '--profile', args.profile] : process.argv
|
|
50
|
+
return resolveFangLayout({ dataRoot: args.dataRoot ?? '', argv, env: process.env })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fmtTime(ms) {
|
|
54
|
+
if (!ms) return '—'
|
|
55
|
+
try { return new Date(ms).toISOString().replace('T', ' ').slice(0, 19) } catch { return String(ms) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** HTTP base for approval commands; accepts host, host/fang, or host/fang/api. */
|
|
59
|
+
function apiBase(args) {
|
|
60
|
+
const raw = String(args.url ?? process.env.FANG_URL ?? '').trim()
|
|
61
|
+
if (!raw) return ''
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(raw)
|
|
64
|
+
let pathname = url.pathname.replace(/\/+$/, '')
|
|
65
|
+
if (pathname.endsWith('/fang/api')) pathname = pathname.slice(0, -9)
|
|
66
|
+
else if (pathname.endsWith('/fang')) pathname = pathname.slice(0, -5)
|
|
67
|
+
else pathname = ''
|
|
68
|
+
return `${url.origin}${pathname}`
|
|
69
|
+
} catch { return '' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function resolveToken(args, base) {
|
|
73
|
+
const direct = String(args.token ?? process.env.FANG_TOKEN ?? '').trim()
|
|
74
|
+
if (direct) return direct
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${base}/fang/api/auth-token`, { headers: { accept: 'application/json' } })
|
|
77
|
+
const body = await res.json()
|
|
78
|
+
if (res.ok && body?.ok && body.token) return String(body.token)
|
|
79
|
+
} catch { /* fall through */ }
|
|
80
|
+
return ''
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function approvalRequest(args, pathname, options = {}) {
|
|
84
|
+
const base = apiBase(args)
|
|
85
|
+
if (!base) throw new Error('--url is required for HTTP commands')
|
|
86
|
+
const token = await resolveToken(args, base)
|
|
87
|
+
if (!token) throw new Error('token unavailable; pass --token or FANG_TOKEN')
|
|
88
|
+
const controller = new AbortController()
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), 10000)
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${base}${pathname}`, {
|
|
92
|
+
method: options.method ?? 'GET',
|
|
93
|
+
headers: {
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
'content-type': 'application/json',
|
|
96
|
+
'x-fang-auth': token,
|
|
97
|
+
},
|
|
98
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
})
|
|
101
|
+
const text = await res.text()
|
|
102
|
+
let body = null
|
|
103
|
+
try { body = text ? JSON.parse(text) : null } catch { body = null }
|
|
104
|
+
return { status: res.status, body, text }
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timer)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printApprovals(body, json) {
|
|
111
|
+
if (json) {
|
|
112
|
+
console.log(JSON.stringify(body, null, 2))
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const items = Array.isArray(body?.items) ? body.items : []
|
|
116
|
+
if (!items.length) { console.log('(no pending approvals)'); return }
|
|
117
|
+
console.log(`${'SOURCE'.padEnd(7)} ${'APPROVAL'.padEnd(28)} ${'RISK'.padEnd(7)} ${'DEADLINE'.padEnd(12)} TITLE`)
|
|
118
|
+
for (const item of items) {
|
|
119
|
+
console.log([
|
|
120
|
+
String(item.source ?? '').padEnd(7),
|
|
121
|
+
String(item.approvalId ?? '').padEnd(28),
|
|
122
|
+
String(item.risk ?? '').padEnd(7),
|
|
123
|
+
String(fmtTime(Number(item.deadlineAt))).padEnd(12),
|
|
124
|
+
String(item.title ?? ''),
|
|
125
|
+
].join(' '))
|
|
126
|
+
}
|
|
127
|
+
for (const failed of Array.isArray(body?.failed) ? body.failed : []) {
|
|
128
|
+
console.log(`! unavailable: ${failed.taskId ?? 'mother'} ${failed.error ?? ''}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseApprovalTarget(raw, items) {
|
|
133
|
+
const parts = String(raw ?? '').split(':').filter(Boolean)
|
|
134
|
+
const source = parts[0]
|
|
135
|
+
if (source !== 'mother' && source !== 'fork') return null
|
|
136
|
+
if (source === 'mother') {
|
|
137
|
+
const approvalId = parts.slice(1).join(':')
|
|
138
|
+
return approvalId ? { source, taskId: null, approvalId } : null
|
|
139
|
+
}
|
|
140
|
+
if (parts.length >= 3) return { source, taskId: parts[1], approvalId: parts.slice(2).join(':') }
|
|
141
|
+
const approvalId = parts.slice(1).join(':')
|
|
142
|
+
if (!approvalId) return null
|
|
143
|
+
const matches = (items ?? []).filter((item) => item.source === 'fork' && item.approvalId === approvalId)
|
|
144
|
+
if (matches.length !== 1) return null
|
|
145
|
+
return { source: 'fork', taskId: matches[0].taskId, approvalId }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function cmdApprovals(args) {
|
|
149
|
+
const result = await approvalRequest(args, '/fang/api/approvals')
|
|
150
|
+
if (result.status !== 200 || result.body?.ok === false) {
|
|
151
|
+
console.error(JSON.stringify(result.body ?? { status: result.status, text: result.text }))
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
154
|
+
printApprovals(result.body, args.json)
|
|
155
|
+
return result.body?.degraded ? 2 : 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function cmdDecide(args, outcome) {
|
|
159
|
+
const raw = args._[1]
|
|
160
|
+
if (!raw) { console.error('usage: fangctl approve|deny <source:id> [--note ...]'); return 64 }
|
|
161
|
+
const listed = await approvalRequest(args, '/fang/api/approvals')
|
|
162
|
+
const target = parseApprovalTarget(raw, Array.isArray(listed.body?.items) ? listed.body.items : [])
|
|
163
|
+
if (!target) { console.error(`cannot resolve approval target: ${raw}`); return 64 }
|
|
164
|
+
const result = await approvalRequest(args, '/fang/api/approvals/decide', {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
body: {
|
|
167
|
+
...target,
|
|
168
|
+
outcome,
|
|
169
|
+
note: args.note ?? null,
|
|
170
|
+
by: args.by ?? 'fangctl',
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
if (args.json) console.log(JSON.stringify(result.body ?? { status: result.status }, null, 2))
|
|
174
|
+
else if (result.status === 200 && result.body?.ok !== false) console.log(`${outcome}: ${target.source}:${target.approvalId}`)
|
|
175
|
+
else console.error(`${result.status}: ${result.body?.error ?? result.text ?? 'failed'}`)
|
|
176
|
+
if (result.status === 200 && result.body?.ok !== false) return 0
|
|
177
|
+
return result.status === 409 ? 2 : 1
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function readForks(layout) {
|
|
181
|
+
const raw = readJson(layout.forksFile, [])
|
|
182
|
+
const list = Array.isArray(raw) ? raw : Array.isArray(raw?.forks) ? raw.forks : []
|
|
183
|
+
return list
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cmdStatus(layout) {
|
|
187
|
+
const forks = readForks(layout)
|
|
188
|
+
console.log(`dsh-Fang ${FANG_VERSION}`)
|
|
189
|
+
console.log(` profile : ${layout.profileTag}`)
|
|
190
|
+
console.log(` DSH_HOME : ${layout.dshHome}`)
|
|
191
|
+
console.log(` data dir : ${layout.dataDir}${exists(layout.dataDir) ? '' : ' (不存在:该 profile 未启动过 Fang)'}`)
|
|
192
|
+
console.log(` ledger : ${layout.ledgerFile}${exists(layout.ledgerFile) ? '' : ' (尚未写入)'}`)
|
|
193
|
+
console.log(` buffer : ${layout.bufferDir}`)
|
|
194
|
+
console.log(` forks : ${forks.length}`)
|
|
195
|
+
for (const f of forks) {
|
|
196
|
+
console.log(` - ${f.forkId} ${f.status ?? '?'} port=${f.port ?? '—'} pid=${f.pid ?? '—'} ${f.taskTitle ?? ''}`)
|
|
197
|
+
}
|
|
198
|
+
if (exists(layout.ledgerFile)) {
|
|
199
|
+
const v = verifyChain(layout.ledgerFile)
|
|
200
|
+
console.log(` audit : ${v.ok ? 'OK' : 'CORRUPT'} (${v.count} 条${v.ok ? '' : `, 首个坏行 #${v.badIndex} ${v.reason}`})`)
|
|
201
|
+
}
|
|
202
|
+
return 0
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function cmdVerify(layout) {
|
|
206
|
+
if (!exists(layout.ledgerFile)) {
|
|
207
|
+
console.log(`no ledger at ${layout.ledgerFile}`)
|
|
208
|
+
return 1
|
|
209
|
+
}
|
|
210
|
+
const v = verifyChain(layout.ledgerFile)
|
|
211
|
+
console.log(JSON.stringify(v, null, 2))
|
|
212
|
+
return v.ok ? 0 : 2
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cmdTail(layout, n = 20) {
|
|
216
|
+
const lines = readJsonl(layout.ledgerFile)
|
|
217
|
+
const tail = lines.slice(-n)
|
|
218
|
+
for (const r of tail) {
|
|
219
|
+
const data = r.data && typeof r.data === 'object' ? r.data : {}
|
|
220
|
+
const brief = Object.keys(data).slice(0, 4).map(k => `${k}=${JSON.stringify(data[k])}`).join(' ')
|
|
221
|
+
console.log(`${String(r.seq).padStart(3)} ${fmtTime(r.at)} ${String(r.kind).padEnd(18)} ${brief}`)
|
|
222
|
+
}
|
|
223
|
+
if (!tail.length) console.log('(no audit records)')
|
|
224
|
+
return 0
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cmdForks(layout) {
|
|
228
|
+
const forks = readForks(layout)
|
|
229
|
+
if (!forks.length) { console.log('(no forks recorded)'); return 0 }
|
|
230
|
+
for (const f of forks) {
|
|
231
|
+
console.log(`${f.forkId}\t${f.status}\tport=${f.port ?? '—'}\tpid=${f.pid ?? '—'}\t${f.taskTitle ?? ''}`)
|
|
232
|
+
}
|
|
233
|
+
return 0
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 只读:重启守卫状态 + 授权清单摘要。
|
|
238
|
+
* 有 --url:走 GET /fang/api/restart(验证运行中的宿主是否已载入新 bundle)
|
|
239
|
+
* 无 --url:offline 读 <dataDir>/restart(与 HTTP/UI 同一投影)
|
|
240
|
+
* 两种模式都不触发任何执行。
|
|
241
|
+
*/
|
|
242
|
+
async function cmdRestartStatus(layout, args) {
|
|
243
|
+
if (args.url) {
|
|
244
|
+
const result = await approvalRequest(args, '/fang/api/restart')
|
|
245
|
+
if (args.json) {
|
|
246
|
+
console.log(JSON.stringify({ status: result.status, ...(result.body ?? {}) }, null, 2))
|
|
247
|
+
return result.status === 200 ? 0 : 1
|
|
248
|
+
}
|
|
249
|
+
if (result.status !== 200) {
|
|
250
|
+
console.error(`HTTP ${result.status}: ${result.body?.error ?? result.text ?? 'restart route unavailable'}`)
|
|
251
|
+
console.error('提示:旧 bundle 没有 /fang/api/restart;需重启 DSH Desktop 载入新代码。')
|
|
252
|
+
return 1
|
|
253
|
+
}
|
|
254
|
+
return printRemoteRestartStatus(result.body)
|
|
255
|
+
}
|
|
256
|
+
const limitArg = Number(args._[1])
|
|
257
|
+
const status = readRestartStatus(layout.dataDir, {
|
|
258
|
+
limit: Number.isFinite(limitArg) && limitArg > 0 ? limitArg : 50,
|
|
259
|
+
})
|
|
260
|
+
if (args.json) {
|
|
261
|
+
console.log(JSON.stringify(status, null, 2))
|
|
262
|
+
return 0
|
|
263
|
+
}
|
|
264
|
+
printLocalRestartStatus(status)
|
|
265
|
+
return 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function printRemoteRestartStatus(body) {
|
|
269
|
+
if (!body || body.ok === false || body.available === false) {
|
|
270
|
+
console.error(`restart-guard unavailable: ${body?.error ?? 'service missing'}`)
|
|
271
|
+
return 1
|
|
272
|
+
}
|
|
273
|
+
const s = body.state ?? {}
|
|
274
|
+
const p = body.policy ?? {}
|
|
275
|
+
const counts = body.approvals?.counts ?? {}
|
|
276
|
+
console.log('restart guard (remote, read-only)')
|
|
277
|
+
const pending = s.pending?.id ? ` (pending ${s.pending.id} risk=${s.pending.risk ?? '?'} phase=${s.pending.phase ?? '?'})` : ''
|
|
278
|
+
console.log(` state : ${s.state ?? 'unknown'}${pending}`)
|
|
279
|
+
console.log(` mode : ${p.mode ?? '—'} authMode=${p.authMode ?? '—'} strictRedLine=${p.strictRedLine === false ? 'false(红灯可自动放行)' : 'true'}`)
|
|
280
|
+
const armedNow = p.mode === 'enforce' && p.allowProcessRestart === true
|
|
281
|
+
console.log(` armed : ${armedNow ? 'yes' : 'no'} (enforce + allowProcessRestart=true)`)
|
|
282
|
+
console.log(` approvals : total=${counts.total ?? 0} authorized=${counts.authorized ?? 0} executed=${counts.executed ?? 0} stale=${counts.stale ?? 0} notArmed=${counts.notArmed ?? 0} invalid=${counts.invalid ?? 0}`)
|
|
283
|
+
console.log(` contract : decision-only; no execution; executed-proof = receipt execution.executed===true`)
|
|
284
|
+
return 0
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function printLocalRestartStatus(status) {
|
|
288
|
+
const counts = status.approvals.counts
|
|
289
|
+
console.log(`restart guard @ ${status.stateDir}${exists(status.stateDir) ? '' : ' (不存在:守卫尚未启动或未产生状态)'}`)
|
|
290
|
+
const pending = status.pending?.id ? ` (pending ${status.pending.id} risk=${status.pending.risk ?? '?'} phase=${status.pending.phase ?? '?'})` : ''
|
|
291
|
+
console.log(` state : ${status.state}${pending}`)
|
|
292
|
+
console.log(` state file : ${exists(path.join(status.stateDir, 'state.json')) ? 'present' : 'missing'}${status.stateAt ? ` (at ${fmtTime(status.stateAt)})` : ''}`)
|
|
293
|
+
if (status.lastInterrupted) {
|
|
294
|
+
console.log(` interrupted: ${status.lastInterrupted.id ?? '?'} previousState=${status.lastInterrupted.previousState ?? '?'} at=${fmtTime(status.lastInterrupted.at)}`)
|
|
295
|
+
}
|
|
296
|
+
console.log(` approvals : total=${counts.total} authorized=${counts.authorized} executed=${counts.executed} stale=${counts.stale} notArmed=${counts.notArmed} invalid=${counts.invalid}`)
|
|
297
|
+
if (status.approvals.items.length) {
|
|
298
|
+
console.log(`${'ID'.padEnd(22)} ${'RISK'.padEnd(7)} ${'OUTCOME'.padEnd(11)} ${'STATE'.padEnd(14)} ${'APPROVAL'.padEnd(20)} EXPIRES`)
|
|
299
|
+
for (const item of status.approvals.items) {
|
|
300
|
+
console.log([
|
|
301
|
+
String(item.id ?? '').padEnd(22),
|
|
302
|
+
String(item.risk ?? '').padEnd(7),
|
|
303
|
+
String(item.outcome ?? '').padEnd(11),
|
|
304
|
+
String(item.state ?? '').padEnd(14),
|
|
305
|
+
String(item.approvalId ?? '—').padEnd(20),
|
|
306
|
+
item.expiresAt ? fmtTime(item.expiresAt) : '—',
|
|
307
|
+
].join(' '))
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
console.log(' (no approval manifests)')
|
|
311
|
+
}
|
|
312
|
+
console.log(' contract : decision-only; no execution; executed-proof = receipt execution.executed===true')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function cmdDoctor(layout) {
|
|
316
|
+
const checks = []
|
|
317
|
+
const check = (id, ok, detail) => checks.push({ id, ok, detail })
|
|
318
|
+
|
|
319
|
+
const [major, minor] = process.versions.node.split('.').map(Number)
|
|
320
|
+
check('node', major >= 22, `node ${process.versions.node}(需要 >= 22 才有 node:test 稳定行为)`)
|
|
321
|
+
check('data-dir', exists(layout.dataDir), layout.dataDir)
|
|
322
|
+
check('roster', exists(layout.forksFile) || true, 'forks.json 可缺省(首次启动创建)')
|
|
323
|
+
if (exists(layout.ledgerFile)) {
|
|
324
|
+
const v = verifyChain(layout.ledgerFile)
|
|
325
|
+
check('ledger', v.ok, `${v.count} 条,${v.ok ? '链完整' : `${v.reason} @${v.badIndex}`}`)
|
|
326
|
+
} else {
|
|
327
|
+
check('ledger', true, '尚未写入(未启动或未产生审计)')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const profileDir = resolveProfileDir(layout.profileTag)
|
|
331
|
+
check('profile', exists(profileDir), profileDir)
|
|
332
|
+
// Bundle rows do not live in cordis.patch.yml: a bundle is listed in the profile
|
|
333
|
+
// package.json (dsh.profile.bundles) and contributes its own patch layer.
|
|
334
|
+
let pkg = null
|
|
335
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(profileDir, 'package.json'), 'utf8')) } catch { /* ignore */ }
|
|
336
|
+
const bundles = Array.isArray(pkg?.dsh?.profile?.bundles) ? pkg.dsh.profile.bundles : []
|
|
337
|
+
const installed = bundles.includes('@twinsearth/fang-dsh-plugin') || Boolean(pkg?.dependencies?.['@twinsearth/fang-dsh-plugin'])
|
|
338
|
+
check('bundle-installed', installed, installed ? `profiles/${layout.profileTag} 的 bundles 含 @twinsearth/fang-dsh-plugin` : 'profile package.json 未列出 @twinsearth/fang-dsh-plugin(bundle 未安装)')
|
|
339
|
+
|
|
340
|
+
const template = resolveProfileDir(layout.profileTag)
|
|
341
|
+
check('fork-template', exists(template), `${template}(分身 profile 模板;可用 config.profileTemplate 覆盖)`)
|
|
342
|
+
const nodeModules = path.join(template, 'node_modules')
|
|
343
|
+
check('template-node_modules', exists(nodeModules), `${nodeModules}(分身通过 junction 共享)`)
|
|
344
|
+
|
|
345
|
+
const cliCandidates = ['E:\\DS\\resources\\runtime\\cli\\bin\\dsh.cmd', 'dsh']
|
|
346
|
+
const foundCli = cliCandidates.find(c => c === 'dsh' ? true : exists(c))
|
|
347
|
+
check('dsh-cli', !!foundCli, foundCli === 'dsh' ? '仅在 PATH 中假定可用' : foundCli)
|
|
348
|
+
|
|
349
|
+
for (const c of checks) console.log(`${c.ok ? 'PASS' : 'FAIL'} ${c.id.padEnd(22)} ${c.detail}`)
|
|
350
|
+
const failed = checks.filter(c => !c.ok).length
|
|
351
|
+
console.log(failed === 0 ? '\n全部通过。' : `\n${failed} 项失败(这些项会影响派生/加载,见上)。`)
|
|
352
|
+
return failed === 0 ? 0 : 1
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const args = parseArgs(process.argv.slice(2))
|
|
356
|
+
const cmd = args._[0] ?? 'status'
|
|
357
|
+
const layout = layoutOf(args)
|
|
358
|
+
|
|
359
|
+
let code = 0
|
|
360
|
+
switch (cmd) {
|
|
361
|
+
case 'status': code = cmdStatus(layout); break
|
|
362
|
+
case 'forks': code = cmdForks(layout); break
|
|
363
|
+
case 'restart-status': {
|
|
364
|
+
try { code = await cmdRestartStatus(layout, args) } catch (error) { console.error(String(error?.message ?? error)); code = 1 }
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
case 'restart-safe': {
|
|
368
|
+
const action = args._[1] ?? 'status'
|
|
369
|
+
const safeFile = path.join(layout.dataDir, 'restart', 'safe-mode.json')
|
|
370
|
+
if (action === 'clear') {
|
|
371
|
+
const result = clearCrashLoopSafeMode(layout.dataDir, { by: args.by ?? 'fangctl', note: args.note ?? '' })
|
|
372
|
+
if (args.json) console.log(JSON.stringify(result, null, 2))
|
|
373
|
+
else if (result.ok) console.log(result.cleared ? `safe-mode cleared; crash ledger archived: ${result.archived ?? '(none)'}` : 'safe-mode is not active')
|
|
374
|
+
else console.error(`FAIL ${result.reason}: ${result.error ?? ''}`)
|
|
375
|
+
code = result.ok ? 0 : 1
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
let doc = null
|
|
379
|
+
try { doc = JSON.parse(fs.readFileSync(safeFile, 'utf8')) } catch { doc = null }
|
|
380
|
+
if (args.json) console.log(JSON.stringify({ file: safeFile, safeMode: doc }, null, 2))
|
|
381
|
+
else if (!doc || doc.active !== true) console.log(`safe-mode: clear (${safeFile})`)
|
|
382
|
+
else console.log(`safe-mode: ACTIVE reason=${doc.reason ?? '?'} at=${fmtTime(doc.at)} crashCount=${doc.crashCount ?? '?'} file=${safeFile}`)
|
|
383
|
+
code = 0
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
case 'verify': code = cmdVerify(layout); break
|
|
387
|
+
case 'tail': code = cmdTail(layout, Number(args._[1] ?? 20)); break
|
|
388
|
+
case 'doctor': code = cmdDoctor(layout); break
|
|
389
|
+
case 'approvals': {
|
|
390
|
+
try { code = await cmdApprovals(args) } catch (error) { console.error(String(error?.message ?? error)); code = 1 }
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
case 'approve':
|
|
394
|
+
case 'deny': {
|
|
395
|
+
try { code = await cmdDecide(args, cmd === 'approve' ? 'approved' : 'denied') } catch (error) { console.error(String(error?.message ?? error)); code = 1 }
|
|
396
|
+
break
|
|
397
|
+
}
|
|
398
|
+
case 'parse-url': {
|
|
399
|
+
const parsed = parseWebUrl(args._.slice(1).join(' '))
|
|
400
|
+
console.log(JSON.stringify(parsed, null, 2))
|
|
401
|
+
code = parsed ? 0 : 1
|
|
402
|
+
break
|
|
403
|
+
}
|
|
404
|
+
default:
|
|
405
|
+
console.log('usage: fangctl.mjs <status|forks|verify|tail [n]|doctor|restart-status [limit]|restart-safe [status|clear]|parse-url <line>|approvals|approve <source:id>|deny <source:id>> [--profile name] [--data-root dir] [--url url] [--token token] [--note text] [--by who] [--json]')
|
|
406
|
+
code = 64
|
|
407
|
+
}
|
|
408
|
+
process.exitCode = code
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
|
|
4
|
+
//
|
|
5
|
+
// 主城门禁 CLI:任何进主城的动作(打包/发布/安装/部署/新建 profile)先过这里。
|
|
6
|
+
// node tools/gate/main-city.mjs --target source
|
|
7
|
+
// node tools/gate/main-city.mjs --target package --package-dir dist/npm/package --tarball dist/dsh-fang-X.tgz
|
|
8
|
+
// node tools/gate/main-city.mjs --target publish --package-dir ... --tarball ... --tag vX --check-registry
|
|
9
|
+
// node tools/gate/main-city.mjs --target install --home <DSH_HOME> --profile fang-x --package-dir <dir>
|
|
10
|
+
// node tools/gate/main-city.mjs --target deploy --home <DSH_HOME> --profile fang-x --repair
|
|
11
|
+
// node tools/gate/main-city.mjs --target profile --home <DSH_HOME> --profile demo --create
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_MIN_FREE_BYTES,
|
|
18
|
+
DEFAULT_PROFILE_BUNDLES,
|
|
19
|
+
GATE_TARGETS,
|
|
20
|
+
createGatedProfile,
|
|
21
|
+
formatGateFailure,
|
|
22
|
+
runMainGate,
|
|
23
|
+
} from '../../src/lib/main-gate.mjs'
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv = []) {
|
|
26
|
+
const out = { target: 'package', json: false, repair: false, create: false, force: false, allowExisting: false, checkRegistry: false }
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const arg = String(argv[index])
|
|
29
|
+
const next = () => {
|
|
30
|
+
if (index + 1 >= argv.length) throw new Error(`missing value for ${arg}`)
|
|
31
|
+
return String(argv[++index])
|
|
32
|
+
}
|
|
33
|
+
if (arg === '--json') { out.json = true; continue }
|
|
34
|
+
if (arg === '--repair') { out.repair = true; continue }
|
|
35
|
+
if (arg === '--create') { out.create = true; continue }
|
|
36
|
+
if (arg === '--force') { out.force = true; continue }
|
|
37
|
+
if (arg === '--allow-existing') { out.allowExisting = true; continue }
|
|
38
|
+
if (arg === '--require-backup') { out.requireBackup = true; continue }
|
|
39
|
+
if (arg === '--check-registry') { out.checkRegistry = true; continue }
|
|
40
|
+
if (arg === '--supervisor-present') { out.supervisorPresent = true; continue }
|
|
41
|
+
if (arg === '--help' || arg === '-h') { out.help = true; continue }
|
|
42
|
+
if (arg === '--target') { out.target = next(); continue }
|
|
43
|
+
if (arg.startsWith('--target=')) { out.target = arg.slice(9); continue }
|
|
44
|
+
if (arg === '--root') { out.root = next(); continue }
|
|
45
|
+
if (arg.startsWith('--root=')) { out.root = arg.slice(7); continue }
|
|
46
|
+
if (arg === '--version') { out.version = next(); continue }
|
|
47
|
+
if (arg.startsWith('--version=')) { out.version = arg.slice(10); continue }
|
|
48
|
+
if (arg === '--dist') { out.distDir = next(); continue }
|
|
49
|
+
if (arg.startsWith('--dist=')) { out.distDir = arg.slice(7); continue }
|
|
50
|
+
if (arg === '--package-dir') { out.packageDir = next(); continue }
|
|
51
|
+
if (arg.startsWith('--package-dir=')) { out.packageDir = arg.slice(14); continue }
|
|
52
|
+
if (arg === '--tarball') { out.tarball = next(); continue }
|
|
53
|
+
if (arg.startsWith('--tarball=')) { out.tarball = arg.slice(10); continue }
|
|
54
|
+
if (arg === '--tag') { out.tag = next(); continue }
|
|
55
|
+
if (arg.startsWith('--tag=')) { out.tag = arg.slice(6); continue }
|
|
56
|
+
if (arg === '--npm-latest') { out.npmLatest = next(); continue }
|
|
57
|
+
if (arg.startsWith('--npm-latest=')) { out.npmLatest = arg.slice(13); continue }
|
|
58
|
+
if (arg === '--home') { out.homeDir = next(); continue }
|
|
59
|
+
if (arg.startsWith('--home=')) { out.homeDir = arg.slice(7); continue }
|
|
60
|
+
if (arg === '--profile') { out.profile = next(); continue }
|
|
61
|
+
if (arg.startsWith('--profile=')) { out.profile = arg.slice(10); continue }
|
|
62
|
+
if (arg === '--profile-dir') { out.profileDir = next(); continue }
|
|
63
|
+
if (arg.startsWith('--profile-dir=')) { out.profileDir = arg.slice(14); continue }
|
|
64
|
+
if (arg === '--data-dir') { out.dataDir = next(); continue }
|
|
65
|
+
if (arg.startsWith('--data-dir=')) { out.dataDir = arg.slice(11); continue }
|
|
66
|
+
if (arg === '--actor') { out.actor = next(); continue }
|
|
67
|
+
if (arg.startsWith('--actor=')) { out.actor = arg.slice(8); continue }
|
|
68
|
+
if (arg === '--bundles') { out.bundles = next().split(',').map((item) => item.trim()).filter(Boolean); continue }
|
|
69
|
+
if (arg.startsWith('--bundles=')) { out.bundles = arg.slice(10).split(',').map((item) => item.trim()).filter(Boolean); continue }
|
|
70
|
+
if (arg === '--min-free-mb') { out.minFreeBytes = Math.max(0, Number(next()) || 0) * 1024 * 1024; continue }
|
|
71
|
+
if (arg.startsWith('--min-free-mb=')) { out.minFreeBytes = Math.max(0, Number(arg.slice(14)) || 0) * 1024 * 1024; continue }
|
|
72
|
+
throw new Error(`unknown argument: ${arg}`)
|
|
73
|
+
}
|
|
74
|
+
if (!out.minFreeBytes) out.minFreeBytes = DEFAULT_MIN_FREE_BYTES
|
|
75
|
+
return out
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const USAGE = `usage: node tools/gate/main-city.mjs --target <${GATE_TARGETS.join('|')}> [options]
|
|
79
|
+
|
|
80
|
+
options:
|
|
81
|
+
--root <dir> source root (default: process.cwd())
|
|
82
|
+
--version <semver> expected version (default: package.json)
|
|
83
|
+
--dist <dir> dist directory containing manifest.json/tarball (default: dist)
|
|
84
|
+
--package-dir <dir> staged package directory (dist/npm/package)
|
|
85
|
+
--tarball <file> explicit tarball path
|
|
86
|
+
--tag <vX.Y.Z> release tag to match against version
|
|
87
|
+
--npm-latest <version> current registry latest (offline check)
|
|
88
|
+
--check-registry fetch npm latest before publish check
|
|
89
|
+
--allow-existing allow an already-published version (idempotent republish)
|
|
90
|
+
--home <dir> DSH_HOME that owns the profile
|
|
91
|
+
--profile <name> profile name
|
|
92
|
+
--profile-dir <dir> explicit profile directory
|
|
93
|
+
--data-dir <dir> explicit fang data dir (default <home>/fang/<profile>)
|
|
94
|
+
--create create the profile after a green gate
|
|
95
|
+
--force allow replacing an existing profile (backup taken)
|
|
96
|
+
--repair create a .gate-backups snapshot when a profile has none
|
|
97
|
+
--require-backup block deploy when no profile backup exists
|
|
98
|
+
--bundles a,b,c bundles for a newly created profile
|
|
99
|
+
--min-free-mb <n> minimum free disk space (default ${Math.round(DEFAULT_MIN_FREE_BYTES / 1024 / 1024)})
|
|
100
|
+
--actor <id> recorded in the chained audit record
|
|
101
|
+
--json machine-readable report
|
|
102
|
+
-h, --help
|
|
103
|
+
`
|
|
104
|
+
|
|
105
|
+
async function npmLatest(version) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch('https://registry.npmjs.org/@twinsearth%2Ffang-dsh-plugin', { signal: AbortSignal.timeout(15000) })
|
|
108
|
+
if (!res.ok) return { ok: false, detail: `registry HTTP ${res.status}` }
|
|
109
|
+
const body = await res.json()
|
|
110
|
+
return { ok: true, latest: String(body?.['dist-tags']?.latest ?? '') }
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { ok: false, detail: String(error?.message ?? error) }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main() {
|
|
117
|
+
let args
|
|
118
|
+
try { args = parseArgs(process.argv.slice(2)) } catch (error) {
|
|
119
|
+
console.error(`[main-city] ${error?.message ?? error}`)
|
|
120
|
+
console.error(USAGE)
|
|
121
|
+
return 2
|
|
122
|
+
}
|
|
123
|
+
if (args.help) { console.log(USAGE); return 0 }
|
|
124
|
+
if (!GATE_TARGETS.includes(args.target)) {
|
|
125
|
+
console.error(`[main-city] unknown target: ${args.target}`)
|
|
126
|
+
console.error(USAGE)
|
|
127
|
+
return 2
|
|
128
|
+
}
|
|
129
|
+
if (args.checkRegistry && args.target === 'publish') {
|
|
130
|
+
const latest = await npmLatest()
|
|
131
|
+
if (latest.ok) args.npmLatest = latest.latest
|
|
132
|
+
else if (!args.json) console.warn(`[main-city] registry check skipped: ${latest.detail}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let report = runMainGate({
|
|
136
|
+
target: args.target,
|
|
137
|
+
root: args.root,
|
|
138
|
+
version: args.version,
|
|
139
|
+
distDir: args.distDir,
|
|
140
|
+
packageDir: args.packageDir,
|
|
141
|
+
tarball: args.tarball,
|
|
142
|
+
tag: args.tag,
|
|
143
|
+
npmLatest: args.npmLatest,
|
|
144
|
+
allowExisting: args.allowExisting,
|
|
145
|
+
homeDir: args.homeDir,
|
|
146
|
+
profile: args.profile,
|
|
147
|
+
profileDir: args.profileDir,
|
|
148
|
+
dataDir: args.dataDir,
|
|
149
|
+
repair: args.repair,
|
|
150
|
+
requireBackup: args.requireBackup,
|
|
151
|
+
supervisorPresent: args.supervisorPresent,
|
|
152
|
+
create: args.create,
|
|
153
|
+
force: args.force,
|
|
154
|
+
minFreeBytes: args.minFreeBytes,
|
|
155
|
+
actor: args.actor,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
if (args.target === 'profile' && args.create === true && report.ok) {
|
|
159
|
+
const created = createGatedProfile({
|
|
160
|
+
homeDir: args.homeDir,
|
|
161
|
+
profile: args.profile,
|
|
162
|
+
bundles: args.bundles ?? DEFAULT_PROFILE_BUNDLES,
|
|
163
|
+
force: args.force,
|
|
164
|
+
minFreeBytes: args.minFreeBytes,
|
|
165
|
+
})
|
|
166
|
+
report = { ...report, create: created }
|
|
167
|
+
if (!created.ok) {
|
|
168
|
+
report.ok = false
|
|
169
|
+
report.blockers = [...(report.blockers ?? []), { id: 'profile-create: failed', detail: `${created.error}${created.detail ? ': ' + created.detail : ''}` }]
|
|
170
|
+
report.summary = { ...report.summary, blockers: report.summary.blockers + 1 }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const reportFile = path.join(process.cwd(), 'dist', 'main-city-gate.json')
|
|
175
|
+
try {
|
|
176
|
+
fs.mkdirSync(path.dirname(reportFile), { recursive: true })
|
|
177
|
+
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2) + '\n', 'utf8')
|
|
178
|
+
} catch { /* report is best-effort */ }
|
|
179
|
+
|
|
180
|
+
if (args.json) console.log(JSON.stringify(report, null, 2))
|
|
181
|
+
else {
|
|
182
|
+
for (const item of report.checks ?? []) {
|
|
183
|
+
if (item.ok && item.severity === 'info') continue
|
|
184
|
+
console.log(`${item.ok ? 'PASS' : 'FAIL'} ${item.id.padEnd(48)} ${item.detail}`)
|
|
185
|
+
if (item.repair) console.log(` repair: ${item.repair}`)
|
|
186
|
+
}
|
|
187
|
+
console.log(`\n${report.ok ? 'GREEN' : 'RED'} main-city gate ${report.target} ` +
|
|
188
|
+
`blockers=${report.summary?.blockers ?? 0} warnings=${report.summary?.warnings ?? 0} grade=${report.score?.grade ?? '?'}`)
|
|
189
|
+
if (!report.ok) console.log(formatGateFailure(report))
|
|
190
|
+
console.log(`report: ${reportFile}`)
|
|
191
|
+
}
|
|
192
|
+
return report.ok ? 0 : 1
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const isMain = (() => {
|
|
196
|
+
try { return Boolean(process.argv[1]) && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)) } catch { return false }
|
|
197
|
+
})()
|
|
198
|
+
|
|
199
|
+
if (isMain) {
|
|
200
|
+
main().then((code) => { process.exitCode = code }).catch((error) => {
|
|
201
|
+
console.error(`[main-city] ${error?.stack ?? error}`)
|
|
202
|
+
process.exitCode = 1
|
|
203
|
+
})
|
|
204
|
+
}
|