cannwork-rehearsal 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/bin/cannwork.js +59 -6
- package/bin/update-check.js +206 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -41,6 +41,16 @@ Notes:
|
|
|
41
41
|
```
|
|
42
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
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
|
+
|
|
44
54
|
## Getting Started (from source)
|
|
45
55
|
|
|
46
56
|
Install dependencies:
|
|
@@ -77,7 +87,7 @@ Releases are published manually to <https://registry.npmjs.org/> in two phases:
|
|
|
77
87
|
|
|
78
88
|
### Phase 1 — unscoped trial release (kept live)
|
|
79
89
|
|
|
80
|
-
1. Set `name` to `cannwork-rehearsal` and `version` to `0.
|
|
90
|
+
1. Set `name` to `cannwork-rehearsal` and `version` to `0.3.0` in the root `package.json` (single source of truth).
|
|
81
91
|
2. `npm login` (any account with publish rights).
|
|
82
92
|
3. `bun run release` — runs typecheck + tests, cross-compiles, then publishes the 5 platform packages followed by the main package. Add `--dry-run` to pack and verify without uploading.
|
|
83
93
|
4. Check each package on the registry (`npm view <pkg> version`). A `0.0.0-stage` value means npm staged the version for review; wait for it to go live (or approve it on npmjs.com) before smoke-testing.
|
|
@@ -99,7 +109,8 @@ Rollback: `npm unpublish` the affected version within the allowed window; afterw
|
|
|
99
109
|
|
|
100
110
|
```
|
|
101
111
|
├── bin/
|
|
102
|
-
│
|
|
112
|
+
│ ├── cannwork.js # Node-compatible launcher shipped in the npm main package
|
|
113
|
+
│ └── update-check.js # Pre-launch version check, prompt and global install
|
|
103
114
|
├── scripts/ # cross-platform build and publish tooling
|
|
104
115
|
├── src/
|
|
105
116
|
│ └── index.tsx # App entry, renders the TUI with @opentui/react
|
package/bin/cannwork.js
CHANGED
|
@@ -4,6 +4,16 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { accessSync, chmodSync, constants, realpathSync } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
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";
|
|
7
17
|
|
|
8
18
|
export const SUPPORTED_TARGETS = Object.freeze([
|
|
9
19
|
"win32-x64",
|
|
@@ -82,32 +92,75 @@ export function spawnBinary(binaryPath, args) {
|
|
|
82
92
|
});
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
export async function run(argv = process.argv.slice(2)) {
|
|
95
|
+
export async function run(argv = process.argv.slice(2), options = {}) {
|
|
86
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;
|
|
87
101
|
|
|
88
102
|
const requireFromHere = createRequire(import.meta.url);
|
|
89
103
|
let manifest;
|
|
90
104
|
try {
|
|
91
105
|
manifest = requireFromHere("../package.json");
|
|
92
106
|
} catch {
|
|
93
|
-
|
|
107
|
+
stderr.write("cannwork: 无法读取自身包清单,安装可能已损坏。\n");
|
|
94
108
|
return 1;
|
|
95
109
|
}
|
|
96
110
|
const mainName = manifest.name;
|
|
97
111
|
|
|
98
112
|
if (argv.includes("-v") || argv.includes("--version")) {
|
|
99
|
-
|
|
113
|
+
stdout.write(`${manifest.version}\n`);
|
|
100
114
|
return 0;
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
const
|
|
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);
|
|
104
156
|
if (binaryPath === null) {
|
|
105
|
-
|
|
157
|
+
stderr.write(missingPackageMessage(mainName, os, arch));
|
|
106
158
|
return 1;
|
|
107
159
|
}
|
|
108
160
|
|
|
109
161
|
ensureExecutable(binaryPath, os);
|
|
110
|
-
|
|
162
|
+
const spawnChild = options.spawnBinary ?? spawnBinary;
|
|
163
|
+
return await spawnChild(binaryPath, argv);
|
|
111
164
|
}
|
|
112
165
|
|
|
113
166
|
export const ENTRYPOINT_UNDETERMINED_MESSAGE =
|
|
@@ -0,0 +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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cannwork-rehearsal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cannwork": "bin/cannwork.js"
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
12
|
"optionalDependencies": {
|
|
13
|
-
"cannwork-rehearsal-win32-x64": "0.
|
|
14
|
-
"cannwork-rehearsal-linux-x64": "0.
|
|
15
|
-
"cannwork-rehearsal-linux-arm64": "0.
|
|
16
|
-
"cannwork-rehearsal-darwin-x64": "0.
|
|
17
|
-
"cannwork-rehearsal-darwin-arm64": "0.
|
|
13
|
+
"cannwork-rehearsal-win32-x64": "0.3.1",
|
|
14
|
+
"cannwork-rehearsal-linux-x64": "0.3.1",
|
|
15
|
+
"cannwork-rehearsal-linux-arm64": "0.3.1",
|
|
16
|
+
"cannwork-rehearsal-darwin-x64": "0.3.1",
|
|
17
|
+
"cannwork-rehearsal-darwin-arm64": "0.3.1"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"registry": "https://registry.npmjs.org/",
|