@playcraft/cli 0.0.11 → 0.0.13
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/dist/agent/agent.js +202 -0
- package/dist/agent/api-proxy.js +68 -0
- package/dist/agent/cloud-connection.js +233 -0
- package/dist/agent/cloud-connection.test.js +67 -0
- package/dist/agent/fs-backend.js +158 -0
- package/dist/agent/local-backend.js +359 -0
- package/dist/agent/local-backend.test.js +52 -0
- package/dist/commands/build.js +167 -46
- package/dist/commands/fix-ids.js +43 -0
- package/dist/commands/start.js +14 -129
- package/dist/commands/sync.js +40 -0
- package/dist/commands/upgrade.js +71 -0
- package/dist/config.js +2 -0
- package/dist/fs-handler.js +21 -0
- package/dist/index.js +51 -0
- package/dist/server.js +28 -0
- package/dist/socket.js +80 -21
- package/dist/sync/sync-engine.js +213 -0
- package/dist/sync/sync-manager.js +62 -0
- package/dist/sync/sync-manager.test.js +80 -0
- package/dist/utils/package-manager.js +37 -0
- package/dist/utils/updater.js +89 -0
- package/dist/utils/version-checker.js +84 -0
- package/package.json +11 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { SyncManager } from './sync-manager.js';
|
|
6
|
+
describe('SyncManager', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let mockConnection;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sync-manager-test-'));
|
|
11
|
+
mockConnection = {
|
|
12
|
+
request: vi.fn(),
|
|
13
|
+
isConnected: true,
|
|
14
|
+
connectionState: 'connected',
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
if (tmpDir) {
|
|
19
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
it('upload() reads file and calls cloud request', async () => {
|
|
23
|
+
const filePath = 'test.txt';
|
|
24
|
+
const fullPath = path.join(tmpDir, filePath);
|
|
25
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
26
|
+
await fs.writeFile(fullPath, 'hello', 'utf-8');
|
|
27
|
+
vi.mocked(mockConnection.request).mockResolvedValue({ success: true });
|
|
28
|
+
const manager = new SyncManager({
|
|
29
|
+
cloudConnection: mockConnection,
|
|
30
|
+
projectDir: tmpDir,
|
|
31
|
+
});
|
|
32
|
+
const result = await manager.upload(filePath);
|
|
33
|
+
expect(result.success).toBe(true);
|
|
34
|
+
expect(result.direction).toBe('up');
|
|
35
|
+
expect(mockConnection.request).toHaveBeenCalledWith('POST', '/api/sync/upload', expect.objectContaining({
|
|
36
|
+
path: filePath,
|
|
37
|
+
data: expect.any(String),
|
|
38
|
+
compressed: false,
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
it('upload() returns error result when file missing', async () => {
|
|
42
|
+
const manager = new SyncManager({
|
|
43
|
+
cloudConnection: mockConnection,
|
|
44
|
+
projectDir: tmpDir,
|
|
45
|
+
});
|
|
46
|
+
const result = await manager.upload('nonexistent.txt');
|
|
47
|
+
expect(result.success).toBe(false);
|
|
48
|
+
expect(result.error).toBeDefined();
|
|
49
|
+
expect(mockConnection.request).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
it('download() calls cloud request and writes file', async () => {
|
|
52
|
+
const filePath = 'subdir/file.txt';
|
|
53
|
+
const content = Buffer.from('world').toString('base64');
|
|
54
|
+
vi.mocked(mockConnection.request).mockResolvedValue({ data: content });
|
|
55
|
+
const manager = new SyncManager({
|
|
56
|
+
cloudConnection: mockConnection,
|
|
57
|
+
projectDir: tmpDir,
|
|
58
|
+
});
|
|
59
|
+
const result = await manager.download(filePath);
|
|
60
|
+
expect(result.success).toBe(true);
|
|
61
|
+
expect(result.direction).toBe('down');
|
|
62
|
+
const fullPath = path.join(tmpDir, filePath);
|
|
63
|
+
const written = await fs.readFile(fullPath, 'utf-8');
|
|
64
|
+
expect(written).toBe('world');
|
|
65
|
+
});
|
|
66
|
+
it('getPendingUploads returns empty array', () => {
|
|
67
|
+
const manager = new SyncManager({
|
|
68
|
+
cloudConnection: mockConnection,
|
|
69
|
+
projectDir: tmpDir,
|
|
70
|
+
});
|
|
71
|
+
expect(manager.getPendingUploads()).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
it('getPendingDownloads returns empty array', () => {
|
|
74
|
+
const manager = new SyncManager({
|
|
75
|
+
cloudConnection: mockConnection,
|
|
76
|
+
projectDir: tmpDir,
|
|
77
|
+
});
|
|
78
|
+
expect(manager.getPendingDownloads()).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包管理器检测工具
|
|
3
|
+
* 用于检测用户使用的包管理器(npm/pnpm/yarn)并返回相应的更新命令
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 检测当前使用的包管理器
|
|
7
|
+
* 通过环境变量 npm_config_user_agent 来判断
|
|
8
|
+
*/
|
|
9
|
+
export function detectPackageManager() {
|
|
10
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
11
|
+
if (userAgent.includes('pnpm'))
|
|
12
|
+
return 'pnpm';
|
|
13
|
+
if (userAgent.includes('yarn'))
|
|
14
|
+
return 'yarn';
|
|
15
|
+
return 'npm';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 获取更新命令
|
|
19
|
+
* @param pm 包管理器类型
|
|
20
|
+
* @returns 更新命令字符串
|
|
21
|
+
*/
|
|
22
|
+
export function getUpdateCommand(pm) {
|
|
23
|
+
switch (pm) {
|
|
24
|
+
case 'pnpm':
|
|
25
|
+
return 'pnpm install -g @playcraft/cli@latest';
|
|
26
|
+
case 'yarn':
|
|
27
|
+
return 'yarn global add @playcraft/cli@latest';
|
|
28
|
+
default:
|
|
29
|
+
return 'npm install -g @playcraft/cli@latest';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 获取包管理器名称(用于显示)
|
|
34
|
+
*/
|
|
35
|
+
export function getPackageManagerName(pm) {
|
|
36
|
+
return pm.toUpperCase();
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交互式更新工具
|
|
3
|
+
* 提示用户更新并执行更新命令
|
|
4
|
+
*/
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { detectPackageManager, getUpdateCommand, getPackageManagerName } from './package-manager.js';
|
|
10
|
+
/**
|
|
11
|
+
* 交互式提示用户是否更新
|
|
12
|
+
* @param updateInfo 更新信息
|
|
13
|
+
* @param currentVersion 当前版本
|
|
14
|
+
*/
|
|
15
|
+
export async function promptForUpdate(updateInfo, currentVersion) {
|
|
16
|
+
// 非 TTY 环境不显示交互式提示
|
|
17
|
+
if (!process.stdout.isTTY) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { latest, type } = updateInfo;
|
|
21
|
+
console.log(`\n${pc.yellow('⚠')} ${pc.bold('发现新版本')} ${pc.cyan(currentVersion)} ${pc.gray('→')} ${pc.green(latest)}`);
|
|
22
|
+
if (type) {
|
|
23
|
+
const typeLabels = {
|
|
24
|
+
major: '主版本',
|
|
25
|
+
minor: '次版本',
|
|
26
|
+
patch: '补丁版本',
|
|
27
|
+
prerelease: '预发布版本',
|
|
28
|
+
};
|
|
29
|
+
console.log(` 更新类型: ${pc.blue(typeLabels[type] || type)}\n`);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const { shouldUpdate } = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'confirm',
|
|
35
|
+
name: 'shouldUpdate',
|
|
36
|
+
message: '是否立即更新到最新版本?',
|
|
37
|
+
default: true,
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
if (shouldUpdate) {
|
|
41
|
+
await performUpdate();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(pc.gray(`\n提示: 你可以稍后运行 ${pc.bold('playcraft upgrade')} 来更新\n`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// 用户取消(Ctrl+C)时静默退出
|
|
49
|
+
if (error && typeof error === 'object' && 'isTtyError' in error) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 执行更新操作
|
|
57
|
+
*/
|
|
58
|
+
async function performUpdate() {
|
|
59
|
+
const pm = detectPackageManager();
|
|
60
|
+
const command = getUpdateCommand(pm);
|
|
61
|
+
const pmName = getPackageManagerName(pm);
|
|
62
|
+
const spinner = ora(`正在使用 ${pmName} 更新 CLI...`).start();
|
|
63
|
+
try {
|
|
64
|
+
// 执行更新命令
|
|
65
|
+
await execa(command, {
|
|
66
|
+
shell: true,
|
|
67
|
+
stdio: 'pipe', // 隐藏输出,使用 spinner 显示进度
|
|
68
|
+
});
|
|
69
|
+
spinner.succeed(pc.green('更新成功!'));
|
|
70
|
+
console.log(pc.green(`\n✓ CLI 已更新到最新版本,请重新运行命令以使用新版本。\n`));
|
|
71
|
+
// 更新成功后退出,避免使用旧版本的代码
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
spinner.fail(pc.red('更新失败'));
|
|
76
|
+
// 检查是否是权限错误
|
|
77
|
+
if (error?.message?.includes('EACCES') || error?.message?.includes('permission')) {
|
|
78
|
+
console.error(pc.red('\n✗ 权限不足,请使用以下命令之一:\n'));
|
|
79
|
+
console.error(pc.yellow(` sudo ${command}`) + pc.gray(' (Linux/macOS)'));
|
|
80
|
+
console.error(pc.yellow(` 以管理员身份运行: ${command}`) + pc.gray(' (Windows)'));
|
|
81
|
+
console.error(pc.gray('\n或者使用 nvm/pnpm 等工具管理 Node.js 版本,避免权限问题\n'));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.error(pc.red(`\n错误: ${error?.message || String(error)}\n`));
|
|
85
|
+
console.error(pc.yellow(`你可以手动运行: ${pc.bold(command)}\n`));
|
|
86
|
+
}
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本检查工具
|
|
3
|
+
* 使用 update-notifier 进行非阻塞的版本检查
|
|
4
|
+
*/
|
|
5
|
+
import updateNotifier from 'update-notifier';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { promptForUpdate } from './updater.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
let packageJson;
|
|
13
|
+
/**
|
|
14
|
+
* 获取 package.json 内容
|
|
15
|
+
*/
|
|
16
|
+
function getPackageJson() {
|
|
17
|
+
if (!packageJson) {
|
|
18
|
+
packageJson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
return packageJson;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 检查版本更新(非阻塞)
|
|
24
|
+
* 使用 update-notifier 进行后台检查,24小时内最多检查一次
|
|
25
|
+
* @param pkg 可选的 package.json 对象,如果不提供则自动读取
|
|
26
|
+
*/
|
|
27
|
+
export function checkForUpdates(pkg) {
|
|
28
|
+
const packageData = pkg || getPackageJson();
|
|
29
|
+
// 只在 TTY 环境中检查(避免在 CI/CD 中显示提示)
|
|
30
|
+
if (!process.stdout.isTTY) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const notifier = updateNotifier({
|
|
35
|
+
pkg: packageData,
|
|
36
|
+
updateCheckInterval: 1000 * 60 * 60 * 24, // 24 小时
|
|
37
|
+
});
|
|
38
|
+
// 如果有更新可用,触发交互式提示
|
|
39
|
+
if (notifier.update) {
|
|
40
|
+
// 延迟显示,避免影响命令执行
|
|
41
|
+
setImmediate(() => {
|
|
42
|
+
promptForUpdate(notifier.update, packageData.version);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// 静默失败,不影响 CLI 正常使用
|
|
48
|
+
// 网络错误或其他问题不应该阻止用户使用 CLI
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 强制检查最新版本(忽略缓存)
|
|
53
|
+
* 用于 upgrade 命令
|
|
54
|
+
* @param pkg package.json 对象
|
|
55
|
+
* @returns 最新版本号
|
|
56
|
+
*/
|
|
57
|
+
export async function getLatestVersion(pkg) {
|
|
58
|
+
try {
|
|
59
|
+
const { default: latestVersion } = await import('latest-version');
|
|
60
|
+
return await latestVersion(pkg.name);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`无法获取最新版本: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 获取更新信息(用于 upgrade 命令)
|
|
68
|
+
* @param pkg package.json 对象
|
|
69
|
+
* @returns 更新信息
|
|
70
|
+
*/
|
|
71
|
+
export async function getUpdateInfo(pkg) {
|
|
72
|
+
try {
|
|
73
|
+
const notifier = updateNotifier({
|
|
74
|
+
pkg,
|
|
75
|
+
updateCheckInterval: 0, // 强制检查,忽略缓存
|
|
76
|
+
});
|
|
77
|
+
// 触发检查
|
|
78
|
+
await notifier.check();
|
|
79
|
+
return notifier.update || null;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,21 +15,27 @@
|
|
|
15
15
|
"dev": "tsc -w",
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
|
+
"test": "vitest run",
|
|
18
19
|
"link": "pnpm build && npm link",
|
|
19
20
|
"unlink": "npm unlink -g @playcraft/cli",
|
|
20
21
|
"release": "node scripts/release.js"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@playcraft/
|
|
24
|
+
"@playcraft/common": "^0.0.2",
|
|
25
|
+
"@playcraft/build": "^0.0.9",
|
|
24
26
|
"chokidar": "^4.0.3",
|
|
25
27
|
"commander": "^13.1.0",
|
|
26
28
|
"cors": "^2.8.5",
|
|
27
29
|
"cosmiconfig": "^9.0.0",
|
|
28
30
|
"dotenv": "^17.2.3",
|
|
31
|
+
"drizzle-orm": "^0.45.1",
|
|
32
|
+
"execa": "^9.0.0",
|
|
29
33
|
"express": "^5.2.1",
|
|
34
|
+
"latest-version": "^7.0.0",
|
|
30
35
|
"inquirer": "^9.2.12",
|
|
31
36
|
"ora": "^8.2.0",
|
|
32
37
|
"picocolors": "^1.1.1",
|
|
38
|
+
"update-notifier": "^7.3.1",
|
|
33
39
|
"ws": "^8.18.0",
|
|
34
40
|
"zod": "^3.24.1"
|
|
35
41
|
},
|
|
@@ -38,7 +44,9 @@
|
|
|
38
44
|
"@types/express": "^5.0.0",
|
|
39
45
|
"@types/inquirer": "^9.0.7",
|
|
40
46
|
"@types/node": "^22.10.5",
|
|
47
|
+
"@types/update-notifier": "^6.0.4",
|
|
41
48
|
"@types/ws": "^8.5.13",
|
|
42
|
-
"typescript": "^5.7.2"
|
|
49
|
+
"typescript": "^5.7.2",
|
|
50
|
+
"vitest": "^3.2.4"
|
|
43
51
|
}
|
|
44
52
|
}
|