@weiping/wingline 0.0.0-stage → 0.1.1

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/audit-table.d.ts +7 -0
  3. package/dist/audit-table.js +68 -0
  4. package/dist/cli.d.ts +61 -0
  5. package/dist/cli.js +328 -0
  6. package/dist/console.d.ts +33 -0
  7. package/dist/console.js +136 -0
  8. package/dist/main.d.ts +2 -0
  9. package/dist/main.js +11 -0
  10. package/dist/output.d.ts +80 -0
  11. package/dist/output.js +335 -0
  12. package/dist/tui/app.d.ts +28 -0
  13. package/dist/tui/app.js +259 -0
  14. package/dist/tui/command.d.ts +16 -0
  15. package/dist/tui/command.js +35 -0
  16. package/dist/tui/input.d.ts +14 -0
  17. package/dist/tui/input.js +67 -0
  18. package/dist/tui/items.d.ts +15 -0
  19. package/dist/tui/items.js +137 -0
  20. package/dist/tui/keys.d.ts +6 -0
  21. package/dist/tui/keys.js +41 -0
  22. package/dist/tui/reduce.d.ts +10 -0
  23. package/dist/tui/reduce.js +127 -0
  24. package/dist/tui/render.d.ts +7 -0
  25. package/dist/tui/render.js +71 -0
  26. package/dist/tui/screen.d.ts +25 -0
  27. package/dist/tui/screen.js +52 -0
  28. package/dist/tui/text.d.ts +8 -0
  29. package/dist/tui/text.js +28 -0
  30. package/dist/tui/types.d.ts +86 -0
  31. package/dist/tui/types.js +1 -0
  32. package/dist/tui/views/inbox.d.ts +4 -0
  33. package/dist/tui/views/inbox.js +23 -0
  34. package/dist/tui/views/overview.d.ts +6 -0
  35. package/dist/tui/views/overview.js +13 -0
  36. package/dist/tui/views/picker.d.ts +4 -0
  37. package/dist/tui/views/picker.js +44 -0
  38. package/dist/tui/views/timeline.d.ts +4 -0
  39. package/dist/tui/views/timeline.js +11 -0
  40. package/dist/tui/width.d.ts +4 -0
  41. package/dist/tui/width.js +45 -0
  42. package/dist/verified.json +1 -0
  43. package/dist/web/assets/index-DLO1-OIY.css +1 -0
  44. package/dist/web/assets/index-DkiJcfz3.js +2 -0
  45. package/dist/web/index.html +14 -0
  46. package/package.json +34 -3
  47. package/README.md +0 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 weiping liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ import type { AuditTable } from '@weiping/wingline-core';
