lark-remote 0.1.4 → 0.1.5

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.
@@ -0,0 +1,93 @@
1
+ import { execFile, execFileSync } from 'node:child_process';
2
+ import { getLogger } from '../logger/index.js';
3
+ /**
4
+ * Detect which package manager to use for global install.
5
+ *
6
+ * Priority:
7
+ * 1. Environment variable override (LARK_REMOTE_MANAGED_BY)
8
+ * 2. `which` availability: npm → bun → pnpm
9
+ */
10
+ export function detectPackageManager() {
11
+ // 1. Env override (highest priority)
12
+ const env = process.env.LARK_REMOTE_MANAGED_BY;
13
+ if (env === 'npm' || env === 'bun' || env === 'pnpm')
14
+ return env;
15
+ // 2. which availability detection
16
+ const candidates = ['npm', 'bun', 'pnpm'];
17
+ for (const cmd of candidates) {
18
+ try {
19
+ execFileSync('which', [cmd], { stdio: 'pipe', timeout: 3000 });
20
+ return cmd;
21
+ }
22
+ catch {
23
+ // Not found, try next
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ /** Get the install command for a package manager. */
29
+ function getInstallCommand(pm) {
30
+ switch (pm) {
31
+ case 'npm':
32
+ return { cmd: 'npm', args: ['install', '-g', 'lark-remote@latest'] };
33
+ case 'bun':
34
+ return { cmd: 'bun', args: ['install', '-g', 'lark-remote@latest'] };
35
+ case 'pnpm':
36
+ return { cmd: 'pnpm', args: ['add', '-g', 'lark-remote@latest'] };
37
+ }
38
+ }
39
+ /**
40
+ * Run the global install command to upgrade lark-remote to the latest version.
41
+ *
42
+ * @param opts.packageManager - Override detected package manager
43
+ * @param opts.execFn - Override execFile for testing
44
+ */
45
+ export function runInstallLatest(opts) {
46
+ // If packageManager is explicitly null, skip detection and fail immediately.
47
+ // If undefined (not provided), run detection.
48
+ let pm;
49
+ if (opts?.packageManager === null) {
50
+ pm = null;
51
+ }
52
+ else if (opts?.packageManager) {
53
+ pm = opts.packageManager;
54
+ }
55
+ else {
56
+ pm = detectPackageManager();
57
+ }
58
+ if (!pm) {
59
+ return Promise.resolve({
60
+ success: false,
61
+ error: '未检测到可用的包管理器(npm/bun/pnpm),请手动执行 npm install -g lark-remote@latest',
62
+ });
63
+ }
64
+ const { cmd, args } = getInstallCommand(pm);
65
+ const execFn = opts?.execFn ?? ((...args) => execFile(...args));
66
+ const logger = getLogger();
67
+ logger.info(`[update] running: ${cmd} ${args.join(' ')}`);
68
+ return new Promise((resolve) => {
69
+ execFn(cmd, args, { timeout: 120_000, encoding: 'utf8' }, // 2 min timeout for npm install
70
+ (err, _stdout, _stderr) => {
71
+ if (err) {
72
+ const msg = err.message || String(_stderr);
73
+ logger.error(`[update] install failed: ${msg}`);
74
+ // Detect common errors and give actionable advice
75
+ if (msg.includes('EACCES') || msg.includes('permission')) {
76
+ resolve({
77
+ success: false,
78
+ error: `权限不足,尝试 sudo ${cmd} ${args.join(' ')}`,
79
+ });
80
+ return;
81
+ }
82
+ resolve({
83
+ success: false,
84
+ error: msg.split('\n')[0], // First line only
85
+ });
86
+ return;
87
+ }
88
+ logger.info(`[update] install succeeded`);
89
+ resolve({ success: true });
90
+ });
91
+ });
92
+ }
93
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Format an update hint string for the startup notification.
3
+ * Returns null if no newer version is available.
4
+ */
5
+ export declare function formatUpdateHint(current: string, latest: string): string | null;
@@ -0,0 +1,11 @@
1
+ import { isNewer } from './version-check.js';
2
+ /**
3
+ * Format an update hint string for the startup notification.
4
+ * Returns null if no newer version is available.
5
+ */
6
+ export function formatUpdateHint(current, latest) {
7
+ if (!isNewer(current, latest))
8
+ return null;
9
+ return `📦 有新版本 ${latest} 可用(当前 ${current}),发送 /update 升级`;
10
+ }
11
+ //# sourceMappingURL=startup-hint.js.map
@@ -0,0 +1,31 @@
1
+ /** Cache TTL for version check results (1 hour). */
2
+ export declare const CACHE_TTL_MS: number;
3
+ /** Cache file schema. */
4
+ export interface UpdateCache {
5
+ latestVersion: string;
6
+ checkedAt: string;
7
+ }
8
+ /** Result of a version check. */
9
+ export interface VersionCheckResult {
10
+ current: string;
11
+ latest: string;
12
+ }
13
+ /**
14
+ * Compare two semver strings (major.minor.patch).
15
+ * Returns true if latest > current, false if latest <= current,
16
+ * null if either version is not a valid semver string.
17
+ */
18
+ export declare function isNewer(current: string, latest: string): boolean | null;
19
+ /**
20
+ * Check the latest available version of lark-remote.
21
+ * Uses a local cache file with 1-hour TTL to avoid frequent network requests.
22
+ *
23
+ * @param opts.cachePath - Path to cache file (default: <configDir>/update-cache.json)
24
+ * @param opts.bypassCache - If true, always fetch from registry
25
+ * @param opts.registryUrl - Override registry URL (for testing)
26
+ */
27
+ export declare function checkLatestVersion(opts?: {
28
+ cachePath?: string;
29
+ bypassCache?: boolean;
30
+ registryUrl?: string;
31
+ }): Promise<VersionCheckResult>;
@@ -0,0 +1,149 @@
1
+ import https from 'node:https';
2
+ import http from 'node:http';
3
+ import fs from 'node:fs';
4
+ import { getVersion } from '../config/dir.js';
5
+ import { getLogger } from '../logger/index.js';
6
+ import { atomicWrite } from '../persistence/atomic-write.js';
7
+ /** Cache TTL for version check results (1 hour). */
8
+ export const CACHE_TTL_MS = 60 * 60 * 1000;
9
+ /** Default npm registry endpoint for latest version query. */
10
+ const DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org/lark-remote/latest';
11
+ /**
12
+ * Compare two semver strings (major.minor.patch).
13
+ * Returns true if latest > current, false if latest <= current,
14
+ * null if either version is not a valid semver string.
15
+ */
16
+ export function isNewer(current, latest) {
17
+ const parse = (v) => {
18
+ // Split core and pre-release suffix; compare the core numerically and treat
19
+ // a release as newer than any pre-release of the same core version.
20
+ const [core, pre = ''] = v.trim().split('-', 2);
21
+ const parts = core.split('.');
22
+ if (parts.length !== 3)
23
+ return null;
24
+ const nums = parts.map((p) => (/^\d+$/.test(p) ? Number(p) : NaN));
25
+ if (nums.some((n) => !Number.isInteger(n) || n < 0))
26
+ return null;
27
+ return { nums: nums, pre };
28
+ };
29
+ const c = parse(current);
30
+ const l = parse(latest);
31
+ if (!c || !l)
32
+ return null;
33
+ for (let i = 0; i < 3; i++) {
34
+ if (l.nums[i] > c.nums[i])
35
+ return true;
36
+ if (l.nums[i] < c.nums[i])
37
+ return false;
38
+ }
39
+ // Equal core version: a release beats a pre-release of the same version.
40
+ if (c.pre !== l.pre)
41
+ return l.pre === '';
42
+ return false;
43
+ }
44
+ /**
45
+ * Fetch the latest version from npm registry.
46
+ * Returns the version string.
47
+ */
48
+ function fetchLatestVersion(registryUrl) {
49
+ return new Promise((resolve, reject) => {
50
+ const mod = registryUrl.startsWith('https') ? https : http;
51
+ const req = mod.get(registryUrl, { timeout: 10_000 }, (res) => {
52
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
53
+ // Follow one redirect
54
+ fetchLatestVersion(res.headers.location).then(resolve, reject);
55
+ return;
56
+ }
57
+ if (res.statusCode !== 200) {
58
+ reject(new Error(`npm registry returned status ${res.statusCode}`));
59
+ return;
60
+ }
61
+ let data = '';
62
+ res.on('data', (chunk) => {
63
+ data += chunk.toString();
64
+ });
65
+ res.on('end', () => {
66
+ try {
67
+ const parsed = JSON.parse(data);
68
+ if (!parsed.version) {
69
+ reject(new Error('npm registry response missing version field'));
70
+ return;
71
+ }
72
+ resolve(parsed.version);
73
+ }
74
+ catch (err) {
75
+ reject(new Error(`npm registry response parse error: ${err.message}`));
76
+ }
77
+ });
78
+ });
79
+ req.on('error', reject);
80
+ req.on('timeout', () => {
81
+ req.destroy();
82
+ reject(new Error('npm registry request timed out'));
83
+ });
84
+ });
85
+ }
86
+ /**
87
+ * Read cached version check result. Returns null if cache is missing,
88
+ * expired, or corrupt.
89
+ */
90
+ function readCache(cachePath) {
91
+ try {
92
+ if (!fs.existsSync(cachePath))
93
+ return null;
94
+ const raw = fs.readFileSync(cachePath, 'utf8');
95
+ const cached = JSON.parse(raw);
96
+ if (!cached.latestVersion || !cached.checkedAt)
97
+ return null;
98
+ return cached;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ /** Write version check result to cache file. */
105
+ function writeCache(cachePath, latestVersion) {
106
+ try {
107
+ const cache = {
108
+ latestVersion,
109
+ checkedAt: new Date().toISOString(),
110
+ };
111
+ atomicWrite(cachePath, JSON.stringify(cache), 'utf8');
112
+ }
113
+ catch (err) {
114
+ getLogger().warn(`[update] failed to write cache: ${err.message}`);
115
+ }
116
+ }
117
+ /**
118
+ * Check the latest available version of lark-remote.
119
+ * Uses a local cache file with 1-hour TTL to avoid frequent network requests.
120
+ *
121
+ * @param opts.cachePath - Path to cache file (default: <configDir>/update-cache.json)
122
+ * @param opts.bypassCache - If true, always fetch from registry
123
+ * @param opts.registryUrl - Override registry URL (for testing)
124
+ */
125
+ export async function checkLatestVersion(opts) {
126
+ const current = getVersion();
127
+ const registryUrl = opts?.registryUrl ?? DEFAULT_REGISTRY_URL;
128
+ const cachePath = opts?.cachePath;
129
+ // Try cache first (unless bypassed)
130
+ if (cachePath && !opts?.bypassCache) {
131
+ const cached = readCache(cachePath);
132
+ if (cached) {
133
+ const age = Date.now() - new Date(cached.checkedAt).getTime();
134
+ if (age < CACHE_TTL_MS) {
135
+ getLogger().info(`[update] using cached latest version: ${cached.latestVersion}`);
136
+ return { current, latest: cached.latestVersion };
137
+ }
138
+ }
139
+ }
140
+ // Fetch from registry
141
+ getLogger().info(`[update] checking latest version from ${registryUrl}`);
142
+ const latest = await fetchLatestVersion(registryUrl);
143
+ // Update cache
144
+ if (cachePath) {
145
+ writeCache(cachePath, latest);
146
+ }
147
+ return { current, latest };
148
+ }
149
+ //# sourceMappingURL=version-check.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,7 +24,6 @@
24
24
  "CHANGELOG.md"
25
25
  ],
26
26
  "scripts": {
27
- "prepare": "husky",
28
27
  "dev": "bun src/index.ts",
29
28
  "build": "rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
30
29
  "typecheck": "tsc --noEmit",
@@ -108,21 +107,9 @@
108
107
  "eslint": "^10.7.0",
109
108
  "eslint-config-prettier": "^10.1.8",
110
109
  "eslint-plugin-prettier": "^5.5.6",
111
- "husky": "^9.1.7",
112
- "lint-staged": "^17.3.0",
113
110
  "prettier": "^3.9.5",
114
111
  "typescript": "^6.0.0",
115
112
  "typescript-eslint": "^8.63.0",
116
113
  "vitest": "^4.1.10"
117
- },
118
- "lint-staged": {
119
- "src/**/*.ts": [
120
- "eslint --fix",
121
- "prettier --write"
122
- ],
123
- "tests/**/*.ts": [
124
- "eslint --fix",
125
- "prettier --write"
126
- ]
127
114
  }
128
115
  }