dsh-session-superagent 0.8.16 → 0.8.27
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/CHANGELOG.md +199 -2
- package/CHECKLIST.md +26 -0
- package/MAINTENANCE.md +35 -0
- package/README.md +30 -8
- package/UPDATING.md +43 -0
- package/client.js +12 -2
- package/lib/index.js +125 -31
- package/package.json +6 -6
- package/scripts/port-official-ui.mjs +0 -40
- package/scripts/preview-orbit.mjs +0 -136
- package/scripts/render-preview.mjs +0 -213
- package/scripts/sync-profile.mjs +0 -129
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// 把「主理鱼」面板渲染成静态 HTML,用于在真浏览器里看布局(不需要宿主、不需要 React)。
|
|
2
|
-
//
|
|
3
|
-
// 为什么:client.js 是给宿主用的模块(`window.__ModuleLoader__.load` + `require('react')`),
|
|
4
|
-
// 组件树用 h() 构造。这里提供一个极简的假 React,把组件渲染成虚拟节点树,
|
|
5
|
-
// 再序列化成 HTML,并内联移植来的官方 CSS —— 于是可以在浏览器里真实地看布局。
|
|
6
|
-
//
|
|
7
|
-
// 用法: node scripts/render-preview.mjs [输出文件]
|
|
8
|
-
// node scripts/render-preview.mjs preview-panel.html
|
|
9
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
-
import { dirname, join } from 'node:path'
|
|
11
|
-
import { fileURLToPath } from 'node:url'
|
|
12
|
-
import vm from 'node:vm'
|
|
13
|
-
|
|
14
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
|
-
const OUT = process.argv[2] || join(ROOT, 'preview-panel.html')
|
|
16
|
-
|
|
17
|
-
// ── 假 React:只需要 createElement/useState/useEffect/useCallback/useRef/memo/Fragment ──
|
|
18
|
-
let hookQueue = []
|
|
19
|
-
let hookIndex = 0
|
|
20
|
-
const React = {
|
|
21
|
-
Fragment: Symbol('Fragment'),
|
|
22
|
-
createElement(type, props, ...children) {
|
|
23
|
-
return { type, props: { ...(props || {}), children: children.flat(Infinity).filter((c) => c !== null && c !== undefined && c !== false) } }
|
|
24
|
-
},
|
|
25
|
-
memo: (fn) => fn,
|
|
26
|
-
useState(init) {
|
|
27
|
-
const value = hookIndex < hookQueue.length ? hookQueue[hookIndex] : (typeof init === 'function' ? init() : init)
|
|
28
|
-
hookIndex += 1
|
|
29
|
-
return [value, () => {}]
|
|
30
|
-
},
|
|
31
|
-
useEffect() {},
|
|
32
|
-
useCallback: (fn) => fn,
|
|
33
|
-
useRef: (value) => ({ current: value }),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 假宿主:只收集注册,不真的挂任何东西
|
|
37
|
-
const registrations = []
|
|
38
|
-
const sandbox = {
|
|
39
|
-
window: {
|
|
40
|
-
__ModuleLoader__: { load: (mod) => registrations.push(mod) },
|
|
41
|
-
innerWidth: 1280, innerHeight: 900,
|
|
42
|
-
addEventListener() {}, removeEventListener() {},
|
|
43
|
-
},
|
|
44
|
-
document: {
|
|
45
|
-
getElementById: () => null,
|
|
46
|
-
createElement: () => ({ id: '', textContent: '' }),
|
|
47
|
-
head: { appendChild() {} },
|
|
48
|
-
addEventListener() {}, removeEventListener() {},
|
|
49
|
-
},
|
|
50
|
-
fetch: () => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('{}') }),
|
|
51
|
-
setInterval: () => 0, clearInterval: () => {}, setTimeout: () => 0, clearTimeout: () => {},
|
|
52
|
-
console,
|
|
53
|
-
}
|
|
54
|
-
vm.createContext(sandbox)
|
|
55
|
-
vm.runInContext(readFileSync(join(ROOT, 'client.js'), 'utf8'), sandbox)
|
|
56
|
-
|
|
57
|
-
const mod = registrations[0]
|
|
58
|
-
if (!mod) throw new Error('client.js 没有注册任何模块')
|
|
59
|
-
const slots = new Map()
|
|
60
|
-
const ctx = {
|
|
61
|
-
slots: {
|
|
62
|
-
inject: (_name, fn) => fn(),
|
|
63
|
-
register: (opts, Component) => { slots.set(opts.id, Component); return () => {} },
|
|
64
|
-
},
|
|
65
|
-
effect: (fn) => fn(),
|
|
66
|
-
}
|
|
67
|
-
mod.factory((name) => {
|
|
68
|
-
if (name === 'react') return React
|
|
69
|
-
throw new Error('unexpected require: ' + name)
|
|
70
|
-
}).apply(ctx)
|
|
71
|
-
|
|
72
|
-
const Desk = slots.get('session-superagent-desk')
|
|
73
|
-
if (!Desk) throw new Error('没有注册 session-superagent-desk,已注册:' + [...slots.keys()].join(','))
|
|
74
|
-
|
|
75
|
-
// ── 样本快照:覆盖各种状态(正常/暂停/异常/仅观察/隔离/退避/过滤记录/历史)──
|
|
76
|
-
const now = Date.now()
|
|
77
|
-
const iso = (ms) => new Date(now - ms).toISOString()
|
|
78
|
-
const snapshot = {
|
|
79
|
-
loading: false, error: null,
|
|
80
|
-
body: {
|
|
81
|
-
sessionTitle: 'README 收尾与发布',
|
|
82
|
-
sessionGoal: null,
|
|
83
|
-
gh: { ok: true, version: 'gh version 2.101.0' },
|
|
84
|
-
credentials: { token: 'configured', sshKeyPath: 'none' },
|
|
85
|
-
watchers: [
|
|
86
|
-
{
|
|
87
|
-
id: 'MYCF711/dsh-session-superagent#s1', repo: 'MYCF711/dsh-session-superagent', sessionId: 's1',
|
|
88
|
-
intervalSec: 60, enabled: true, observe: false, title: 'README 收尾与发布', note: '把 0.8.x 的文档补齐',
|
|
89
|
-
lastPoll: iso(15000), lastWake: iso(300000), lastWakeCount: 3, lastAbsorbed: 1,
|
|
90
|
-
filters: { labels: ['ai-fix'], ignoreLabels: [], skipBots: true, skipDrafts: true, skipClosed: true, onlyAssigned: false, assignee: null },
|
|
91
|
-
activeHours: '09:00-23:00', isolated: true, workdir: 'E:/temp/iso', lastWorktree: 'E:/temp/iso/.dsh-zhuli/a-b',
|
|
92
|
-
history: [
|
|
93
|
-
{ at: iso(300000), numbers: [12], ok: true, reason: null },
|
|
94
|
-
{ at: iso(600000), numbers: [11], ok: false, reason: 'observe' },
|
|
95
|
-
],
|
|
96
|
-
filteredLog: [{ number: 7, title: '机器人提的依赖升级', reason: '机器人', at: iso(120000) }],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 'MYCF711/dsh-GOkifu-compact#s2', repo: 'MYCF711/dsh-GOkifu-compact', sessionId: 's2',
|
|
100
|
-
intervalSec: 120, enabled: false, observe: true, title: '联调会话',
|
|
101
|
-
lastPoll: iso(900000), lastDeferred: '仅观察:本轮本会唤醒 2 条(#4 #3)',
|
|
102
|
-
filters: { labels: [], ignoreLabels: [], skipBots: true, skipDrafts: true, skipClosed: true, onlyAssigned: false, assignee: null },
|
|
103
|
-
history: [], filteredLog: [],
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: 'a/b#s3', repo: 'someone/broken-repo', sessionId: 's3', intervalSec: 30, enabled: true,
|
|
107
|
-
title: '出错示例', lastPoll: iso(5000), lastError: 'gh: Not Found (HTTP 404)(仓库不存在或没权限:检查 owner/name)',
|
|
108
|
-
consecutiveFailures: 2, nextAt: new Date(now + 180000).toISOString(),
|
|
109
|
-
filters: { labels: [], ignoreLabels: [], skipBots: true, skipDrafts: true, skipClosed: true, onlyAssigned: false, assignee: null },
|
|
110
|
-
history: [], filteredLog: [],
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── 渲染:把元素树序列化成 HTML ───────────────────────────────────────────────
|
|
117
|
-
function escapeHtml(text) {
|
|
118
|
-
return String(text).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]))
|
|
119
|
-
}
|
|
120
|
-
function styleToCss(style) {
|
|
121
|
-
if (!style) return ''
|
|
122
|
-
return Object.entries(style).map(([k, v]) => {
|
|
123
|
-
const prop = k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
|
|
124
|
-
return prop + ':' + v
|
|
125
|
-
}).join(';')
|
|
126
|
-
}
|
|
127
|
-
function attr(name, value) {
|
|
128
|
-
if (value === undefined || value === null || value === false || value === true) {
|
|
129
|
-
return value === true ? ' ' + name : ''
|
|
130
|
-
}
|
|
131
|
-
return ' ' + name + '="' + escapeHtml(value) + '"'
|
|
132
|
-
}
|
|
133
|
-
function render(node) {
|
|
134
|
-
if (node === null || node === undefined || node === false) return ''
|
|
135
|
-
if (typeof node === 'string' || typeof node === 'number') return escapeHtml(node)
|
|
136
|
-
if (Array.isArray(node)) return node.map(render).join('')
|
|
137
|
-
if (typeof node !== 'object') return ''
|
|
138
|
-
const { type, props = {} } = node
|
|
139
|
-
if (typeof type === 'function') return render(type(props))
|
|
140
|
-
if (type === React.Fragment) return (props.children || []).map(render).join('')
|
|
141
|
-
const voidTags = new Set(['input', 'img', 'br', 'hr', 'meta', 'link'])
|
|
142
|
-
const tag = String(type)
|
|
143
|
-
const parts = []
|
|
144
|
-
if (props.className) parts.push(attr('class', props.className))
|
|
145
|
-
if (props.style) parts.push(attr('style', styleToCss(props.style)))
|
|
146
|
-
for (const key of ['data-tone', 'data-state', 'data-open', 'role', 'title', 'aria-label', 'aria-selected', 'aria-pressed', 'aria-modal', 'type', 'placeholder', 'value', 'disabled']) {
|
|
147
|
-
if (props[key] !== undefined && props[key] !== false) parts.push(attr(key, props[key]))
|
|
148
|
-
}
|
|
149
|
-
if (keyIsVoid(tag)) return '<' + tag + parts.join('') + '>'
|
|
150
|
-
return '<' + tag + parts.join('') + '>' + (props.children || []).map(render).join('') + '</' + tag + '>'
|
|
151
|
-
}
|
|
152
|
-
function keyIsVoid(tag) {
|
|
153
|
-
return ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tag)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 取移植来的 CSS
|
|
157
|
-
const clientSrc = readFileSync(join(ROOT, 'client.js'), 'utf8')
|
|
158
|
-
const marker = 'const PORTED_CSS = "'
|
|
159
|
-
const head = clientSrc.indexOf(marker) + marker.length
|
|
160
|
-
let i = head
|
|
161
|
-
while (i < clientSrc.length && clientSrc[i] !== '"') i += clientSrc[i] === '\\' ? 2 : 1
|
|
162
|
-
const PORTED_CSS = JSON.parse('"' + clientSrc.slice(head, i) + '"')
|
|
163
|
-
|
|
164
|
-
const scenarios = [
|
|
165
|
-
{ name: '总览', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
166
|
-
{ name: '判定', hooks: [snapshot, 'judge', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
167
|
-
{ name: '触发', hooks: [snapshot, 'trigger', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
168
|
-
{ name: '设置', hooks: [snapshot, 'settings', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', false] },
|
|
169
|
-
{ name: 'Toast(保存成功)', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, { id: 't1', text: '已保存:MYCF711/dsh-session-superagent', tone: 'info' }, null, {}, '', false] },
|
|
170
|
-
{ name: 'Toast(错误)', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, { id: 't2', text: '保存失败:gh: Not Found (HTTP 404)', tone: 'error' }, null, {}, '', false] },
|
|
171
|
-
{ name: '术语表', hooks: [snapshot, 'overview', null, { repo: '', intervalSec: '60', goal: '', labels: '', onlyAssigned: false, activeHours: '', isolated: false, workdir: '' }, '', '', false, null, null, {}, '', true] },
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
const sections = scenarios.map((scenario) => {
|
|
175
|
-
hookQueue = scenario.hooks
|
|
176
|
-
hookIndex = 0
|
|
177
|
-
const tree = Desk({ sessionId: 'preview-session' })
|
|
178
|
-
return '<h2 class="preview-title">' + scenario.name + '</h2>\n<div class="preview-frame">' + render(tree) + '</div>'
|
|
179
|
-
}).join('\n')
|
|
180
|
-
|
|
181
|
-
// 极简 token 兜底:宿主里这些由 --dsw-alias-* 提供,预览里给一组浅色近似值
|
|
182
|
-
const tokens = `:root{
|
|
183
|
-
--dsw-alias-label-primary:#1f2328; --dsw-alias-label-secondary:#59636e; --dsw-alias-label-tertiary:#818b98;
|
|
184
|
-
--dsw-alias-bg-layer-1:#ffffff; --dsw-alias-bg-layer-2:#f6f8fa; --dsw-alias-bg-layer-3:#eef1f4;
|
|
185
|
-
--dsw-alias-border-default:#d8dee4; --dsw-alias-border-muted:#e6eaef;
|
|
186
|
-
--dsw-alias-accent-primary:#0969da; --dsw-alias-success-500:#1a7f37; --dsw-alias-warning-500:#9a6700;
|
|
187
|
-
--dsw-alias-error-500:#cf222e; --dsw-static-neutral-bluish-00:#ffffff;
|
|
188
|
-
--dsw-shadow-lv3:0 8px 24px rgba(31,35,40,.12);
|
|
189
|
-
/* tag / 状态点真正引用的 token(官方 CSS 用 --dsw-alias-state-*) */
|
|
190
|
-
--dsw-alias-state-success-primary:#1a7f37; --dsw-alias-state-warn-primary:#9a6700;
|
|
191
|
-
--dsw-alias-state-error-primary:#cf222e; --dsw-alias-state-info-primary:#0969da;
|
|
192
|
-
--dsw-alias-toast-bg:#24292f;
|
|
193
|
-
}`
|
|
194
|
-
|
|
195
|
-
const html = `<!doctype html>
|
|
196
|
-
<meta charset="utf-8">
|
|
197
|
-
<title>主理鱼面板预览</title>
|
|
198
|
-
<style>
|
|
199
|
-
${tokens}
|
|
200
|
-
body{font:14px/1.45 -apple-system,"Segoe UI",system-ui,sans-serif;background:#eef1f4;margin:0;padding:24px;color:#1f2328}
|
|
201
|
-
.preview-title{margin:28px 0 8px;font-size:15px;color:#57606a}
|
|
202
|
-
.preview-frame{width:900px;background:#fff;border:1px solid #d8dee4;border-radius:12px;min-height:120px;max-height:820px;overflow:auto}
|
|
203
|
-
.dt-page{height:auto!important;max-height:none!important}
|
|
204
|
-
${PORTED_CSS}
|
|
205
|
-
</style>
|
|
206
|
-
<div style="font:13px/1.5 system-ui;color:#57606a">
|
|
207
|
-
主理鱼面板静态预览 —— 真实 client.js 渲染出的 DOM + 移植的官方 CSS(宽度 900px,token 为浅色近似值)
|
|
208
|
-
</div>
|
|
209
|
-
${sections}
|
|
210
|
-
`
|
|
211
|
-
writeFileSync(OUT, html)
|
|
212
|
-
console.log('已写出:' + OUT)
|
|
213
|
-
console.log('字节:' + html.length)
|
package/scripts/sync-profile.mjs
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// 把插件源码目录同步到 profile 的安装副本。
|
|
2
|
-
//
|
|
3
|
-
// 为什么需要:profile 里是 `file:` 安装 —— pnpm 为**已存在的文件**建了硬链接,
|
|
4
|
-
// 改内容会自动生效;但**新增文件不会**自动出现,而**被原子写入替换的文件会断链**。
|
|
5
|
-
//
|
|
6
|
-
// 真实事故 1(2026-11-01 23:59 重启):新增的 lib/body.js 没进 profile,
|
|
7
|
-
// 宿主 import './body.js' 失败 → 整个插件不挂载 → 工具和 HTTP 路由全消失,
|
|
8
|
-
// 客户端 bundle 却照常加载,表面看起来"插件还在"。
|
|
9
|
-
// 真实事故 2(潜伏):某些工具用「写临时文件再 rename」保存,profile 里那份仍是旧 inode,
|
|
10
|
-
// 于是宿主跑的是旧代码,而仓库里看着已经是新代码。
|
|
11
|
-
//
|
|
12
|
-
// 用法:
|
|
13
|
-
// node scripts/sync-profile.mjs 补齐缺失文件(不动断链,只警告)
|
|
14
|
-
// node scripts/sync-profile.mjs --repair 断链也重建硬链接,并清理 profile 里的陈旧文件
|
|
15
|
-
// node scripts/sync-profile.mjs --check 只检查:有缺失/断链/陈旧就 exit 1(给 CI 用)
|
|
16
|
-
// node scripts/sync-profile.mjs --dir <path> 指定 profile 安装目录
|
|
17
|
-
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
|
18
|
-
import { dirname, join } from 'node:path'
|
|
19
|
-
import { fileURLToPath } from 'node:url'
|
|
20
|
-
|
|
21
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
22
|
-
const args = process.argv.slice(2)
|
|
23
|
-
const REPAIR = args.includes('--repair')
|
|
24
|
-
const CHECK = args.includes('--check')
|
|
25
|
-
const dirFlag = args.indexOf('--dir')
|
|
26
|
-
const DEFAULT_PROFILE = join(
|
|
27
|
-
process.env.DSH_HOME || join(process.env.USERPROFILE || '', '.dsh'),
|
|
28
|
-
'profiles',
|
|
29
|
-
'desktop',
|
|
30
|
-
'node_modules',
|
|
31
|
-
'dsh-session-superagent',
|
|
32
|
-
)
|
|
33
|
-
const DEST = dirFlag >= 0 ? args[dirFlag + 1] : DEFAULT_PROFILE
|
|
34
|
-
|
|
35
|
-
/** 运行时要用的文件(不含 docs / test / scripts 这些开发件)。 */
|
|
36
|
-
const TARGETS = [
|
|
37
|
-
'client.js', 'package.json', 'cordis.patch.yml', 'README.md', 'CHANGELOG.md', 'LICENSE',
|
|
38
|
-
'lib/index.js', 'lib/watch.js', 'lib/redact.js', 'lib/body.js',
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
if (!existsSync(DEST)) {
|
|
42
|
-
console.error('找不到 profile 安装目录:' + DEST)
|
|
43
|
-
process.exit(1)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const drift = { missing: [], broken: [], stale: [] }
|
|
47
|
-
let linked = 0
|
|
48
|
-
|
|
49
|
-
for (const rel of TARGETS) {
|
|
50
|
-
const src = join(ROOT, rel)
|
|
51
|
-
const dst = join(DEST, rel)
|
|
52
|
-
if (!existsSync(src)) continue
|
|
53
|
-
if (!existsSync(dst)) {
|
|
54
|
-
drift.missing.push(rel)
|
|
55
|
-
if (!CHECK) {
|
|
56
|
-
mkdirSync(dirname(dst), { recursive: true })
|
|
57
|
-
try { linkSync(src, dst); linked += 1; console.log('linked ' + rel) } catch (error) {
|
|
58
|
-
console.log('link 失败:' + rel + ' — ' + (error && error.message))
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
const same = statSync(src).ino === statSync(dst).ino
|
|
64
|
-
if (same) continue
|
|
65
|
-
drift.broken.push(rel)
|
|
66
|
-
if (REPAIR && !CHECK) {
|
|
67
|
-
try {
|
|
68
|
-
unlinkSync(dst)
|
|
69
|
-
linkSync(src, dst)
|
|
70
|
-
linked += 1
|
|
71
|
-
console.log('relinked ' + rel + '(原来断了)')
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.log('重建失败:' + rel + ' — ' + (error && error.message))
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
console.log('注意 ' + rel + '(inode 不同:内容改动不会同步到 profile,用 --repair 重建)')
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 陈旧文件:profile 里有、仓库里已经没有(例如删掉的模块)
|
|
81
|
-
for (const name of readdirSync(join(DEST, 'lib')).filter((f) => f.endsWith('.js'))) {
|
|
82
|
-
if (!existsSync(join(ROOT, 'lib', name))) {
|
|
83
|
-
drift.stale.push('lib/' + name)
|
|
84
|
-
if (REPAIR && !CHECK) {
|
|
85
|
-
try { unlinkSync(join(DEST, 'lib', name)); console.log('pruned lib/' + name) } catch {}
|
|
86
|
-
} else {
|
|
87
|
-
console.log('陈旧 lib/' + name + '(仓库已删除,profile 里还在)')
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// lib/ 里每个相对 import 都必须能解析(这正是事故 1 的直接原因)
|
|
93
|
-
const libDir = join(ROOT, 'lib')
|
|
94
|
-
const missingImports = []
|
|
95
|
-
for (const name of readdirSync(libDir)) {
|
|
96
|
-
if (!name.endsWith('.js')) continue
|
|
97
|
-
const code = readFileSync(join(libDir, name), 'utf8')
|
|
98
|
-
for (const line of code.split(String.fromCharCode(10))) {
|
|
99
|
-
const trimmed = line.trim()
|
|
100
|
-
if (!trimmed.startsWith('import ')) continue
|
|
101
|
-
const at = trimmed.lastIndexOf("from '")
|
|
102
|
-
let spec = null
|
|
103
|
-
if (at >= 0) {
|
|
104
|
-
spec = trimmed.slice(at + 6).split("'")[0]
|
|
105
|
-
} else if (trimmed.slice('import '.length).startsWith("'")) {
|
|
106
|
-
spec = trimmed.slice('import '.length + 1).split("'")[0]
|
|
107
|
-
}
|
|
108
|
-
if (spec && spec.startsWith('.') && !existsSync(join(libDir, spec))) {
|
|
109
|
-
missingImports.push(name + ' -> ' + spec)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const drifted = drift.missing.length + drift.broken.length + drift.stale.length
|
|
115
|
-
console.log('---')
|
|
116
|
-
console.log(`目标:${DEST}`)
|
|
117
|
-
console.log(`缺失 ${drift.missing.length} / 断链 ${drift.broken.length} / 陈旧 ${drift.stale.length}` +
|
|
118
|
-
(linked ? `,本次处理 ${linked} 个` : ''))
|
|
119
|
-
|
|
120
|
-
if (missingImports.length > 0) {
|
|
121
|
-
console.error('相对 import 解析失败:' + missingImports.join(', '))
|
|
122
|
-
process.exit(2)
|
|
123
|
-
}
|
|
124
|
-
console.log('相对 import 全部可解析 ✓')
|
|
125
|
-
|
|
126
|
-
if (CHECK && drifted > 0) {
|
|
127
|
-
console.error('profile 与仓库不一致(跑 --repair 修)')
|
|
128
|
-
process.exit(1)
|
|
129
|
-
}
|