linkitty 0.0.0-stage → 0.17.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/README.en.md +70 -0
- package/README.md +69 -2
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/artifacts.json +10 -0
- package/bin/linkitty.cjs +189 -0
- package/lib/i18n.cjs +31 -0
- package/lib/install.cjs +95 -0
- package/lib/messages.json +457 -0
- package/node_modules/qrcode-terminal/.travis.yml +3 -0
- package/node_modules/qrcode-terminal/LICENSE +222 -0
- package/node_modules/qrcode-terminal/README.md +82 -0
- package/node_modules/qrcode-terminal/bin/qrcode-terminal.js +101 -0
- package/node_modules/qrcode-terminal/example/basic.js +2 -0
- package/node_modules/qrcode-terminal/example/basic.png +0 -0
- package/node_modules/qrcode-terminal/example/callback.js +4 -0
- package/node_modules/qrcode-terminal/example/small-qrcode.js +6 -0
- package/node_modules/qrcode-terminal/lib/main.js +100 -0
- package/node_modules/qrcode-terminal/package.json +37 -0
- package/node_modules/qrcode-terminal/test/main.js +63 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QR8bitByte.js +22 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRBitBuffer.js +38 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js +7 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRMaskPattern.js +10 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRMath.js +44 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js +6 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRPolynomial.js +66 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRRSBlock.js +298 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/QRUtil.js +272 -0
- package/node_modules/qrcode-terminal/vendor/QRCode/index.js +462 -0
- package/package.json +29 -4
- package/payload/darwin-arm64.tgz +0 -0
- package/payload/linux-x64.tgz +0 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
English · [简体中文](README.md)
|
|
2
|
+
|
|
3
|
+
# Linkitty
|
|
4
|
+
|
|
5
|
+
Connect services on your own computer and keep using them from your phone or desktop.
|
|
6
|
+
|
|
7
|
+
Download the CLI tarball from the [official website](https://linkitty.spark-data.cn/?lang=en#downloads). On macOS, install the matching desktop tarball alongside it; Linux installs only the CLI. Follow the system tabs on the website. Public npm registry publication remains deferred.
|
|
8
|
+
|
|
9
|
+
linkitty --lang en
|
|
10
|
+
linkitty login
|
|
11
|
+
linkitty connect codex
|
|
12
|
+
|
|
13
|
+
The interface supports Chinese, English, Japanese and Russian. Save a default with `linkitty --lang zh|en|ja|ru`, or override the current process with `LINKITTY_LANG`. Language never changes account region. Desktop and CLI share `~/.linkitty/config/preferences.json`; the desktop reads this preference on its next launch.
|
|
14
|
+
|
|
15
|
+
Shared Codex integration currently supports macOS. For first-time integration, if Codex is running independently, finish your tasks and quit Codex before running the connection command. After it succeeds, open Codex normally. Integration neither starts a second executor alongside it nor takes over existing sessions. System background startup uses enabled macOS system proxies; existing proxy environment variables take precedence.
|
|
16
|
+
|
|
17
|
+
Claim from your phone:
|
|
18
|
+
|
|
19
|
+
linkitty connect codex -qr
|
|
20
|
+
|
|
21
|
+
Regions and websites:
|
|
22
|
+
|
|
23
|
+
linkitty --region cn
|
|
24
|
+
linkitty --region sg --endpoint https://你的新加坡入口/gateway
|
|
25
|
+
linkitty connect web 127.0.0.1:8080 --name 我的工作台
|
|
26
|
+
|
|
27
|
+
The first background connection registers a service for the current system user. CLI and desktop share one Linkitty connection process, restored after computer login and restarted by the system after unexpected exits. Connections are unreachable while the computer sleeps or is offline; the business application must also remain running. Pause a connection with `linkitty disconnect ID`, or stop the system background service with `linkitty service uninstall`; configuration and bindings are retained.
|
|
28
|
+
|
|
29
|
+
Permanently delete a service with `linkitty delete 实例ID --yes`; this requires a platform version supporting the deletion API. It physically deletes relay registration, shares, and access grants while retaining computer projects, native sessions, and files. Reconnecting afterward requires an explicit connection command. Desktop and phone provide the same deletion action in service details under My Services, separate from disablement.
|
|
30
|
+
|
|
31
|
+
The cn development version uses the currently verified endpoint; sg/us require configuration before use. Switching regions does not migrate accounts or services. Platform accounts and Codex model login are managed separately.
|
|
32
|
+
|
|
33
|
+
Six connection types are supported:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
linkitty connect codex
|
|
37
|
+
# Optional: linkitty connect codex --workspace /absolute/project
|
|
38
|
+
linkitty connect pi --workspace /absolute/project --executable /absolute/path/to/pi
|
|
39
|
+
linkitty connect claude --workspace /absolute/project --profile /absolute/private/claude/profile.json
|
|
40
|
+
linkitty connect dsh --workspace /absolute/project --profile /absolute/private/deepseek/profile.json
|
|
41
|
+
linkitty connect web 127.0.0.1:8080
|
|
42
|
+
linkitty connect api 127.0.0.1:9000 --routes /absolute/api-routes.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
All `connect` commands accept `-qr`; when logged in, omit it to claim directly. DSH means DeepSeek Harness. Codex/Pi use their existing login environments, while Claude/DSH use existing private runtime configuration. Linkitty does not replace model login. Pi, Claude, and DSH retain their own tool permissions and approval mechanisms and must not be treated as a Codex sandbox. Use `--agent-dir` for a custom Pi authentication directory. All four engines require project directories separate from Linkitty's private directories; using the entire user home directory as a project is discouraged.
|
|
46
|
+
|
|
47
|
+
Minimal API route file:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"business_api": { "id": "my.service.api", "version": "1.0.0" },
|
|
52
|
+
"routes": [
|
|
53
|
+
{ "methods": ["GET"], "path": "/health", "requires": ["api.read"] },
|
|
54
|
+
{ "methods": ["POST"], "path": "/jobs", "requires": ["api.write"] }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The upstream is fixed to a local HTTP origin. HTTP/SSE retain original business paths; WebSocket routes require `websocket: true`. Unknown routes and requests missing required capabilities are rejected. Changing the permission file creates a new instance; pause the old connection yourself.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
linkitty list
|
|
63
|
+
linkitty access 实例ID --capabilities api.read --output /absolute/private/access.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The private file contains a short-lived `endpoint` and `access_token` with mode 600. Requests use `Relay-Authorization: Bearer <access_token>`; never commit or share this file. Generate a new file after expiry without overwriting an existing one. CLI never prints credentials to stdout.
|
|
67
|
+
|
|
68
|
+
The CLI contains macOS ARM64 and Linux x64 payloads; the installer selects the system and checks its hash. Linux targets Ubuntu 24.04 / glibc 2.39; binaries and installation are checked in containers, while real user-level systemd startup remains unverified. Windows uses the WSL 2 / Ubuntu 24.04 guide and has not been tested on a real machine. Native Windows, Linux ARM64, musl and other systems without payloads fail explicitly. Codex desktop sharing supports macOS only; use Pi, Claude or DSH on Linux / WSL.
|
|
69
|
+
|
|
70
|
+
Coding connections do not bind a project by default and use the user's `~` when an execution directory is needed; explicit `--workspace` settings are retained. The first Codex connection automatically prepares the fixed shared service and subsequent launch configuration. If a standalone Codex is currently running, restart it normally once after finishing its tasks. Later launches work directly without a shared-control script.
|
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Linkitty
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.en.md) · 简体中文
|
|
4
|
+
|
|
5
|
+
连接自己的电脑服务,在手机或桌面继续使用。
|
|
6
|
+
|
|
7
|
+
从[官网下载页](https://linkitty.spark-data.cn/#downloads)取得 CLI tarball。macOS 桌面需同版桌面 tarball 一起安装,Linux 仅安装 CLI。按页面的系统页签操作;npm 公共仓库暂缓发布。
|
|
8
|
+
|
|
9
|
+
linkitty --lang zh
|
|
10
|
+
linkitty login
|
|
11
|
+
linkitty connect codex
|
|
12
|
+
|
|
13
|
+
界面语言支持中文、英文、日文和俄文:`linkitty --lang zh|en|ja|ru` 保存默认值,`LINKITTY_LANG` 覆盖本次进程。语言不改变账号地域;桌面与 CLI 共用 `~/.linkitty/config/preferences.json`,桌面下次启动读取该偏好。
|
|
14
|
+
|
|
15
|
+
Codex 共享接入目前支持 macOS。首次接入时,若 Codex 正在独立运行,请先完成任务并退出 Codex,再执行接入命令;成功后正常打开 Codex 即可。接入不会在旁边另起执行器,也不会抢占现有会话。系统后台启动会使用已启用的 macOS 系统代理,已有代理环境变量优先。
|
|
16
|
+
|
|
17
|
+
使用手机认领:
|
|
18
|
+
|
|
19
|
+
linkitty connect codex -qr
|
|
20
|
+
|
|
21
|
+
区域与网站:
|
|
22
|
+
|
|
23
|
+
linkitty --region cn
|
|
24
|
+
linkitty --region sg --endpoint https://你的新加坡入口/gateway
|
|
25
|
+
linkitty connect web 127.0.0.1:8080 --name 我的工作台
|
|
26
|
+
|
|
27
|
+
首次使用后台连接会注册当前系统用户的服务。CLI 和桌面共用同一个 Linkitty 连接进程,电脑登录后恢复,异常退出由系统重新拉起。电脑睡眠或断网期间不可达,业务软件仍需运行。暂停连接使用 linkitty disconnect ID,停止系统后台使用 linkitty service uninstall;配置和绑定会保留。
|
|
28
|
+
|
|
29
|
+
永久删除服务使用 `linkitty delete 实例ID --yes`,需要配套平台支持删除接口。此操作物理删除中继登记、分享和访问授权,电脑项目、原生会话和文件保留;再次使用需明确执行接入。桌面和手机在“我的服务”的详情中提供同一删除能力,与停用独立。
|
|
30
|
+
|
|
31
|
+
中国区默认入口为 `https://cn.linkitty.spark-data.cn/gateway`,既有 IP 配置和账号继续保留;sg/us 未开放时拒绝登录。切区不迁移账号或服务。平台账号与 Codex 模型登录分别处理。
|
|
32
|
+
|
|
33
|
+
支持六种接入:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
linkitty connect codex
|
|
37
|
+
# Optional: linkitty connect codex --workspace /absolute/project
|
|
38
|
+
linkitty connect pi --workspace /absolute/project --executable /absolute/path/to/pi
|
|
39
|
+
linkitty connect claude --workspace /absolute/project --profile /absolute/private/claude/profile.json
|
|
40
|
+
linkitty connect dsh --workspace /absolute/project --profile /absolute/private/deepseek/profile.json
|
|
41
|
+
linkitty connect web 127.0.0.1:8080
|
|
42
|
+
linkitty connect api 127.0.0.1:9000 --routes /absolute/api-routes.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
所有 connect 都可使用 `-qr`;已登录时省略该参数可直接认领。DSH 对应 DeepSeek Harness。Codex/Pi 使用已有登录环境,Claude/DSH 使用既有私有运行配置,Linkitty 不代替模型登录。Pi、Claude 和 DSH 保留各自工具权限与审批机制,不能视为 Codex 沙箱。自定义 Pi 认证目录用 `--agent-dir`。四种引擎的项目目录必须与 Linkitty 私有目录分离,不建议把整个用户主目录作为项目。
|
|
46
|
+
|
|
47
|
+
API 路由文件最小示例:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"business_api": { "id": "my.service.api", "version": "1.0.0" },
|
|
52
|
+
"routes": [
|
|
53
|
+
{ "methods": ["GET"], "path": "/health", "requires": ["api.read"] },
|
|
54
|
+
{ "methods": ["POST"], "path": "/jobs", "requires": ["api.write"] }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
上游固定为本机 HTTP origin。HTTP/SSE 使用原业务路径;WebSocket 路由需设置 `websocket: true`。未知路由和缺少能力的请求被拒绝。更改权限文件产生新的实例,旧连接需自行暂停。
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
linkitty list
|
|
63
|
+
linkitty access 实例ID --capabilities api.read --output /absolute/private/access.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
私有文件包含短期 `endpoint` 与 `access_token`,权限为 600,请求使用 `Relay-Authorization: Bearer <access_token>`;不要提交或分享该文件。过期后重新生成,不覆盖已有文件。CLI 不向标准输出显示凭据。
|
|
67
|
+
|
|
68
|
+
当前 CLI 包含 macOS ARM64、Linux x64 两种原生组件,安装器按系统选择并校验摘要。Linux 基线 Ubuntu 24.04 / glibc 2.39,容器已验证二进制与安装;真实用户级 systemd 自启仍需实机验收。Windows 通过 WSL 2 / Ubuntu 24.04 使用 Linux 指引,尚未实机验证。原生 Windows、Linux ARM64、musl 和其他缺少制品的系统会明确拒绝安装。Codex 桌面共享连接仅支持 macOS;Linux / WSL 使用 Pi、Claude 或 DSH。
|
|
69
|
+
|
|
70
|
+
Coding 连接默认不绑定项目,执行目录需要时使用用户 `~`;显式 `--workspace` 保留。Codex 首次连接自动准备固定共享服务与后续启动配置。当前独立 Codex 正在运行时,只需在任务结束后正常重启一次;以后直接打开,无需共控脚本。
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# 第三方软件 / Third-party software
|
|
2
|
+
|
|
3
|
+
原生组件使用 Rust/iroh、Flutter 及各自依赖,许可证不因 Linkitty 包名改变。制品内 licenses 目录保留对应通知。Linkitty 自有代码未另行授予开源许可。
|
|
4
|
+
|
|
5
|
+
Native components use Rust/iroh, Flutter and their dependencies. Their licenses are unchanged by the Linkitty package name; corresponding notices are included in the payload licenses directory. No open-source license is granted for Linkitty first-party code at this stage.
|
package/artifacts.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"darwin-arm64": {
|
|
3
|
+
"archive": "payload/darwin-arm64.tgz",
|
|
4
|
+
"sha256": "4a5b0d54b36dd8f2da2b0763ee549bf691e991cb7ead55efb8879349bfbc8624"
|
|
5
|
+
},
|
|
6
|
+
"linux-x64": {
|
|
7
|
+
"archive": "payload/linux-x64.tgz",
|
|
8
|
+
"sha256": "566a65fba4d69b6e5c60d9fa8bf6a9ac7f85359bdc15b16da55d962ea9a027cc"
|
|
9
|
+
}
|
|
10
|
+
}
|
package/bin/linkitty.cjs
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const {t,setLanguage}=require("../lib/i18n.cjs");
|
|
4
|
+
const fs=require('node:fs');
|
|
5
|
+
const path=require('node:path');
|
|
6
|
+
const os=require('node:os');
|
|
7
|
+
const readline=require('node:readline/promises');
|
|
8
|
+
const {spawnSync}=require('node:child_process');
|
|
9
|
+
const {runtime}=require('../lib/install.cjs');
|
|
10
|
+
const pkg=require('../package.json');
|
|
11
|
+
const help=()=>'Linkitty '+pkg.version+'\n\n'+
|
|
12
|
+
t(' linkitty login 登录中继账号\n')+
|
|
13
|
+
t(' linkitty connect codex [-qr] 接入 Codex,不默认绑定项目;-qr 显示认领码\n')+
|
|
14
|
+
t(' linkitty connect pi|claude|dsh [-qr] 接入现有 Coding 运行环境\n')+
|
|
15
|
+
t(' linkitty connect web 127.0.0.1:8080 接入本机网站\n')+
|
|
16
|
+
t(' linkitty connect api 地址 --routes 文件 按明确路由接入纯 API\n')+
|
|
17
|
+
t(' linkitty access 实例ID --capabilities api.read --output 文件\n')+
|
|
18
|
+
t(' linkitty --region cn|sg|us 保存默认区域\n')+
|
|
19
|
+
t(' linkitty --lang zh|en|ja|ru 保存界面语言\n')+
|
|
20
|
+
t(' linkitty --region sg --endpoint URL 配置该区域的 HTTPS 平台入口\n')+
|
|
21
|
+
t(' linkitty status | list 查看登录、后台与连接\n')+
|
|
22
|
+
t(' linkitty disconnect ID 暂停一个连接\n')+
|
|
23
|
+
t(' linkitty delete 实例ID --yes 永久删除服务登记和分享授权\n')+
|
|
24
|
+
t(' linkitty logout 退出账号并暂停其连接\n')+
|
|
25
|
+
' linkitty service install|status|uninstall\n\n'+
|
|
26
|
+
t('通用选项:--region、--endpoint、--json;connect 可用 --workspace、--name。\n')+
|
|
27
|
+
t('Coding 可用 --executable;Claude/DSH 可用 --profile;Pi 可用 --agent-dir。\n')+
|
|
28
|
+
t('login --stdin 从标准输入读取账号密码 JSON。首次连接会安装用户后台服务。\n');
|
|
29
|
+
function parse(argv) {
|
|
30
|
+
const opts={},pos=[];
|
|
31
|
+
const values=new Set(['--lang','--region','--endpoint','--workspace','--name','--username','--adapter','--codex','--executable','--profile','--agent-dir','--routes','--capabilities','--output']);
|
|
32
|
+
for(let i=0;i<argv.length;i++){
|
|
33
|
+
const arg=argv[i];
|
|
34
|
+
if(values.has(arg)){
|
|
35
|
+
const value=argv[++i];if(!value||value.startsWith('--'))throw new Error(arg+t(' 缺少值。'));
|
|
36
|
+
opts[arg.slice(2)]=value;
|
|
37
|
+
}else if(['--json','--stdin','--qr','-qr','--yes'].includes(arg)){opts[arg==='-qr'?'qr':arg.slice(2)]=true;}
|
|
38
|
+
else if(arg.startsWith('-'))throw new Error(t('未知选项:')+arg);
|
|
39
|
+
else pos.push(arg);
|
|
40
|
+
}
|
|
41
|
+
if(opts.region&&!['cn','sg','us'].includes(opts.region))throw new Error(t('区域必须是 cn、sg 或 us。'));
|
|
42
|
+
if(opts.endpoint&&!opts.region)throw new Error(t('--endpoint 需要同时指定 --region。'));
|
|
43
|
+
return {opts,pos};
|
|
44
|
+
}
|
|
45
|
+
function native(args,input) {
|
|
46
|
+
const result=spawnSync(runtime(),args,{input:input===undefined?undefined:JSON.stringify(input),encoding:'utf8',maxBuffer:8*1024*1024,timeout:250000,windowsHide:true});
|
|
47
|
+
if(result.error)throw new Error(result.error.code==='ETIMEDOUT'?t('请求结果待核实,请用 linkitty status 或 list 查看。'):result.error.message);
|
|
48
|
+
let output;
|
|
49
|
+
try{output=JSON.parse(result.stdout);}catch{throw new Error(t('后台操作失败,请运行 linkitty service status 检查。'));}
|
|
50
|
+
if(result.status!==0||output.error)throw new Error(friendly(output.error||'runtime_failed'));
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
function friendly(code) {
|
|
54
|
+
const messages={
|
|
55
|
+
desktop_environment_conflict:t('已有另一个 Codex 启动连接,本次没有覆盖;请核对已有启动设置。'),
|
|
56
|
+
desktop_startup_registration_failed:t('未能保存启动设置,原有启动环境已保留,请重试。'),
|
|
57
|
+
login_required_or_use_qr:t('请先运行 linkitty login,或添加 -qr 用手机扫码认领。'),
|
|
58
|
+
codex_component_required:t('当前制品缺少 Coding 接入组件,请重新安装 Linkitty。'),
|
|
59
|
+
codex_executable_missing:t('请先安装官方 Codex 桌面版,再重新接入。'),
|
|
60
|
+
desktop_shared_connection_required:t('请先完成当前任务并退出 Codex,再次执行接入命令;接入成功后正常打开 Codex 即可。'),
|
|
61
|
+
codex_shared_platform_unsupported:t('当前 Codex 共享接入仅支持 macOS;此系统不会启动独立执行器冒充共控。'),
|
|
62
|
+
codex_execution_state_unavailable:t('暂时无法确认 Codex 的共享连接状态,请稍后重试;已有会话和服务绑定保留。'),
|
|
63
|
+
shared_runtime_busy:t('其他 Codex 会话或执行器仍占用任务,请完成任务并关闭对应会话后再接入。本次没有启动新执行器。'),
|
|
64
|
+
shared_runtime_state_unknown:t('暂时无法核对已有 Codex 执行器状态,请稍后重试;本次没有启动新执行器。'),
|
|
65
|
+
codex_prepare_failed:t('Coding 接入准备失败,请核对执行器、认证环境和 profile 配置。'),
|
|
66
|
+
harness_profile_required:t('请先配置本机 Claude/DSH 运行环境,再用 --profile 指定受保护的 profile.json。'),
|
|
67
|
+
coding_executable_missing:t('没有找到本机执行器,请用 --executable 指定完整路径。'),
|
|
68
|
+
harness_file_missing:t('运行配置中的执行器、桥接程序或凭据文件不存在。'),
|
|
69
|
+
api_routes_required:t('纯 API 接入需要 --routes 文件,包含 business_api 和 routes。'),
|
|
70
|
+
legacy_connection_running:t('此项目的旧版连接仍在运行。请完成当前任务并停止旧连接,再接入 Linkitty;将复用原实例绑定。'),
|
|
71
|
+
legacy_connection_ambiguous:t('发现多个旧版实例指向此项目,请先在原管理入口确认需要保留的连接。'),
|
|
72
|
+
workspace_access_denied:t('系统未允许后台访问此项目目录。请在系统设置中授予 Linkitty 后台组件目录权限,或选择可访问的项目目录。'),
|
|
73
|
+
workspace_unavailable:t('项目目录不存在或无法读取,请核对 --workspace。'),
|
|
74
|
+
background_service_disabled:t('后台连接已停止。运行 linkitty service install 恢复,或重新执行 connect。'),
|
|
75
|
+
local_service_unreachable:t('没有连接到本机服务,请确认程序已启动且端口正确。'),
|
|
76
|
+
local_request_outcome_unknown:t('请求结果待核实,请查看 linkitty status 或 list;不会自动重发操作。'),
|
|
77
|
+
system_service_command_failed:t('系统后台服务注册失败,请查看交接文档中的系统设置。'),
|
|
78
|
+
application_bound_to_other_account:t('这个服务已属于另一个账号,请使用原拥有者账号。'),
|
|
79
|
+
};
|
|
80
|
+
if(String(code).startsWith('region_endpoint_not_configured:'))return t('该区域尚未配置,请运行 linkitty --region ')+code.split(':')[1]+t(' --endpoint https://你的域名/gateway');
|
|
81
|
+
return messages[code]||String(code);
|
|
82
|
+
}
|
|
83
|
+
async function password() {
|
|
84
|
+
process.stdout.write(t('密码:'));
|
|
85
|
+
const stdin=process.stdin;
|
|
86
|
+
if(!stdin.isTTY)throw new Error(t('非交互登录请使用 login --stdin。'));
|
|
87
|
+
return new Promise((resolve,reject)=>{
|
|
88
|
+
let value='';
|
|
89
|
+
const wasRaw=stdin.isRaw;
|
|
90
|
+
const finish=(error)=>{stdin.off('data',onData);stdin.setRawMode(Boolean(wasRaw));stdin.pause();process.stdout.write('\n');error?reject(error):resolve(value);};
|
|
91
|
+
const onData=(data)=>{
|
|
92
|
+
for(const char of data.toString('utf8')){
|
|
93
|
+
if(char==='\r'||char==='\n'){finish();return;}
|
|
94
|
+
if(char==='\u0003'||char==='\u0004'){finish(new Error(t('登录已取消。')));return;}
|
|
95
|
+
if(char==='\u007f'||char==='\b')value=Array.from(value).slice(0,-1).join('');
|
|
96
|
+
else if(char>=' '&&value.length<4096)value+=char;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
stdin.setRawMode(true);stdin.resume();stdin.on('data',onData);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function findExecutable(name) {
|
|
103
|
+
for(const dir of (process.env.PATH||'').split(path.delimiter)){
|
|
104
|
+
for(const suffix of process.platform==='win32'?['.exe','.cmd','']:['']){
|
|
105
|
+
const file=path.resolve(dir,name+suffix);
|
|
106
|
+
if(fs.existsSync(file)&&fs.statSync(file).isFile())return fs.realpathSync(file);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
async function main(argv) {
|
|
112
|
+
const languageIndex=argv.indexOf('--lang');
|
|
113
|
+
if(languageIndex>=0)setLanguage(argv[languageIndex+1]);
|
|
114
|
+
if(argv.includes('--version')||argv.includes('-v')){console.log(pkg.version);return;}
|
|
115
|
+
if(!argv.length||argv.includes('--help')||argv.includes('-h')){console.log(help());return;}
|
|
116
|
+
const {opts,pos}=parse(argv);
|
|
117
|
+
if(opts.lang&&!pos.length&&!opts.region&&!opts.endpoint){console.log(opts.json?JSON.stringify({language:opts.lang}):t('语言已保存:')+opts.lang);return;}
|
|
118
|
+
const request=(op,args={})=>native(['request'],{op,args,region:opts.region});
|
|
119
|
+
if(opts.endpoint||!pos.length){
|
|
120
|
+
const result=request('region',{region:opts.region,endpoint:opts.endpoint});
|
|
121
|
+
if(!pos.length){console.log(opts.json?JSON.stringify(result):t('默认区域:')+result.selected);return;}
|
|
122
|
+
}
|
|
123
|
+
const command=pos[0];let result;
|
|
124
|
+
switch(command){
|
|
125
|
+
case 'login': {
|
|
126
|
+
if(pos.length!==1)throw new Error(t('用法:linkitty login'));
|
|
127
|
+
let credentials;
|
|
128
|
+
if(opts.stdin){
|
|
129
|
+
const bytes=Buffer.alloc(65537);let count=0;
|
|
130
|
+
while(count<bytes.length){const n=fs.readSync(0,bytes,count,bytes.length-count,null);if(n===0)break;count+=n;}
|
|
131
|
+
if(count>65536)throw new Error(t('登录输入过大。'));
|
|
132
|
+
try{credentials=JSON.parse(bytes.subarray(0,count).toString('utf8'));}catch{throw new Error(t('登录输入不是有效的 JSON。'));}
|
|
133
|
+
}else{
|
|
134
|
+
if(!process.stdin.isTTY)throw new Error(t('非交互登录请使用 login --stdin。'));
|
|
135
|
+
let username=opts.username;
|
|
136
|
+
if(!username){const rl=readline.createInterface({input:process.stdin,output:process.stdout});username=await rl.question(t('账号:'));rl.close();}
|
|
137
|
+
credentials={username,password:await password()};
|
|
138
|
+
}
|
|
139
|
+
if(typeof credentials.username!=='string'||typeof credentials.password!=='string')throw new Error(t('需要 username 和 password。'));
|
|
140
|
+
result=request('login',credentials);break;
|
|
141
|
+
}
|
|
142
|
+
case 'connect': {
|
|
143
|
+
if(!['codex','pi','claude','dsh','web','api'].includes(pos[1]))throw new Error(t('接入类型:codex、pi、claude、dsh、web、api。'));
|
|
144
|
+
const proxy=['web','api'].includes(pos[1]);
|
|
145
|
+
if(proxy&&(!pos[2]||pos.length!==3))throw new Error(t('请提供本机地址,例如 127.0.0.1:8080。'));
|
|
146
|
+
if(!proxy&&pos.length!==2)throw new Error(t('请使用 --workspace 指定项目目录。'));
|
|
147
|
+
let routes;
|
|
148
|
+
if(pos[1]==='api'){
|
|
149
|
+
if(!opts.routes)throw new Error(t('纯 API 接入需要 --routes 权限文件。'));
|
|
150
|
+
const file=path.resolve(opts.routes);
|
|
151
|
+
if(fs.statSync(file).size>128*1024)throw new Error(t('路由文件过大。'));
|
|
152
|
+
try{routes=JSON.parse(fs.readFileSync(file,'utf8'));}catch{throw new Error(t('路由文件不是有效的 JSON。'));}
|
|
153
|
+
}
|
|
154
|
+
const executable=opts.executable?path.resolve(opts.executable):pos[1]==='pi'?findExecutable('pi'):undefined;
|
|
155
|
+
result=request('connect',{application:pos[1],upstream:pos[2],workspace:path.resolve(opts.workspace||os.homedir()),name:opts.name,qr:opts.qr===true,adapter:opts.adapter?path.resolve(opts.adapter):undefined,codex:opts.codex?path.resolve(opts.codex):undefined,codex_home:process.env.CODEX_HOME,search_path:process.env.PATH,executable,profile:opts.profile?path.resolve(opts.profile):process.env.CODING_HARNESS_HOME&&['claude','dsh'].includes(pos[1])?path.join(process.env.CODING_HARNESS_HOME,pos[1]==='dsh'?'deepseek':'claude','profile.json'):undefined,agent_dir:opts['agent-dir']?path.resolve(opts['agent-dir']):process.env.PI_CODING_AGENT_DIR,routes});
|
|
156
|
+
if(result.qr&&!opts.json){require('qrcode-terminal').generate(result.qr,{small:true});console.log(t('使用已登录的 Linkitty 手机端扫码认领。'));}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'access': {
|
|
160
|
+
if(pos.length!==2||!opts.capabilities||!opts.output)throw new Error(t('用法:linkitty access 实例ID --capabilities api.read,api.write --output 私有文件'));
|
|
161
|
+
const capabilities=opts.capabilities.split(',').map(s=>s.trim()).filter(Boolean);
|
|
162
|
+
const output=path.resolve(opts.output);
|
|
163
|
+
const fd=fs.openSync(output,'wx',0o600);
|
|
164
|
+
try{
|
|
165
|
+
const access=request('open_api',{id:pos[1],client_id:'linkitty-cli',capabilities,origins:[]});
|
|
166
|
+
fs.writeFileSync(fd,JSON.stringify(access,null,2)+'\n');
|
|
167
|
+
result={output,endpoint:access.endpoint,expires_at:access.expires_at};
|
|
168
|
+
}catch(error){fs.closeSync(fd);fs.unlinkSync(output);throw error;}
|
|
169
|
+
fs.closeSync(fd);break;
|
|
170
|
+
}
|
|
171
|
+
case 'status':result=request('linkitty_status');break;
|
|
172
|
+
case 'list':result=request('connections');break;
|
|
173
|
+
case 'logout':result=request('logout');break;
|
|
174
|
+
case 'disconnect':if(!pos[1]||pos.length!==2)throw new Error(t('用法:linkitty disconnect ID'));result=request('disconnect',{id:pos[1]});break;
|
|
175
|
+
case 'delete':if(pos.length!==2||!opts.yes)throw new Error(t('用法:linkitty delete 实例ID --yes;永久删除登记和分享授权,电脑文件保留。'));result=request('service_delete',{id:pos[1],confirm_delete:true});break;
|
|
176
|
+
case 'service':if(!['install','status','uninstall'].includes(pos[1])||pos.length!==2)throw new Error(t('用法:linkitty service install|status|uninstall'));result=native(['service',pos[1]]);break;
|
|
177
|
+
default:throw new Error(t('未知命令:')+command+t('\n运行 linkitty --help 查看用法。'));
|
|
178
|
+
}
|
|
179
|
+
if(opts.json){console.log(JSON.stringify(result));return;}
|
|
180
|
+
if(command==='connect'){
|
|
181
|
+
console.log(result.connected?t('服务已接入,连接由后台持续维护。'):t('已登记,等待认领或运行端确认。'));
|
|
182
|
+
if(result.desktop_restart_required)console.log(t('Codex 启动连接已配置。请在当前任务结束后正常重启一次 Codex;之后直接打开即可,无需共控脚本。'));
|
|
183
|
+
}
|
|
184
|
+
else if(command==='login')console.log(t('已登录。运行 linkitty connect codex 接入,不默认绑定项目。'));
|
|
185
|
+
else if(command==='logout')console.log(t('已退出账号。'));
|
|
186
|
+
else console.log(JSON.stringify(result,null,2));
|
|
187
|
+
}
|
|
188
|
+
module.exports={parse,main};
|
|
189
|
+
if(require.main===module)main(process.argv.slice(2)).catch(error=>{console.error(error.message);process.exitCode=1;});
|
package/lib/i18n.cjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const messages = require('./messages.json');
|
|
7
|
+
const supported = ['zh', 'en', 'ja', 'ru'];
|
|
8
|
+
const root = path.resolve(process.env.LINKITTY_HOME || path.join(os.homedir(), '.linkitty'));
|
|
9
|
+
const preferences = path.join(root, 'config', 'preferences.json');
|
|
10
|
+
let saved = {};
|
|
11
|
+
try { saved = JSON.parse(fs.readFileSync(preferences, 'utf8')); } catch {}
|
|
12
|
+
const requested = process.env.LINKITTY_LANG || saved.language || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || 'en';
|
|
13
|
+
let language = supported.find(code => String(requested).toLowerCase().startsWith(code)) || 'en';
|
|
14
|
+
function t(key, ...values) {
|
|
15
|
+
if (Array.isArray(key)) key = key.reduce((text, part, index) => text + (index ? '{{' + (index - 1) + '}}' : '') + part, '');
|
|
16
|
+
const value = language === 'zh' ? key : messages[key]?.[language] || messages[key]?.en || key;
|
|
17
|
+
return value.replace(/\{\{(\d+)\}\}/g, (match, index) => index < values.length ? String(values[index] ?? '') : match);
|
|
18
|
+
}
|
|
19
|
+
function setLanguage(value) {
|
|
20
|
+
if (!supported.includes(value)) throw new Error(t('语言必须是 zh、en、ja 或 ru。'));
|
|
21
|
+
const directory = path.dirname(preferences);
|
|
22
|
+
fs.mkdirSync(directory, {recursive: true, mode: 0o700});
|
|
23
|
+
for (const folder of [root, directory]) fs.chmodSync(folder, 0o700);
|
|
24
|
+
let current = {};
|
|
25
|
+
if (fs.existsSync(preferences)) current = JSON.parse(fs.readFileSync(preferences, 'utf8'));
|
|
26
|
+
const temporary = preferences + '.tmp-' + crypto.randomBytes(6).toString('hex');
|
|
27
|
+
fs.writeFileSync(temporary, JSON.stringify({...current, language: value}), {flag: 'wx', mode: 0o600});
|
|
28
|
+
fs.renameSync(temporary, preferences);
|
|
29
|
+
language = value;
|
|
30
|
+
}
|
|
31
|
+
module.exports = {t, setLanguage, getLanguage: () => language, supported};
|
package/lib/install.cjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {t}=require("./i18n.cjs");
|
|
3
|
+
// npm packages install only pinned-version artifacts without starting business logic or reading model credentials.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const {execFileSync} = require('node:child_process');
|
|
9
|
+
function home() {
|
|
10
|
+
const root=path.resolve(process.env.LINKITTY_HOME || path.join(os.homedir(), '.linkitty'));
|
|
11
|
+
fs.mkdirSync(path.join(root,'config'),{recursive:true,mode:0o700});
|
|
12
|
+
for(const name of ['installation.json','desktop.json']) {
|
|
13
|
+
const old=path.join(root,name), next=path.join(root,'config',name);
|
|
14
|
+
if(fs.existsSync(old)&&!fs.existsSync(next))fs.renameSync(old,next);
|
|
15
|
+
}
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
function platformTag() {
|
|
19
|
+
let arch=process.arch;
|
|
20
|
+
if(process.platform==='darwin'){
|
|
21
|
+
try{if(execFileSync('/usr/sbin/sysctl',['-n','hw.optional.arm64'],{encoding:'utf8'}).trim()==='1')arch='arm64';}catch{}
|
|
22
|
+
}
|
|
23
|
+
return process.platform+'-'+arch;
|
|
24
|
+
}
|
|
25
|
+
function write(file, value) {
|
|
26
|
+
fs.mkdirSync(path.dirname(file), {recursive:true, mode:0o700});
|
|
27
|
+
const temp = file + '.tmp-' + crypto.randomBytes(6).toString('hex');
|
|
28
|
+
fs.writeFileSync(temp, JSON.stringify(value), {mode:0o600, flag:'wx'});
|
|
29
|
+
fs.renameSync(temp,file);
|
|
30
|
+
}
|
|
31
|
+
function install(packageRoot, name) {
|
|
32
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot,'artifacts.json'),'utf8'));
|
|
33
|
+
const platform = platformTag();
|
|
34
|
+
const artifact = manifest[platform];
|
|
35
|
+
if (!artifact) throw new Error(t('此安装包尚未包含 ')+platform+t(' 制品。请使用已发布的平台版本。'));
|
|
36
|
+
if (!/^[a-f0-9]{64}$/.test(artifact.sha256)) throw new Error(t('制品摘要缺失。'));
|
|
37
|
+
const archive = path.resolve(packageRoot,artifact.archive);
|
|
38
|
+
if (!archive.startsWith(packageRoot+path.sep)) throw new Error(t('无效制品路径。'));
|
|
39
|
+
const hash = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex');
|
|
40
|
+
if (hash !== artifact.sha256) throw new Error(t('安装制品校验失败。'));
|
|
41
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
|
|
42
|
+
const destination = path.join(home(),'packages',name,pkg.version,platform+'-'+hash.slice(0,12));
|
|
43
|
+
if (!fs.existsSync(path.join(destination,'.complete'))) {
|
|
44
|
+
fs.mkdirSync(path.dirname(destination),{recursive:true,mode:0o700});
|
|
45
|
+
const stage=fs.mkdtempSync(destination+'.install-');
|
|
46
|
+
try {
|
|
47
|
+
const entries=execFileSync('tar',['-tzf',archive],{encoding:'utf8',maxBuffer:16*1024*1024}).split('\n').filter(Boolean);
|
|
48
|
+
if (entries.some(p=>p.startsWith('/')||p.includes('\\')||p.split('/').includes('..'))) throw new Error(t('安装包包含不安全路径。'));
|
|
49
|
+
execFileSync('tar',['-xzf',archive,'-C',stage],{stdio:'pipe'});
|
|
50
|
+
fs.writeFileSync(path.join(stage,'.complete'),hash,{mode:0o600});
|
|
51
|
+
try { fs.renameSync(stage,destination); }
|
|
52
|
+
catch(error) { if(!fs.existsSync(path.join(destination,'.complete'))) throw error; fs.rmSync(stage,{recursive:true,force:true}); }
|
|
53
|
+
} catch(error) {fs.rmSync(stage,{recursive:true,force:true});throw error;}
|
|
54
|
+
}
|
|
55
|
+
const pointer=path.join(home(),'config',name==='linkitty'?'installation.json':'desktop.json');
|
|
56
|
+
if (name==='linkitty') {
|
|
57
|
+
const runtime=path.join(destination,'bin',process.platform==='win32'?'linkitty-runtime.exe':'linkitty-runtime');
|
|
58
|
+
if(!fs.existsSync(runtime)) throw new Error(t('原生运行时不在安装包内。'));
|
|
59
|
+
const adapter=path.join(destination,'bin',process.platform==='win32'?'relay-coding.exe':'relay-coding');
|
|
60
|
+
write(pointer,{version:pkg.version,platform,runtime,codex_adapter:fs.existsSync(adapter)?adapter:null});
|
|
61
|
+
} else {
|
|
62
|
+
const relative=artifact.executable;
|
|
63
|
+
const executable=path.resolve(destination,relative);
|
|
64
|
+
if(!executable.startsWith(destination+path.sep)||!fs.existsSync(executable)) throw new Error(t('桌面程序不在安装包内。'));
|
|
65
|
+
write(pointer,{version:pkg.version,platform,executable});
|
|
66
|
+
if(process.platform==='darwin'){
|
|
67
|
+
const applications=process.env.LINKITTY_APPLICATIONS_DIR?path.resolve(process.env.LINKITTY_APPLICATIONS_DIR):path.join(os.homedir(),'Applications');
|
|
68
|
+
fs.mkdirSync(applications,{recursive:true});
|
|
69
|
+
const link=path.join(applications,'Linkitty.app');
|
|
70
|
+
const existing=fs.existsSync(link)||(()=>{try{return fs.lstatSync(link).isSymbolicLink();}catch{return false;}})();
|
|
71
|
+
if(!existing || (fs.lstatSync(link).isSymbolicLink() && fs.readlinkSync(link).startsWith(home()+path.sep))){
|
|
72
|
+
const next=link+'.linkitty-next';
|
|
73
|
+
try{fs.unlinkSync(next);}catch{}
|
|
74
|
+
fs.symlinkSync(executable,next);
|
|
75
|
+
fs.renameSync(next,link);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return JSON.parse(fs.readFileSync(pointer,'utf8'));
|
|
80
|
+
}
|
|
81
|
+
function runtime() {
|
|
82
|
+
const override=process.env.LINKITTY_RUNTIME_BIN;
|
|
83
|
+
if(override) {
|
|
84
|
+
if(!path.isAbsolute(override)||!fs.existsSync(override)) throw new Error(t('LINKITTY_RUNTIME_BIN 必须指向实际原生程序。'));
|
|
85
|
+
return override;
|
|
86
|
+
}
|
|
87
|
+
const pointer=path.join(home(),'config','installation.json');
|
|
88
|
+
if(!fs.existsSync(pointer)) install(path.resolve(__dirname,'..'),'linkitty');
|
|
89
|
+
return JSON.parse(fs.readFileSync(pointer,'utf8')).runtime;
|
|
90
|
+
}
|
|
91
|
+
module.exports={home,write,install,runtime,platformTag};
|
|
92
|
+
if(require.main===module) {
|
|
93
|
+
try{install(path.resolve(__dirname,'..'),'linkitty');process.stdout.write(t('Linkitty 已安装。运行 linkitty --help 开始。\n'));}
|
|
94
|
+
catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
|
|
95
|
+
}
|