zames_pro 2.75.5 → 2.76.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/CHANGELOG.md +25 -0
- package/README.md +10 -4
- package/dist/agent-loop.js +61 -1
- package/dist/browser.js +40 -8
- package/dist/commands.js +0 -1
- package/dist/config.js +8 -3
- package/dist/context.js +99 -0
- package/dist/deepseek-ui.js +1 -3
- package/dist/i18n/common.js +13 -16
- package/dist/i18n/config.js +2 -2
- package/dist/i18n/help.js +48 -6
- package/dist/i18n/spinner.js +7 -1
- package/dist/index.js +10 -50
- package/dist/net-capture.js +43 -11
- package/dist/provider-handoff.js +0 -4
- package/dist/qwen-ui.js +0 -8
- package/dist/run-task.js +24 -1
- package/dist/shell.js +0 -6
- package/dist/system-prompt.js +52 -2
- package/dist/tools.js +33 -0
- package/dist/tui/AskQuestion.js +108 -0
- package/dist/tui/FullScreenApp.js +2 -1
- package/dist/tui/InputLine.js +0 -3
- package/dist/tui/StatusBar.js +0 -9
- package/dist/tui/TuiEditor.js +4 -43
- package/dist/tui/edit.js +13 -22
- package/dist/tui/keys.js +12 -14
- package/dist/tui/run-ask.js +22 -0
- package/dist/tui/store.js +0 -2
- package/dist/tui/widgets.js +3 -9
- package/package.json +2 -2
- package/scripts/build-readme.mts +0 -101
- package/scripts/changelog-draft.mts +0 -93
- package/scripts/clean-dist.mts +0 -17
- package/scripts/coverage-gate.mts +0 -112
- package/scripts/ctd-live-driver.mts +0 -51
- package/scripts/ctd-live.tsx +0 -33
- package/scripts/escd-live-driver.mts +0 -37
- package/scripts/eslint-ts6.mts +0 -58
- package/scripts/inline-status-driver.mts +0 -31
- package/scripts/inline-status.tsx +0 -33
- package/scripts/mouse-churn.mts +0 -34
- package/scripts/qwen-models-smoke.mts +0 -117
- package/scripts/qwen-smoke.mts +0 -221
- package/scripts/release.mts +0 -199
- package/scripts/render-demo.mts +0 -154
- package/scripts/render-how-it-works.mts +0 -229
- package/scripts/ru-shot.mts +0 -20
- package/scripts/sched-smoke.mts +0 -150
- package/scripts/self-smoke.mts +0 -309
- package/scripts/tui-comp-shot.mts +0 -63
- package/scripts/tui-components.tsx +0 -62
- package/scripts/tui-config-driver.mts +0 -82
- package/scripts/tui-config-probe.tsx +0 -74
- package/scripts/tui-edit-driver.mts +0 -100
- package/scripts/tui-edit-probe.tsx +0 -38
- package/scripts/tui-editor-probe.tsx +0 -24
- package/scripts/tui-input-driver.mts +0 -54
- package/scripts/tui-input-shot.tsx +0 -59
- package/scripts/tui-log-driver.mts +0 -35
- package/scripts/tui-log-shot.tsx +0 -48
- package/scripts/tui-longpaste-driver.mts +0 -56
- package/scripts/tui-longpaste.tsx +0 -35
- package/scripts/tui-manyselect-shot.mts +0 -34
- package/scripts/tui-manyselect.tsx +0 -17
- package/scripts/tui-mouse-churn-probe.tsx +0 -28
- package/scripts/tui-paste-driver.mts +0 -88
- package/scripts/tui-paste-kinds.tsx +0 -76
- package/scripts/tui-pastetest.mts +0 -38
- package/scripts/tui-probe.tsx +0 -62
- package/scripts/tui-repro.tsx +0 -64
- package/scripts/tui-resize.mts +0 -70
- package/scripts/tui-scroll-driver.mts +0 -53
- package/scripts/tui-scroll-probe.tsx +0 -46
- package/scripts/tui-shot.mts +0 -106
- package/scripts/tui-status-driver.mts +0 -24
- package/scripts/tui-status-shot.tsx +0 -27
- package/scripts/tui-submit.tsx +0 -57
- package/scripts/tui-wheel-driver.mts +0 -61
- package/scripts/tui-wheel-probe.tsx +0 -48
- package/scripts/tui-widgets-driver.mts +0 -20
- package/scripts/tui-widgets.tsx +0 -44
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { render } from 'ink'
|
|
3
|
-
import { TuiStore } from '../src/tui/store.js'
|
|
4
|
-
import { TuiApp } from '../src/tui/TuiApp.js'
|
|
5
|
-
import { initialEditState } from '../src/tui/edit.js'
|
|
6
|
-
const store = new TuiStore()
|
|
7
|
-
store.set({
|
|
8
|
-
title: 'z',
|
|
9
|
-
version: '1',
|
|
10
|
-
workdir: '/tmp',
|
|
11
|
-
prompt: '> ',
|
|
12
|
-
locale: 'en',
|
|
13
|
-
commands: [],
|
|
14
|
-
edit: { ...initialEditState(), buf: 'abc' },
|
|
15
|
-
statusText: 'STATUS HERE',
|
|
16
|
-
})
|
|
17
|
-
const p = {
|
|
18
|
-
onSubmit: () => {},
|
|
19
|
-
onEscape: () => {},
|
|
20
|
-
onCtrlC: () => {},
|
|
21
|
-
onCopyCode: () => {},
|
|
22
|
-
onAttach: () => null,
|
|
23
|
-
onClipboard: () => Promise.resolve(null),
|
|
24
|
-
onHistoryChange: () => {},
|
|
25
|
-
onClearScreen: () => {},
|
|
26
|
-
attachments: [],
|
|
27
|
-
onPastesChange: () => {},
|
|
28
|
-
}
|
|
29
|
-
render(React.createElement(TuiApp, { store, inputProps: p }), {
|
|
30
|
-
exitOnCtrlC: false,
|
|
31
|
-
patchConsole: false,
|
|
32
|
-
})
|
|
33
|
-
setInterval(() => {}, 1 << 30)
|
package/scripts/mouse-churn.mts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Prove the mouse-mode churn is ZERO: mount the REAL TuiEditor, force several
|
|
2
|
-
// store updates (like the 1s badge refresh does), and COUNT how many times the
|
|
3
|
-
// raw stdout receives the mouse-mode sequences. With the effect depending on
|
|
4
|
-
// an inline onWheel, the count grew per render (the wheel-vs-history bug);
|
|
5
|
-
// with the ref fix, each mode byte is written ONCE.
|
|
6
|
-
//
|
|
7
|
-
// Exit code 0 = OK, 1 = churn detected. Prints a one-line verdict so the pty
|
|
8
|
-
// test can assert on it.
|
|
9
|
-
|
|
10
|
-
import { spawn } from 'node:child_process'
|
|
11
|
-
import path from 'node:path'
|
|
12
|
-
|
|
13
|
-
const child = spawn(
|
|
14
|
-
'script',
|
|
15
|
-
['-qec', 'npx tsx scripts/tui-mouse-churn-probe.tsx', '/dev/null'],
|
|
16
|
-
{
|
|
17
|
-
cwd: path.resolve('.'),
|
|
18
|
-
env: { ...process.env, COLUMNS: '80', LIMES: '24' },
|
|
19
|
-
},
|
|
20
|
-
)
|
|
21
|
-
let out = ''
|
|
22
|
-
child.stdout.on('data', (d) => (out += d.toString('utf-8')))
|
|
23
|
-
child.stderr.on('data', (d) => (out += d.toString('utf-8')))
|
|
24
|
-
setTimeout(() => child.kill('SIGTERM'), 8000)
|
|
25
|
-
child.on('close', () => {
|
|
26
|
-
const count = (seq: string): number => out.split(seq).length - 1
|
|
27
|
-
const on = count('\u001b[?1000h')
|
|
28
|
-
const altOff = count('\u001b[?1007l')
|
|
29
|
-
const altOn = count('\u001b[?1007h')
|
|
30
|
-
console.log('MOUSE_CHURN 1000h=' + on + ' 1007l=' + altOff + ' 1007h=' + altOn)
|
|
31
|
-
const ok = on === 1 && altOff === 1 && altOn === 0
|
|
32
|
-
console.log(ok ? 'MOUSE_CHURN_OK' : 'MOUSE_CHURN_FAIL')
|
|
33
|
-
process.exit(ok ? 0 : 1)
|
|
34
|
-
})
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// Qwen model-switching self-smoke: launch a REAL isolated QwenBrowser, list
|
|
2
|
-
// the model catalogue, then SWITCH to several models and ask a question with
|
|
3
|
-
// each — the operator explicitly asked that model switching be exercised.
|
|
4
|
-
//
|
|
5
|
-
// Isolation mirrors qwen-smoke.mts: a throwaway HOME + a read-only COPY of the
|
|
6
|
-
// real qwen-profile (the live profile is never opened).
|
|
7
|
-
//
|
|
8
|
-
// Usage: npx tsx scripts/qwen-models-smoke.mts [--headed] [--all]
|
|
9
|
-
|
|
10
|
-
import fs from 'fs'
|
|
11
|
-
import path from 'path'
|
|
12
|
-
import os from 'os'
|
|
13
|
-
|
|
14
|
-
const HEADED = process.argv.includes('--headed')
|
|
15
|
-
const ALL = process.argv.includes('--all')
|
|
16
|
-
const REAL_HOME = os.homedir()
|
|
17
|
-
const PROFILE_SRC = path.join(REAL_HOME, '.zames', 'qwen-profile')
|
|
18
|
-
|
|
19
|
-
if (!process.env.ZAMES_QWEN_MODELS_HOME) {
|
|
20
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-qwen-models-'))
|
|
21
|
-
process.env.HOME = tmp
|
|
22
|
-
process.env.ZAMES_QWEN_MODELS_HOME = tmp
|
|
23
|
-
fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
|
|
24
|
-
fs.mkdirSync(path.join(tmp, '.cache'), { recursive: true })
|
|
25
|
-
const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
|
|
26
|
-
if (fs.existsSync(realCfg)) {
|
|
27
|
-
fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
|
|
28
|
-
}
|
|
29
|
-
const realPw = path.join(REAL_HOME, '.cache', 'ms-playwright')
|
|
30
|
-
if (fs.existsSync(realPw)) {
|
|
31
|
-
fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
|
|
32
|
-
}
|
|
33
|
-
if (fs.existsSync(PROFILE_SRC)) {
|
|
34
|
-
fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'qwen-profile'), {
|
|
35
|
-
recursive: true,
|
|
36
|
-
filter: (src) => !/^Singleton/.test(path.basename(src)),
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const { QwenBrowser } = await import('../src/qwen-browser.ts')
|
|
42
|
-
const { loadConfig } = await import('../src/config.ts')
|
|
43
|
-
const cfg = loadConfig()
|
|
44
|
-
|
|
45
|
-
const results: Array<{ name: string; ok: boolean; note?: string }> = []
|
|
46
|
-
const record = (name: string, ok: boolean, note?: string): void => {
|
|
47
|
-
results.push({ name, ok, note })
|
|
48
|
-
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const browser = new QwenBrowser({
|
|
52
|
-
headless: !HEADED,
|
|
53
|
-
debug: false,
|
|
54
|
-
channel: cfg.browserChannel,
|
|
55
|
-
auth: cfg.browser.auth,
|
|
56
|
-
locale: 'ru',
|
|
57
|
-
minSendIntervalMs: 0,
|
|
58
|
-
answerTimeoutMs: 180000,
|
|
59
|
-
webSearch: false,
|
|
60
|
-
deepThinking: false,
|
|
61
|
-
})
|
|
62
|
-
browser.onNotice = (t: string) => console.log(' [notice] ' + t)
|
|
63
|
-
|
|
64
|
-
async function main(): Promise<void> {
|
|
65
|
-
await browser.launch()
|
|
66
|
-
const timedOut = await Promise.race([
|
|
67
|
-
browser.waitForLogin().then(() => false),
|
|
68
|
-
new Promise<boolean>((r) => setTimeout(() => r(true), 60000)),
|
|
69
|
-
])
|
|
70
|
-
if (timedOut) {
|
|
71
|
-
record('login', false, 'timed out')
|
|
72
|
-
await browser.close().catch(() => {})
|
|
73
|
-
process.exit(2)
|
|
74
|
-
}
|
|
75
|
-
record('login', true)
|
|
76
|
-
|
|
77
|
-
const models = await browser.listModels()
|
|
78
|
-
record(
|
|
79
|
-
'listModels',
|
|
80
|
-
models.length > 0,
|
|
81
|
-
'n=' + models.length + ' ' + models.map((m) => m.id).join(','),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
// Switch through a few models and ask with each. Prefer distinct vendors so
|
|
85
|
-
// the dropdown selection really changes the backend.
|
|
86
|
-
const wanted = ALL ? models : models.slice(0, 3)
|
|
87
|
-
for (const m of wanted) {
|
|
88
|
-
try {
|
|
89
|
-
await browser.newChat()
|
|
90
|
-
browser.model = m.id
|
|
91
|
-
const ans = await browser.ask('Reply with exactly: OK', { agent: true })
|
|
92
|
-
const ok = /OK/i.test(ans)
|
|
93
|
-
record('model:' + m.id, ok, ok ? '' : 'got: ' + ans.slice(0, 60))
|
|
94
|
-
} catch (e) {
|
|
95
|
-
record('model:' + m.id, false, (e as Error).message)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
await browser.close().catch(() => {})
|
|
100
|
-
const failed = results.filter((r) => !r.ok)
|
|
101
|
-
console.log(
|
|
102
|
-
'\nqwen-models-smoke: ' +
|
|
103
|
-
(results.length - failed.length) +
|
|
104
|
-
'/' +
|
|
105
|
-
results.length +
|
|
106
|
-
' passed',
|
|
107
|
-
)
|
|
108
|
-
if (!process.env.ZAMES_QWEN_MODELS_KEEP) {
|
|
109
|
-
fs.rmSync(process.env.ZAMES_QWEN_MODELS_HOME as string, {
|
|
110
|
-
recursive: true,
|
|
111
|
-
force: true,
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
process.exit(failed.length ? 1 : 0)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
void main()
|
package/scripts/qwen-smoke.mts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// Qwen (chat.qwen.ai) self-smoke: a REAL isolated QwenBrowser instance against
|
|
2
|
-
// the live site, to catch integration regressions (login, send, answer) that
|
|
3
|
-
// unit tests cannot.
|
|
4
|
-
//
|
|
5
|
-
// Isolation: HOME is pointed at a throwaway dir BEFORE importing any project
|
|
6
|
-
// module (they compute ~/.zames paths at load). The real Qwen profile
|
|
7
|
-
// (~/.zames/qwen-profile) is COPIED read-only into the temp HOME so the
|
|
8
|
-
// browser starts already signed in; the live profile is NEVER opened. The
|
|
9
|
-
// Playwright browser cache is symlinked read-only. Nothing in the operator's
|
|
10
|
-
// ~/.zames is written.
|
|
11
|
-
//
|
|
12
|
-
// Usage: npx tsx scripts/qwen-smoke.mts [--headed] [--no-reuse-profile]
|
|
13
|
-
// Exit code 0 = all scenarios passed; 2 = login failed (could not start).
|
|
14
|
-
|
|
15
|
-
import fs from 'fs'
|
|
16
|
-
import path from 'path'
|
|
17
|
-
import os from 'os'
|
|
18
|
-
|
|
19
|
-
const HEADED = process.argv.includes('--headed')
|
|
20
|
-
const REUSE_PROFILE = !process.argv.includes('--no-reuse-profile')
|
|
21
|
-
const REAL_HOME = os.homedir()
|
|
22
|
-
const PROFILE_SRC = path.join(REAL_HOME, '.zames', 'qwen-profile')
|
|
23
|
-
|
|
24
|
-
if (!process.env.ZAMES_QWEN_SMOKE_HOME) {
|
|
25
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-qwen-smoke-'))
|
|
26
|
-
process.env.HOME = tmp
|
|
27
|
-
process.env.ZAMES_QWEN_SMOKE_HOME = tmp
|
|
28
|
-
fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
|
|
29
|
-
fs.mkdirSync(path.join(tmp, '.cache'), { recursive: true })
|
|
30
|
-
const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
|
|
31
|
-
if (fs.existsSync(realCfg)) {
|
|
32
|
-
fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
|
|
33
|
-
}
|
|
34
|
-
const realPw = path.join(REAL_HOME, '.cache', 'ms-playwright')
|
|
35
|
-
if (fs.existsSync(realPw)) {
|
|
36
|
-
fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
|
|
37
|
-
}
|
|
38
|
-
if (REUSE_PROFILE && fs.existsSync(PROFILE_SRC)) {
|
|
39
|
-
fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'qwen-profile'), {
|
|
40
|
-
recursive: true,
|
|
41
|
-
filter: (src) => !/^Singleton/.test(path.basename(src)),
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const SMOKE_HOME = process.env.ZAMES_QWEN_SMOKE_HOME as string
|
|
46
|
-
|
|
47
|
-
// --- project modules (dynamic: they read HOME at import) --------------------
|
|
48
|
-
const { QwenBrowser } = await import('../src/qwen-browser.ts')
|
|
49
|
-
const { loadConfig } = await import('../src/config.ts')
|
|
50
|
-
|
|
51
|
-
const cfg = loadConfig()
|
|
52
|
-
|
|
53
|
-
interface SmokeResult {
|
|
54
|
-
name: string
|
|
55
|
-
ok: boolean
|
|
56
|
-
note?: string
|
|
57
|
-
}
|
|
58
|
-
const results: SmokeResult[] = []
|
|
59
|
-
let loginFailed = false
|
|
60
|
-
const record = (name: string, ok: boolean, note?: string): void => {
|
|
61
|
-
results.push({ name, ok, note })
|
|
62
|
-
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const browser = new QwenBrowser({
|
|
66
|
-
headless: !HEADED,
|
|
67
|
-
debug: process.env.ZAMES_QWEN_SMOKE_DEBUG === '1',
|
|
68
|
-
channel: cfg.browserChannel,
|
|
69
|
-
auth: cfg.browser.auth,
|
|
70
|
-
locale: 'ru',
|
|
71
|
-
minSendIntervalMs: 0,
|
|
72
|
-
answerTimeoutMs: 180000,
|
|
73
|
-
webSearch: false,
|
|
74
|
-
deepThinking: false,
|
|
75
|
-
})
|
|
76
|
-
browser.onNotice = (t: string) => console.log(' [notice] ' + t)
|
|
77
|
-
|
|
78
|
-
async function main() {
|
|
79
|
-
console.log(
|
|
80
|
-
'qwen-smoke: HOME=' +
|
|
81
|
-
SMOKE_HOME +
|
|
82
|
-
' headed=' +
|
|
83
|
-
HEADED +
|
|
84
|
-
' reuse-profile=' +
|
|
85
|
-
REUSE_PROFILE,
|
|
86
|
-
)
|
|
87
|
-
await browser.launch()
|
|
88
|
-
|
|
89
|
-
// Bound the login wait so a cold/expired session fails fast with a clear
|
|
90
|
-
// hint instead of hanging until the caller's tool timeout.
|
|
91
|
-
const loginPromise = browser.waitForLogin()
|
|
92
|
-
const timedOut = await Promise.race([
|
|
93
|
-
loginPromise.then(() => false),
|
|
94
|
-
new Promise<boolean>((r) => setTimeout(() => r(true), 60000)),
|
|
95
|
-
])
|
|
96
|
-
if (timedOut) {
|
|
97
|
-
loginFailed = true
|
|
98
|
-
record('login', false, 'timed out after 60s')
|
|
99
|
-
} else {
|
|
100
|
-
const logged = await browser.isLoggedIn().catch(() => false)
|
|
101
|
-
record('login', logged, logged ? '' : 'not signed in (guest)')
|
|
102
|
-
if (!logged) loginFailed = true
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!loginFailed) {
|
|
106
|
-
// 1) new chat + a SIMPLE ask that needs no tools: the model must answer.
|
|
107
|
-
try {
|
|
108
|
-
await browser.newChat()
|
|
109
|
-
const id = await browser.getCurrentChatId()
|
|
110
|
-
record('newChat', true, 'id=' + (id || '(none)'))
|
|
111
|
-
} catch (e) {
|
|
112
|
-
record('newChat', false, (e as Error).message)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const ans = await browser.ask('Reply with exactly: PONG', {
|
|
117
|
-
agent: true,
|
|
118
|
-
})
|
|
119
|
-
const ok = /PONG/i.test(ans)
|
|
120
|
-
record('ask:simple', ok, ok ? '' : 'got: ' + ans.slice(0, 80))
|
|
121
|
-
} catch (e) {
|
|
122
|
-
record('ask:simple', false, (e as Error).message)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 2) a second send right after the first: guards the send-verify path (the
|
|
126
|
-
// "text entered but send button not pressed" regression).
|
|
127
|
-
try {
|
|
128
|
-
const ans = await browser.ask('Reply with exactly: PONG2', {
|
|
129
|
-
agent: true,
|
|
130
|
-
})
|
|
131
|
-
const ok = /PONG2/i.test(ans)
|
|
132
|
-
record('ask:second-send', ok, ok ? '' : 'got: ' + ans.slice(0, 80))
|
|
133
|
-
} catch (e) {
|
|
134
|
-
record('ask:second-send', false, (e as Error).message)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 3) model catalogue + token usage (best-effort, non-fatal).
|
|
138
|
-
try {
|
|
139
|
-
const models = await browser.listModels()
|
|
140
|
-
record('listModels', models.length > 0, 'n=' + models.length)
|
|
141
|
-
} catch (e) {
|
|
142
|
-
record('listModels', false, (e as Error).message)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const usage = browser.getLastTokenUsage()
|
|
146
|
-
record('tokenUsage', usage !== null, 'usage=' + String(usage))
|
|
147
|
-
|
|
148
|
-
// 4) tool-call emission: the agent-loop depends on the model returning a
|
|
149
|
-
// parseable JSON tool call. Ask for one explicitly and check the PARSER
|
|
150
|
-
// (not the model's prose).
|
|
151
|
-
try {
|
|
152
|
-
const { parseToolCall } = await import('../src/answer-parse.ts')
|
|
153
|
-
const ans = await browser.ask(
|
|
154
|
-
'Call the Read tool for the file package.json. Reply with EXACTLY one ' +
|
|
155
|
-
'JSON tool-call object and nothing else: ' +
|
|
156
|
-
'{"tool":"Read","args":{"path":"..."}}',
|
|
157
|
-
{ agent: true },
|
|
158
|
-
)
|
|
159
|
-
const parsed = parseToolCall(ans)
|
|
160
|
-
const call = Array.isArray(parsed) ? parsed[0] : parsed
|
|
161
|
-
const ok = !!call && call.tool === 'Read'
|
|
162
|
-
record('toolCall', ok, ok ? '' : 'got: ' + ans.slice(0, 100))
|
|
163
|
-
} catch (e) {
|
|
164
|
-
record('toolCall', false, (e as Error).message)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// History restore: after the sends above the chat must have messages.
|
|
168
|
-
try {
|
|
169
|
-
const id = await browser.getCurrentChatId()
|
|
170
|
-
const msgs = id ? await browser.fetchChatMessages(id) : []
|
|
171
|
-
record(
|
|
172
|
-
'history',
|
|
173
|
-
msgs.length > 0,
|
|
174
|
-
'id=' + (id || '-') + ' msgs=' + msgs.length,
|
|
175
|
-
)
|
|
176
|
-
} catch (e) {
|
|
177
|
-
record('history', false, (e as Error).message)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Attachments: a SIGNED-IN session uploads a file; a GUEST cannot, so a
|
|
181
|
-
// guest failure is reported as a SKIP (not a failure) — the guest limit is
|
|
182
|
-
// a site restriction, not a regression.
|
|
183
|
-
try {
|
|
184
|
-
const tmpFile = path.join(SMOKE_HOME, 'zames-smoke-note.txt')
|
|
185
|
-
fs.writeFileSync(tmpFile, 'The secret word is BANANA')
|
|
186
|
-
const failed = await browser._attachFiles([
|
|
187
|
-
{ path: tmpFile, name: 'zames-smoke-note.txt', mime: 'text/plain' },
|
|
188
|
-
])
|
|
189
|
-
const attached = failed.length === 0
|
|
190
|
-
const isGuest = !(await browser.isLoggedIn().catch(() => false))
|
|
191
|
-
if (attached) record('attachments', true, '')
|
|
192
|
-
else if (isGuest)
|
|
193
|
-
record('attachments', true, 'skipped (guest cannot upload)')
|
|
194
|
-
else record('attachments', false, JSON.stringify(failed))
|
|
195
|
-
} catch (e) {
|
|
196
|
-
record('attachments', false, (e as Error).message)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
await browser.close().catch(() => {})
|
|
201
|
-
|
|
202
|
-
const failed = results.filter((r) => !r.ok)
|
|
203
|
-
console.log(
|
|
204
|
-
'\nqwen-smoke: ' +
|
|
205
|
-
(results.length - failed.length) +
|
|
206
|
-
'/' +
|
|
207
|
-
results.length +
|
|
208
|
-
' passed',
|
|
209
|
-
)
|
|
210
|
-
if (!process.env.ZAMES_QWEN_SMOKE_KEEP) {
|
|
211
|
-
fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
|
|
212
|
-
} else {
|
|
213
|
-
console.log('kept HOME: ' + SMOKE_HOME)
|
|
214
|
-
}
|
|
215
|
-
process.exit(loginFailed ? 2 : failed.length ? 1 : 0)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
main().catch((e) => {
|
|
219
|
-
console.error('qwen-smoke crashed:', (e as Error).message)
|
|
220
|
-
process.exit(1)
|
|
221
|
-
})
|
package/scripts/release.mts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import {
|
|
6
|
-
parseConventionalCommit,
|
|
7
|
-
groupCommits,
|
|
8
|
-
renderChangelogDraft,
|
|
9
|
-
} from '../src/changelog.js'
|
|
10
|
-
|
|
11
|
-
// One-command release prep (BACKLOG G4). The manual sequence — bump
|
|
12
|
-
// package.json, write a dated CHANGELOG section, commit `chore: release X.Y.Z`,
|
|
13
|
-
// tag — is easy to get wrong, so this script does the deterministic parts and
|
|
14
|
-
// STOPS before the push. Pushing the branch and the tag is deliberately a
|
|
15
|
-
// separate, explicit step: the tag triggers the npm publish workflow, and that
|
|
16
|
-
// must never happen by accident.
|
|
17
|
-
//
|
|
18
|
-
// Usage:
|
|
19
|
-
// npm run release patch # bug fixes
|
|
20
|
-
// npm run release minor # new features
|
|
21
|
-
// npm run release major # breaking changes
|
|
22
|
-
// npm run release patch --no-tests # skip the local test run (CI still runs)
|
|
23
|
-
// npm run release patch --dry-run # print the plan, change nothing
|
|
24
|
-
//
|
|
25
|
-
// Run via tsx so it can reuse src/changelog.ts (the conventional-commit
|
|
26
|
-
// parser) — a plain .mjs cannot import the TypeScript sources. The other
|
|
27
|
-
// helper scripts are .mts for the same reason; postinstall stays .mjs because
|
|
28
|
-
// it runs before tsx is guaranteed to be installed.
|
|
29
|
-
|
|
30
|
-
const NL = String.fromCharCode(10)
|
|
31
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
32
|
-
|
|
33
|
-
function git(args: string[], allowFail = false): string {
|
|
34
|
-
const r = spawnSync('git', args, { cwd: root, encoding: 'utf-8' })
|
|
35
|
-
if (r.status !== 0 && !allowFail) {
|
|
36
|
-
console.error('git ' + args.join(' ') + ' failed:' + NL + (r.stderr || ''))
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
39
|
-
return (r.stdout || '').toString()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function fail(msg: string): never {
|
|
43
|
-
console.error('release: ' + msg)
|
|
44
|
-
process.exit(1)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function run(cmd: string, args: string[]): void {
|
|
48
|
-
const r = spawnSync(cmd, args, {
|
|
49
|
-
cwd: root,
|
|
50
|
-
stdio: 'inherit',
|
|
51
|
-
shell: process.platform === 'win32',
|
|
52
|
-
})
|
|
53
|
-
if (r.status !== 0) fail(cmd + ' ' + args.join(' ') + ' exited ' + r.status)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const argv = process.argv.slice(2)
|
|
57
|
-
const flags = new Set(argv.filter((a) => a.startsWith('--')))
|
|
58
|
-
const bump = argv.find((a) => !a.startsWith('--'))
|
|
59
|
-
const dryRun = flags.has('--dry-run')
|
|
60
|
-
const skipTests = flags.has('--no-tests')
|
|
61
|
-
|
|
62
|
-
if (bump !== 'patch' && bump !== 'minor' && bump !== 'major') {
|
|
63
|
-
fail('usage: npm run release patch|minor|major [--no-tests] [--dry-run]')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 1. Clean tree: a release commit must contain ONLY the version bump and the
|
|
67
|
-
// changelog section, never half-finished work.
|
|
68
|
-
const dirty = git(['status', '--porcelain']).trim()
|
|
69
|
-
if (dirty) {
|
|
70
|
-
fail('working tree is not clean:' + NL + dirty)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 2. Gates. The git hooks run these on commit/push too, but a release should
|
|
74
|
-
// fail EARLY, before package.json/CHANGELOG are touched.
|
|
75
|
-
if (!skipTests) {
|
|
76
|
-
run('npm', ['run', 'typecheck'])
|
|
77
|
-
run('npm', ['test'])
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 3. Compute the next version from package.json.
|
|
81
|
-
const pkgPath = path.join(root, 'package.json')
|
|
82
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {
|
|
83
|
-
version: string
|
|
84
|
-
}
|
|
85
|
-
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(pkg.version)
|
|
86
|
-
if (!m) fail('package.json version is not a plain x.y.z: ' + pkg.version)
|
|
87
|
-
let [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
88
|
-
if (bump === 'major') {
|
|
89
|
-
major += 1
|
|
90
|
-
minor = 0
|
|
91
|
-
patch = 0
|
|
92
|
-
} else if (bump === 'minor') {
|
|
93
|
-
minor += 1
|
|
94
|
-
patch = 0
|
|
95
|
-
} else {
|
|
96
|
-
patch += 1
|
|
97
|
-
}
|
|
98
|
-
const next = major + '.' + minor + '.' + patch
|
|
99
|
-
const tag = 'v' + next
|
|
100
|
-
|
|
101
|
-
if (git(['tag', '--list', tag]).trim()) {
|
|
102
|
-
fail('tag ' + tag + ' already exists')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 4. Build the dated CHANGELOG section from conventional commits since the
|
|
106
|
-
// last tag. English commit subjects become the section body; the operator can
|
|
107
|
-
// still reword the section afterwards.
|
|
108
|
-
const latestTag = git(['describe', '--tags', '--abbrev=0'], true).trim()
|
|
109
|
-
const ref =
|
|
110
|
-
latestTag ||
|
|
111
|
-
git(['rev-list', '--max-parents=0', 'HEAD']).trim().split(NL).pop() ||
|
|
112
|
-
'HEAD'
|
|
113
|
-
const subjects = git([
|
|
114
|
-
'log',
|
|
115
|
-
'--no-merges',
|
|
116
|
-
'--pretty=format:%s',
|
|
117
|
-
ref + '..HEAD',
|
|
118
|
-
])
|
|
119
|
-
.split(NL)
|
|
120
|
-
.map((s) => s.trim())
|
|
121
|
-
.filter(Boolean)
|
|
122
|
-
const parsed = subjects
|
|
123
|
-
.map(parseConventionalCommit)
|
|
124
|
-
.filter((c): c is NonNullable<typeof c> => c !== null)
|
|
125
|
-
let body = renderChangelogDraft(groupCommits(parsed))
|
|
126
|
-
|
|
127
|
-
// The conventional draft skips chore/docs/test/etc, so a release whose only
|
|
128
|
-
// change is a docs fix (a README tweak users WILL see on the npm page) would
|
|
129
|
-
// otherwise collapse to a useless "Internal improvements." placeholder. Fall
|
|
130
|
-
// back to the raw commit subjects under Changed instead of that stub.
|
|
131
|
-
if (!body) {
|
|
132
|
-
const fallback = subjects.filter((s) => !/^chore(:|\(release\))/i.test(s))
|
|
133
|
-
if (fallback.length) {
|
|
134
|
-
body = ['## Changed', ''].concat(fallback.map((s) => '- ' + s)).join(NL)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// CHANGELOG groups are `### Added` (Keep a Changelog), the draft renders
|
|
139
|
-
// `## Added` — normalize here.
|
|
140
|
-
const sectionBody = body
|
|
141
|
-
.split(NL)
|
|
142
|
-
.map((line) => (line.startsWith('## ') ? '#' + line : line))
|
|
143
|
-
.join(NL)
|
|
144
|
-
const date = new Date().toISOString().slice(0, 10)
|
|
145
|
-
const section =
|
|
146
|
-
'## [' +
|
|
147
|
-
next +
|
|
148
|
-
'] - ' +
|
|
149
|
-
date +
|
|
150
|
-
NL +
|
|
151
|
-
NL +
|
|
152
|
-
(sectionBody || '### Changed' + NL + NL + '- Internal improvements.') +
|
|
153
|
-
NL
|
|
154
|
-
|
|
155
|
-
if (dryRun) {
|
|
156
|
-
console.log('release(dry-run): ' + pkg.version + ' -> ' + next)
|
|
157
|
-
console.log('tag: ' + tag)
|
|
158
|
-
console.log('since: ' + ref)
|
|
159
|
-
console.log('CHANGELOG section:' + NL + section)
|
|
160
|
-
process.exit(0)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 5. Insert the version section in place of the Unreleased BODY. The
|
|
164
|
-
// Unreleased body is auto-synced from commits (see changelog:sync), so it is
|
|
165
|
-
// the same content as the new section — leaving it in place duplicated every
|
|
166
|
-
// entry (the real "### Fixed" block twice). Replace the whole body up to the
|
|
167
|
-
// next version heading with the new section.
|
|
168
|
-
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
169
|
-
const changelog = fs.readFileSync(changelogPath, 'utf-8')
|
|
170
|
-
const anchor = '## [Unreleased]'
|
|
171
|
-
const idx = changelog.indexOf(anchor)
|
|
172
|
-
if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
|
|
173
|
-
const afterAnchor = idx + anchor.length
|
|
174
|
-
const rest = changelog.slice(afterAnchor)
|
|
175
|
-
const nextHeading = rest.search(/\n## \[/)
|
|
176
|
-
const tail = nextHeading < 0 ? NL : rest.slice(nextHeading)
|
|
177
|
-
const updated = changelog.slice(0, afterAnchor) + NL + NL + section + tail
|
|
178
|
-
fs.writeFileSync(changelogPath, updated)
|
|
179
|
-
|
|
180
|
-
// 6. Bump package.json, preserving its exact formatting (single-line replace
|
|
181
|
-
// of the version value only).
|
|
182
|
-
const pkgText = fs.readFileSync(pkgPath, 'utf-8')
|
|
183
|
-
fs.writeFileSync(
|
|
184
|
-
pkgPath,
|
|
185
|
-
pkgText.replace(
|
|
186
|
-
'"version": "' + pkg.version + '"',
|
|
187
|
-
'"version": "' + next + '"',
|
|
188
|
-
),
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
// 7. Commit + tag. The push is intentionally NOT done here.
|
|
192
|
-
git(['add', 'package.json', 'CHANGELOG.md'])
|
|
193
|
-
git(['commit', '-m', 'chore: release ' + next])
|
|
194
|
-
git(['tag', tag])
|
|
195
|
-
|
|
196
|
-
console.log(NL + 'release: committed and tagged ' + tag)
|
|
197
|
-
console.log('Next (explicit) step — publish via CI:')
|
|
198
|
-
console.log(' git push')
|
|
199
|
-
console.log(' git push origin ' + tag)
|