@kin-tio/cli 0.6.1 → 0.7.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/.env.example CHANGED
@@ -28,8 +28,10 @@ WECOM_AUTH_CONFIRMATION=Code accepted. You can continue the conversation.
28
28
  # WECOM_MCP_OBSERVE_MS=5000
29
29
  # SHUTDOWN_TIMEOUT_MS=10000
30
30
 
31
- # Optional Weixin iLink channel. Generate a 32-byte base64url key and keep it in
32
- # the deployment secret store; Bot and context tokens are encrypted with it.
31
+ # Enable Weixin iLink inside the combined `kintio start` runtime. Standalone
32
+ # `kintio ilink login` and `kintio ilink start` do not require this flag.
33
+ # Generate a 32-byte base64url key and keep it in the deployment secret store;
34
+ # Bot and context tokens are encrypted with it.
33
35
  ILINK_ENABLED=false
34
36
  # Prefer a deployment-secret value. If omitted, the service creates a private
35
37
  # 0600 key file beside SQLite; never register that key with a chat Agent.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ This file records important user-visible changes after the first public release.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.7.0
8
+
9
+ - Added `kintio ilink login`, which reuses the iLink enrollment state machine
10
+ while rendering its five-minute QR code directly in an interactive terminal;
11
+ no WeChat KF conversation or Agent turn is required. Accounts
12
+ enrolled locally receive host-level Agent access and inherit the host runtime
13
+ configuration, while remotely offered iLink accounts remain restricted
14
+ ([#54](https://github.com/Gkxie/kintio/issues/54)).
15
+ - Added an explicit `--qr-output <file>` view for `kintio ilink login`, allowing
16
+ graphical and non-terminal callers to consume a temporary raw PNG directly
17
+ from the QR payload without parsing ANSI terminal output. The file is created
18
+ exclusively and removed when the login attempt ends
19
+ ([#57](https://github.com/Gkxie/kintio/issues/57)).
20
+ - Made iLink a standalone lifecycle: `kintio ilink login` now initializes and
21
+ persists an account without setup, an environment file, Hono, or a running
22
+ Worker, while safely delegating to a running instance when present;
23
+ `kintio ilink start` starts polling and the host Agent in the foreground without
24
+ a public HTTP listener. The iLink Runtime configuration no longer contains a
25
+ synthetic WeChat KF adapter
26
+ ([#59](https://github.com/Gkxie/kintio/issues/59)).
27
+ - Added per-account `kintio ilink list`, `start`, `stop`, and confirmed `delete`
28
+ lifecycle commands. One Runtime can reconcile multiple selected listeners;
29
+ complete deletion atomically purges the selected account and all Kintio data
30
+ scoped to it while preserving unrelated accounts and channels
31
+ ([#60](https://github.com/Gkxie/kintio/issues/60)).
32
+
33
+ ## 0.6.2
34
+
35
+ - Added a custom Kintio wordmark and its circular-safe TIO avatar to the English
36
+ and Simplified Chinese entry pages and public package.
37
+ - Added a repository-scoped Release App that deterministically maintains one
38
+ fully checked Release PR from the reviewed Unreleased notes; maintainers now
39
+ authorize a release by reviewing and merging that PR only
40
+ ([#48](https://github.com/Gkxie/kintio/issues/48)).
41
+ - Made a merged, owner-authored Release PR the sole human release authorization;
42
+ Kintio now creates the annotated tag and dispatches its verified OIDC release
43
+ automatically
44
+ ([#45](https://github.com/Gkxie/kintio/issues/45)).
45
+
7
46
  ## 0.6.1 - 2026-08-31
8
47
 
9
48
  - Added approval-gated npm Trusted Publishing with an exact artifact integrity
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  <div align="center">
2
2
 
3
- # Kintio
3
+ <h1>
4
+ <img src="assets/logo.svg" alt="Kintio" width="320" />
5
+ </h1>
4
6
 
5
7
  **Connect chat channels to an Agent you control.**
6
8
 
@@ -65,33 +67,67 @@ Prerequisites:
65
67
 
66
68
  ```bash
67
69
  npm install --global @kin-tio/cli
68
- kintio setup
69
70
  codex login status
70
71
  ```
71
72
 
72
- `kintio setup` creates a private instance under `~/.kintio`, installs the bundled Agent
73
- skill, and writes the channel configuration template. POSIX systems use mode `0600`;
74
- Windows keeps the instance inside the current user's profile. No adapter is enabled
75
- by default. Follow the [setup guide](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)
76
- and edit `~/.kintio/.env` to configure one adapter:
73
+ For an iLink-only instance, no setup file or public HTTP listener is required:
77
74
 
78
- - For WeChat KF API, set its callback token, EncodingAESKey, CorpID, and secret. A temporary
79
- `WECOM_AUTH_TRIGGER` can authorize the first user without knowing their
80
- `external_userid` in advance.
81
- - For an existing Weixin iLink binding, set `ILINK_ENABLED=true`. Creating a new binding
82
- currently starts from an authorized WeChat KF conversation.
75
+ ```bash
76
+ kintio ilink login
77
+ kintio ilink start
78
+ ```
83
79
 
84
- Start Kintio:
80
+ `ilink login` performs one encrypted enrollment, starts no listener, and exits. `ilink start` then runs provider
81
+ polling and the host Agent in the foreground without Hono or a TCP listener. Both commands
82
+ use `~/.kintio` by default and accept `--home`. With multiple accounts, use `ilink list`
83
+ and pass the displayed provider ID or account key through `--account`. Repeated `start`
84
+ commands add accounts to the live runtime; `stop` removes one.
85
+
86
+ For a callback-based adapter, create and edit the deployment configuration instead:
85
87
 
86
88
  ```bash
89
+ kintio setup
87
90
  kintio start
88
91
  kintio status
89
92
  kintio logs --lines 100
90
93
  ```
91
94
 
92
- After startup, confirm that `kintio logs` contains `Hono server is listening on port 8888`.
93
- Complete the callback or binding checks described in the setup guide before sending traffic.
94
- Use `kintio run` when a foreground process is preferable to the native daemon.
95
+ `kintio setup` creates a private instance under `~/.kintio`, installs the managed Agent
96
+ skill, and writes the channel configuration template. Follow the
97
+ [setup guide](https://github.com/Gkxie/kintio/blob/master/docs/setup.md):
98
+
99
+ - For WeChat KF API, set its callback token, EncodingAESKey, CorpID, and secret. A temporary
100
+ `WECOM_AUTH_TRIGGER` can authorize the first user without knowing their
101
+ `external_userid` in advance.
102
+ - A combined callback + iLink deployment may additionally set `ILINK_ENABLED=true`.
103
+
104
+ For a graphical or non-terminal caller, select a temporary raw PNG instead of ANSI blocks:
105
+
106
+ ```bash
107
+ kintio ilink login --qr-output ~/.kintio/ilink-login.png
108
+ ```
109
+
110
+ The target must be directly inside the selected Kintio instance directory and must not
111
+ already exist. Kintio removes the PNG when login succeeds, expires, is cancelled, or fails;
112
+ the QR payload is never printed. Without `--qr-output`, the command
113
+ requires an interactive terminal. Both forms stop waiting after five minutes and never
114
+ start an Agent turn. The resulting iLink identity represents
115
+ the local operator and inherits the host Agent configuration without Kintio's untrusted-
116
+ channel capability restrictions. Show this QR code only to someone authorized to control
117
+ the host Agent. Run `kintio ilink start` after enrollment to process messages without Hono.
118
+
119
+ To permanently remove an account and every Kintio record scoped to it, use:
120
+
121
+ ```bash
122
+ kintio ilink delete --account <provider-id-or-account-key> --yes
123
+ ```
124
+
125
+ The explicit confirmation is mandatory. Credentials, conversations, messages, media,
126
+ delivery records, and enrollment audit rows for that account are deleted atomically.
127
+
128
+ For callback deployments, confirm that `kintio logs` contains
129
+ `Hono server is listening on port 8888`. Use `kintio run` when a foreground process is
130
+ preferable to the native daemon.
95
131
  Existing source-based deployments can keep their current state after the one-time
96
132
  process-manager migration described in the setup guide.
97
133
 
@@ -109,6 +145,9 @@ Source builds and contributor setup are documented in
109
145
  Hono route; every action still requires a short-lived conversation capability.
110
146
  - Project-level Agent capability restrictions are not an operating-system sandbox. Use a
111
147
  dedicated system account and additional isolation appropriate to the Agent's real powers.
148
+ - An iLink account enrolled by `kintio ilink login` is explicitly host-authorized; its owner
149
+ receives the capabilities allowed by the host Agent configuration. Accounts enrolled from
150
+ a remote adapter remain restricted, and chat input cannot change this persisted trust level.
112
151
  - A provider accepting an outbound request does not prove that a client displayed it;
113
152
  uncertain outcomes remain explicit to avoid duplicate delivery.
114
153
 
package/README.zh-CN.md CHANGED
@@ -1,6 +1,8 @@
1
1
  <div align="center">
2
2
 
3
- # Kintio
3
+ <h1>
4
+ <img src="assets/logo.svg" alt="Kintio" width="320" />
5
+ </h1>
4
6
 
5
7
  **把聊天通道连接到你掌控的 Agent。**
6
8
 
@@ -41,25 +43,49 @@
41
43
 
42
44
  ```bash
43
45
  npm install --global @kin-tio/cli
44
- kintio setup
45
46
  codex login status
46
47
  ```
47
48
 
48
- `kintio setup` 会在 `~/.kintio` 创建私有实例目录、安装内置 Agent Skill,并生成渠道配置
49
- 模板 `.env`。macOS/Linux 使用 `0600`,Windows 则限定在当前用户目录的 ACL
50
- 边界内。默认不启用任何适配器;请按英文
51
- [部署指南](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)配置 WeChat KF API,或为已有 Weixin iLink 绑定设置
52
- `ILINK_ENABLED=true`。
49
+ iLink 可以完全独立使用,不需要 `setup`、`.env` 或公网 HTTP:
53
50
 
54
51
  ```bash
52
+ kintio ilink login
53
+ kintio ilink start
54
+ ```
55
+
56
+ `ilink login` 完成一次扫码、加密保存凭据后退出,不会自行启动监听;`ilink start` 不启动 Hono 或 TCP 端口,
57
+ 只以前台方式运行 iLink 长轮询和宿主 Agent。两者默认使用 `~/.kintio`。
58
+ 存在多个账号时,先用 `kintio ilink list` 查看账号,再通过 `--account` 指定
59
+ `start`、`stop` 或 `delete` 的目标;正在运行时可继续执行 `start` 增加监听账号。
60
+
61
+ 需要部署公网回调渠道时,再使用:
62
+
63
+ ```bash
64
+ kintio setup
55
65
  kintio start
56
66
  kintio status
57
67
  kintio logs --lines 100
58
68
  ```
59
69
 
60
- 启动后应确认 `kintio logs` 包含 `Hono server is listening on port 8888`;投入使用前仍需按
61
- 部署指南完成回调或绑定验证。需要前台进程时使用 `kintio run`;现有源码目录部署可以在完成
62
- 部署指南中的一次性进程管理器迁移后,继续使用原数据库和配置。
70
+ 具体配置见英文[部署指南](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)。
71
+
72
+ 图形界面或非交互调用方可以显式选择临时的原始 PNG,而不是解析终端字符:
73
+
74
+ ```bash
75
+ kintio ilink login --qr-output ~/.kintio/ilink-login.png
76
+ ```
77
+
78
+ 目标文件必须直接位于所选 Kintio 实例目录中且不能预先存在;登录成功、过期、取消或失败后,Kintio 会自动删除该文件,并且
79
+ 不会打印二维码原始内容。二维码五分钟后过期;该命令不会唤醒 Agent。通过本机命令建立的 iLink 身份代表宿主机
80
+ 所有者的明确授权,后续对话直接继承宿主 Agent 配置,不再套用不可信渠道的能力限制。
81
+ 只应让获准控制宿主 Agent 的人扫描该二维码。登录后运行 `kintio ilink start` 即可在不
82
+ 启动 Hono 的情况下处理消息。
83
+
84
+ `kintio ilink delete --account <账号> --yes` 会不可恢复地删除该账号及其在 Kintio
85
+ 中的凭据、会话、消息、媒体、发送记录和登录审计;`--yes` 为强制确认参数。
86
+
87
+ 公网回调部署启动后,应确认 `kintio logs` 包含
88
+ `Hono server is listening on port 8888`。
63
89
  源码构建与贡献者开发环境见英文
64
90
  [贡献指南](https://github.com/Gkxie/kintio/blob/master/CONTRIBUTING.md)。
65
91
 
@@ -0,0 +1,32 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="200"
4
+ height="200"
5
+ viewBox="190 15 190 190"
6
+ role="img"
7
+ aria-labelledby="kintio-avatar-title"
8
+ aria-describedby="kintio-avatar-description"
9
+ >
10
+ <!-- Letterform foundation: Manrope by Mikhail Sharanda, SIL Open Font License 1.1. -->
11
+ <title id="kintio-avatar-title">Kintio TIO avatar</title>
12
+ <desc id="kintio-avatar-description">A circular Kinetic Panels background with the Kintio TIO return aperture.</desc>
13
+ <defs>
14
+ <clipPath id="kintio-kinetic-panels-circle">
15
+ <circle cx="285" cy="110" r="95"/>
16
+ </clipPath>
17
+ </defs>
18
+ <g clip-path="url(#kintio-kinetic-panels-circle)">
19
+ <rect x="190" y="15" width="190" height="190" fill="#17152B"/>
20
+ <polygon points="190,15 304,15 270,58 190,88" fill="#5268FF"/>
21
+ <polygon points="302,152 380,112 380,205 270,205" fill="#3B2F73"/>
22
+ <polygon points="190,88 214,78 214,104 190,116" fill="#8C78FF"/>
23
+ <g transform="translate(285 110) scale(0.86) translate(-285 -110)">
24
+ <path d="M766 0Q661 -20 561 -17Q461 -14 382 21Q303 56 262 132Q226 201 224 272Q222 343 222 433L222 1380L447 1380L447 445Q447 380 448.5 331.5Q450 283 469 251Q505 190 583.5 182.5Q662 175 766 190ZM20 902L20 1080L766 1080L766 902Z" fill="#F6F3EE" transform="translate(204.393 150) scale(0.063 -0.063)"/>
25
+ <path d="M160 1251L160 1463L385 1463L385 1251ZM160 0L160 1080L385 1080L385 0Z" fill="#FF7656" transform="translate(257.289 150) scale(0.063 -0.063)"/>
26
+ <path d="M613 -30Q451 -30 331 43Q211 116 145.5 244.5Q80 373 80 541Q80 710 147 838.5Q214 967 334 1038.5Q454 1110 613 1110Q776 1110 896 1037Q1016 964 1082 836Q1148 708 1148 541Q1148 372 1081.5 243.5Q1015 115 894.5 42.5Q774 -30 613 -30ZM613 182Q762 182 835.5 282Q909 382 909 541Q909 704 835 801.5Q761 899 613 899Q512 899 447 853.5Q382 808 350.5 727.5Q319 647 319 541Q319 377 393.5 279.5Q468 182 613 182Z" fill="#F6F3EE" transform="translate(290.624 150) scale(0.063 -0.063)"/>
27
+ <rect x="286" y="108" width="23" height="18" fill="#17152B"/>
28
+ <rect x="289" y="113" width="11" height="8" fill="#FF7656"/>
29
+ </g>
30
+ </g>
31
+ </svg>
32
+
@@ -0,0 +1,22 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="374"
4
+ height="142"
5
+ viewBox="12.757 33.831 374.191 142.059"
6
+ role="img"
7
+ aria-labelledby="kintio-wordmark-title"
8
+ aria-describedby="kintio-wordmark-description"
9
+ >
10
+ <!-- Letterform foundation: Manrope by Mikhail Sharanda, SIL Open Font License 1.1. -->
11
+ <title id="kintio-wordmark-title">Kintio</title>
12
+ <desc id="kintio-wordmark-description">Kintio wordmark with an Ember i and an open return aperture.</desc>
13
+ <rect x="12.757" y="33.831" width="374.191" height="142.059" fill="#211920"/>
14
+ <path d="M139 0L140 1440L368 1440L368 560L778 1080L1061 1080L628 540L1102 0L801 0L368 520L368 0Z" fill="#F6F3EE" transform="translate(28.000 150) scale(0.063000 -0.063000)"/>
15
+ <path d="M160 1251L160 1463L385 1463L385 1251ZM160 0L160 1080L385 1080L385 0Z" fill="#F6F3EE" transform="translate(93.906 150) scale(0.063000 -0.063000)"/>
16
+ <path d="M893 0L893 526Q893 582 884 646.5Q875 711 847.5 768.5Q820 826 768 862Q716 898 630 898Q579 898 531.5 881Q484 864 447 825.5Q410 787 388.5 721.5Q367 656 367 559L230 614Q230 756 284.5 868.5Q339 981 442.5 1045.5Q546 1110 695 1110Q810 1110 887.5 1072Q965 1034 1011.5 973.5Q1058 913 1081.5 844Q1105 775 1113 710.5Q1121 646 1121 602L1121 0ZM139 0L139 1080L340 1080L340 757L367 757L367 0Z" fill="#F6F3EE" transform="translate(127.241 150) scale(0.063000 -0.063000)"/>
17
+ <path d="M766 0Q661 -20 561 -17Q461 -14 382 21Q303 56 262 132Q226 201 224 272Q222 343 222 433L222 1380L447 1380L447 445Q447 380 448.5 331.5Q450 283 469 251Q505 190 583.5 182.5Q662 175 766 190ZM20 902L20 1080L766 1080L766 902Z" fill="#F6F3EE" transform="translate(204.393 150) scale(0.063000 -0.063000)"/>
18
+ <path d="M160 1251L160 1463L385 1463L385 1251ZM160 0L160 1080L385 1080L385 0Z" fill="#FF7656" transform="translate(257.289 150) scale(0.063000 -0.063000)"/>
19
+ <path d="M613 -30Q451 -30 331 43Q211 116 145.5 244.5Q80 373 80 541Q80 710 147 838.5Q214 967 334 1038.5Q454 1110 613 1110Q776 1110 896 1037Q1016 964 1082 836Q1148 708 1148 541Q1148 372 1081.5 243.5Q1015 115 894.5 42.5Q774 -30 613 -30ZM613 182Q762 182 835.5 282Q909 382 909 541Q909 704 835 801.5Q761 899 613 899Q512 899 447 853.5Q382 808 350.5 727.5Q319 647 319 541Q319 377 393.5 279.5Q468 182 613 182Z" fill="#F6F3EE" transform="translate(290.624 150) scale(0.063000 -0.063000)"/>
20
+ <rect x="286" y="108" width="23" height="18" fill="#211920"/>
21
+ <rect x="289" y="113" width="11" height="8" fill="#FF7656"/>
22
+ </svg>
package/dist/src/cli.js CHANGED
@@ -5,9 +5,12 @@ import path from 'node:path';
5
5
  import { setTimeout as delay } from 'node:timers/promises';
6
6
  import { parseArgs } from 'node:util';
7
7
  import crossSpawn from 'cross-spawn';
8
- import { DAEMON_STOP_TIMEOUT_MS, loadConfig, parseStartTimeout, resolveProjectRoot, WORKER_GRACEFUL_TIMEOUT_MS, } from './config.js';
8
+ import { DAEMON_STOP_TIMEOUT_MS, loadConfig, loadIlinkEnrollmentConfig, loadIlinkRuntimeConfig, parseStartTimeout, resolveProjectRoot, WORKER_GRACEFUL_TIMEOUT_MS, } from './config.js';
9
9
  import { isPathInside, samePath } from './lib/path-identity.js';
10
10
  import { assertTrustedDirectory, ensureContainedDirectory, ensurePrivateDirectory, } from './lib/private-directory.js';
11
+ import { runIlinkCliLogin } from './ilink/cli-login.js';
12
+ import { runIlinkAccountCommand } from './ilink/cli-accounts.js';
13
+ import { startIlinkCliRuntime } from './ilink/cli-start.js';
11
14
  import { daemonRecordPath, readDaemonRecord, requestControl, } from './runtime/daemon-protocol.js';
12
15
  import { acquireSingleInstanceLock, processIsAlive, SingleInstanceLockError, } from './runtime/single-instance-lock.js';
13
16
  import { installManagedSkill } from './runtime/managed-skill.js';
@@ -22,6 +25,11 @@ Commands:
22
25
  restart Restart Kintio with the current installation and config
23
26
  status Show the background process status
24
27
  logs Follow Kintio logs
28
+ ilink login [options] Connect an iLink account with a QR code
29
+ ilink list List enrolled iLink accounts
30
+ ilink start [options] Start one iLink account without Hono
31
+ ilink stop [options] Stop one iLink account
32
+ ilink delete [options] Permanently delete one iLink account and its data
25
33
 
26
34
  Options:
27
35
  --home <directory> Instance directory (default: ~/.kintio)
@@ -30,7 +38,132 @@ Options:
30
38
  --no-follow Print logs without following
31
39
  -h, --help Show this help
32
40
  -v, --version Show the Kintio version
41
+
42
+ Run "kintio ilink --help" for iLink account commands.
43
+ `;
44
+ const ILINK_LOGIN_HELP = `Usage: kintio ilink login [options]
45
+
46
+ Connect one iLink account, save its encrypted credentials, and exit. This
47
+ command does not require setup, an environment file, Hono, or a running Kintio
48
+ instance. By default, the QR code is rendered directly in an interactive
49
+ terminal and expires after five minutes.
50
+
51
+ The PNG option is required when stdout is not an interactive terminal. Whoever
52
+ scans this locally issued QR receives the capabilities allowed by the host Agent
53
+ configuration; show it only to an authorized operator.
54
+
55
+ Options:
56
+ --qr-output <file> Write a temporary raw QR PNG instead of terminal blocks
57
+ The file must be directly inside the instance directory
58
+ The file is removed when the login attempt ends
59
+ --home <directory> Instance directory (default: ~/.kintio)
60
+ --config <file> Optional environment overrides
61
+ -h, --help Show this help
62
+ `;
63
+ const ILINK_START_HELP = `Usage: kintio ilink start [options]
64
+
65
+ Run iLink long polling and the host Agent in the foreground without starting
66
+ Hono or opening a TCP listener. This command does not require setup or an
67
+ environment file. One account is selected automatically; multiple accounts
68
+ require --account. While this runtime is active, additional start commands add
69
+ accounts to the same process.
70
+
71
+ Options:
72
+ --account <id> Provider account ID or Kintio account key
73
+ --home <directory> Instance directory (default: ~/.kintio)
74
+ --config <file> Optional environment overrides
75
+ -h, --help Show this help
76
+ `;
77
+ const ILINK_STOP_HELP = `Usage: kintio ilink stop [options]
78
+
79
+ Stop one iLink account. Stopping the last account also exits a foreground
80
+ "kintio ilink start" runtime. One account is selected automatically; multiple
81
+ accounts require --account.
82
+
83
+ Options:
84
+ --account <id> Provider account ID or Kintio account key
85
+ --home <directory> Instance directory (default: ~/.kintio)
86
+ --config <file> Optional environment overrides
87
+ -h, --help Show this help
88
+ `;
89
+ const ILINK_LIST_HELP = `Usage: kintio ilink list [options]
90
+
91
+ List enrolled iLink accounts and whether each account is currently running.
92
+
93
+ Options:
94
+ --home <directory> Instance directory (default: ~/.kintio)
95
+ --config <file> Optional environment overrides
96
+ -h, --help Show this help
97
+ `;
98
+ const ILINK_DELETE_HELP = `Usage: kintio ilink delete [options]
99
+
100
+ Permanently delete one iLink account and all Kintio data scoped to it,
101
+ including credentials, conversations, messages, media, send records, and
102
+ enrollment audit records. This operation cannot be undone.
103
+
104
+ Options:
105
+ --account <id> Provider account ID or Kintio account key
106
+ --yes Confirm permanent deletion
107
+ --home <directory> Instance directory (default: ~/.kintio)
108
+ --config <file> Optional environment overrides
109
+ -h, --help Show this help
110
+ `;
111
+ const ILINK_HELP = `Usage: kintio ilink <command>
112
+
113
+ Commands:
114
+ login [options] Connect an iLink account with a QR code
115
+ list List enrolled accounts
116
+ start [options] Start one account without Hono
117
+ stop [options] Stop one account
118
+ delete [options] Permanently delete one account and its data
119
+
120
+ Run "kintio ilink <command> --help" for command options.
33
121
  `;
122
+ const ILINK_COMMANDS = new Set(['login', 'list', 'start', 'stop', 'delete']);
123
+ const COMMANDS = new Set([
124
+ 'setup',
125
+ 'start',
126
+ 'run',
127
+ 'stop',
128
+ 'restart',
129
+ 'status',
130
+ 'logs',
131
+ 'ilink',
132
+ ]);
133
+ const ILINK_SIGNALS = process.platform === 'win32'
134
+ ? ['SIGINT', 'SIGTERM']
135
+ : ['SIGINT', 'SIGTERM', 'SIGHUP'];
136
+ function signalExitCode(signal) {
137
+ if (signal === 'SIGHUP')
138
+ return 129;
139
+ if (signal === 'SIGTERM')
140
+ return 143;
141
+ return 130;
142
+ }
143
+ async function runWithIlinkSignals(operation) {
144
+ const controller = new AbortController();
145
+ let interruptedBy;
146
+ const interrupt = (signal) => {
147
+ interruptedBy ||= signal;
148
+ controller.abort();
149
+ };
150
+ const listeners = ILINK_SIGNALS.map((signal) => ({
151
+ signal,
152
+ listener: () => interrupt(signal),
153
+ }));
154
+ for (const { signal, listener } of listeners)
155
+ process.once(signal, listener);
156
+ try {
157
+ const result = await operation(controller.signal);
158
+ return result === 130 && interruptedBy
159
+ ? signalExitCode(interruptedBy)
160
+ : result;
161
+ }
162
+ finally {
163
+ for (const { signal, listener } of listeners)
164
+ process.off(signal, listener);
165
+ }
166
+ }
34
167
  function defaultExecute(request) {
35
168
  return new Promise((resolve, reject) => {
36
169
  const child = crossSpawn(request.file, [...request.args], {
@@ -108,6 +241,11 @@ function runtimeDefaults() {
108
241
  launchDaemon: defaultLaunchDaemon,
109
242
  stdout: (text) => process.stdout.write(text),
110
243
  stderr: (text) => process.stderr.write(text),
244
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
245
+ stdoutColumns: process.stdout.columns || 80,
246
+ ilinkLogin: runIlinkCliLogin,
247
+ ilinkAccount: runIlinkAccountCommand,
248
+ ilinkStart: startIlinkCliRuntime,
111
249
  };
112
250
  }
113
251
  function resolveInputPath(value, cwd) {
@@ -508,6 +646,9 @@ export async function runCli(args, overrides = {}) {
508
646
  config: { type: 'string' },
509
647
  lines: { type: 'string' },
510
648
  'no-follow': { type: 'boolean' },
649
+ 'qr-output': { type: 'string' },
650
+ account: { type: 'string' },
651
+ yes: { type: 'boolean' },
511
652
  help: { type: 'boolean', short: 'h' },
512
653
  version: { type: 'boolean', short: 'v' },
513
654
  },
@@ -517,18 +658,118 @@ export async function runCli(args, overrides = {}) {
517
658
  return 0;
518
659
  }
519
660
  const command = parsed.positionals[0];
520
- if (parsed.values.help || !command || command === 'help') {
661
+ const subcommand = parsed.positionals[1];
662
+ if (!command) {
663
+ if (parsed.values['qr-output'] !== undefined) {
664
+ throw new Error('--qr-output is valid only for "kintio ilink login"');
665
+ }
521
666
  runtime.stdout(HELP);
522
667
  return 0;
523
668
  }
524
- if (parsed.positionals.length !== 1) {
525
- throw new Error(`Unexpected argument: ${parsed.positionals[1]}`);
669
+ if (command === 'help') {
670
+ if (parsed.positionals.length !== 1) {
671
+ throw new Error(`Unexpected argument: ${subcommand}`);
672
+ }
673
+ runtime.stdout(HELP);
674
+ return 0;
675
+ }
676
+ if (!COMMANDS.has(command))
677
+ throw new Error(`Unknown command: ${command}`);
678
+ if (command === 'ilink') {
679
+ if (parsed.values.help && parsed.positionals.length === 1) {
680
+ runtime.stdout(ILINK_HELP);
681
+ return 0;
682
+ }
683
+ if (!subcommand || !ILINK_COMMANDS.has(subcommand) ||
684
+ parsed.positionals.length !== 2) {
685
+ throw new Error('Usage: kintio ilink <login|list|start|stop|delete>');
686
+ }
687
+ }
688
+ else if (parsed.positionals.length !== 1) {
689
+ throw new Error(`Unexpected argument: ${subcommand}`);
690
+ }
691
+ if (parsed.values.help) {
692
+ runtime.stdout(command !== 'ilink'
693
+ ? HELP
694
+ : subcommand === 'login' ? ILINK_LOGIN_HELP
695
+ : subcommand === 'list' ? ILINK_LIST_HELP
696
+ : subcommand === 'start' ? ILINK_START_HELP
697
+ : subcommand === 'stop' ? ILINK_STOP_HELP
698
+ : ILINK_DELETE_HELP);
699
+ return 0;
526
700
  }
527
701
  if (command !== 'logs' &&
528
702
  (parsed.values.lines !== undefined || parsed.values['no-follow'])) {
529
703
  throw new Error('--lines and --no-follow are valid only for "kintio logs"');
530
704
  }
705
+ if ((command !== 'ilink' || subcommand !== 'login') &&
706
+ parsed.values['qr-output'] !== undefined) {
707
+ throw new Error('--qr-output is valid only for "kintio ilink login"');
708
+ }
709
+ if (parsed.values['qr-output'] === '') {
710
+ throw new Error('--qr-output requires a non-empty file path');
711
+ }
712
+ if (parsed.values.account !== undefined &&
713
+ (command !== 'ilink' || !['start', 'stop', 'delete'].includes(subcommand || ''))) {
714
+ throw new Error('--account is valid only for "kintio ilink start|stop|delete"');
715
+ }
716
+ if (parsed.values.account === '') {
717
+ throw new Error('--account requires a non-empty account ID or key');
718
+ }
719
+ if (parsed.values.yes && (command !== 'ilink' || subcommand !== 'delete')) {
720
+ throw new Error('--yes is valid only for "kintio ilink delete"');
721
+ }
531
722
  const location = instanceLocation(parsed.values, runtime);
723
+ const qrOutputPath = parsed.values['qr-output'] === undefined
724
+ ? undefined
725
+ : resolveInputPath(parsed.values['qr-output'], runtime.cwd);
726
+ if (qrOutputPath && !samePath(path.dirname(qrOutputPath), location.home)) {
727
+ throw new Error('iLink QR output must be directly inside the instance directory');
728
+ }
729
+ if (command === 'ilink') {
730
+ if (privateFile(location.configFile, 'Kintio config')) {
731
+ assertTrustedDirectory(path.dirname(location.configFile), 'Kintio config directory', false);
732
+ }
733
+ prepareDirectories(location.home);
734
+ return await runWithIlinkSignals(async (signal) => {
735
+ const enrollmentConfig = loadIlinkEnrollmentConfig({
736
+ environment: { ...runtime.env },
737
+ envFile: location.configFile,
738
+ root: location.home,
739
+ });
740
+ if (subcommand === 'login') {
741
+ return await runtime.ilinkLogin({
742
+ config: enrollmentConfig,
743
+ packageRoot: runtime.packageRoot,
744
+ stdout: runtime.stdout,
745
+ stdoutIsTTY: runtime.stdoutIsTTY,
746
+ stdoutColumns: runtime.stdoutColumns,
747
+ ...(qrOutputPath ? { qrOutputPath } : {}),
748
+ signal,
749
+ });
750
+ }
751
+ const commandResult = await runtime.ilinkAccount({
752
+ command: subcommand,
753
+ ...(parsed.values.account ? { selector: parsed.values.account } : {}),
754
+ confirmed: Boolean(parsed.values.yes),
755
+ config: enrollmentConfig,
756
+ packageRoot: runtime.packageRoot,
757
+ signal,
758
+ stdout: runtime.stdout,
759
+ });
760
+ if (subcommand !== 'start' || !commandResult.startForeground)
761
+ return 0;
762
+ return await runtime.ilinkStart({
763
+ config: loadIlinkRuntimeConfig({
764
+ environment: { ...runtime.env },
765
+ envFile: location.configFile,
766
+ root: location.home,
767
+ }),
768
+ signal,
769
+ stdout: runtime.stdout,
770
+ });
771
+ });
772
+ }
532
773
  if (command === 'setup')
533
774
  return setup(location, runtime);
534
775
  if (command === 'start')