2
+ import type { Commands } from './cli.js';
3
+ /**
4
+ * 写命令 → 审计行(spec 6.2、6.3)。摘要只放位置参数与开关:正文与理由类字段统一由
5
+ * `auditedCommands` 截到 60 字,规格文件、webhook 地址、会话标记都不进摘要。
6
+ */
7
+ export declare const AUDIT: AuditTable<Commands>;
@@ -0,0 +1,68 @@
1
+ /** 地址参数取 `@` 之后的团队名;解析不出(没有 `@`)记 `-`。 */
2
+ const ofAddress = (a) => a.address.split('@')[1] ?? '-';
3
+ /** 任务 id 的团队名从注册表查;查不到(任务不存在、已被清理)记 `-`。 */
4
+ const byTask = (idOf) => (_a, ctx) => ctx.deps.registry.getTaskExt(idOf(_a))?.team ?? '-';
5
+ const byGate = (gateId) => (_a, ctx) => ctx.deps.registry.getGate(gateId(_a))?.team ?? '-';
6
+ /** 只有结果里才有团队名(up 成功时);失败或没跑出结果记 `-`。 */
7
+ const byResult = (_a, ctx) => ctx.result?.team ?? '-';
8
+ const id = (a) => a.id;
9
+ /**
10
+ * 写命令 → 审计行(spec 6.2、6.3)。摘要只放位置参数与开关:正文与理由类字段统一由
11
+ * `auditedCommands` 截到 60 字,规格文件、webhook 地址、会话标记都不进摘要。
12
+ */
13
+ export const AUDIT = {
14
+ up: {
15
+ command: 'up',
16
+ team: byResult,
17
+ summary: (a) => ({ adoptRun: a.adoptRun, newRun: a.newRun, draft: a.draft }),
18
+ },
19
+ down: {
20
+ command: 'down',
21
+ team: (a) => a.team,
22
+ summary: (a) => ({ close: a.close }),
23
+ },
24
+ restore: {
25
+ command: 'restore',
26
+ team: (a) => a.team,
27
+ skip: (a) => !!a.dryRun,
28
+ summary: (a) => ({ dryRun: !!a.dryRun, noTasks: !!a.noTasks, session: Object.keys(a.sessions ?? {}) }),
29
+ },
30
+ send: {
31
+ command: 'send',
32
+ team: ofAddress,
33
+ target: (a) => a.address,
34
+ summary: (a) => ({ message: a.message }),
35
+ },
36
+ createTask: {
37
+ command: 'task create',
38
+ team: (a) => a.team,
39
+ target: (_a, ctx) => ctx.result?.id ?? null,
40
+ summary: (a) => ({ title: a.title, post: a.post, tags: a.tags, parent: a.parent, as: a.as }),
41
+ },
42
+ claimTask: { command: 'task claim', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
43
+ releaseTask: { command: 'task release', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
44
+ reassignTask: { command: 'task reassign', team: byTask(id), target: id, summary: (a) => ({ to: a.to, as: a.as }) },
45
+ startTask: { command: 'task start', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
46
+ planTask: { command: 'task plan', team: byTask(id), target: id, summary: (a) => ({ text: a.text }) },
47
+ doneTask: { command: 'task done', team: byTask(id), target: id, summary: (a) => ({ as: a.as, resumeEpoch: a.resumeEpoch }) },
48
+ escalateTask: { command: 'task escalate', team: byTask(id), target: id, summary: (a) => ({ reason: a.reason, reopen: a.reopen, as: a.as }) },
49
+ resolveGate: {
50
+ command: 'task resolve',
51
+ team: byGate((a) => a.gateId),
52
+ target: (a) => a.gateId,
53
+ summary: (a) => ({ resolution: a.resolution, reassign: a.reassign, as: a.as }),
54
+ },
55
+ recoverTask: { command: 'task recover', team: byTask(id), target: id, summary: (a) => ({ as: a.as }) },
56
+ dispatcherUp: { command: 'dispatcher up', team: (a) => a.team, summary: (a) => ({ platform: a.platform }) },
57
+ dispatcherDown: { command: 'dispatcher down', team: (a) => a.team, summary: (a) => ({ close: a.close }) },
58
+ freezePostCmd: { command: 'dispatcher freeze', team: ofAddress, target: (a) => a.address, summary: (a) => ({ reason: a.reason }) },
59
+ unfreezePostCmd: { command: 'dispatcher unfreeze', team: ofAddress, target: (a) => a.address },
60
+ hushCmd: { command: 'dispatcher hush', team: ofAddress, target: (a) => a.address, summary: (a) => ({ forMs: a.forMs }) },
61
+ unhushCmd: { command: 'dispatcher unhush', team: ofAddress, target: (a) => a.address },
62
+ dispatcherReply: {
63
+ command: 'dispatcher reply',
64
+ team: (a) => a.team,
65
+ target: (a) => a.messageId,
66
+ summary: (a) => ({ body: a.body, again: a.again }),
67
+ },
68
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { Writable } from 'node:stream';
2
+ import { type CallLogOptions, attention, claimTask, createTask, doneTask, down, escalateTask, followTimeline, listStale, listTasks, planTask, ps, readTimeline, reassignTask, recoverTask, releaseTask, resolveGate, restore, send, showTask, startTask, type AutostartSys, up, dispatcherUp, dispatcherDown, dispatcherStatus, dispatcherReply, freezePostCmd, unfreezePostCmd, hushCmd, unhushCmd, runDispatcherLoop, type CommandDeps } from '@weiping/wingline-core';
3
+ import { consoleCmd } from './console.js';
4
+ import { tuiCmd } from './tui/command.js';
5
+ export interface RunIO {
6
+ stdout: Writable;
7
+ stderr: Writable;
8
+ env: NodeJS.ProcessEnv;
9
+ /** 只有 wl tui 用;缺省取 process.stdin。 */
10
+ stdin?: NodeJS.ReadableStream;
11
+ }
12
+ export interface Commands {
13
+ up: typeof up;
14
+ ps: typeof ps;
15
+ send: typeof send;
16
+ down: typeof down;
17
+ restore: typeof restore;
18
+ log: typeof readTimeline;
19
+ followLog: typeof followTimeline;
20
+ console: typeof consoleCmd;
21
+ createTask: typeof createTask;
22
+ listTasks: typeof listTasks;
23
+ listStale: typeof listStale;
24
+ showTask: typeof showTask;
25
+ claimTask: typeof claimTask;
26
+ releaseTask: typeof releaseTask;
27
+ reassignTask: typeof reassignTask;
28
+ startTask: typeof startTask;
29
+ planTask: typeof planTask;
30
+ doneTask: typeof doneTask;
31
+ escalateTask: typeof escalateTask;
32
+ resolveGate: typeof resolveGate;
33
+ recoverTask: typeof recoverTask;
34
+ dispatcherUp: typeof dispatcherUp;
35
+ dispatcherDown: typeof dispatcherDown;
36
+ runDispatcherLoop: typeof runDispatcherLoop;
37
+ dispatcherStatus: typeof dispatcherStatus;
38
+ freezePostCmd: typeof freezePostCmd;
39
+ unfreezePostCmd: typeof unfreezePostCmd;
40
+ hushCmd: typeof hushCmd;
41
+ unhushCmd: typeof unhushCmd;
42
+ dispatcherReply: typeof dispatcherReply;
43
+ attention: typeof attention;
44
+ tui: typeof tuiCmd;
45
+ }
46
+ /** 只读轮询(spec 第 8 节):调度员的 CallLog 只记摘要。 */
47
+ export declare const isReadOnlyCall: (args: string[]) => boolean;
48
+ /** `dispatcher run` 的覆盖项:Orca 启动的终端不继承 WINGLINE_HOME/WINGLINE_ORCA,所以由命令行显式传入。 */
49
+ export interface DepsOverrides {
50
+ home?: string;
51
+ orca?: string;
52
+ summarize?: boolean;
53
+ }
54
+ /** CallLog 选项:summarize 为真时只读轮询只记摘要。 */
55
+ export declare const callLogOptions: (o: DepsOverrides) => CallLogOptions;
56
+ export declare function defaultMakeDeps(inputEnv: NodeJS.ProcessEnv, warn: (m: string) => void, o?: DepsOverrides): CommandDeps;
57
+ /** node:sqlite 在 Node 22 上会打印实验特性警告;只屏蔽它,其他警告照常输出。 */
58
+ export declare function installWarningFilter(stderr: Writable): void;
59
+ /** 真实宿主:系统命令以参数数组启动(不经 shell),入口脚本取符号链接解析后的真实路径。 */
60
+ export declare function realAutostartSys(): AutostartSys;
61
+ export declare function run(argv: string[], io: RunIO, commands?: Commands, makeDeps?: (env: NodeJS.ProcessEnv, warn: (m: string) => void, overrides?: DepsOverrides) => CommandDeps, autostartSys?: AutostartSys): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,328 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { Command, CommanderError, InvalidArgumentError, Option } from 'commander';
6
+ import { READ_ONLY_CALLS, Registry, WinglineError, attention, auditedCommands, claimTask, createCallLog, createOrcaClient, createOrcaRunner, createTask, doneTask, down, escalateTask, exitCodeOf, followTimeline, listStale, listTasks, parseDuration, planTask, ps, readTimeline, reassignTask, recoverTask, releaseTask, resolveGate, resolveOrcaExecutable, restore, restoreAll, restoreAllExitCode, restoreExitCode, send, showTask, startTask, autostartInstall, autostartStatusOf, autostartUninstall, up, winglineHome, dispatcherUp, dispatcherDown, dispatcherStatus, dispatcherReply, freezePostCmd, unfreezePostCmd, hushCmd, unhushCmd, runDispatcherLoop, } from '@weiping/wingline-core';
7
+ import { consoleCmd, renderConsole, renderReady } from './console.js';
8
+ import { tuiCmd } from './tui/command.js';
9
+ import { AUDIT } from './audit-table.js';
10
+ import verified from './verified.json' with { type: 'json' };
11
+ import { errorDoc, renderAttention, renderDispatcherDown, renderDispatcherStatus, renderDispatcherUp, renderUnfreeze, renderCreate, renderDone, renderDown, renderError, renderEscalate, renderList, renderLog, renderPs, renderReassign, renderRecover, renderResolve, renderRestore, renderRestoreAll, renderAutostartInstall, renderAutostartStatus, renderAutostartUninstall, logPrinter, renderSend, renderShow, renderStale, renderStart, renderUp, } from './output.js';
12
+ const rawCommands = {
13
+ up, ps, send, down, restore, log: readTimeline, followLog: followTimeline, createTask, listTasks, listStale, showTask, claimTask, releaseTask, reassignTask, startTask, planTask, doneTask,
14
+ escalateTask, resolveGate, recoverTask, dispatcherUp, dispatcherDown, runDispatcherLoop, dispatcherStatus, freezePostCmd, unfreezePostCmd,
15
+ hushCmd, unhushCmd, dispatcherReply, attention,
16
+ console: consoleCmd, tui: tuiCmd,
17
+ };
18
+ /** 表里登记过的写命令每执行一次写一行审计(spec 6.2)。 */
19
+ const realCommands = auditedCommands(rawCommands, AUDIT);
20
+ const splitList = (s) => s?.split(',').filter(Boolean);
21
+ /** 参数组合不合法时返回输入错误(交给 execute 统一渲染);合法返回 null。 */
22
+ function restoreOptsError(team, o) {
23
+ const bad = (message) => new WinglineError('input', 'restore_args_invalid', message);
24
+ if (o.actor !== undefined && o.actor !== 'autostart')
25
+ return new WinglineError('input', 'actor_invalid', `--actor 只接受 autostart,不接受 ${o.actor}`);
26
+ if (o.all && (team || o.session.length > 0))
27
+ return bad('--all 不能与 <team> 或 --session 同用');
28
+ if (!o.all && !team)
29
+ return bad('需要给出团队名,或用 --all 恢复所有运行中的团队');
30
+ if (!o.all && o.waitOrca !== undefined)
31
+ return bad('--wait-orca 只能与 --all 同用');
32
+ return null;
33
+ }
34
+ const parseSessions = (list) => {
35
+ const out = {};
36
+ for (const s of list) {
37
+ const i = s.indexOf('=');
38
+ if (i < 1)
39
+ throw new WinglineError('input', 'session_override_invalid', `--session ${s} 无效:应写成 <post>=<sessionId>`);
40
+ out[s.slice(0, i)] = s.slice(i + 1);
41
+ }
42
+ return out;
43
+ };
44
+ const collect = (v, acc) => [...acc, v];
45
+ const LOG_SOURCES = ['task', 'patrol', 'command'];
46
+ /** wl log 的筛选参数;--limit 必须是正整数,--source 必须是已知来源,否则按用法错处理而不是静默读空。 */
47
+ function logArgs(team, o) {
48
+ if (o.limit !== undefined && !(Number.isInteger(o.limit) && o.limit >= 1)) {
49
+ throw new WinglineError('input', 'limit_invalid', `--limit ${String(o.limit)} 无效:应为正整数`);
50
+ }
51
+ if (o.source !== undefined && !LOG_SOURCES.includes(o.source)) {
52
+ throw new WinglineError('input', 'source_invalid', `--source ${o.source} 无效:应为 ${LOG_SOURCES.join('/')} 之一`);
53
+ }
54
+ return { team, task: o.task, post: o.post, since: o.since, limit: o.limit, source: o.source };
55
+ }
56
+ /** done:通过 0;gate 失败/转人工/绕过后补跑失败 1。 */
57
+ const exitCodeOfDone = (r) => (r.outcome === 'done' ? 0 : 1);
58
+ const PACKAGE_VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
59
+ function which(name, env) {
60
+ for (const dir of (env.PATH ?? env.Path ?? '').split(path.delimiter).filter(Boolean)) {
61
+ const file = path.join(dir, name);
62
+ if (fs.existsSync(file) && fs.statSync(file).isFile())
63
+ return file;
64
+ }
65
+ return null;
66
+ }
67
+ /** 只读轮询(spec 第 8 节):调度员的 CallLog 只记摘要。 */
68
+ export const isReadOnlyCall = (args) => READ_ONLY_CALLS.some(([group, sub]) => args[0] === group && args[1] === sub);
69
+ /** CallLog 选项:summarize 为真时只读轮询只记摘要。 */
70
+ export const callLogOptions = (o) => (o.summarize ? { summarize: isReadOnlyCall } : {});
71
+ export function defaultMakeDeps(inputEnv, warn, o = {}) {
72
+ const env = { ...inputEnv, ...(o.home ? { WINGLINE_HOME: o.home } : {}), ...(o.orca ? { WINGLINE_ORCA: o.orca } : {}) };
73
+ const home = winglineHome(env);
74
+ const exe = resolveOrcaExecutable({ env, platform: process.platform, which: (n) => which(n, env) });
75
+ const runner = createOrcaRunner(exe, createCallLog(path.join(home, 'logs'), undefined, callLogOptions(o)));
76
+ return {
77
+ orca: createOrcaClient(runner),
78
+ registry: Registry.open(path.join(home, 'state.db')),
79
+ warn,
80
+ verifiedOrca: verified.verifiedOrca,
81
+ multilineSafe: !exe.viaCmd,
82
+ home,
83
+ orcaPath: exe.cmdPath ?? exe.file,
84
+ env,
85
+ now: () => new Date(),
86
+ monotonic: () => performance.now(),
87
+ };
88
+ }
89
+ /** node:sqlite 在 Node 22 上会打印实验特性警告;只屏蔽它,其他警告照常输出。 */
90
+ export function installWarningFilter(stderr) {
91
+ process.removeAllListeners('warning');
92
+ process.on('warning', (w) => {
93
+ if (w.name === 'ExperimentalWarning' && w.message.includes('SQLite'))
94
+ return;
95
+ stderr.write(`(${w.name}) ${w.message}\n`);
96
+ });
97
+ }
98
+ const RUN_EXIT = { ambiguous: 1, failed: 2, lost: 2, uncertain: 3 };
99
+ /** 岗位结果与 Run 步骤取最严重者:失败/丢失 2 > 不确定 3 > 候选不唯一 1。 */
100
+ const exitCodeOfUp = (r) => {
101
+ const codes = [...r.posts.map((p) => (p.outcome === 'failed' ? 2 : p.outcome === 'uncertain' ? 3 : 0)), RUN_EXIT[r.run?.outcome ?? ''] ?? 0];
102
+ return codes.includes(2) ? 2 : codes.includes(3) ? 3 : codes.includes(1) ? 1 : 0;
103
+ };
104
+ function registerDispatcherCommands(program, commands, execute, io) {
105
+ const disp = program.command('dispatcher').description('调度员:收件箱中继与巡查');
106
+ const sub = (spec, desc) => disp.command(spec).description(desc).option('--json', '输出 JSON');
107
+ sub('up <team>', '启动调度员').action((team, o) => execute(!!o.json, (d) => commands.dispatcherUp(d, { team }), renderDispatcherUp));
108
+ sub('down <team>', '停止调度员;加 --close 关闭其终端').option('--close', '关闭调度员终端')
109
+ .action((team, o) => execute(!!o.json, (d) => commands.dispatcherDown(d, { team, close: !!o.close }), renderDispatcherDown));
110
+ disp.command('run <team>').description('在当前终端运行调度员循环(长期运行)').option('--home <path>', '数据目录(等价 WINGLINE_HOME)')
111
+ .option('--orca <path>', 'Orca 可执行文件(等价 WINGLINE_ORCA)')
112
+ .action((team, o) => {
113
+ const ctl = new AbortController();
114
+ const stop = () => { io.stderr.write('收到信号,正在停止…\n'); ctl.abort(); };
115
+ process.once('SIGINT', stop);
116
+ process.once('SIGTERM', stop);
117
+ process.once('SIGHUP', stop);
118
+ return execute(false, (d) => commands.runDispatcherLoop(d, team, { signal: ctl.signal }).then(() => ({})), () => '', undefined, { home: o.home, orca: o.orca, summarize: true }).finally(() => { process.off('SIGINT', stop); process.off('SIGTERM', stop); process.off('SIGHUP', stop); });
119
+ });
120
+ sub('status <team>', '调度员状态').action((team, o) => execute(!!o.json, (d) => commands.dispatcherStatus(d, team), renderDispatcherStatus));
121
+ sub('freeze <post@team>', '冻结岗位(不再派发给它)').requiredOption('--reason <text>', '理由')
122
+ .action((address, o) => execute(!!o.json, (d) => commands.freezePostCmd(d, { address, reason: o.reason }).then(() => ({ address })), (r) => `${r.address} 已冻结\n`));
123
+ sub('unfreeze <post@team>', '解冻岗位').action((address, o) => execute(!!o.json, (d) => commands.unfreezePostCmd(d, { address }), (r) => renderUnfreeze(r, address)));
124
+ sub('hush <post@team>', '暂停往岗位终端自动打字').option('--for <时长>', '如 15m、2h;默认 30 分钟')
125
+ .action((address, o) => execute(!!o.json, async (d) => commands.hushCmd(d, { address, forMs: o.for === undefined ? undefined : parseDuration(o.for) }), (r) => `${address} 静音至 ${r.until}\n`));
126
+ sub('unhush <post@team>', '取消静音').action((address, o) => execute(!!o.json, (d) => commands.unhushCmd(d, { address }).then(() => ({ address })), (r) => `${r.address} 已取消静音\n`));
127
+ sub('reply <team> <message_id>', '以协调者身份回答岗位的提问').requiredOption('--body <text>', '回答').option('--again', '再回答一次已答过的问题')
128
+ .action((team, messageId, o) => execute(!!o.json, (d) => commands.dispatcherReply(d, { team, messageId, body: o.body, again: !!o.again }), (r) => (r.duplicate ? `消息 ${r.messageId} 已回答过,未重复发送\n` : `已回答,消息 ${r.messageId}\n`)));
129
+ }
130
+ function registerTaskCommands(program, commands, execute) {
131
+ const task = program.command('task').description('任务工作流');
132
+ const sub = (spec, desc) => task.command(spec).description(desc).option('--json', '输出 JSON');
133
+ sub('create <team>', '建任务').requiredOption('--title <t>', '标题').option('--post <addr>', '直接派给岗位').option('--tags <a,b>', '标签')
134
+ .option('--parent <id>', '父任务').option('--as <addr>', '以该岗位身份').option('--retry-request <id>', '重跑上次不确定的创建')
135
+ .action((team, o) => execute(!!o.json, (d) => commands.createTask(d, { team, title: o.title, post: o.post, tags: splitList(o.tags), parent: o.parent, as: o.as, retryRequest: o.retryRequest }), (r) => renderCreate(r, o.post)));
136
+ sub('list [team]', '列出任务').option('--mine', '只看我的').option('--status <s>', '按状态').option('--tags <a,b>', '按标签')
137
+ .option('--stale', '只列卡住的任务').option('--as <addr>', '以该岗位身份')
138
+ .action((team, o) => {
139
+ if (!o.stale)
140
+ return execute(!!o.json, (d) => commands.listTasks(d, { team, mine: o.mine, status: o.status, tags: splitList(o.tags), as: o.as }), renderList);
141
+ return execute(!!o.json, (d) => commands.listStale(d, team), renderStale);
142
+ });
143
+ sub('show <id>', '查看任务').option('--team <team>', '团队').option('--history', '附变迁记录')
144
+ .action((id, o) => execute(!!o.json, (d) => commands.showTask(d, { id, team: o.team, history: !!o.history }), renderShow));
145
+ sub('claim <id>', '认领').option('--as <addr>', '以该岗位身份')
146
+ .action((id, o) => execute(!!o.json, (d) => commands.claimTask(d, { id, as: o.as }).then(() => ({ id })), (r) => `任务 ${r.id} 已认领\n`));
147
+ sub('release <id>', '放弃认领').option('--as <addr>', '以该岗位身份')
148
+ .action((id, o) => execute(!!o.json, (d) => commands.releaseTask(d, { id, as: o.as }).then(() => ({ id })), (r) => `任务 ${r.id} 已放回\n`));
149
+ sub('reassign <id>', '改派').requiredOption('--to <addr>', '新主人').option('--as <addr>', '以该岗位身份')
150
+ .action((id, o) => execute(!!o.json, (d) => commands.reassignTask(d, { id, to: o.to, as: o.as }), (r) => renderReassign(r, id, o.to)));
151
+ sub('start <id>', '开始').option('--as <addr>', '以该岗位身份')
152
+ .action((id, o) => execute(!!o.json, (d) => commands.startTask(d, { id, as: o.as }), (r) => renderStart(r, id, o.as)));
153
+ sub('plan <id> <text>', '写计划')
154
+ .action((id, text, o) => execute(!!o.json, (d) => commands.planTask(d, { id, text }).then(() => ({ id })), (r) => `任务 ${r.id} 计划已记录\n`));
155
+ sub('done <id>', '完成').option('--as <addr>', '以该岗位身份')
156
+ .action((id, o) => execute(!!o.json, (d) => commands.doneTask(d, { id, as: o.as }), renderDone, exitCodeOfDone));
157
+ sub('escalate <id>', '转人工决策').requiredOption('--reason <text>', '理由').option('--reopen', '重开已终态的任务').option('--as <addr>', '以该岗位身份')
158
+ .option('--retry-request <id>', '重跑上次不确定的 escalate')
159
+ .action((id, o) => execute(!!o.json, (d) => commands.escalateTask(d, { id, reason: o.reason, reopen: !!o.reopen, as: o.as, retryRequest: o.retryRequest }), renderEscalate));
160
+ sub('resolve <gateId>', '决策').requiredOption('--resolution <text>', '决定').option('--reassign <addr>', '同时改派').option('--as <addr>', '以该岗位身份')
161
+ .action((gateId, o) => execute(!!o.json, (d) => commands.resolveGate(d, { gateId, resolution: o.resolution, reassign: o.reassign, as: o.as }), renderResolve));
162
+ sub('recover <id>', '恢复派给已消失终端的任务').option('--as <addr>', '以该岗位身份')
163
+ .action((id, o) => execute(!!o.json, (d) => commands.recoverTask(d, { id, as: o.as }), (r) => renderRecover(r, id)));
164
+ }
165
+ function registerBaseCommands(program, commands, execute, io) {
166
+ program.command('up <team.yaml>').description('拉起团队的全部岗位').option('--json', '输出 JSON')
167
+ .option('--adopt-run <id>', '找回指定的 Run').option('--new-run', '丢弃旧 Run,新建一个')
168
+ .option('--draft', '先把同目录的 team.draft.yaml 应用为正式规格(旧规格备份为 .bak)')
169
+ .action((specPath, o) => execute(!!o.json, async (d) => {
170
+ if (o.adoptRun && o.newRun)
171
+ throw new WinglineError('input', 'run_flags_conflict', '--adopt-run 与 --new-run 互斥');
172
+ return commands.up(d, { specPath, adoptRun: o.adoptRun, newRun: o.newRun, draft: !!o.draft });
173
+ }, (r) => renderUp(r, { newRun: o.newRun }), exitCodeOfUp));
174
+ program.command('ps [team]').description('查看岗位状态').option('--json', '输出 JSON')
175
+ .action((team, o) => execute(!!o.json, (d) => commands.ps(d, { team }), renderPs));
176
+ program.command('attention <team>').description('需要人处理的事项汇总').option('--json', '输出 JSON')
177
+ .action((team, o) => execute(!!o.json, (d) => commands.attention(d, team), renderAttention));
178
+ program.command('tui [team]').description('终端里的团队视图:总览、收件箱、时间线,可处理待办')
179
+ .action((team) => execute(false, (d) => commands.tui(d, { team, io: { stdin: io.stdin ?? process.stdin, stdout: io.stdout } }), () => ''));
180
+ program.command('send <post@team> <message>').description('给岗位发消息').option('--json', '输出 JSON')
181
+ .option('--retry-request <id>', '确认上次未确认的发送:传入当时报告的 Orca 请求 ID,消息须与原来完全相同,不会重复发送')
182
+ .action((address, message, o) => execute(!!o.json, (d) => commands.send(d, { address, message, retryRequest: o.retryRequest }), renderSend));
183
+ program.command('down <team>').description('停止团队;加 --close 关闭终端').option('--close', '关闭岗位终端').option('--json', '输出 JSON')
184
+ .action((team, o) => execute(!!o.json, (d) => commands.down(d, { team, close: !!o.close }), renderDown));
185
+ program.command('restore [team]').description('重启之后把团队接回来:恢复岗位会话、Run、调度员与任务;--all 恢复所有运行中的团队')
186
+ .option('--all', '恢复注册表里所有运行中的团队(登录后自动恢复用)')
187
+ .option('--wait-orca <时长>', '与 --all 同用:先等 Orca 可达且终端列表稳定,如 15m')
188
+ .option('--dry-run', '只看会做什么,不改动任何东西').option('--no-tasks', '不恢复任务,只接回岗位')
189
+ .option('--session <post=id>', '跳过搜索,直接用给定的会话接回该岗位(可重复)', collect, [])
190
+ .addOption(new Option('--actor <name>', '审计里的执行者(只接受 autostart)').hideHelp())
191
+ .option('--home <path>', '数据目录(等价 WINGLINE_HOME)').option('--orca <path>', 'Orca 可执行文件(等价 WINGLINE_ORCA)')
192
+ .option('--json', '输出 JSON')
193
+ .action((team, o) => {
194
+ const overrides = { home: o.home, orca: o.orca };
195
+ const bad = restoreOptsError(team, o);
196
+ if (bad)
197
+ return execute(!!o.json, async () => { throw bad; }, () => '');
198
+ const base = { dryRun: !!o.dryRun, noTasks: o.tasks === false, ...(o.actor ? { as: o.actor } : {}) };
199
+ if (o.all) {
200
+ return execute(!!o.json, (d) => restoreAll(d, { ...base, waitOrcaMs: o.waitOrca === undefined ? undefined : parseDuration(o.waitOrca) }, { restoreOne: (a) => commands.restore(d, a) }), renderRestoreAll, restoreAllExitCode, overrides);
201
+ }
202
+ return execute(!!o.json, (d) => commands.restore(d, { team: team, ...base, sessions: parseSessions(o.session) }), renderRestore, restoreExitCode, overrides);
203
+ });
204
+ program.command('log <team>').description('团队时间线:任务、巡查与命令事件按时间合并')
205
+ .option('--task <id>', '只看某个任务').option('--post <post>', '只看某个岗位')
206
+ .option('--since <when>', '只看这段时间之后:90s、15m、2h 或 ISO 时间')
207
+ .option('--limit <n>', '只看最新 n 条', Number).option('--source <s>', '只看某个来源:task/patrol/command')
208
+ .option('--follow', '持续输出新事件,Ctrl-C 停止').option('--json', '每行一个 JSON(NDJSON)')
209
+ .action(async (team, o) => {
210
+ if (!o.follow)
211
+ return execute(!!o.json, (d) => commands.log(d, logArgs(team, o)), (r) => renderLog(r.events));
212
+ const ctl = new AbortController();
213
+ const stop = () => ctl.abort();
214
+ process.once('SIGINT', stop);
215
+ process.once('SIGTERM', stop);
216
+ try {
217
+ // 先把已有的事件打出来,再从那个游标往后跟;日期行与一次性输出共用同一套格式。
218
+ return await execute(false, async (d) => {
219
+ const print = o.json ? (e) => JSON.stringify(e) + '\n' : logPrinter();
220
+ const args = logArgs(team, o);
221
+ const first = await commands.log(d, args);
222
+ for (const e of first.events)
223
+ io.stdout.write(print(e));
224
+ await commands.followLog(d, { ...args, cursor: first.cursor }, (e) => io.stdout.write(print(e)), { signal: ctl.signal });
225
+ return {};
226
+ }, () => '');
227
+ }
228
+ finally {
229
+ process.off('SIGINT', stop);
230
+ process.off('SIGTERM', stop);
231
+ }
232
+ });
233
+ }
234
+ /** 真实宿主:系统命令以参数数组启动(不经 shell),入口脚本取符号链接解析后的真实路径。 */
235
+ export function realAutostartSys() {
236
+ const exec = (file, args) => new Promise((resolve, reject) => {
237
+ const child = spawn(file, args, { shell: false });
238
+ let stdout = '';
239
+ let stderr = '';
240
+ child.stdout.on('data', (d) => { stdout += String(d); });
241
+ child.stderr.on('data', (d) => { stderr += String(d); });
242
+ child.on('error', reject);
243
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
244
+ });
245
+ const entry = process.argv[1] ?? '';
246
+ let script = path.resolve(entry);
247
+ try {
248
+ script = fs.realpathSync(entry);
249
+ }
250
+ catch { /* 保持 resolve 的结果 */ }
251
+ return { platform: process.platform, homeDir: os.homedir(), uid: process.getuid?.(), user: os.userInfo().username, node: process.execPath, script, exec, fs };
252
+ }
253
+ function registerAutostartCommands(program, execute, sys) {
254
+ const auto = program.command('autostart').description('登录后自动执行 wl restore --all(macOS launchd、Linux systemd 用户服务)');
255
+ const sub = (spec, desc) => auto.command(spec).description(desc).option('--json', '输出 JSON');
256
+ sub('install', '安装(重复执行会覆盖自己的那一个文件)').option('--wait-orca <时长>', '登录时等 Orca 就绪的最长时间,默认 15m')
257
+ .action((o) => execute(!!o.json, (d) => autostartInstall(d, sys(), { waitOrca: o.waitOrca }), renderAutostartInstall));
258
+ sub('uninstall', '卸载(只删雁阵自己的文件,日志保留)')
259
+ .action((o) => execute(!!o.json, (d) => autostartUninstall(d, sys()), renderAutostartUninstall));
260
+ sub('status', '是否已安装、将执行的命令与最近的日志')
261
+ .action((o) => execute(!!o.json, (d) => autostartStatusOf(d, sys()), renderAutostartStatus));
262
+ }
263
+ export async function run(argv, io, commands = realCommands, makeDeps = defaultMakeDeps, autostartSys) {
264
+ let code = 0;
265
+ const json = argv.includes('--json');
266
+ async function execute(json, fn, render, exitOf = () => 0, overrides) {
267
+ let deps;
268
+ try {
269
+ deps = makeDeps(io.env, (m) => io.stderr.write(`警告:${m}\n`), overrides);
270
+ const result = await fn(deps);
271
+ io.stdout.write(json ? JSON.stringify({ ok: true, result }) + '\n' : render(result));
272
+ code = exitOf(result);
273
+ }
274
+ catch (err) {
275
+ const doc = errorDoc(err);
276
+ if (json)
277
+ io.stdout.write(JSON.stringify({ ok: false, error: doc }) + '\n');
278
+ else
279
+ io.stderr.write(renderError(doc));
280
+ code = exitCodeOf(err);
281
+ }
282
+ finally {
283
+ deps?.registry.close();
284
+ }
285
+ }
286
+ const program = new Command()
287
+ .name('wingline')
288
+ .description('雁阵 Wingline:在 Orca 之上运行常驻 agent 团队')
289
+ .version(PACKAGE_VERSION)
290
+ .exitOverride()
291
+ .configureOutput({ writeOut: (s) => io.stdout.write(s), writeErr: (s) => { if (!json)
292
+ io.stderr.write(s); } });
293
+ registerBaseCommands(program, commands, execute, io);
294
+ registerTaskCommands(program, commands, execute);
295
+ registerDispatcherCommands(program, commands, execute, io);
296
+ registerAutostartCommands(program, execute, () => autostartSys ?? realAutostartSys());
297
+ program
298
+ .command('console')
299
+ .description('本地控制台服务')
300
+ .option('--port <n>', '监听端口', (v) => {
301
+ const n = Number(v);
302
+ if (!(Number.isInteger(n) && n >= 1 && n <= 65535))
303
+ throw new InvalidArgumentError(`不是有效端口:${v}`);
304
+ return n;
305
+ })
306
+ .option('--open', '在 Orca 内置浏览器中打开')
307
+ .option('--worktree <selector>', '工作区选择器')
308
+ .option('--link', '获取一次性登录链接')
309
+ .option('--home <path>', '数据目录(等价 WINGLINE_HOME)')
310
+ .option('--orca <path>', 'Orca 可执行文件(等价 WINGLINE_ORCA)')
311
+ .option('--json', '输出 JSON')
312
+ .action((o) => execute(json, (deps) => commands.console(deps, {
313
+ port: o.port, open: o.open, worktree: o.worktree, link: o.link,
314
+ onReady: json ? undefined : (r) => io.stdout.write(renderReady(r)),
315
+ }), renderConsole, undefined, { home: o.home, orca: o.orca }));
316
+ try {
317
+ await program.parseAsync(argv, { from: 'user' });
318
+ }
319
+ catch (err) {
320
+ if (!(err instanceof CommanderError))
321
+ throw err;
322
+ code = err.exitCode === 0 ? 0 : 1;
323
+ // --json 时用法错误也要给出一份合法的 JSON 文档
324
+ if (json && code !== 0)
325
+ io.stdout.write(JSON.stringify({ ok: false, error: { kind: 'input', code: 'usage', message: err.message.replace(/^error: /, '') } }) + '\n');
326
+ }
327
+ return code;
328
+ }
@@ -0,0 +1,33 @@
1
+ import { type CommandDeps } from '@weiping/wingline-core';
2
+ export interface ConsoleArgs {
3
+ port?: number;
4
+ open?: boolean;
5
+ worktree?: string;
6
+ link?: boolean;
7
+ webRoot?: string;
8
+ /** 新起服务的就绪确认窗口;缺省 10_000。 */
9
+ confirmMs?: number;
10
+ /** 确认窗口里的等待;缺省真的是 setTimeout。 */
11
+ sleep?: (ms: number) => Promise<void>;
12
+ /** 前台服务就绪后调用一次,用来先打印登录链接(服务随后阻塞到收到信号)。 */
13
+ onReady?: (r: ConsoleResult) => void;
14
+ }
15
+ export interface ConsoleResult {
16
+ outcome: 'started' | 'link' | 'opened';
17
+ url: string;
18
+ port?: number;
19
+ tab?: 'opened' | 'unsupported';
20
+ tabReason?: 'worktree';
21
+ }
22
+ /**
23
+ * 先关服务、再删 console.json(关闭失败也删):另一个 wl 进程读到文件时服务一定还能连上,
24
+ * 不会读到指向已关闭服务的文件(复审 I-3 的残余窗口)。
25
+ */
26
+ export declare function stopServer(srv: {
27
+ close(): Promise<void>;
28
+ }, cleanup: () => void): Promise<void>;
29
+ export declare function consoleCmd(deps: CommandDeps, a: ConsoleArgs): Promise<ConsoleResult>;
30
+ /** 启动就绪时打印(前台服务还在跑)。 */
31
+ export declare function renderReady(r: ConsoleResult): string;
32
+ /** 最终结果(服务停止后,或其他三种结果)。 */
33
+ export declare function renderConsole(r: ConsoleResult): string;