@goodandready/dsh-moa 0.2.15 → 0.2.17
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 +7 -0
- package/lib/best-effort.js +38 -0
- package/lib/client.js +172 -4
- package/lib/file-workspace.js +71 -37
- package/lib/history.js +4 -4
- package/lib/index.js +18 -382
- package/lib/moa-candidates.js +225 -0
- package/lib/moa-parser.js +6 -0
- package/lib/moa-runner.js +29 -224
- package/lib/routes.js +412 -0
- package/lib/updater.js +282 -0
- package/package.json +1 -2
- package/docs/README.ru.md +0 -287
- package/docs/README.zh.md +0 -243
- package/docs/design/DESIGN.md +0 -79
- package/docs/plans/47-client-js-split-plan.md +0 -57
- package/docs/plans/55-resilience-plan.md +0 -33
- package/docs/plans/59-power-pack-plan.md +0 -73
- package/docs/plans/63-stability-polish-plan.md +0 -15
package/README.md
CHANGED
|
@@ -282,6 +282,13 @@ npm test
|
|
|
282
282
|
|
|
283
283
|
---
|
|
284
284
|
|
|
285
|
+
|
|
286
|
+
## 🛠️ Internal Tooling & Development
|
|
287
|
+
|
|
288
|
+
For local verification and package integrity validation:
|
|
289
|
+
- `npm test`: runs the full test suite (95 tests)
|
|
290
|
+
- `./deploy.sh`: local infrastructure validation script (verifies package size, identity parity in package.json/cordis.patch.yml/client.js, and tests). Excluded from the published npm package.
|
|
291
|
+
|
|
285
292
|
## 📄 License
|
|
286
293
|
|
|
287
294
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a non-critical side effect; never throw.
|
|
3
|
+
* Logs at debug/warn when a logger is provided or via console.debug.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} label - Context identifier for telemetry / diagnostics
|
|
6
|
+
* @param {Function} fn - Operation to execute
|
|
7
|
+
* @param {object} [logger] - Optional logger instance
|
|
8
|
+
*/
|
|
9
|
+
export function bestEffort(label, fn, logger) {
|
|
10
|
+
const log = (err) => {
|
|
11
|
+
try {
|
|
12
|
+
if (logger && typeof logger.debug === 'function') {
|
|
13
|
+
logger.debug(`[dsh-moa:bestEffort] ${label}:`, err?.message || err)
|
|
14
|
+
} else if (typeof console !== 'undefined' && typeof console.debug === 'function') {
|
|
15
|
+
console.debug(`[dsh-moa:bestEffort] ${label}:`, err?.message || err)
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// Logger itself must never crash the process
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = fn()
|
|
24
|
+
if (result && typeof result.then === 'function') {
|
|
25
|
+
return result.then(
|
|
26
|
+
(value) => value,
|
|
27
|
+
(err) => {
|
|
28
|
+
log(err)
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
} catch (err) {
|
|
35
|
+
log(err)
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -113,6 +113,17 @@ window.__ModuleLoader__.load({
|
|
|
113
113
|
'badge.disabled': 'MoA Disabled',
|
|
114
114
|
'config.enabled_label': 'Enable MoA (slash command and turn routing)',
|
|
115
115
|
'slash.desc': 'Run turn with Mixture of Agents ensemble synthesis (/moa [preset] <prompt>)',
|
|
116
|
+
'updater.title': 'Plugin Updates',
|
|
117
|
+
'updater.desc': 'Check npm registry for new versions and update with one click.',
|
|
118
|
+
'updater.btnCheck': 'Check Updates',
|
|
119
|
+
'updater.btnUpdate': 'Update to v{version}',
|
|
120
|
+
'updater.checking': 'Checking registry...',
|
|
121
|
+
'updater.updating': 'Updating plugin...',
|
|
122
|
+
'updater.current': 'Current version: v{version}',
|
|
123
|
+
'updater.available': 'Update available: v{version}',
|
|
124
|
+
'updater.upToDate': 'Up to date',
|
|
125
|
+
'updater.checkFailed': 'Registry check failed',
|
|
126
|
+
'updater.success': 'Successfully updated to v{version}. Restart DSH to apply.',
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
const zh = {
|
|
@@ -205,6 +216,17 @@ window.__ModuleLoader__.load({
|
|
|
205
216
|
'badge.disabled': 'MoA 已禁用',
|
|
206
217
|
'config.enabled_label': '启用 MoA(斜杠命令与轮次拦截路由)',
|
|
207
218
|
'slash.desc': '使用智能体混合 (MoA) 协同综合运行此轮次 (/moa [预设] <提示词>)',
|
|
219
|
+
'updater.title': '插件在线更新',
|
|
220
|
+
'updater.desc': '检查 npm 镜像源上的最新版本并支持一键无感升级。',
|
|
221
|
+
'updater.btnCheck': '检查更新',
|
|
222
|
+
'updater.btnUpdate': '升级至 v{version}',
|
|
223
|
+
'updater.checking': '正在检查版本...',
|
|
224
|
+
'updater.updating': '正在执行更新...',
|
|
225
|
+
'updater.current': '当前版本:v{version}',
|
|
226
|
+
'updater.available': '发现新版本:v{version}',
|
|
227
|
+
'updater.upToDate': '已是最新版本',
|
|
228
|
+
'updater.checkFailed': '检查版本失败',
|
|
229
|
+
'updater.success': '成功更新至 v{version},请重启 DSH 使其生效。',
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
|
|
@@ -572,6 +594,9 @@ window.__ModuleLoader__.load({
|
|
|
572
594
|
handleSave,
|
|
573
595
|
saveStatus,
|
|
574
596
|
reload,
|
|
597
|
+
updateState,
|
|
598
|
+
checkUpdates,
|
|
599
|
+
runUpdate,
|
|
575
600
|
}) {
|
|
576
601
|
const [selectedPresetName, setSelectedPresetName] = React.useState(defaultPreset || (presets[0] && presets[0].name) || 'default')
|
|
577
602
|
const [isCreating, setIsCreating] = React.useState(false)
|
|
@@ -1381,6 +1406,84 @@ window.__ModuleLoader__.load({
|
|
|
1381
1406
|
)
|
|
1382
1407
|
),
|
|
1383
1408
|
|
|
1409
|
+
/* ── PLUGIN UPDATES ── */
|
|
1410
|
+
React.createElement(
|
|
1411
|
+
'div',
|
|
1412
|
+
{ className: 'moa-section-card' },
|
|
1413
|
+
React.createElement(
|
|
1414
|
+
'div',
|
|
1415
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
|
|
1416
|
+
React.createElement('div', { className: 'moa-sec-title' }, '✨ ' + (t('updater.title') || 'Plugin Updates')),
|
|
1417
|
+
React.createElement(
|
|
1418
|
+
'div',
|
|
1419
|
+
{ style: { display: 'flex', gap: 8 } },
|
|
1420
|
+
React.createElement(
|
|
1421
|
+
'button',
|
|
1422
|
+
{
|
|
1423
|
+
type: 'button',
|
|
1424
|
+
className: 'moa-btn moa-btn-sub',
|
|
1425
|
+
disabled: updateState?.checking || updateState?.updating,
|
|
1426
|
+
onClick: checkUpdates,
|
|
1427
|
+
},
|
|
1428
|
+
updateState?.checking ? (t('updater.checking') || 'Checking...') : (t('updater.btnCheck') || 'Check Updates')
|
|
1429
|
+
),
|
|
1430
|
+
updateState?.updateAvailable
|
|
1431
|
+
? React.createElement(
|
|
1432
|
+
'button',
|
|
1433
|
+
{
|
|
1434
|
+
type: 'button',
|
|
1435
|
+
className: 'moa-btn moa-btn-sub moa-btn-primary',
|
|
1436
|
+
disabled: updateState?.updating,
|
|
1437
|
+
onClick: runUpdate,
|
|
1438
|
+
},
|
|
1439
|
+
updateState?.updating
|
|
1440
|
+
? (t('updater.updating') || 'Updating...')
|
|
1441
|
+
: (t('updater.btnUpdate') || 'Update to v{version}').replace('{version}', updateState.latestVersion || '')
|
|
1442
|
+
)
|
|
1443
|
+
: null
|
|
1444
|
+
)
|
|
1445
|
+
),
|
|
1446
|
+
React.createElement(
|
|
1447
|
+
'div',
|
|
1448
|
+
{ className: 'moa-sec-desc', style: { marginBottom: 8 } },
|
|
1449
|
+
t('updater.desc') || 'Check npm registry for new versions and update with one click.'
|
|
1450
|
+
),
|
|
1451
|
+
React.createElement(
|
|
1452
|
+
'div',
|
|
1453
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, fontSize: 12 } },
|
|
1454
|
+
React.createElement(
|
|
1455
|
+
'span',
|
|
1456
|
+
{ style: { color: 'var(--dsw-alias-label-secondary)' } },
|
|
1457
|
+
(t('updater.current') || 'Current version: v{version}').replace('{version}', updateState?.currentVersion || '0.2.16')
|
|
1458
|
+
),
|
|
1459
|
+
updateState?.checking
|
|
1460
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))' } }, t('updater.checking'))
|
|
1461
|
+
: updateState?.updateAvailable
|
|
1462
|
+
? React.createElement(
|
|
1463
|
+
'span',
|
|
1464
|
+
{ style: { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 600 } },
|
|
1465
|
+
(t('updater.available') || 'Update available: v{version}').replace('{version}', updateState?.latestVersion || '')
|
|
1466
|
+
)
|
|
1467
|
+
: updateState?.currentVersion && !updateState?.error
|
|
1468
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, '✓ ' + (t('updater.upToDate') || 'Up to date'))
|
|
1469
|
+
: null
|
|
1470
|
+
),
|
|
1471
|
+
updateState?.notice
|
|
1472
|
+
? React.createElement(
|
|
1473
|
+
'div',
|
|
1474
|
+
{ className: 'moa-alert-ok', style: { marginTop: 8 } },
|
|
1475
|
+
updateState.notice
|
|
1476
|
+
)
|
|
1477
|
+
: null,
|
|
1478
|
+
updateState?.error
|
|
1479
|
+
? React.createElement(
|
|
1480
|
+
'div',
|
|
1481
|
+
{ className: 'moa-alert-err', style: { marginTop: 8 } },
|
|
1482
|
+
updateState.error
|
|
1483
|
+
)
|
|
1484
|
+
: null
|
|
1485
|
+
),
|
|
1486
|
+
|
|
1384
1487
|
/* ── ENABLED TOGGLE ── */
|
|
1385
1488
|
React.createElement(
|
|
1386
1489
|
'label',
|
|
@@ -1444,7 +1547,7 @@ window.__ModuleLoader__.load({
|
|
|
1444
1547
|
const snapshot = React.useSyncExternalStore
|
|
1445
1548
|
? React.useSyncExternalStore(
|
|
1446
1549
|
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
1447
|
-
React.useCallback(() => (scope ? scope.getSnapshot() : { status: '
|
|
1550
|
+
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'unavailable' }), [scope]),
|
|
1448
1551
|
React.useCallback(() => ({ status: 'loading' }), [])
|
|
1449
1552
|
)
|
|
1450
1553
|
: null
|
|
@@ -1454,6 +1557,65 @@ window.__ModuleLoader__.load({
|
|
|
1454
1557
|
const [defaultPreset, setDefaultPreset] = React.useState('default')
|
|
1455
1558
|
const [availableModels, setAvailableModels] = React.useState([])
|
|
1456
1559
|
const [saveStatus, setSaveStatus] = React.useState('')
|
|
1560
|
+
const [updateState, setUpdateState] = React.useState({
|
|
1561
|
+
checking: false,
|
|
1562
|
+
updating: false,
|
|
1563
|
+
currentVersion: '0.2.16',
|
|
1564
|
+
latestVersion: undefined,
|
|
1565
|
+
updateAvailable: false,
|
|
1566
|
+
notice: null,
|
|
1567
|
+
error: null,
|
|
1568
|
+
})
|
|
1569
|
+
|
|
1570
|
+
const checkUpdates = async () => {
|
|
1571
|
+
setUpdateState((prev) => ({ ...prev, checking: true, error: null, notice: null }))
|
|
1572
|
+
try {
|
|
1573
|
+
const res = await fetch('/api/dsh-moa/update', { cache: 'no-store' })
|
|
1574
|
+
const data = await res.json()
|
|
1575
|
+
if (data && data.currentVersion) {
|
|
1576
|
+
setUpdateState((prev) => ({
|
|
1577
|
+
...prev,
|
|
1578
|
+
checking: false,
|
|
1579
|
+
currentVersion: data.currentVersion,
|
|
1580
|
+
latestVersion: data.latestVersion,
|
|
1581
|
+
updateAvailable: Boolean(data.updateAvailable),
|
|
1582
|
+
error: data.latestCheckFailed ? (t('updater.checkFailed') || 'Registry check failed') : null,
|
|
1583
|
+
}))
|
|
1584
|
+
} else {
|
|
1585
|
+
setUpdateState((prev) => ({ ...prev, checking: false, error: data.error || 'Check failed' }))
|
|
1586
|
+
}
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
setUpdateState((prev) => ({ ...prev, checking: false, error: String(err?.message || err) }))
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const runUpdate = async () => {
|
|
1593
|
+
setUpdateState((prev) => ({ ...prev, updating: true, error: null, notice: null }))
|
|
1594
|
+
try {
|
|
1595
|
+
const res = await fetch('/api/dsh-moa/update', {
|
|
1596
|
+
method: 'POST',
|
|
1597
|
+
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' },
|
|
1598
|
+
})
|
|
1599
|
+
const data = await res.json()
|
|
1600
|
+
if (data && data.updated) {
|
|
1601
|
+
setUpdateState((prev) => ({
|
|
1602
|
+
...prev,
|
|
1603
|
+
updating: false,
|
|
1604
|
+
updateAvailable: false,
|
|
1605
|
+
currentVersion: data.updatedVersion || data.latestVersion,
|
|
1606
|
+
notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}', data.updatedVersion || data.latestVersion),
|
|
1607
|
+
}))
|
|
1608
|
+
} else {
|
|
1609
|
+
setUpdateState((prev) => ({
|
|
1610
|
+
...prev,
|
|
1611
|
+
updating: false,
|
|
1612
|
+
error: data.error || data.message || 'Update failed',
|
|
1613
|
+
}))
|
|
1614
|
+
}
|
|
1615
|
+
} catch (err) {
|
|
1616
|
+
setUpdateState((prev) => ({ ...prev, updating: false, error: String(err?.message || err) }))
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1457
1619
|
const [enabled, setEnabled] = React.useState(true)
|
|
1458
1620
|
const [hostStatus, setHostStatus] = React.useState('connecting')
|
|
1459
1621
|
const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
|
|
@@ -1461,8 +1623,11 @@ window.__ModuleLoader__.load({
|
|
|
1461
1623
|
const [leaderboardPresetFilter, setLeaderboardPresetFilter] = React.useState('all')
|
|
1462
1624
|
|
|
1463
1625
|
React.useEffect(() => {
|
|
1464
|
-
if (!scope || !snapshot)
|
|
1465
|
-
|
|
1626
|
+
if (!scope || !snapshot) {
|
|
1627
|
+
setStatus('unavailable')
|
|
1628
|
+
return
|
|
1629
|
+
}
|
|
1630
|
+
const snapStatus = snapshot.status || 'unavailable'
|
|
1466
1631
|
if (snapStatus === 'ready') {
|
|
1467
1632
|
const val = snapshot.value || {}
|
|
1468
1633
|
if (Array.isArray(val.presets)) setPresets(val.presets)
|
|
@@ -1616,6 +1781,9 @@ window.__ModuleLoader__.load({
|
|
|
1616
1781
|
handleSave,
|
|
1617
1782
|
saveStatus,
|
|
1618
1783
|
reload,
|
|
1784
|
+
updateState,
|
|
1785
|
+
checkUpdates,
|
|
1786
|
+
runUpdate,
|
|
1619
1787
|
}
|
|
1620
1788
|
}
|
|
1621
1789
|
|
|
@@ -1730,7 +1898,7 @@ window.__ModuleLoader__.load({
|
|
|
1730
1898
|
return () => {
|
|
1731
1899
|
for (const off of undo) {
|
|
1732
1900
|
if (typeof off === 'function') {
|
|
1733
|
-
try { off() } catch {}
|
|
1901
|
+
try { off() } catch (offErr) { /* unsubscribe best-effort */ }
|
|
1734
1902
|
}
|
|
1735
1903
|
}
|
|
1736
1904
|
}
|
package/lib/file-workspace.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bestEffort } from './best-effort.js'
|
|
1
2
|
import vm from 'node:vm'
|
|
2
3
|
import fs from 'node:fs/promises'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -94,16 +95,25 @@ function sanitizePath(filePath) {
|
|
|
94
95
|
* Formats collected project files into structured Markdown code blocks
|
|
95
96
|
* suitable for inclusion in candidate system prompts.
|
|
96
97
|
*/
|
|
97
|
-
export function formatProjectContext(projectFiles = []) {
|
|
98
|
+
export function formatProjectContext(projectFiles = [], options = {}) {
|
|
98
99
|
if (!Array.isArray(projectFiles) || projectFiles.length === 0) return ''
|
|
100
|
+
const { skippedFiles = 0, skippedList = [] } = options
|
|
99
101
|
const fence = '```'
|
|
100
|
-
|
|
102
|
+
let formatted = projectFiles
|
|
101
103
|
.map((f) => `### File: ${f.relativePath}\n${fence}\n${f.content}\n${fence}`)
|
|
102
104
|
.join('\n\n')
|
|
105
|
+
|
|
106
|
+
if (skippedFiles > 0) {
|
|
107
|
+
const preview = skippedList.slice(0, 5).join(', ')
|
|
108
|
+
const more = skippedList.length > 5 ? ` and ${skippedList.length - 5} more` : ''
|
|
109
|
+
const warning = `> ⚠️ **Workspace Scan Notice:** ${skippedFiles} workspace file(s) were skipped or truncated due to size/budget limits or read errors (${preview}${more}). The workspace context below is partial.`
|
|
110
|
+
formatted = `${warning}\n\n${formatted}`
|
|
111
|
+
}
|
|
112
|
+
return formatted
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
106
|
-
if (!baseDir) return { files: [], totalChars: 0 }
|
|
116
|
+
if (!baseDir) return { files: [], totalChars: 0, skippedFiles: 0, skippedList: [] }
|
|
107
117
|
|
|
108
118
|
const ignoreDirs = new Set(['node_modules', '.git', '.moa', '.worktrees', 'dist', 'build', '.dsh', '.next', '.nuxt', '.cache', 'coverage', '.turbo', '.idea', '.vscode'])
|
|
109
119
|
const codeExts = new Set([
|
|
@@ -114,43 +124,66 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
|
|
|
114
124
|
const collected = []
|
|
115
125
|
let totalChars = 0
|
|
116
126
|
|
|
127
|
+
let skippedFiles = 0
|
|
128
|
+
const skippedList = []
|
|
129
|
+
|
|
117
130
|
async function scan(currentDir, relPrefix = '') {
|
|
118
131
|
if (totalChars >= maxCharBudget) return
|
|
132
|
+
let list
|
|
119
133
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
list = await fs.readdir(currentDir, { withFileTypes: true })
|
|
135
|
+
} catch (readDirErr) {
|
|
136
|
+
skippedFiles++
|
|
137
|
+
skippedList.push(relPrefix || '.')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
for (const item of list) {
|
|
142
|
+
// Skip all dotfiles, including .env and .env.* variants: their
|
|
143
|
+
// contents must never be shipped to third-party model providers.
|
|
144
|
+
if (item.name.startsWith('.')) continue
|
|
129
145
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
|
|
147
|
+
const full = path.join(currentDir, item.name)
|
|
148
|
+
|
|
149
|
+
if (totalChars >= maxCharBudget) {
|
|
150
|
+
if (item.isFile() && codeExts.has(path.extname(item.name).toLowerCase())) {
|
|
151
|
+
skippedFiles++
|
|
152
|
+
skippedList.push(`${rel} (budget limit)`)
|
|
153
|
+
}
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (item.isDirectory()) {
|
|
158
|
+
if (!ignoreDirs.has(item.name)) {
|
|
159
|
+
await scan(full, rel)
|
|
160
|
+
}
|
|
161
|
+
} else if (item.isFile()) {
|
|
162
|
+
const ext = path.extname(item.name).toLowerCase()
|
|
163
|
+
if (codeExts.has(ext)) {
|
|
164
|
+
try {
|
|
165
|
+
const stat = await fs.stat(full)
|
|
166
|
+
if (stat.size > 100000) {
|
|
167
|
+
skippedFiles++
|
|
168
|
+
skippedList.push(`${rel} (>100KB)`)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
const content = await fs.readFile(full, 'utf8')
|
|
172
|
+
const budgetRemaining = maxCharBudget - totalChars
|
|
173
|
+
const truncated = content.length > budgetRemaining ? content.slice(0, budgetRemaining) + '\n... [truncated]' : content
|
|
174
|
+
totalChars += truncated.length
|
|
175
|
+
collected.push({ relativePath: rel, content: truncated, size: stat.size })
|
|
176
|
+
} catch (fileErr) {
|
|
177
|
+
skippedFiles++
|
|
178
|
+
skippedList.push(rel)
|
|
146
179
|
}
|
|
147
180
|
}
|
|
148
181
|
}
|
|
149
|
-
}
|
|
182
|
+
}
|
|
150
183
|
}
|
|
151
184
|
|
|
152
185
|
await scan(baseDir)
|
|
153
|
-
return { files: collected, totalChars }
|
|
186
|
+
return { files: collected, totalChars, skippedFiles, skippedList }
|
|
154
187
|
}
|
|
155
188
|
|
|
156
189
|
/**
|
|
@@ -224,7 +257,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
|
|
|
224
257
|
const entries = await getFilesRecursively(winningDir)
|
|
225
258
|
if (entries.length === 0) {
|
|
226
259
|
if (!options.keepMoa) {
|
|
227
|
-
|
|
260
|
+
await bestEffort('clean moaRoot on empty candidate', () => fs.rm(moaRoot, { recursive: true, force: true }))
|
|
228
261
|
}
|
|
229
262
|
return []
|
|
230
263
|
}
|
|
@@ -247,7 +280,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
|
|
|
247
280
|
return promoted
|
|
248
281
|
} catch (err) {
|
|
249
282
|
if (!options.keepMoa) {
|
|
250
|
-
|
|
283
|
+
await bestEffort('clean moaRoot on promote error', () => fs.rm(moaRoot, { recursive: true, force: true }))
|
|
251
284
|
}
|
|
252
285
|
return []
|
|
253
286
|
}
|
|
@@ -259,9 +292,7 @@ export async function promoteCandidateWorkspace(baseDir, winningIndex, options =
|
|
|
259
292
|
export async function cleanMoaWorkspaces(baseDir) {
|
|
260
293
|
if (!baseDir) return
|
|
261
294
|
const moaRoot = path.join(baseDir, '.moa')
|
|
262
|
-
|
|
263
|
-
await fs.rm(moaRoot, { recursive: true, force: true })
|
|
264
|
-
} catch {}
|
|
295
|
+
await bestEffort('cleanMoaWorkspaces', () => fs.rm(moaRoot, { recursive: true, force: true }))
|
|
265
296
|
}
|
|
266
297
|
|
|
267
298
|
async function getFilesRecursively(dir) {
|
|
@@ -269,13 +300,14 @@ async function getFilesRecursively(dir) {
|
|
|
269
300
|
try {
|
|
270
301
|
const stat = await fs.stat(dir)
|
|
271
302
|
if (!stat.isDirectory()) return []
|
|
272
|
-
} catch {
|
|
303
|
+
} catch (statErr) {
|
|
273
304
|
return []
|
|
274
305
|
}
|
|
275
306
|
|
|
276
307
|
async function scan(current, relPrefix = '') {
|
|
308
|
+
let list
|
|
277
309
|
try {
|
|
278
|
-
|
|
310
|
+
list = await fs.readdir(current, { withFileTypes: true })
|
|
279
311
|
for (const item of list) {
|
|
280
312
|
const rel = relPrefix ? `${relPrefix}/${item.name}` : item.name
|
|
281
313
|
const full = path.join(current, item.name)
|
|
@@ -285,7 +317,9 @@ async function getFilesRecursively(dir) {
|
|
|
285
317
|
results.push(rel)
|
|
286
318
|
}
|
|
287
319
|
}
|
|
288
|
-
} catch {
|
|
320
|
+
} catch (readErr) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
289
323
|
}
|
|
290
324
|
await scan(dir)
|
|
291
325
|
return results
|
package/lib/history.js
CHANGED
|
@@ -15,7 +15,7 @@ const DEFAULT_HISTORY_FILE = path.join(DEFAULT_HISTORY_DIR, 'moa-history.jsonl')
|
|
|
15
15
|
/** Max history file size before rotation (10 MB) */
|
|
16
16
|
export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
function getHistoryFilePath(customDir) {
|
|
19
19
|
if (customDir) {
|
|
20
20
|
return path.join(customDir, '.moa-history.jsonl')
|
|
21
21
|
}
|
|
@@ -33,7 +33,7 @@ export function rotateHistoryFileIfNeeded(filePath = DEFAULT_HISTORY_FILE, maxBy
|
|
|
33
33
|
if (stat.size >= maxBytes) {
|
|
34
34
|
const backupPath = `${filePath}.1`
|
|
35
35
|
if (fs.existsSync(backupPath)) {
|
|
36
|
-
try { fs.unlinkSync(backupPath) } catch {}
|
|
36
|
+
try { fs.unlinkSync(backupPath) } catch (unlinkErr) { /* backup unlink best-effort */ }
|
|
37
37
|
}
|
|
38
38
|
fs.renameSync(filePath, backupPath)
|
|
39
39
|
return true
|
|
@@ -241,7 +241,7 @@ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
|
|
|
241
241
|
try {
|
|
242
242
|
const item = JSON.parse(lines[i])
|
|
243
243
|
if (item && item.id === runId) return item
|
|
244
|
-
} catch {}
|
|
244
|
+
} catch (parseErr) { /* corrupt line skipped */ }
|
|
245
245
|
}
|
|
246
246
|
return null
|
|
247
247
|
} catch {
|
|
@@ -270,7 +270,7 @@ export function exportMoaHistory(filePath = DEFAULT_HISTORY_FILE, options = {})
|
|
|
270
270
|
if (!item) continue
|
|
271
271
|
if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
|
|
272
272
|
records.push(item)
|
|
273
|
-
} catch {}
|
|
273
|
+
} catch (parseErr) { /* corrupt line skipped */ }
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
if (format === 'csv') {
|