cannwork-rehearsal 0.4.1 → 0.5.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.md CHANGED
@@ -1,141 +1,142 @@
1
- # cannwork-cli
2
-
3
- A terminal launcher for AI coding agents (starting with [opencode](https://opencode.ai)), built with [OpenTUI](https://github.com/msmps/opentui) and React, running on [Bun](https://bun.sh/).
4
-
5
- ## Requirements
6
-
7
- - [Bun](https://bun.sh/) **1.3.0** or later — to run from source and to build
8
- - Node.js 18+ — only to launch the tiny `cannwork` shim when installed from npm; the packaged executable embeds its own runtime and does not need Bun
9
-
10
- ## Install
11
-
12
- Trial-period name (currently available — the official scoped name is not published yet):
13
-
14
- ```bash
15
- npm install -g cannwork-rehearsal
16
- # or run without installing
17
- npx cannwork-rehearsal
18
- ```
19
-
20
- Official name (to be published later): `@zhoufangchaojs/cannwork`, same usage once available.
21
-
22
- The npm package ships standalone executables per platform. Supported platforms:
23
-
24
- | Platform | Suffix |
25
- | --- | --- |
26
- | Windows x64 | `win32-x64` |
27
- | Linux x64 (glibc) | `linux-x64` |
28
- | Linux arm64 (glibc) | `linux-arm64` |
29
- | macOS x64 | `darwin-x64` |
30
- | macOS arm64 | `darwin-arm64` |
31
-
32
- The installed platform package is `<package-name>-<suffix>` (currently `cannwork-rehearsal-<suffix>`, later `@zhoufangchaojs/cannwork-<suffix>`).
33
-
34
- Notes:
35
-
36
- - Do not install with `--omit=optional`: the matching platform package is an optional dependency.
37
- - Linux binaries link glibc; Alpine/musl is not covered yet — `OPENTUI_LIBC`/musl builds are not published.
38
- - The macOS binaries are not codesigned/notarized. If Gatekeeper blocks the binary, clear the quarantine flag:
39
- ```bash
40
- xattr -d com.apple.quarantine "$(npm root -g)/cannwork-rehearsal-darwin-arm64/bin/cannwork"
41
- ```
42
- - `cannwork-rehearsal` is the **trial-period release name**: it is published and installable on purpose so real users can try it while the official `@zhoufangchaojs/cannwork` name is not released yet. It will be retired (unpublished/deprecated) once the official name is published.
43
-
44
- ### Automatic update check
45
-
46
- When you run `cannwork` in an interactive terminal, the launcher asks the npm registry whether a newer version exists. If one does, it prompts before the TUI starts:
47
-
48
- - `[1] 更新` — runs `npm install -g <package>@latest` in place, then asks you to run `cannwork` again
49
- - `[2] 跳过` — skips this time only; the same version will be offered again next launch
50
- - `[3] 跳过此版本` — remembers this version and stops prompting for it (a newer version still prompts)
51
-
52
- The check is skipped entirely for `cannwork -v` / `cannwork --version`, on non-interactive terminals (stdin or stdout not a TTY), or when `CANNWORK_NO_UPDATE_CHECK=1` is set. Network failures never block startup — the launcher simply continues with the installed version.
53
-
54
- ## Getting Started (from source)
55
-
56
- Install dependencies:
57
-
58
- ```bash
59
- bun install
60
- ```
61
-
62
- Start the app in watch mode:
63
-
64
- ```bash
65
- bun dev
66
- ```
67
-
68
- ## Credentials and login
69
-
70
- - The access token stays in process memory only and is never written to `~/.cannwork-tui/cannwork.json`.
71
- - The refresh token is encrypted with AES-256-GCM using a key derived from a stable machine fingerprint, then stored as `refreshTokenEnc`; `refreshTokenSavedAt` records when it was written and is metadata only.
72
- - **Breaking upgrade:** on first launch after upgrading from a version that stored plaintext tokens, the legacy `accessToken`, `refreshToken` and `accessTokenExpiresAt` fields are deleted and you must log in again with `/login` or `/gitcode-token`.
73
- - If the machine fingerprint changes (host identity or OS user), the stored refresh token can no longer be decrypted and you need to log in again.
74
- - Multi-instance refreshes are serialized through a SQLite lease lock (`token_refresh_lock`). If the local database is unavailable, the launcher falls back to best-effort refreshes.
75
-
76
- ## Scripts
77
-
78
- | Script | Description |
79
- | --- | --- |
80
- | `bun dev` | Run the app with auto-reload on file changes |
81
- | `bun run typecheck` | Type-check the source with `tsc --noEmit` |
82
- | `bun run test` | Run the test suite |
83
- | `bun run build` | Build a JS bundle (requires Bun on the target machine) |
84
- | `bun run build:exe` | Build a standalone executable for the current platform |
85
- | `bun run build:publish` | Cross-compile all 5 platforms and assemble `dist/publish/<pkg>/` |
86
- | `bun run release` | Check → build → publish all packages (sub-packages first) |
87
-
88
- Build artifacts are written to `dist/`.
89
-
90
- `build:publish` prepares any missing target-platform native packages (`@opentui/core-*`) automatically by fetching them from npm into `node_modules`, so the build machine needs network access on first run. Pass `--manifests-only` to assemble manifests without compiling.
91
-
92
- ## Maintainer release runbook
93
-
94
- Releases are published **manually by the maintainer** to <https://registry.npmjs.org/> in two phases: an unscoped **trial release** that is kept live for real users, then (later) the official scoped release, after which the trial name is retired. AI assistants and other automation must not run a real release — they may only read release files, run checks/tests/builds, and use `--dry-run`.
95
-
96
- ### Phase 1 — unscoped trial release (kept live)
97
-
98
- 1. Set `name` to `cannwork-rehearsal` and `version` to the intended version in the root `package.json` (single source of truth). The script never bumps the version or creates git tags for you.
99
- 2. `npm login` (any account with publish rights).
100
- 3. Optional preview: `bun run release --dry-run`. It prints the release plan (package names, version, package count, sub-packages-before-main order), runs typecheck + tests, cross-compiles, and verifies the packed artifacts **without uploading or asking for confirmation**.
101
- 4. Real release: run `bun run release` in an **interactive terminal**. After checks, build, and artifact verification it prints the same plan and asks `确认发布 <name>@<version>…`; only `y` / `yes` continues. Any other input (including an empty line) cancels with exit code 0 and uploads nothing.
102
- 5. The script publishes the 5 platform packages first, then the main package, and finally queries the registry for every package. It succeeds only when all 6 target versions are `live`. If any package is still staged (`0.0.0-stage`) or the query fails, the command exits non-zero, names the package, and tells you to wait for npm to promote it (or approve it on npmjs.com) before smoke-testing.
103
- 6. Smoke-test in a clean directory: `npx cannwork-rehearsal`. On success the script writes `dist/.rehearsal-ok`, which unlocks phase 2.
104
- 7. **Keep it live**: do not unpublish. During the trial, ship updates by repeating steps 3–6 with a bumped version.
105
-
106
- ### Phase 2 — official scoped release (deferred), then retire the trial name
107
-
108
- 8. Set `name` to `@zhoufangchaojs/cannwork`, then run `bun run release` in an interactive terminal and confirm. The script refuses to publish a scoped name until the trial-release marker exists, and it performs the same registry check afterwards.
109
- 9. Verify: `npm view @zhoufangchaojs/cannwork version`, smoke-test `npx @zhoufangchaojs/cannwork`, then tag the release yourself (tagging is intentionally outside the script):
110
- ```bash
111
- git tag <version>
112
- ```
113
- 10. Retire the trial packages: `npm unpublish` all 6 (`cannwork-rehearsal` and `cannwork-rehearsal-*`) within the 72-hour window, otherwise `npm deprecate` them as trial packages. If the active credential is a `Granular access token` that bypasses 2FA, unpublish is rejected with 403 — delete the packages on npmjs.com instead, or run `npm login --auth-type=web` first.
114
-
115
- ### Release script options
116
-
117
- - `--dry-run` — plan, check, build, and pack without uploading or confirming.
118
- - `--yes` — skip the interactive confirmation. Only for rehearsals or explicitly authorized automation; a real release without a TTY is rejected unless this flag is present.
119
- - `--no-build` / `--skip-checks` — reuse existing artifacts / skip typecheck and tests (use with care).
120
- - `--root <dir>` — run against another checkout.
121
-
122
- Rollback: `npm unpublish` the affected version within the allowed window; afterwards `npm deprecate` and publish a fixed version.
123
-
124
- ## Project Structure
125
-
126
- ```
127
- ├── bin/
128
- │ ├── cannwork.js # Node-compatible launcher shipped in the npm main package
129
- │ └── update-check.js # Pre-launch version check, prompt and global install
130
- ├── scripts/ # cross-platform build and publish tooling
131
- ├── src/
132
- │ └── index.tsx # App entry, renders the TUI with @opentui/react
133
- ├── openspec/ # OpenSpec change management workspace
134
- ├── dist/ # Build output
135
- ├── package.json
136
- └── tsconfig.json
137
- ```
138
-
139
- ## Notes
140
-
141
- This project was created using `bun create tui`. [create-tui](https://github.com/msmps/create-tui) is the easiest way to get started with OpenTUI.
1
+ # cannwork-cli
2
+
3
+ A terminal launcher for AI coding agents (starting with [opencode](https://opencode.ai)), built with [OpenTUI](https://github.com/msmps/opentui) and React, running on [Bun](https://bun.sh/).
4
+
5
+ ## Requirements
6
+
7
+ - [Bun](https://bun.sh/) **1.3.0** or later — to run from source and to build
8
+ - Node.js 18+ — only to launch the tiny `cannbot` shim when installed from npm; the packaged executable embeds its own runtime and does not need Bun
9
+
10
+ ## Install
11
+
12
+ Trial-period name (currently available — the official scoped name is not published yet):
13
+
14
+ ```bash
15
+ npm install -g cannwork-rehearsal
16
+ # or run without installing
17
+ npx cannwork-rehearsal
18
+ ```
19
+
20
+ Official name (to be published later): `@zhoufangchaojs/cannwork`, same usage once available.
21
+
22
+ The npm package ships standalone executables per platform. Supported platforms:
23
+
24
+ | Platform | Suffix |
25
+ | --- | --- |
26
+ | Windows x64 | `win32-x64` |
27
+ | Linux x64 (glibc) | `linux-x64` |
28
+ | Linux arm64 (glibc) | `linux-arm64` |
29
+ | macOS x64 | `darwin-x64` |
30
+ | macOS arm64 | `darwin-arm64` |
31
+
32
+ The installed platform package is `<package-name>-<suffix>` (currently `cannwork-rehearsal-<suffix>`, later `@zhoufangchaojs/cannwork-<suffix>`).
33
+
34
+ Notes:
35
+
36
+ - The installed command is `cannbot` (the launcher is `bin/cannbot.js`). The former `cannwork` command is no longer installed; uninstall any previous global install and remove the old `cannwork` shim if your shell still resolves it.
37
+ - Do not install with `--omit=optional`: the matching platform package is an optional dependency.
38
+ - Linux binaries link glibc; Alpine/musl is not covered yet — `OPENTUI_LIBC`/musl builds are not published.
39
+ - The macOS binaries are not codesigned/notarized. If Gatekeeper blocks the binary, clear the quarantine flag:
40
+ ```bash
41
+ xattr -d com.apple.quarantine "$(npm root -g)/cannwork-rehearsal-darwin-arm64/bin/cannwork"
42
+ ```
43
+ - `cannwork-rehearsal` is the **trial-period release name**: it is published and installable on purpose so real users can try it while the official `@zhoufangchaojs/cannwork` name is not released yet. It will be retired (unpublished/deprecated) once the official name is published.
44
+
45
+ ### Automatic update check
46
+
47
+ When you run `cannbot` in an interactive terminal, the launcher asks the npm registry whether a newer version exists. If one does, it prompts before the TUI starts:
48
+
49
+ - `[1] 更新` — runs `npm install -g <package>@latest` in place, then asks you to run `cannbot` again
50
+ - `[2] 跳过` — skips this time only; the same version will be offered again next launch
51
+ - `[3] 跳过此版本` — remembers this version and stops prompting for it (a newer version still prompts)
52
+
53
+ The check is skipped entirely for `cannbot -v` / `cannbot --version`, on non-interactive terminals (stdin or stdout not a TTY), or when `CANNWORK_NO_UPDATE_CHECK=1` is set. Network failures never block startup — the launcher simply continues with the installed version.
54
+
55
+ ## Getting Started (from source)
56
+
57
+ Install dependencies:
58
+
59
+ ```bash
60
+ bun install
61
+ ```
62
+
63
+ Start the app in watch mode:
64
+
65
+ ```bash
66
+ bun dev
67
+ ```
68
+
69
+ ## Credentials and login
70
+
71
+ - The access token stays in process memory only and is never written to `~/.cannwork-tui/cannwork.json`.
72
+ - The refresh token is encrypted with AES-256-GCM using a key derived from a stable machine fingerprint, then stored as `refreshTokenEnc`; `refreshTokenSavedAt` records when it was written and is metadata only.
73
+ - **Breaking upgrade:** on first launch after upgrading from a version that stored plaintext tokens, the legacy `accessToken`, `refreshToken` and `accessTokenExpiresAt` fields are deleted and you must log in again with `/login` or `/gitcode-token`.
74
+ - If the machine fingerprint changes (host identity or OS user), the stored refresh token can no longer be decrypted and you need to log in again.
75
+ - Multi-instance refreshes are serialized through a SQLite lease lock (`token_refresh_lock`). If the local database is unavailable, the launcher falls back to best-effort refreshes.
76
+
77
+ ## Scripts
78
+
79
+ | Script | Description |
80
+ | --- | --- |
81
+ | `bun dev` | Run the app with auto-reload on file changes |
82
+ | `bun run typecheck` | Type-check the source with `tsc --noEmit` |
83
+ | `bun run test` | Run the test suite |
84
+ | `bun run build` | Build a JS bundle (requires Bun on the target machine) |
85
+ | `bun run build:exe` | Build a standalone executable for the current platform |
86
+ | `bun run build:publish` | Cross-compile all 5 platforms and assemble `dist/publish/<pkg>/` |
87
+ | `bun run release` | Check → build → publish all packages (sub-packages first) |
88
+
89
+ Build artifacts are written to `dist/`.
90
+
91
+ `build:publish` prepares any missing target-platform native packages (`@opentui/core-*`) automatically by fetching them from npm into `node_modules`, so the build machine needs network access on first run. Pass `--manifests-only` to assemble manifests without compiling.
92
+
93
+ ## Maintainer release runbook
94
+
95
+ Releases are published **manually by the maintainer** to <https://registry.npmjs.org/> in two phases: an unscoped **trial release** that is kept live for real users, then (later) the official scoped release, after which the trial name is retired. AI assistants and other automation must not run a real release — they may only read release files, run checks/tests/builds, and use `--dry-run`.
96
+
97
+ ### Phase 1 — unscoped trial release (kept live)
98
+
99
+ 1. Set `name` to `cannwork-rehearsal` and `version` to the intended version in the root `package.json` (single source of truth). The script never bumps the version or creates git tags for you.
100
+ 2. `npm login` (any account with publish rights).
101
+ 3. Optional preview: `bun run release --dry-run`. It prints the release plan (package names, version, package count, sub-packages-before-main order), runs typecheck + tests, cross-compiles, and verifies the packed artifacts **without uploading or asking for confirmation**.
102
+ 4. Real release: run `bun run release` in an **interactive terminal**. After checks, build, and artifact verification it prints the same plan and asks `确认发布 <name>@<version>…`; only `y` / `yes` continues. Any other input (including an empty line) cancels with exit code 0 and uploads nothing.
103
+ 5. The script publishes the 5 platform packages first, then the main package, and finally queries the registry for every package. It succeeds only when all 6 target versions are `live`. If any package is still staged (`0.0.0-stage`) or the query fails, the command exits non-zero, names the package, and tells you to wait for npm to promote it (or approve it on npmjs.com) before smoke-testing.
104
+ 6. Smoke-test in a clean directory: `npx cannwork-rehearsal`. On success the script writes `dist/.rehearsal-ok`, which unlocks phase 2.
105
+ 7. **Keep it live**: do not unpublish. During the trial, ship updates by repeating steps 3–6 with a bumped version.
106
+
107
+ ### Phase 2 — official scoped release (deferred), then retire the trial name
108
+
109
+ 8. Set `name` to `@zhoufangchaojs/cannwork`, then run `bun run release` in an interactive terminal and confirm. The script refuses to publish a scoped name until the trial-release marker exists, and it performs the same registry check afterwards.
110
+ 9. Verify: `npm view @zhoufangchaojs/cannwork version`, smoke-test `npx @zhoufangchaojs/cannwork`, then tag the release yourself (tagging is intentionally outside the script):
111
+ ```bash
112
+ git tag <version>
113
+ ```
114
+ 10. Retire the trial packages: `npm unpublish` all 6 (`cannwork-rehearsal` and `cannwork-rehearsal-*`) within the 72-hour window, otherwise `npm deprecate` them as trial packages. If the active credential is a `Granular access token` that bypasses 2FA, unpublish is rejected with 403 — delete the packages on npmjs.com instead, or run `npm login --auth-type=web` first.
115
+
116
+ ### Release script options
117
+
118
+ - `--dry-run` — plan, check, build, and pack without uploading or confirming.
119
+ - `--yes` — skip the interactive confirmation. Only for rehearsals or explicitly authorized automation; a real release without a TTY is rejected unless this flag is present.
120
+ - `--no-build` / `--skip-checks` — reuse existing artifacts / skip typecheck and tests (use with care).
121
+ - `--root <dir>` — run against another checkout.
122
+
123
+ Rollback: `npm unpublish` the affected version within the allowed window; afterwards `npm deprecate` and publish a fixed version.
124
+
125
+ ## Project Structure
126
+
127
+ ```
128
+ ├── bin/
129
+ │ ├── cannbot.js # Node-compatible launcher shipped in the npm main package (`cannbot` command)
130
+ │ └── update-check.js # Pre-launch version check, prompt and global install
131
+ ├── scripts/ # cross-platform build and publish tooling
132
+ ├── src/
133
+ │ └── index.tsx # App entry, renders the TUI with @opentui/react
134
+ ├── openspec/ # OpenSpec change management workspace
135
+ ├── dist/ # Build output
136
+ ├── package.json
137
+ └── tsconfig.json
138
+ ```
139
+
140
+ ## Notes
141
+
142
+ This project was created using `bun create tui`. [create-tui](https://github.com/msmps/create-tui) is the easiest way to get started with OpenTUI.
@@ -1,191 +1,191 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- import { spawn } from "node:child_process";
4
- import { accessSync, chmodSync, constants, realpathSync } from "node:fs";
5
- import { resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import {
8
- askUpdateChoice,
9
- checkForUpdate,
10
- DISABLE_UPDATE_CHECK_ENV,
11
- formatUpdateNotice,
12
- manualInstallCommand,
13
- readSkippedVersion,
14
- runGlobalInstall,
15
- writeSkippedVersion,
16
- } from "./update-check.js";
17
-
18
- export const SUPPORTED_TARGETS = Object.freeze([
19
- "win32-x64",
20
- "linux-x64",
21
- "linux-arm64",
22
- "darwin-x64",
23
- "darwin-arm64",
24
- ]);
25
-
26
- export function subPackageName(mainName, os, arch) {
27
- if (typeof mainName !== "string" || mainName.length === 0) return null;
28
- if (!SUPPORTED_TARGETS.includes(`${os}-${arch}`)) return null;
29
- const suffix = `-${os}-${arch}`;
30
- if (mainName.startsWith("@")) {
31
- const slash = mainName.indexOf("/");
32
- if (slash <= 1 || slash === mainName.length - 1) return null;
33
- return `${mainName}${suffix}`;
34
- }
35
- return `${mainName}${suffix}`;
36
- }
37
-
38
- export function binarySubpath(os) {
39
- return os === "win32" ? "bin/cannwork.exe" : "bin/cannwork";
40
- }
41
-
42
- export function missingPackageMessage(mainName, os, arch) {
43
- const pkg = subPackageName(mainName, os, arch) ?? `${mainName}-${os}-${arch}`;
44
- return [
45
- `${mainName}: 缺少当前平台 (${os}-${arch}) 的运行时包 ${pkg}。`,
46
- "",
47
- " 可能原因:安装时使用了 --omit=optional,或该平台尚未提供预编译产物。",
48
- ` 受支持平台:${SUPPORTED_TARGETS.join(", ")}`,
49
- "",
50
- " 修复:重新完整安装(保留可选依赖),例如",
51
- ` npm install -g ${mainName}`,
52
- "",
53
- ].join("\n");
54
- }
55
-
56
- export function resolveBinaryPath(mainName, os, arch, requireFn) {
57
- const subPackage = subPackageName(mainName, os, arch);
58
- if (subPackage === null) return null;
59
- try {
60
- return requireFn.resolve(`${subPackage}/${binarySubpath(os)}`);
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- function ensureExecutable(binaryPath, os) {
67
- if (os === "win32") return;
68
- try {
69
- accessSync(binaryPath, constants.X_OK);
70
- } catch {
71
- try {
72
- chmodSync(binaryPath, 0o755);
73
- } catch {
74
- return;
75
- }
76
- }
77
- }
78
-
79
- export function spawnBinary(binaryPath, args) {
80
- return new Promise((resolveCode) => {
81
- let child;
82
- try {
83
- child = spawn(binaryPath, args, { stdio: "inherit", windowsHide: false });
84
- } catch {
85
- resolveCode(1);
86
- return;
87
- }
88
- child.on("error", () => resolveCode(1));
89
- child.on("exit", (code) => {
90
- resolveCode(typeof code === "number" ? code : 1);
91
- });
92
- });
93
- }
94
-
95
- export async function run(argv = process.argv.slice(2), options = {}) {
96
- const { platform: os, arch } = process;
97
- const stdin = options.stdin ?? process.stdin;
98
- const stdout = options.stdout ?? process.stdout;
99
- const stderr = options.stderr ?? process.stderr;
100
- const env = options.env ?? process.env;
101
-
102
- const requireFromHere = createRequire(import.meta.url);
103
- let manifest;
104
- try {
105
- manifest = requireFromHere("../package.json");
106
- } catch {
107
- stderr.write("cannwork: 无法读取自身包清单,安装可能已损坏。\n");
108
- return 1;
109
- }
110
- const mainName = manifest.name;
111
-
112
- if (argv.includes("-v") || argv.includes("--version")) {
113
- stdout.write(`${manifest.version}\n`);
114
- return 0;
115
- }
116
-
117
- const interactive = stdin.isTTY === true && stdout.isTTY === true;
118
- if (!options.skipUpdateCheck && interactive && env[DISABLE_UPDATE_CHECK_ENV] !== "1") {
119
- try {
120
- const result = await checkForUpdate({
121
- name: mainName,
122
- localVersion: manifest.version,
123
- manifest,
124
- fetchImpl: options.fetchImpl,
125
- timeoutMs: options.timeoutMs,
126
- });
127
- if (result.updateAvailable) {
128
- const stateDir = options.stateDir;
129
- if (readSkippedVersion(stateDir) !== result.latestVersion) {
130
- const notice = formatUpdateNotice({
131
- name: mainName,
132
- current: manifest.version,
133
- latest: result.latestVersion,
134
- });
135
- const ask = options.askChoice ?? askUpdateChoice;
136
- const choice = await ask({ notice, stdin, stdout });
137
- if (choice === "update") {
138
- const install = options.installGlobal ?? runGlobalInstall;
139
- if (install(mainName)) {
140
- stdout.write(`\n已更新到 ${result.latestVersion},请重新执行 cannwork。\n`);
141
- return 0;
142
- }
143
- stderr.write(`\n更新失败。请手动执行:${manualInstallCommand(mainName)}\n\n`);
144
- } else if (choice === "skip-version") {
145
- writeSkippedVersion(result.latestVersion, stateDir);
146
- }
147
- }
148
- }
149
- } catch {
150
- // 升级检查是可选增强:任何异常都静默放行,绝不阻断启动
151
- }
152
- }
153
-
154
- const resolveBinary = options.resolveBinary ?? resolveBinaryPath;
155
- const binaryPath = resolveBinary(mainName, os, arch, requireFromHere);
156
- if (binaryPath === null) {
157
- stderr.write(missingPackageMessage(mainName, os, arch));
158
- return 1;
159
- }
160
-
161
- ensureExecutable(binaryPath, os);
162
- const spawnChild = options.spawnBinary ?? spawnBinary;
163
- return await spawnChild(binaryPath, argv);
164
- }
165
-
166
- export const ENTRYPOINT_UNDETERMINED_MESSAGE =
167
- "cannwork: 无法确认启动器入口,已跳过执行。\n";
168
-
169
- export function entrypointStatus(metaUrl, argv1) {
170
- if (typeof argv1 !== "string" || argv1.length === 0) return "not-entrypoint";
171
- try {
172
- const selfPath = realpathSync(fileURLToPath(metaUrl));
173
- const argvPath = realpathSync(resolve(argv1));
174
- return selfPath === argvPath ? "entrypoint" : "not-entrypoint";
175
- } catch {
176
- return "undetermined";
177
- }
178
- }
179
-
180
- export function isEntrypoint(metaUrl, argv1) {
181
- return entrypointStatus(metaUrl, argv1) === "entrypoint";
182
- }
183
-
184
- const entryStatus = entrypointStatus(import.meta.url, process.argv[1]);
185
- if (entryStatus === "entrypoint") {
186
- run().then((code) => {
187
- process.exit(code);
188
- });
189
- } else if (entryStatus === "undetermined") {
190
- process.stderr.write(ENTRYPOINT_UNDETERMINED_MESSAGE);
191
- }
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { spawn } from "node:child_process";
4
+ import { accessSync, chmodSync, constants, realpathSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import {
8
+ askUpdateChoice,
9
+ checkForUpdate,
10
+ DISABLE_UPDATE_CHECK_ENV,
11
+ formatUpdateNotice,
12
+ manualInstallCommand,
13
+ readSkippedVersion,
14
+ runGlobalInstall,
15
+ writeSkippedVersion,
16
+ } from "./update-check.js";
17
+
18
+ export const SUPPORTED_TARGETS = Object.freeze([
19
+ "win32-x64",
20
+ "linux-x64",
21
+ "linux-arm64",
22
+ "darwin-x64",
23
+ "darwin-arm64",
24
+ ]);
25
+
26
+ export function subPackageName(mainName, os, arch) {
27
+ if (typeof mainName !== "string" || mainName.length === 0) return null;
28
+ if (!SUPPORTED_TARGETS.includes(`${os}-${arch}`)) return null;
29
+ const suffix = `-${os}-${arch}`;
30
+ if (mainName.startsWith("@")) {
31
+ const slash = mainName.indexOf("/");
32
+ if (slash <= 1 || slash === mainName.length - 1) return null;
33
+ return `${mainName}${suffix}`;
34
+ }
35
+ return `${mainName}${suffix}`;
36
+ }
37
+
38
+ export function binarySubpath(os) {
39
+ return os === "win32" ? "bin/cannwork.exe" : "bin/cannwork";
40
+ }
41
+
42
+ export function missingPackageMessage(mainName, os, arch) {
43
+ const pkg = subPackageName(mainName, os, arch) ?? `${mainName}-${os}-${arch}`;
44
+ return [
45
+ `${mainName}: 缺少当前平台 (${os}-${arch}) 的运行时包 ${pkg}。`,
46
+ "",
47
+ " 可能原因:安装时使用了 --omit=optional,或该平台尚未提供预编译产物。",
48
+ ` 受支持平台:${SUPPORTED_TARGETS.join(", ")}`,
49
+ "",
50
+ " 修复:重新完整安装(保留可选依赖),例如",
51
+ ` npm install -g ${mainName}`,
52
+ "",
53
+ ].join("\n");
54
+ }
55
+
56
+ export function resolveBinaryPath(mainName, os, arch, requireFn) {
57
+ const subPackage = subPackageName(mainName, os, arch);
58
+ if (subPackage === null) return null;
59
+ try {
60
+ return requireFn.resolve(`${subPackage}/${binarySubpath(os)}`);
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function ensureExecutable(binaryPath, os) {
67
+ if (os === "win32") return;
68
+ try {
69
+ accessSync(binaryPath, constants.X_OK);
70
+ } catch {
71
+ try {
72
+ chmodSync(binaryPath, 0o755);
73
+ } catch {
74
+ return;
75
+ }
76
+ }
77
+ }
78
+
79
+ export function spawnBinary(binaryPath, args) {
80
+ return new Promise((resolveCode) => {
81
+ let child;
82
+ try {
83
+ child = spawn(binaryPath, args, { stdio: "inherit", windowsHide: false });
84
+ } catch {
85
+ resolveCode(1);
86
+ return;
87
+ }
88
+ child.on("error", () => resolveCode(1));
89
+ child.on("exit", (code) => {
90
+ resolveCode(typeof code === "number" ? code : 1);
91
+ });
92
+ });
93
+ }
94
+
95
+ export async function run(argv = process.argv.slice(2), options = {}) {
96
+ const { platform: os, arch } = process;
97
+ const stdin = options.stdin ?? process.stdin;
98
+ const stdout = options.stdout ?? process.stdout;
99
+ const stderr = options.stderr ?? process.stderr;
100
+ const env = options.env ?? process.env;
101
+
102
+ const requireFromHere = createRequire(import.meta.url);
103
+ let manifest;
104
+ try {
105
+ manifest = requireFromHere("../package.json");
106
+ } catch {
107
+ stderr.write("cannwork: 无法读取自身包清单,安装可能已损坏。\n");
108
+ return 1;
109
+ }
110
+ const mainName = manifest.name;
111
+
112
+ if (argv.includes("-v") || argv.includes("--version")) {
113
+ stdout.write(`${manifest.version}\n`);
114
+ return 0;
115
+ }
116
+
117
+ const interactive = stdin.isTTY === true && stdout.isTTY === true;
118
+ if (!options.skipUpdateCheck && interactive && env[DISABLE_UPDATE_CHECK_ENV] !== "1") {
119
+ try {
120
+ const result = await checkForUpdate({
121
+ name: mainName,
122
+ localVersion: manifest.version,
123
+ manifest,
124
+ fetchImpl: options.fetchImpl,
125
+ timeoutMs: options.timeoutMs,
126
+ });
127
+ if (result.updateAvailable) {
128
+ const stateDir = options.stateDir;
129
+ if (readSkippedVersion(stateDir) !== result.latestVersion) {
130
+ const notice = formatUpdateNotice({
131
+ name: mainName,
132
+ current: manifest.version,
133
+ latest: result.latestVersion,
134
+ });
135
+ const ask = options.askChoice ?? askUpdateChoice;
136
+ const choice = await ask({ notice, stdin, stdout });
137
+ if (choice === "update") {
138
+ const install = options.installGlobal ?? runGlobalInstall;
139
+ if (install(mainName)) {
140
+ stdout.write(`\n已更新到 ${result.latestVersion},请重新执行 cannbot。\n`);
141
+ return 0;
142
+ }
143
+ stderr.write(`\n更新失败。请手动执行:${manualInstallCommand(mainName)}\n\n`);
144
+ } else if (choice === "skip-version") {
145
+ writeSkippedVersion(result.latestVersion, stateDir);
146
+ }
147
+ }
148
+ }
149
+ } catch {
150
+ // 升级检查是可选增强:任何异常都静默放行,绝不阻断启动
151
+ }
152
+ }
153
+
154
+ const resolveBinary = options.resolveBinary ?? resolveBinaryPath;
155
+ const binaryPath = resolveBinary(mainName, os, arch, requireFromHere);
156
+ if (binaryPath === null) {
157
+ stderr.write(missingPackageMessage(mainName, os, arch));
158
+ return 1;
159
+ }
160
+
161
+ ensureExecutable(binaryPath, os);
162
+ const spawnChild = options.spawnBinary ?? spawnBinary;
163
+ return await spawnChild(binaryPath, argv);
164
+ }
165
+
166
+ export const ENTRYPOINT_UNDETERMINED_MESSAGE =
167
+ "cannwork: 无法确认启动器入口,已跳过执行。\n";
168
+
169
+ export function entrypointStatus(metaUrl, argv1) {
170
+ if (typeof argv1 !== "string" || argv1.length === 0) return "not-entrypoint";
171
+ try {
172
+ const selfPath = realpathSync(fileURLToPath(metaUrl));
173
+ const argvPath = realpathSync(resolve(argv1));
174
+ return selfPath === argvPath ? "entrypoint" : "not-entrypoint";
175
+ } catch {
176
+ return "undetermined";
177
+ }
178
+ }
179
+
180
+ export function isEntrypoint(metaUrl, argv1) {
181
+ return entrypointStatus(metaUrl, argv1) === "entrypoint";
182
+ }
183
+
184
+ const entryStatus = entrypointStatus(import.meta.url, process.argv[1]);
185
+ if (entryStatus === "entrypoint") {
186
+ run().then((code) => {
187
+ process.exit(code);
188
+ });
189
+ } else if (entryStatus === "undetermined") {
190
+ process.stderr.write(ENTRYPOINT_UNDETERMINED_MESSAGE);
191
+ }
@@ -1,206 +1,206 @@
1
- import { spawnSync } from "node:child_process";
2
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- export const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
7
- export const UPDATE_CHECK_TIMEOUT_MS = 1500;
8
- export const DISABLE_UPDATE_CHECK_ENV = "CANNWORK_NO_UPDATE_CHECK";
9
- export const SKIP_FILE_NAME = "upgrade.json";
10
-
11
- const VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
12
-
13
- export function parseVersion(raw) {
14
- if (typeof raw !== "string") return null;
15
- const match = VERSION_PATTERN.exec(raw.trim());
16
- if (match === null) return null;
17
- return {
18
- major: Number(match[1]),
19
- minor: Number(match[2]),
20
- patch: Number(match[3]),
21
- prerelease: match[4] ?? null,
22
- };
23
- }
24
-
25
- function comparePrerelease(left, right) {
26
- const leftIds = left.split(".");
27
- const rightIds = right.split(".");
28
- const length = Math.min(leftIds.length, rightIds.length);
29
- for (let index = 0; index < length; index += 1) {
30
- const leftId = leftIds[index] ?? "";
31
- const rightId = rightIds[index] ?? "";
32
- const leftNumeric = /^\d+$/u.test(leftId);
33
- const rightNumeric = /^\d+$/u.test(rightId);
34
- if (leftNumeric && rightNumeric) {
35
- const diff = Number(leftId) - Number(rightId);
36
- if (diff !== 0) return diff > 0 ? 1 : -1;
37
- continue;
38
- }
39
- if (leftNumeric) return -1;
40
- if (rightNumeric) return 1;
41
- if (leftId !== rightId) return leftId < rightId ? -1 : 1;
42
- }
43
- if (leftIds.length === rightIds.length) return 0;
44
- return leftIds.length < rightIds.length ? -1 : 1;
45
- }
46
-
47
- export function compareVersions(left, right) {
48
- const a = parseVersion(left);
49
- const b = parseVersion(right);
50
- if (a === null || b === null) return null;
51
- for (const key of ["major", "minor", "patch"]) {
52
- if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
53
- }
54
- if (a.prerelease === null && b.prerelease === null) return 0;
55
- if (a.prerelease === null) return 1;
56
- if (b.prerelease === null) return -1;
57
- return comparePrerelease(a.prerelease, b.prerelease);
58
- }
59
-
60
- export function isUpdateAvailable(localVersion, latestVersion) {
61
- return compareVersions(latestVersion, localVersion) === 1;
62
- }
63
-
64
- export function resolveRegistry(manifest) {
65
- const configured = manifest?.publishConfig?.registry;
66
- return typeof configured === "string" && configured.trim().length > 0
67
- ? configured.trim()
68
- : DEFAULT_REGISTRY;
69
- }
70
-
71
- export function encodePackageName(name) {
72
- return encodeURI(name).replace(/\//gu, "%2F");
73
- }
74
-
75
- export function packageLatestUrl(name, registry = DEFAULT_REGISTRY) {
76
- const base =
77
- typeof registry === "string" && registry.trim().length > 0 ? registry.trim() : DEFAULT_REGISTRY;
78
- const withSlash = base.endsWith("/") ? base : `${base}/`;
79
- return `${withSlash}${encodePackageName(name)}/latest`;
80
- }
81
-
82
- export async function fetchLatestVersion(name, options = {}) {
83
- const {
84
- registry = DEFAULT_REGISTRY,
85
- timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
86
- fetchImpl = globalThis.fetch,
87
- } = options;
88
- if (typeof fetchImpl !== "function" || typeof name !== "string" || name.length === 0) {
89
- return null;
90
- }
91
- const controller = new AbortController();
92
- const timer = setTimeout(() => controller.abort(), Math.max(1, Number(timeoutMs) || 1));
93
- try {
94
- const response = await fetchImpl(packageLatestUrl(name, registry), {
95
- headers: { Accept: "application/json" },
96
- signal: controller.signal,
97
- });
98
- if (response === null || typeof response !== "object" || response.ok !== true) return null;
99
- const body = await response.json();
100
- const version = body?.version;
101
- return typeof version === "string" && version.trim().length > 0 ? version.trim() : null;
102
- } catch {
103
- return null;
104
- } finally {
105
- clearTimeout(timer);
106
- }
107
- }
108
-
109
- export async function checkForUpdate(options = {}) {
110
- const {
111
- name,
112
- localVersion,
113
- manifest,
114
- fetchImpl,
115
- timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
116
- } = options;
117
- const latestVersion = await fetchLatestVersion(name, {
118
- registry: resolveRegistry(manifest),
119
- timeoutMs,
120
- fetchImpl,
121
- });
122
- return {
123
- latestVersion,
124
- updateAvailable: latestVersion !== null && isUpdateAvailable(localVersion, latestVersion),
125
- };
126
- }
127
-
128
- export function upgradeStateDir() {
129
- return join(homedir(), ".cannwork-tui");
130
- }
131
-
132
- export function upgradeStatePath(dir = upgradeStateDir()) {
133
- return join(dir, SKIP_FILE_NAME);
134
- }
135
-
136
- export function readSkippedVersion(dir = upgradeStateDir()) {
137
- try {
138
- const parsed = JSON.parse(readFileSync(upgradeStatePath(dir), "utf8"));
139
- const value = parsed?.skippedVersion;
140
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
141
- } catch {
142
- return null;
143
- }
144
- }
145
-
146
- export function writeSkippedVersion(version, dir = upgradeStateDir()) {
147
- if (typeof version !== "string" || version.trim().length === 0) return false;
148
- try {
149
- mkdirSync(dir, { recursive: true });
150
- writeFileSync(
151
- upgradeStatePath(dir),
152
- `${JSON.stringify({ skippedVersion: version.trim() }, null, 2)}\n`,
153
- "utf8",
154
- );
155
- return true;
156
- } catch {
157
- return false;
158
- }
159
- }
160
-
161
- export function parseUpdateChoice(raw) {
162
- const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
163
- if (value === "1" || value === "u" || value === "y") return "update";
164
- if (value === "3" || value === "v") return "skip-version";
165
- return "skip";
166
- }
167
-
168
- export function formatUpdateNotice({ name, current, latest }) {
169
- return [
170
- `发现 cannwork 新版本:${current} -> ${latest}`,
171
- ` [1] 更新(执行 npm install -g ${name}@latest)`,
172
- " [2] 跳过(仅本次)",
173
- " [3] 跳过此版本(不再提示该版本)",
174
- ].join("\n");
175
- }
176
-
177
- export async function askUpdateChoice({ notice, stdin = process.stdin, stdout = process.stdout }) {
178
- const readline = await import("node:readline/promises");
179
- const rl = readline.createInterface({ input: stdin, output: stdout });
180
- try {
181
- const answer = await rl.question(`${notice}\n请选择 [1/2/3]: `);
182
- return parseUpdateChoice(answer);
183
- } catch {
184
- return "skip";
185
- } finally {
186
- rl.close();
187
- }
188
- }
189
-
190
- export function manualInstallCommand(name) {
191
- return `npm install -g ${name}@latest`;
192
- }
193
-
194
- export function runGlobalInstall(name, options = {}) {
195
- const { spawnImpl = spawnSync, platform = process.platform } = options;
196
- if (typeof name !== "string" || name.trim().length === 0) return false;
197
- try {
198
- const result = spawnImpl("npm", ["install", "-g", `${name.trim()}@latest`], {
199
- stdio: "inherit",
200
- shell: platform === "win32",
201
- });
202
- return result?.status === 0;
203
- } catch {
204
- return false;
205
- }
206
- }
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
7
+ export const UPDATE_CHECK_TIMEOUT_MS = 1500;
8
+ export const DISABLE_UPDATE_CHECK_ENV = "CANNWORK_NO_UPDATE_CHECK";
9
+ export const SKIP_FILE_NAME = "upgrade.json";
10
+
11
+ const VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
12
+
13
+ export function parseVersion(raw) {
14
+ if (typeof raw !== "string") return null;
15
+ const match = VERSION_PATTERN.exec(raw.trim());
16
+ if (match === null) return null;
17
+ return {
18
+ major: Number(match[1]),
19
+ minor: Number(match[2]),
20
+ patch: Number(match[3]),
21
+ prerelease: match[4] ?? null,
22
+ };
23
+ }
24
+
25
+ function comparePrerelease(left, right) {
26
+ const leftIds = left.split(".");
27
+ const rightIds = right.split(".");
28
+ const length = Math.min(leftIds.length, rightIds.length);
29
+ for (let index = 0; index < length; index += 1) {
30
+ const leftId = leftIds[index] ?? "";
31
+ const rightId = rightIds[index] ?? "";
32
+ const leftNumeric = /^\d+$/u.test(leftId);
33
+ const rightNumeric = /^\d+$/u.test(rightId);
34
+ if (leftNumeric && rightNumeric) {
35
+ const diff = Number(leftId) - Number(rightId);
36
+ if (diff !== 0) return diff > 0 ? 1 : -1;
37
+ continue;
38
+ }
39
+ if (leftNumeric) return -1;
40
+ if (rightNumeric) return 1;
41
+ if (leftId !== rightId) return leftId < rightId ? -1 : 1;
42
+ }
43
+ if (leftIds.length === rightIds.length) return 0;
44
+ return leftIds.length < rightIds.length ? -1 : 1;
45
+ }
46
+
47
+ export function compareVersions(left, right) {
48
+ const a = parseVersion(left);
49
+ const b = parseVersion(right);
50
+ if (a === null || b === null) return null;
51
+ for (const key of ["major", "minor", "patch"]) {
52
+ if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
53
+ }
54
+ if (a.prerelease === null && b.prerelease === null) return 0;
55
+ if (a.prerelease === null) return 1;
56
+ if (b.prerelease === null) return -1;
57
+ return comparePrerelease(a.prerelease, b.prerelease);
58
+ }
59
+
60
+ export function isUpdateAvailable(localVersion, latestVersion) {
61
+ return compareVersions(latestVersion, localVersion) === 1;
62
+ }
63
+
64
+ export function resolveRegistry(manifest) {
65
+ const configured = manifest?.publishConfig?.registry;
66
+ return typeof configured === "string" && configured.trim().length > 0
67
+ ? configured.trim()
68
+ : DEFAULT_REGISTRY;
69
+ }
70
+
71
+ export function encodePackageName(name) {
72
+ return encodeURI(name).replace(/\//gu, "%2F");
73
+ }
74
+
75
+ export function packageLatestUrl(name, registry = DEFAULT_REGISTRY) {
76
+ const base =
77
+ typeof registry === "string" && registry.trim().length > 0 ? registry.trim() : DEFAULT_REGISTRY;
78
+ const withSlash = base.endsWith("/") ? base : `${base}/`;
79
+ return `${withSlash}${encodePackageName(name)}/latest`;
80
+ }
81
+
82
+ export async function fetchLatestVersion(name, options = {}) {
83
+ const {
84
+ registry = DEFAULT_REGISTRY,
85
+ timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
86
+ fetchImpl = globalThis.fetch,
87
+ } = options;
88
+ if (typeof fetchImpl !== "function" || typeof name !== "string" || name.length === 0) {
89
+ return null;
90
+ }
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), Math.max(1, Number(timeoutMs) || 1));
93
+ try {
94
+ const response = await fetchImpl(packageLatestUrl(name, registry), {
95
+ headers: { Accept: "application/json" },
96
+ signal: controller.signal,
97
+ });
98
+ if (response === null || typeof response !== "object" || response.ok !== true) return null;
99
+ const body = await response.json();
100
+ const version = body?.version;
101
+ return typeof version === "string" && version.trim().length > 0 ? version.trim() : null;
102
+ } catch {
103
+ return null;
104
+ } finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
108
+
109
+ export async function checkForUpdate(options = {}) {
110
+ const {
111
+ name,
112
+ localVersion,
113
+ manifest,
114
+ fetchImpl,
115
+ timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
116
+ } = options;
117
+ const latestVersion = await fetchLatestVersion(name, {
118
+ registry: resolveRegistry(manifest),
119
+ timeoutMs,
120
+ fetchImpl,
121
+ });
122
+ return {
123
+ latestVersion,
124
+ updateAvailable: latestVersion !== null && isUpdateAvailable(localVersion, latestVersion),
125
+ };
126
+ }
127
+
128
+ export function upgradeStateDir() {
129
+ return join(homedir(), ".cannwork-tui");
130
+ }
131
+
132
+ export function upgradeStatePath(dir = upgradeStateDir()) {
133
+ return join(dir, SKIP_FILE_NAME);
134
+ }
135
+
136
+ export function readSkippedVersion(dir = upgradeStateDir()) {
137
+ try {
138
+ const parsed = JSON.parse(readFileSync(upgradeStatePath(dir), "utf8"));
139
+ const value = parsed?.skippedVersion;
140
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ export function writeSkippedVersion(version, dir = upgradeStateDir()) {
147
+ if (typeof version !== "string" || version.trim().length === 0) return false;
148
+ try {
149
+ mkdirSync(dir, { recursive: true });
150
+ writeFileSync(
151
+ upgradeStatePath(dir),
152
+ `${JSON.stringify({ skippedVersion: version.trim() }, null, 2)}\n`,
153
+ "utf8",
154
+ );
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ export function parseUpdateChoice(raw) {
162
+ const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
163
+ if (value === "1" || value === "u" || value === "y") return "update";
164
+ if (value === "3" || value === "v") return "skip-version";
165
+ return "skip";
166
+ }
167
+
168
+ export function formatUpdateNotice({ name, current, latest }) {
169
+ return [
170
+ `发现 cannwork 新版本:${current} -> ${latest}`,
171
+ ` [1] 更新(执行 npm install -g ${name}@latest)`,
172
+ " [2] 跳过(仅本次)",
173
+ " [3] 跳过此版本(不再提示该版本)",
174
+ ].join("\n");
175
+ }
176
+
177
+ export async function askUpdateChoice({ notice, stdin = process.stdin, stdout = process.stdout }) {
178
+ const readline = await import("node:readline/promises");
179
+ const rl = readline.createInterface({ input: stdin, output: stdout });
180
+ try {
181
+ const answer = await rl.question(`${notice}\n请选择 [1/2/3]: `);
182
+ return parseUpdateChoice(answer);
183
+ } catch {
184
+ return "skip";
185
+ } finally {
186
+ rl.close();
187
+ }
188
+ }
189
+
190
+ export function manualInstallCommand(name) {
191
+ return `npm install -g ${name}@latest`;
192
+ }
193
+
194
+ export function runGlobalInstall(name, options = {}) {
195
+ const { spawnImpl = spawnSync, platform = process.platform } = options;
196
+ if (typeof name !== "string" || name.trim().length === 0) return false;
197
+ try {
198
+ const result = spawnImpl("npm", ["install", "-g", `${name.trim()}@latest`], {
199
+ stdio: "inherit",
200
+ shell: platform === "win32",
201
+ });
202
+ return result?.status === 0;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "cannwork-rehearsal",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
- "cannwork": "bin/cannwork.js"
6
+ "cannbot": "bin/cannbot.js"
7
7
  },
8
8
  "files": [
9
9
  "bin",
10
10
  "README.md"
11
11
  ],
12
12
  "optionalDependencies": {
13
- "cannwork-rehearsal-win32-x64": "0.4.1",
14
- "cannwork-rehearsal-linux-x64": "0.4.1",
15
- "cannwork-rehearsal-linux-arm64": "0.4.1",
16
- "cannwork-rehearsal-darwin-x64": "0.4.1",
17
- "cannwork-rehearsal-darwin-arm64": "0.4.1"
13
+ "cannwork-rehearsal-win32-x64": "0.5.0",
14
+ "cannwork-rehearsal-linux-x64": "0.5.0",
15
+ "cannwork-rehearsal-linux-arm64": "0.5.0",
16
+ "cannwork-rehearsal-darwin-x64": "0.5.0",
17
+ "cannwork-rehearsal-darwin-arm64": "0.5.0"
18
18
  },
19
19
  "publishConfig": {
20
20
  "registry": "https://registry.npmjs.org/",