workoffdesk 1.0.0 → 1.0.2
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 +27 -0
- package/README.md +5 -3
- package/dist/build.json +1 -1
- package/dist/server/agents/claude/screen/background.js +7 -0
- package/dist/server/agents/claude/screen/prompt.js +34 -0
- package/dist/server/agents/claude/screen/screen.js +42 -3
- package/dist/server/agents/claude/terminals.js +67 -12
- package/dist/server/agents/claude/transcript/chat.js +98 -135
- package/dist/server/agents/claude/transcript/events.js +158 -0
- package/dist/server/agents/claude/transcript/transcriptFile.js +50 -10
- package/dist/server/agents/claude/transcript/transcripts.js +118 -30
- package/dist/server/agents/claude/transcript/turn.js +40 -61
- package/dist/server/agents/codex/agent.js +9 -2
- package/dist/server/agents/codex/events.js +107 -0
- package/dist/server/agents/codex/items.js +54 -74
- package/dist/server/agents/codex/notifications.js +14 -13
- package/dist/server/agents/codex/threads.js +54 -40
- package/dist/server/agents/events.js +4 -0
- package/dist/server/agents/turnSays.js +58 -0
- package/dist/server/cli.js +2 -0
- package/dist/server/commands/activity.js +28 -0
- package/dist/server/http/app.js +8 -4
- package/dist/server/http/auth/guard.js +8 -4
- package/dist/server/http/auth/passkey.js +6 -3
- package/dist/server/http/routes/appUpdate.js +1 -1
- package/dist/server/http/routes/chat.js +10 -0
- package/dist/server/http/routes/claude.js +70 -16
- package/dist/server/http/routes/codex.js +38 -3
- package/dist/server/http/routes/journal.js +3 -1
- package/dist/server/http/routes/push.js +36 -5
- package/dist/server/http/routes/pushRelay.js +62 -0
- package/dist/server/http/routes/sessions.js +32 -1
- package/dist/server/http/serve.js +8 -2
- package/dist/server/http/sessionSocket.js +12 -11
- package/dist/server/journal/db.js +4 -0
- package/dist/server/journal/index.js +4 -0
- package/dist/server/journal/ingest.js +26 -13
- package/dist/server/notify/apns.js +12 -7
- package/dist/server/notify/push.js +30 -4
- package/dist/server/notify/seal.js +12 -0
- package/dist/server/ops/doctor.js +7 -6
- package/dist/server/ops/problems.js +12 -7
- package/dist/server/sessions/control.js +65 -0
- package/dist/server/sessions/events.js +7 -5
- package/dist/server/sessions/machine/input.js +21 -2
- package/dist/server/sessions/machine/lifecycle.js +151 -81
- package/dist/server/sessions/machine/moves.js +65 -13
- package/dist/server/sessions/machine/naming.js +85 -0
- package/dist/server/sessions/machine/state.js +2 -0
- package/dist/server/sessions/machine/status.js +1 -2
- package/dist/server/sessions/machine/takeOvers.js +92 -0
- package/dist/server/sessions/machine.js +87 -23
- package/dist/server/sessions/outbox.js +7 -1
- package/dist/server/system/config.js +16 -1
- package/dist/server/system/errors.js +2 -2
- package/dist/server/system/messages.js +14 -176
- package/dist/server/system/messages.zh.js +186 -0
- package/dist/server/system/project.js +65 -0
- package/dist/server/system/store.js +1 -1
- package/dist/server/system/tmux.js +1 -1
- package/dist/web/assets/{Setup-DinzDcF1.js → Setup-OaIjxfwl.js} +1 -1
- package/dist/web/assets/main-DFndf3Z8.js +132 -0
- package/dist/web/assets/main-mChS_ic8.css +1 -0
- package/dist/web/icon-180.png +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/icon.svg +14 -6
- package/dist/web/index.html +2 -2
- package/dist/web/manifest.webmanifest +1 -1
- package/dist/web/sw.js +4 -4
- package/package.json +3 -3
- package/dist/web/assets/main-C8GiLoJy.css +0 -1
- package/dist/web/assets/main-w7i8PvU7.js +0 -132
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export const zh = {
|
|
2
|
+
cli: {
|
|
3
|
+
usage: '用法:workoffdesk <命令>',
|
|
4
|
+
unknownCommand: (cmd) => `没有 ${cmd} 这个命令。`,
|
|
5
|
+
help: {
|
|
6
|
+
setup: [
|
|
7
|
+
['setup', '设用户名和密码;再跑一次会换新密码,所有设备都要重新登录'],
|
|
8
|
+
['setup --web', '在浏览器里做设置,包括地址和服务'],
|
|
9
|
+
],
|
|
10
|
+
install: [['install [--tunnel <名字>]', '写好 launchd 服务(不启动),并打印接下来要跑的命令']],
|
|
11
|
+
start: [['start', '启动服务,等它应答']],
|
|
12
|
+
stop: [['stop', '停掉服务,直到下次登录或 start']],
|
|
13
|
+
restart: [['restart', '重启服务;会话继续跑']],
|
|
14
|
+
doctor: [['doctor', '检查所有项,每项没过都给出修复方法']],
|
|
15
|
+
status: [['status --json', '以 JSON 输出检查结果、版本、保留的各版本和最近的错误']],
|
|
16
|
+
activity: [['activity --json', '几个会话正在干活:现在输出一行 JSON,之后每次变化再输出一行']],
|
|
17
|
+
claude: [['claude [--resume <会话 id>]', '在当前目录开一个 Claude 会话并接上']],
|
|
18
|
+
codex: [['codex [--resume <对话 id>]', '在当前目录开一个 Codex 会话并接上']],
|
|
19
|
+
attach: [['attach [id]', '在当前终端接上一个会话;不给 id 就让你挑']],
|
|
20
|
+
account: [
|
|
21
|
+
['account [--json]', '这台 Mac 上的 Claude 账号,以及新会话用哪一个'],
|
|
22
|
+
['account use <邮箱或目录> [--move]', '新会话改用它;加 --move 把开着的会话也搬过去'],
|
|
23
|
+
['account move', '把开着的会话搬到正在用的账号,每个等它这轮回复完再搬'],
|
|
24
|
+
],
|
|
25
|
+
serve: [['serve', '在当前终端里跑服务器,而不是作为后台服务']],
|
|
26
|
+
deploy: [['deploy <构建目录>', '把一个构建装成新版本并切过去']],
|
|
27
|
+
rollback: [['rollback', '回到上一个版本']],
|
|
28
|
+
'--version': [['--version', '打印装的版本']],
|
|
29
|
+
'--help': [['--help', '打印这张列表']],
|
|
30
|
+
},
|
|
31
|
+
statusUsage: '用法:workoffdesk status --json(给菜单栏 App 读的;给人看用 doctor)',
|
|
32
|
+
activityUsage: '用法:workoffdesk activity --json(给菜单栏 App 读的)',
|
|
33
|
+
notConfigured: (example) => `还没有配置。告诉 setup 这个站点的访问地址:\n workoffdesk setup --origin ${example}`,
|
|
34
|
+
askOrigin: (example) => `站点的访问地址(手机打开的那个,比如 ${example}):`,
|
|
35
|
+
askUsername: '用户名:',
|
|
36
|
+
emptyUsername: '用户名不能为空',
|
|
37
|
+
askPassword: (min) => `密码(至少 ${min} 个字符):`,
|
|
38
|
+
askAgain: '再输一次:',
|
|
39
|
+
passwordsDiffer: '两次输入的密码不一致',
|
|
40
|
+
passwordTooShort: (min) => `密码至少要 ${min} 个字符`,
|
|
41
|
+
setUp: (username, origin, configFile, port) => `用户 ${username} 已设置好,地址 ${origin}。配置:${configFile}
|
|
42
|
+
|
|
43
|
+
下一步:
|
|
44
|
+
workoffdesk serve 先在本机试一下(http://127.0.0.1:${port})
|
|
45
|
+
workoffdesk install 装成登录时自动启动的服务,并看看怎么在前面接隧道或反向代理
|
|
46
|
+
开放到公网之前,先读 SECURITY.md:谁进得了这个站点,谁就拿到了这台电脑的终端。`,
|
|
47
|
+
notSetUp: '还没有设置用户。运行:workoffdesk setup',
|
|
48
|
+
scan: (origin) => `用手机相机扫一下,打开 ${origin}:`,
|
|
49
|
+
running: (port, origin) => `workoffdesk 正在 http://127.0.0.1:${port} 上运行,访问地址 ${origin}`,
|
|
50
|
+
noSessions: '没有会话。',
|
|
51
|
+
whichOne: '选哪一个?',
|
|
52
|
+
noSuchNumber: '没有这个编号',
|
|
53
|
+
notRunning: 'WorkOffDesk 服务没有在运行。先启动它(workoffdesk serve,或已安装的服务)。',
|
|
54
|
+
couldNotOpen: (why) => `打不开会话:${why}`,
|
|
55
|
+
noTunnel: (name) => `没有叫 ${name} 的隧道。运行:\n cloudflared tunnel create ${name}\n然后再运行一次 workoffdesk install --tunnel ${name}。`,
|
|
56
|
+
noCloudflared: '找不到 cloudflared。先运行 brew install cloudflared。',
|
|
57
|
+
notBuilt: (cliPath) => `注意:${cliPath} 还不存在,先构建(pnpm build)。`,
|
|
58
|
+
noCredentials: (file) => `注意:隧道凭据 ${file} 不在这台电脑上;请在创建这条隧道的电脑上运行它。`,
|
|
59
|
+
frontDoor: (port, origin) => `服务只监听 http://127.0.0.1:${port}。需要在它前面放一个能把 ${origin} 转到这里的东西:
|
|
60
|
+
Cloudflare 隧道:cloudflared tunnel create <名字>,然后 workoffdesk install --tunnel <名字>
|
|
61
|
+
或任何反向代理(Caddy、nginx…),把 ${origin} 转发到 http://127.0.0.1:${port},并支持 WebSocket`,
|
|
62
|
+
installed: (o) => `node:${o.nodePath}(换了 node 版本之后,用 --node <路径> 再运行一次 install)
|
|
63
|
+
已写入:
|
|
64
|
+
${o.written.map((f) => ` ${f}`).join('\n')}
|
|
65
|
+
${o.warnings.length ? `\n${o.warnings.join('\n')}\n` : ''}
|
|
66
|
+
现在要运行的命令(只需第一次):
|
|
67
|
+
${o.frontDoor}
|
|
68
|
+
|
|
69
|
+
以后更新:workoffdesk deploy <构建目录>;需要时 workoffdesk rollback。
|
|
70
|
+
只重启服务:launchctl kickstart -k gui/$(id -u)/${o.label}`,
|
|
71
|
+
deployUsage: '用法:workoffdesk deploy <构建目录> [--new-app-runtime](构建目录是 pnpm deploy --prod 生成的那个)',
|
|
72
|
+
},
|
|
73
|
+
doctor: {
|
|
74
|
+
notFound: '找不到',
|
|
75
|
+
unknownVersion: (version) => `看不出版本(${version})`,
|
|
76
|
+
tooOld: (version, min) => `${version};需要 ${min} 或更高`,
|
|
77
|
+
macosOnly: '服务只支持 macOS(它用 launchd,管的是这台 Mac 上的 Claude Code 会话)。',
|
|
78
|
+
nodeFix: (min) => `装 node ${min} 或更高版本(brew install node,或 nvm install 22),然后再运行一次 workoffdesk install。`,
|
|
79
|
+
tmuxFix: 'brew install tmux',
|
|
80
|
+
claudeFix: '安装 Claude Code:curl -fsSL https://claude.ai/install.sh | bash',
|
|
81
|
+
noCodex: '没有安装(可选:用来开 Codex 会话)',
|
|
82
|
+
setupFix: 'workoffdesk setup',
|
|
83
|
+
setUp: (user, origin) => `用户 ${user},地址 ${origin}`,
|
|
84
|
+
notSetUp: (home) => `还没有设置(${home} 里没有 config.json)`,
|
|
85
|
+
noUser: '还没有设置用户',
|
|
86
|
+
running: (local, version) => `在 ${local} 上运行${version ? `,版本 ${version}` : ''}`,
|
|
87
|
+
notAnswering: (local) => `${local} 上没有回应`,
|
|
88
|
+
otherVersion: (local, running, mine) => `${local} 上跑的是 ${running},这个命令是 ${mine}`,
|
|
89
|
+
restartFix: (label) => `重启它:launchctl kickstart -k gui/$(id -u)/${label}`,
|
|
90
|
+
installFix: '把它装成服务:workoffdesk install,再执行它打印的那行 launchctl bootstrap(或者用 workoffdesk serve 先试)',
|
|
91
|
+
bootstrapFix: (plist) => `启动它:launchctl bootstrap gui/$(id -u) ${plist}`,
|
|
92
|
+
kickstartFix: (label, log) => `重启它:launchctl kickstart -k gui/$(id -u)/${label};如果又停了,原因在 ${log}`,
|
|
93
|
+
localOnly: (origin) => `${origin}:只有这台 Mac 能访问`,
|
|
94
|
+
notReached: (origin) => `${origin} 到不了服务`,
|
|
95
|
+
originFix: (origin, local) => `检查把 ${origin} 转到 ${local} 的隧道或反向代理。`,
|
|
96
|
+
sleepsOff: 'preventSleep 是 false:Mac 空闲睡眠之后,手机就连不上,直到有人把它叫醒',
|
|
97
|
+
sleepFix: (file) => `把 ${file} 里的 "preventSleep": false 删掉,然后重启服务。`,
|
|
98
|
+
awake: '服务运行时 Mac 不会空闲睡眠',
|
|
99
|
+
noApns: '没有推送密钥:自己编译的 iPhone App 收不到通知;App Store 版走它的中转,网页推送也不受影响',
|
|
100
|
+
apnsKey: (keyId) => `推送密钥 ${keyId}`,
|
|
101
|
+
apnsFix: (file) => `改好 ${file};不用 iPhone App 的话删掉它。`,
|
|
102
|
+
failed: '有检查没通过;每个 ✗ 下面写了怎么办。',
|
|
103
|
+
},
|
|
104
|
+
ops: {
|
|
105
|
+
started: (port) => `服务已在 http://127.0.0.1:${port} 运行`,
|
|
106
|
+
stopped: '服务已停止;下次登录时会再启动,或运行 workoffdesk start',
|
|
107
|
+
restarted: '服务已重启',
|
|
108
|
+
notUp: (log) => `服务 30 秒内没有应答;原因多半在 ${log}`,
|
|
109
|
+
},
|
|
110
|
+
wizard: {
|
|
111
|
+
open: (url) => `接下来在浏览器里设置。如果没有自动打开,在这台 Mac 上打开这个地址:\n ${url}\n按 Ctrl-C 结束。`,
|
|
112
|
+
idle: '设置页 30 分钟没人用,已经结束。再运行 workoffdesk setup --web 重新开始。',
|
|
113
|
+
done: '设置完成。',
|
|
114
|
+
notReady: '先设好账号和地址',
|
|
115
|
+
noTailscale: '这台 Mac 上的 Tailscale 没有运行',
|
|
116
|
+
tailscaleFailed: (out) => `tailscale serve 失败:${out}`,
|
|
117
|
+
noTunnel: (name) => `这台 Mac 上没有叫 ${name} 的 Cloudflare 隧道`,
|
|
118
|
+
badHostname: '主机名是 workoffdesk.example.com 这样的名字,不带 https:// 也不带路径',
|
|
119
|
+
routeFailed: (out) => `cloudflared 没能把主机名指到隧道:${out}`,
|
|
120
|
+
},
|
|
121
|
+
node: {
|
|
122
|
+
doesNotRun: (node) => `${node} 运行不了。`,
|
|
123
|
+
tooOld: (node, version, min) => `${node} 是 ${version};WorkOffDesk 需要 node ${min} 或更高版本。`,
|
|
124
|
+
},
|
|
125
|
+
config: {
|
|
126
|
+
wrong: (field, expected) => `config.json:${field} 不对,应该是${expected}`,
|
|
127
|
+
origin: '站点的访问地址,比如 https://workoffdesk.example.com(在本机试用就用 http://localhost:<端口>)',
|
|
128
|
+
port: '1 到 65535 之间的整数',
|
|
129
|
+
rpId: (hostname) => `站点的主机名(${hostname})或它的上级域名`,
|
|
130
|
+
pinnedDirs: '文件夹路径的数组',
|
|
131
|
+
serviceLabel: '由字母、数字、点和连字符组成的 launchd 标签',
|
|
132
|
+
claudeArgs: '启动 claude 时带的参数数组,比如 ["--chrome"]',
|
|
133
|
+
extraOrigins: '站点另外几个 https 访问地址的数组,比如 ["https://other.example.com"]',
|
|
134
|
+
preventSleep: 'true(服务运行时不让 Mac 空闲睡眠)或 false',
|
|
135
|
+
missing: (file) => `已设置的,但 ${file} 还不存在;先运行 workoffdesk setup`,
|
|
136
|
+
format: '格式',
|
|
137
|
+
notJson: (why) => `合法的 JSON(${why})`,
|
|
138
|
+
},
|
|
139
|
+
release: {
|
|
140
|
+
notABuild: (dir) => `${dir} 不是一个构建(需要有 package.json 和 dist/server/cli.js)`,
|
|
141
|
+
smokeFailed: (version) => `${version} 没通过冒烟测试;正在运行的版本没动`,
|
|
142
|
+
didNotComeUp: (version, prev) => `${version} 30 秒内没有起来;${prev ? `已切回 ${prev}` : '没有更早的版本可以切回'}`,
|
|
143
|
+
staleBuild: (version, built) => `这个构建写的是 ${version},但它的 dist ${built ? `是为 ${built} 构建的` : '不是一次完整构建产出的'};重新运行 pnpm build`,
|
|
144
|
+
deployed: (version, name) => `已部署 ${version}(${name})`,
|
|
145
|
+
builtFrom: (s) => (s.commit ? `构建自 ${s.commit.slice(0, 12)}${s.dirty ? ',带未提交的改动' : ''}` : '不是在 git 里构建的'),
|
|
146
|
+
appUpdate: (runtime) => (runtime ? `iPhone 应用更新,运行时 ${runtime}` : '这个构建没有 iPhone 应用更新'),
|
|
147
|
+
appRuntimeChanged: (live, built) => `线上的 iPhone 应用更新是给运行时 ${live} 的,这个构建的${built ? `却是给 ${built} 的` : '没有(设了 WORKOFFDESK_IOS_BUNDLE_ID 吗?)'},装着 ${live} 的手机就再也收不到更新。App 的 JavaScript 以外的改动(它的 package.json 或 app 配置)会改变运行时。只有 App 的新版本发出去以后,才用 --new-app-runtime 部署`,
|
|
148
|
+
nothingToRollBackTo: '没有更早的版本可以回滚',
|
|
149
|
+
rollbackFailed: (target, current) => `${target} 没有起来;已切回 ${current}`,
|
|
150
|
+
rolledBack: (version, name) => `已回滚到 ${version}(${name})`,
|
|
151
|
+
},
|
|
152
|
+
account: {
|
|
153
|
+
usage: '用法:workoffdesk account [--json] | account use <邮箱或目录> [--move] | account move',
|
|
154
|
+
signedOut: '已退出登录',
|
|
155
|
+
notReady: '先在终端里用它打开一次 Claude Code',
|
|
156
|
+
apart: '它的对话没有和这台 Mac 的共用',
|
|
157
|
+
notSeen: '还没看到它的用量',
|
|
158
|
+
fiveHour: (p) => `5 小时 ${p}`,
|
|
159
|
+
sevenDay: (p) => `7 天 ${p}`,
|
|
160
|
+
reset: '已重置',
|
|
161
|
+
elsewhere: (n) => `${n} 个开着的会话在别的账号上。搬过来:workoffdesk account move`,
|
|
162
|
+
moving: (n) => `${n} 个会话等这轮回复完就搬过来。`,
|
|
163
|
+
noSuch: (who) => `这台 Mac 上没有 ${who} 这个 Claude 账号。`,
|
|
164
|
+
twoWithEmail: (email, folders) => `不止一个账号是 ${email},改用目录指定:${folders.join('、')}`,
|
|
165
|
+
nonePicked: '还没有选过账号。先选一个:workoffdesk account use <邮箱或目录>',
|
|
166
|
+
},
|
|
167
|
+
takeOver: {
|
|
168
|
+
openOnMac: (ttys, agent) => `这个对话正开在 Mac 的终端里${ttys.some(Boolean) ? `(${ttys.filter(Boolean).join('、')})` : ''};先在那里退出 ${agent},或者从手机上接管`,
|
|
169
|
+
macUnanswered: 'Mac 没及时说清这个对话是不是还开在终端里;稍等再试一次',
|
|
170
|
+
wouldNotQuit: (agent) => `Mac 终端里的 ${agent} 10 秒内没有退出;没有续上`,
|
|
171
|
+
openElsewhere: '这个对话开在别的 Codex 应用里(编辑器或 Codex 桌面版);先在那里关掉',
|
|
172
|
+
goneToPhone: (id) => `workoffdesk:这个对话已转到 workoffdesk 里接着进行。要在这台电脑上接着用:workoffdesk attach ${id}`,
|
|
173
|
+
},
|
|
174
|
+
goOn: {
|
|
175
|
+
onlyClaude: '只有 Claude 已退出的 Claude 会话能原地接着',
|
|
176
|
+
terminalBusy: '这个会话在 Mac 的终端里正跑着别的程序;先在那里退出它,或者重新开一个对话',
|
|
177
|
+
conversationGone: (agent) => `${agent} 已不再保留这个对话,没法接着聊了;聊过的内容在这里还能看`,
|
|
178
|
+
},
|
|
179
|
+
moveThis: {
|
|
180
|
+
cannot: '只有 Claude 还在运行、文件夹也还在的 Claude 会话才能换账号',
|
|
181
|
+
busy: '要等 Claude 回到输入框、也没有待发的消息才能换;等这轮回复完再试',
|
|
182
|
+
macBusy: '这个对话在 Mac 的终端里也开着,那边的 Claude 正在干活或在问你问题;先让它在那边停下来',
|
|
183
|
+
failed: (why) => `没换成,还在原来的账号上:${why}`,
|
|
184
|
+
},
|
|
185
|
+
folderGone: (path) => `它的文件夹已经不在了:${path}`,
|
|
186
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
// The git repository a folder is in, by its main folder, read from what git writes: a checkout's `.git` is a
|
|
4
|
+
// directory; a worktree's is a file naming its git directory, whose `commondir` names the repository's (a bare
|
|
5
|
+
// repository's worktree too). A `.git` file whose git directory has no `commondir` (a submodule), or that git
|
|
6
|
+
// could not read, makes its own folder the project. Null outside git. Its real path, so one reached through a
|
|
7
|
+
// link is one project with its worktrees. Kept per folder: a folder's repository does not change under a session.
|
|
8
|
+
const known = new Map();
|
|
9
|
+
export function projectOf(cwd) {
|
|
10
|
+
if (!known.has(cwd))
|
|
11
|
+
known.set(cwd, real(find(resolve(cwd))));
|
|
12
|
+
return known.get(cwd);
|
|
13
|
+
}
|
|
14
|
+
function find(dir) {
|
|
15
|
+
for (let at = dir;; at = dirname(at)) {
|
|
16
|
+
const dotGit = join(at, '.git');
|
|
17
|
+
const kind = kindOf(dotGit);
|
|
18
|
+
if (kind === 'dir')
|
|
19
|
+
return at;
|
|
20
|
+
if (kind === 'file')
|
|
21
|
+
return fromFile(dotGit) ?? at;
|
|
22
|
+
if (dirname(at) === at)
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function fromFile(dotGit) {
|
|
27
|
+
const gitdir = /^gitdir: (.+)$/m.exec(read(dotGit) ?? '')?.[1]?.trim();
|
|
28
|
+
if (!gitdir)
|
|
29
|
+
return null;
|
|
30
|
+
const own = isAbsolute(gitdir) ? gitdir : resolve(dirname(dotGit), gitdir);
|
|
31
|
+
const common = read(join(own, 'commondir'))?.trim();
|
|
32
|
+
if (!common)
|
|
33
|
+
return null;
|
|
34
|
+
const repo = resolve(own, common);
|
|
35
|
+
if (kindOf(repo) !== 'dir')
|
|
36
|
+
return null;
|
|
37
|
+
return basename(repo) === '.git' ? dirname(repo) : repo.replace(/\.git$/, '');
|
|
38
|
+
}
|
|
39
|
+
function kindOf(path) {
|
|
40
|
+
try {
|
|
41
|
+
const s = statSync(path);
|
|
42
|
+
return s.isDirectory() ? 'dir' : s.isFile() ? 'file' : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function read(path) {
|
|
49
|
+
try {
|
|
50
|
+
return readFileSync(path, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function real(path) {
|
|
57
|
+
if (!path)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
return realpathSync(path);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return path;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -8,7 +8,7 @@ export function shownTitle(meta) {
|
|
|
8
8
|
return named ? meta.title : (meta.customTitle ?? meta.aiTitle ?? meta.title);
|
|
9
9
|
}
|
|
10
10
|
export function emptyState() {
|
|
11
|
-
return { passkeys: [], logins: [], pushSubs: [], apnsDevices: [], sessions: {}, claudeNames: {} };
|
|
11
|
+
return { passkeys: [], logins: [], pushSubs: [], apnsDevices: [], sessions: {}, past: {}, claudeNames: {} };
|
|
12
12
|
}
|
|
13
13
|
export class Store {
|
|
14
14
|
file;
|
|
@@ -130,7 +130,7 @@ export class Tmux {
|
|
|
130
130
|
}
|
|
131
131
|
// One tmux command, so nothing can slip in between the check and the cancel.
|
|
132
132
|
async leaveCopyMode(name) {
|
|
133
|
-
await this.run(['if-shell', '-F', '-t', `=${name}:`, '#{pane_in_mode}',
|
|
133
|
+
await this.run(['if-shell', '-F', '-t', `=${name}:`, '#{pane_in_mode}', `send-keys -t =${name}: -X cancel`]);
|
|
134
134
|
}
|
|
135
135
|
async capture(name, lines) {
|
|
136
136
|
return this.run(['capture-pane', '-p', '-e', '-J', '-t', `=${name}:`, '-S', `-${lines}`]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as _,t,A as z,e as $,j as e,r as i,C as q,I as G}from"./main-
|
|
1
|
+
import{c as _,t,A as z,e as $,j as e,r as i,C as q,I as G}from"./main-DFndf3Z8.js";const k=["checks","account","address","service","phone"],O=["macos","node","tmux","claude"],R=s=>s.checks.filter(n=>O.includes(n.id)&&!n.ok);function L(s){return R(s).length?"checks":s.user?s.origin?s.checks.some(n=>n.id==="service"&&n.ok)?"phone":"service":"address":"account"}const P="setup-token";function M(s,n){const u=s.replace(/^#/,"");try{return u&&n.setItem(P,u),u||n.getItem(P)}catch{return u||null}}const F=12,W={getItem:s=>sessionStorage.getItem(s),setItem:(s,n)=>sessionStorage.setItem(s,n),removeItem:s=>sessionStorage.removeItem(s)},S=M(location.hash,W);location.hash&&history.replaceState(null,"",location.pathname);const{api:D}=_({base:()=>location.origin,headers:()=>S?{"x-setup-token":S}:{},onUnauthorized:()=>{}}),w=(s,n)=>D(s,{method:"POST",json:n}),v=s=>s instanceof z&&s.status===403?t.setupNoToken:$(s,t.failed,t.setupGone);function C({busy:s,children:n}){return s?e.jsx("span",{className:"spinner small"}):e.jsx(e.Fragment,{children:n})}function Y({state:s,reload:n,next:u}){const[a,r]=i.useState(!1),l=R(s),d=s.checks.filter(o=>O.includes(o.id)),x=async()=>{r(!0),await n(),r(!1)};return e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"setup-note",children:l.length?t.setupChecksFix:t.setupChecksNote}),e.jsx("div",{className:"group",children:[...l,...d.filter(o=>o.ok)].map(o=>e.jsx(q,{check:o},o.id))}),e.jsx("div",{className:"setup-actions",children:l.length?e.jsx("button",{className:"btn primary",onClick:x,disabled:a,children:e.jsx(C,{busy:a,children:t.setupCheckAgain})}):e.jsx("button",{className:"btn primary",onClick:u,children:t.setupContinue})})]})}function J({state:s,reload:n,next:u}){const[a,r]=i.useState(!s.user),[l,d]=i.useState(""),[x,o]=i.useState(""),[b,N]=i.useState(""),[g,f]=i.useState(null),[c,p]=i.useState(!1);if(!a&&s.user)return e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"setup-note",children:t.setupAccountHave(s.user)}),e.jsxs("div",{className:"setup-actions",children:[e.jsx("button",{className:"btn primary",onClick:u,children:t.setupKeep}),e.jsx("button",{className:"btn",onClick:()=>r(!0),children:t.setupReplace})]})]});const j=async h=>{if(h.preventDefault(),x!==b)return f(t.setupPasswordsDiffer);p(!0),f(null);try{await w("/api/setup/account",{username:l,password:x}),await n(),u()}catch(y){f(v(y))}finally{p(!1)}};return e.jsxs("form",{className:"setup-form",onSubmit:j,children:[e.jsx("p",{className:"setup-note",children:t.setupAccountNote}),s.user&&e.jsx("p",{className:"setup-warn",children:t.setupReplaceNote}),e.jsxs("label",{className:"field",children:[t.username,e.jsx("input",{value:l,onChange:h=>d(h.target.value),autoComplete:"username",autoCapitalize:"none",spellCheck:!1,required:!0})]}),e.jsxs("label",{className:"field",children:[t.password,e.jsx("input",{type:"password",value:x,onChange:h=>o(h.target.value),autoComplete:"new-password",minLength:F,required:!0}),e.jsx("small",{children:t.setupPasswordRule(F)})]}),e.jsxs("label",{className:"field",children:[t.setupPasswordAgain,e.jsx("input",{type:"password",value:b,onChange:h=>N(h.target.value),autoComplete:"new-password",required:!0})]}),g&&e.jsx("div",{className:"alert",role:"alert",children:g}),e.jsxs("div",{className:"setup-actions",children:[e.jsx("button",{className:"btn primary",type:"submit",disabled:c,children:e.jsx(C,{busy:c,children:t.setupSaveAccount})}),s.user&&e.jsx("button",{className:"btn",type:"button",onClick:()=>r(!1),children:t.cancel})]})]})}function Q({state:s,reload:n,next:u}){const{tailscale:a,cloudflared:r}=s.remote,l=(r==null?void 0:r.tunnels)??[],[d,x]=i.useState(a!=null&&a.running?"tailscale":l.length?"cloudflare":"other"),[o,b]=i.useState(l[0]??""),[N,g]=i.useState(""),[f,c]=i.useState("https://"),[p,j]=i.useState(null),[h,y]=i.useState(null),H=async m=>{m.preventDefault();const E=d==="tailscale"?{kind:d}:d==="cloudflare"?{kind:d,tunnel:o,hostname:N.trim()}:{kind:d,origin:f.trim()};y("save"),j(null);try{await w("/api/setup/address",E),await n(),u()}catch(T){j(v(T))}finally{y(null)}},K=async()=>{y("check"),await n(),y(null)},A=(m,E,T,I,U)=>e.jsxs("label",{className:`setup-choice${d===m?" chosen":""}${I?"":" off"}`,children:[e.jsx("input",{type:"radio",name:"kind",checked:d===m,disabled:!I,onChange:()=>x(m)}),e.jsxs("span",{className:"setup-choice-text",children:[e.jsx("strong",{children:E}),e.jsx("span",{children:T}),d===m&&U]})]});return e.jsxs("form",{className:"setup-form",onSubmit:H,children:[e.jsx("p",{className:"setup-note",children:t.setupAddressNote}),s.origin&&e.jsxs("div",{className:"setup-now",children:[e.jsx("span",{children:t.setupAddressNow(s.origin)}),e.jsx("button",{className:"btn small",type:"button",onClick:u,children:t.setupKeep})]}),e.jsxs("div",{className:"setup-choices",children:[a&&A("tailscale",t.setupTailscale,a.running?t.setupTailscaleUse(a.dnsName):t.setupTailscaleDown,a.running),l.length>0&&A("cloudflare",t.setupCloudflare,t.setupCloudflareNote,!0,e.jsxs("span",{className:"setup-choice-fields",children:[e.jsxs("label",{className:"field",children:[t.setupTunnel,e.jsx("select",{value:o,onChange:m=>b(m.target.value),children:l.map(m=>e.jsx("option",{children:m},m))})]}),e.jsxs("label",{className:"field",children:[t.setupHostname,e.jsx("input",{value:N,onChange:m=>g(m.target.value),placeholder:"workoffdesk.example.com",autoCapitalize:"none",spellCheck:!1,required:!0})]})]})),A("other",t.setupOther,t.setupOtherNote(s.port),!0,e.jsx("span",{className:"setup-choice-fields",children:e.jsx("label",{className:"field",children:e.jsx("input",{value:f,onChange:m=>c(m.target.value),type:"url",autoCapitalize:"none",spellCheck:!1,required:!0,"aria-label":t.setupOther})})}))]}),!(a!=null&&a.running)&&!l.length&&e.jsx("p",{className:"setup-note",children:t.setupNoWayIn}),p&&e.jsx("div",{className:"alert",role:"alert",children:p}),e.jsxs("div",{className:"setup-actions",children:[e.jsx("button",{className:"btn primary",type:"submit",disabled:h!==null,children:e.jsx(C,{busy:h==="save",children:t.setupUseAddress})}),e.jsx("button",{className:"btn",type:"button",onClick:K,disabled:h!==null,children:e.jsx(C,{busy:h==="check",children:t.setupCheckAgain})})]})]})}function V({reload:s,next:n}){const[u,a]=i.useState(!1),[r,l]=i.useState(null),d=async()=>{a(!0),l(null);try{const x=await w("/api/setup/service",{});await s(),x.ok?n():l(x.message)}catch(x){l(v(x))}finally{a(!1)}};return e.jsxs(e.Fragment,{children:[e.jsx("p",{className:"setup-note",children:t.setupServiceNote}),u&&e.jsxs("div",{className:"setup-busy",role:"status",children:[e.jsx("span",{className:"spinner small"}),t.setupInstalling]}),r&&e.jsx("div",{className:"alert setup-pre",role:"alert",children:r}),e.jsx("div",{className:"setup-actions",children:e.jsx("button",{className:"btn primary",onClick:d,disabled:u,children:r?t.tryAgain:t.setupInstall})})]})}function B({state:s}){return e.jsxs(e.Fragment,{children:[s.qr&&e.jsx("div",{className:"setup-qr",dangerouslySetInnerHTML:{__html:s.qr}}),e.jsx("p",{className:"setup-origin",children:s.origin}),s.user&&e.jsx("p",{className:"setup-note centered",children:t.setupPhoneNote(s.user)})]})}function X({state:s,finish:n}){const[u,a]=i.useState(!1),r=s.checks.filter(l=>!l.ok);return e.jsxs(e.Fragment,{children:[e.jsx(B,{state:s}),e.jsx("div",{className:`mac-summary ${r.length?"bad":"ok"}`,children:r.length?t.macProblemCount(r.length):t.macAllGood}),r.length>0&&e.jsx("div",{className:"group",children:r.map(l=>e.jsx(q,{check:l},l.id))}),e.jsx("div",{className:"setup-actions",children:e.jsx("button",{className:"btn primary",disabled:u,onClick:async()=>{a(!0),await n(),a(!1)},children:t.setupFinish})})]})}const Z=(s,n)=>s==="account"?n.user:s==="address"?n.origin:s==="service"?t.macRunning:null,ee=["account","address"];function te(){const[s,n]=i.useState(null),[u,a]=i.useState(null),[r,l]=i.useState(S?null:t.setupNoToken),[d,x]=i.useState(!1),o=i.useCallback(async()=>{try{const c=await D("/api/setup/state");n(c),l(null),a(p=>p??L(c))}catch(c){l(v(c))}},[]);i.useEffect(()=>{document.title=t.setupTitle,S&&o()},[o]);const b=async()=>{try{await w("/api/setup/done",{}),x(!0)}catch(c){l(v(c))}},N=u?k.indexOf(u):-1,g=()=>a(k[Math.min(N+1,k.length-1)]),f=(c,p)=>c==="checks"?e.jsx(Y,{state:p,reload:o,next:g}):c==="account"?e.jsx(J,{state:p,reload:o,next:g}):c==="address"?e.jsx(Q,{state:p,reload:o,next:g}):c==="service"?e.jsx(V,{reload:o,next:g}):e.jsx(X,{state:p,finish:b});return e.jsxs("main",{className:"setup",children:[e.jsxs("header",{className:"brand setup-brand",children:[e.jsx("img",{src:"/icon.svg",alt:""}),e.jsx("h1",{children:t.setupTitle}),e.jsx("p",{children:t.setupSub})]}),d?e.jsxs("div",{className:"setup-done",children:[e.jsx("div",{className:"mac-summary ok",role:"status",children:t.setupFinished}),s&&e.jsx(B,{state:s})]}):e.jsxs(e.Fragment,{children:[r&&e.jsx("div",{className:"alert setup-gap",role:"alert",children:r}),!s&&!r&&e.jsx("div",{className:"mac-loading",children:e.jsx("span",{className:"spinner"})}),s&&u&&e.jsx("ol",{className:"setup-steps",children:k.map((c,p)=>{const j=p<N?"done":p===N?"now":"later",h=j==="done"?Z(c,s):null;return e.jsxs("li",{className:`setup-step ${j}`,"aria-current":j==="now"?"step":void 0,children:[e.jsxs("div",{className:"setup-step-head",children:[e.jsx("span",{className:"setup-num",children:j==="done"?e.jsx(G,{}):p+1}),e.jsx("strong",{children:t.setupSteps[c]}),h&&e.jsx("span",{className:"setup-summary",children:h}),j==="done"&&ee.includes(c)&&e.jsx("button",{className:"setup-change",onClick:()=>a(c),children:t.setupChange})]}),j==="now"&&e.jsx("div",{className:"setup-body",children:f(c,s)})]},c)})})]})]})}export{te as Setup};
|