@playcraft/cli 0.0.49 → 0.0.52
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 +65 -44
- package/REMIX.md +760 -0
- package/dist/commands/login.js +82 -32
- package/dist/commands/remix.js +1210 -176
- package/dist/utils/playcraft-http-client.js +172 -0
- package/dist/utils/remix-image-attachments.js +125 -0
- package/package.json +8 -3
- package/dist/agent/cloud-connection.test.js +0 -67
- package/dist/agent/local-backend.test.js +0 -52
- package/dist/commands/fix-ids.test.js +0 -264
- package/dist/commands/tools-research.test.js +0 -216
- package/dist/prad/check.test.js +0 -49
- package/dist/sync/sync-manager.test.js +0 -80
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { loadGlobalConfig } from '../config.js';
|
|
3
|
+
import { DEFAULT_PLAYCRAFT_CLOUD_BASE } from '../commands/login.js';
|
|
4
|
+
export class PlayCraftApiError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
details;
|
|
8
|
+
retryable;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options.message);
|
|
11
|
+
this.name = 'PlayCraftApiError';
|
|
12
|
+
this.code = options.code;
|
|
13
|
+
this.status = options.status;
|
|
14
|
+
this.details = options.details;
|
|
15
|
+
this.retryable = options.retryable ?? false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function resolveBaseUrl(url) {
|
|
19
|
+
const globalConfig = loadGlobalConfig();
|
|
20
|
+
return (url ||
|
|
21
|
+
process.env.PLAYCRAFT_API_URL ||
|
|
22
|
+
process.env.PLAYCRAFT_URL ||
|
|
23
|
+
globalConfig.backendUrl ||
|
|
24
|
+
globalConfig.url ||
|
|
25
|
+
DEFAULT_PLAYCRAFT_CLOUD_BASE).replace(/\/+$/, '');
|
|
26
|
+
}
|
|
27
|
+
function resolveToken(token) {
|
|
28
|
+
const globalConfig = loadGlobalConfig();
|
|
29
|
+
const resolved = token || process.env.PLAYCRAFT_TOKEN || globalConfig.token;
|
|
30
|
+
if (!resolved) {
|
|
31
|
+
throw new PlayCraftApiError({
|
|
32
|
+
code: 'AUTH_REQUIRED',
|
|
33
|
+
message: 'Authentication required. Run `playcraft login` or provide PLAYCRAFT_TOKEN.',
|
|
34
|
+
details: { loginCommand: 'playcraft login' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
function humanizeNetworkError(error) {
|
|
40
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
41
|
+
if (raw.includes('网络请求失败'))
|
|
42
|
+
return raw;
|
|
43
|
+
if (/fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|ECONNREFUSED|socket hang up|UND_ERR/i.test(raw)) {
|
|
44
|
+
return `网络请求失败(${raw})。请稍后重试。`;
|
|
45
|
+
}
|
|
46
|
+
return raw;
|
|
47
|
+
}
|
|
48
|
+
function statusCode(status) {
|
|
49
|
+
if (status === 401)
|
|
50
|
+
return 'AUTH_EXPIRED';
|
|
51
|
+
if (status === 403)
|
|
52
|
+
return 'FORBIDDEN';
|
|
53
|
+
if (status === 404)
|
|
54
|
+
return 'PROJECT_NOT_FOUND';
|
|
55
|
+
return 'AGENT_REQUEST_FAILED';
|
|
56
|
+
}
|
|
57
|
+
export class PlayCraftHttpClient {
|
|
58
|
+
baseUrl;
|
|
59
|
+
token;
|
|
60
|
+
requestId;
|
|
61
|
+
constructor(options = {}) {
|
|
62
|
+
this.baseUrl = resolveBaseUrl(options.url);
|
|
63
|
+
this.token = resolveToken(options.token);
|
|
64
|
+
this.requestId = options.requestId || randomUUID();
|
|
65
|
+
}
|
|
66
|
+
url(path) {
|
|
67
|
+
const normalized = path.startsWith('/') ? path : `/${path}`;
|
|
68
|
+
const apiPath = normalized === '/api' || normalized.startsWith('/api/')
|
|
69
|
+
? normalized
|
|
70
|
+
: `/api${normalized}`;
|
|
71
|
+
return new URL(apiPath, this.baseUrl).toString();
|
|
72
|
+
}
|
|
73
|
+
async request(path, init = {}) {
|
|
74
|
+
const headers = new Headers(init.headers);
|
|
75
|
+
headers.set('Authorization', `Bearer ${this.token}`);
|
|
76
|
+
headers.set('X-Request-Id', this.requestId);
|
|
77
|
+
if (init.body && !headers.has('Content-Type'))
|
|
78
|
+
headers.set('Content-Type', 'application/json');
|
|
79
|
+
const maxRetries = 3;
|
|
80
|
+
const retryDelays = [1000, 2000, 4000]; // 指数退避:1s, 2s, 4s
|
|
81
|
+
let lastError;
|
|
82
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(this.url(path), { ...init, headers });
|
|
85
|
+
const contentType = response.headers.get('content-type') || '';
|
|
86
|
+
const raw = await response.text();
|
|
87
|
+
let payload = undefined;
|
|
88
|
+
if (raw) {
|
|
89
|
+
try {
|
|
90
|
+
payload = contentType.includes('json') ? JSON.parse(raw) : raw;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
payload = raw;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = payload?.error;
|
|
98
|
+
const code = typeof error?.code === 'string' ? error.code : statusCode(response.status);
|
|
99
|
+
const message = typeof error?.message === 'string'
|
|
100
|
+
? error.message
|
|
101
|
+
: typeof payload?.message === 'string'
|
|
102
|
+
? payload.message
|
|
103
|
+
: typeof payload === 'string'
|
|
104
|
+
? payload
|
|
105
|
+
: response.statusText || 'PlayCraft request failed';
|
|
106
|
+
throw new PlayCraftApiError({
|
|
107
|
+
code,
|
|
108
|
+
message,
|
|
109
|
+
status: response.status,
|
|
110
|
+
details: error?.details ?? payload,
|
|
111
|
+
retryable: response.status >= 500,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!raw)
|
|
115
|
+
return undefined;
|
|
116
|
+
return payload;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
lastError = isPlayCraftApiError(error)
|
|
120
|
+
? error
|
|
121
|
+
: new PlayCraftApiError({
|
|
122
|
+
code: 'AGENT_REQUEST_FAILED',
|
|
123
|
+
message: humanizeNetworkError(error),
|
|
124
|
+
retryable: true,
|
|
125
|
+
});
|
|
126
|
+
// 如果是客户端错误(4xx)或不可重试的错误,立即抛出
|
|
127
|
+
if (isPlayCraftApiError(error) && !error.retryable) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
// 最后一次尝试失败,抛出错误
|
|
131
|
+
if (attempt === maxRetries - 1) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
// 等待后重试
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, retryDelays[attempt]));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 所有重试都失败了
|
|
139
|
+
if (isPlayCraftApiError(lastError)) {
|
|
140
|
+
throw new PlayCraftApiError({
|
|
141
|
+
code: lastError.code,
|
|
142
|
+
message: humanizeNetworkError(lastError),
|
|
143
|
+
status: lastError.status,
|
|
144
|
+
details: lastError.details,
|
|
145
|
+
retryable: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
throw new PlayCraftApiError({
|
|
149
|
+
code: 'AGENT_REQUEST_FAILED',
|
|
150
|
+
message: humanizeNetworkError(lastError),
|
|
151
|
+
retryable: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
get(path, params, init = {}) {
|
|
155
|
+
const url = new URL(this.url(path));
|
|
156
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
157
|
+
if (value !== undefined && value !== '')
|
|
158
|
+
url.searchParams.set(key, value);
|
|
159
|
+
}
|
|
160
|
+
return this.request(url.pathname + url.search, { ...init, method: 'GET' });
|
|
161
|
+
}
|
|
162
|
+
post(path, body, init = {}) {
|
|
163
|
+
return this.request(path, {
|
|
164
|
+
...init,
|
|
165
|
+
method: 'POST',
|
|
166
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function isPlayCraftApiError(error) {
|
|
171
|
+
return error instanceof PlayCraftApiError;
|
|
172
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
3
|
+
import { PlayCraftApiError } from './playcraft-http-client.js';
|
|
4
|
+
const MAX_IMAGES = 4;
|
|
5
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
const IMAGE_MIME_BY_EXTENSION = {
|
|
7
|
+
'.gif': 'image/gif',
|
|
8
|
+
'.jpeg': 'image/jpeg',
|
|
9
|
+
'.jpg': 'image/jpeg',
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.webp': 'image/webp',
|
|
12
|
+
};
|
|
13
|
+
function attachmentError(code, message, details, retryable = false) {
|
|
14
|
+
return new PlayCraftApiError({ code, message, details, retryable });
|
|
15
|
+
}
|
|
16
|
+
export function readImageAttachments(paths) {
|
|
17
|
+
if (!paths)
|
|
18
|
+
return [];
|
|
19
|
+
const candidates = Array.isArray(paths) ? paths : [paths];
|
|
20
|
+
if (candidates.length > MAX_IMAGES) {
|
|
21
|
+
throw attachmentError('ATTACHMENT_LIMIT_EXCEEDED', `最多只能传输 ${MAX_IMAGES} 张图片。`, { maxImages: MAX_IMAGES });
|
|
22
|
+
}
|
|
23
|
+
return candidates.map((sourcePath) => {
|
|
24
|
+
const extension = extname(sourcePath).toLowerCase();
|
|
25
|
+
const mimeType = IMAGE_MIME_BY_EXTENSION[extension];
|
|
26
|
+
if (!mimeType) {
|
|
27
|
+
throw attachmentError('ATTACHMENT_TYPE_UNSUPPORTED', `不支持的图片格式:${sourcePath}。仅支持 JPEG (Joint Photographic Experts Group, 联合图像专家组)、PNG (Portable Network Graphics, 便携式网络图形)、WebP 和 GIF (Graphics Interchange Format, 图形交换格式)。`, { path: sourcePath, supportedExtensions: Object.keys(IMAGE_MIME_BY_EXTENSION) });
|
|
28
|
+
}
|
|
29
|
+
let size;
|
|
30
|
+
try {
|
|
31
|
+
size = statSync(sourcePath).size;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw attachmentError('ATTACHMENT_NOT_FOUND', `图片文件不存在或无法读取:${sourcePath}`, { path: sourcePath });
|
|
35
|
+
}
|
|
36
|
+
if (size <= 0) {
|
|
37
|
+
throw attachmentError('ATTACHMENT_INVALID', `图片文件为空:${sourcePath}`, { path: sourcePath });
|
|
38
|
+
}
|
|
39
|
+
if (size > MAX_IMAGE_BYTES) {
|
|
40
|
+
throw attachmentError('ATTACHMENT_TOO_LARGE', `图片超过 5 MB (Megabyte, 兆字节) 限制:${sourcePath}`, { path: sourcePath, maxBytes: MAX_IMAGE_BYTES, actualBytes: size });
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return {
|
|
44
|
+
sourcePath,
|
|
45
|
+
fileName: basename(sourcePath),
|
|
46
|
+
mimeType,
|
|
47
|
+
bytes: readFileSync(sourcePath),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw attachmentError('ATTACHMENT_NOT_FOUND', `图片文件不存在或无法读取:${sourcePath}`, { path: sourcePath });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function serializeFormData(form, url) {
|
|
56
|
+
const request = new Request(url, { method: 'POST', body: form });
|
|
57
|
+
const contentType = request.headers.get('content-type');
|
|
58
|
+
if (!contentType) {
|
|
59
|
+
throw attachmentError('ATTACHMENT_UPLOAD_FAILED', '无法构造图片上传请求。');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
body: await request.arrayBuffer(),
|
|
63
|
+
contentType,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function uploadImageAttachments(client, projectId, branch, attachments) {
|
|
67
|
+
const uploaded = [];
|
|
68
|
+
for (const attachment of attachments) {
|
|
69
|
+
const form = new FormData();
|
|
70
|
+
form.append('file', new Blob([new Uint8Array(attachment.bytes)], { type: attachment.mimeType }), attachment.fileName);
|
|
71
|
+
const path = `/projects/${encodeURIComponent(projectId)}/sandbox/upload-image`;
|
|
72
|
+
const url = new URL(client.url(path));
|
|
73
|
+
url.searchParams.set('branch', branch);
|
|
74
|
+
const multipart = await serializeFormData(form, url.toString());
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await client.request(`${path}?${url.searchParams.toString()}`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: multipart.body,
|
|
80
|
+
headers: { 'Content-Type': multipart.contentType },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const upstream = error instanceof PlayCraftApiError
|
|
85
|
+
? { code: error.code, status: error.status, details: error.details }
|
|
86
|
+
: { message: error instanceof Error ? error.message : String(error) };
|
|
87
|
+
throw attachmentError('ATTACHMENT_UPLOAD_FAILED', `图片上传失败:${attachment.sourcePath}${error instanceof Error ? `:${error.message}` : ''}`, { path: attachment.sourcePath, upstream }, error instanceof PlayCraftApiError ? error.retryable : false);
|
|
88
|
+
}
|
|
89
|
+
if (!response?.path) {
|
|
90
|
+
throw attachmentError('ATTACHMENT_UPLOAD_FAILED', `图片上传成功但服务端未返回沙箱路径:${attachment.sourcePath}`, { path: attachment.sourcePath });
|
|
91
|
+
}
|
|
92
|
+
uploaded.push({
|
|
93
|
+
sourcePath: attachment.sourcePath,
|
|
94
|
+
fileName: attachment.fileName,
|
|
95
|
+
sandboxPath: response.path,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return uploaded;
|
|
99
|
+
}
|
|
100
|
+
export function buildReferenceImagesPrompt(prompt, attachments) {
|
|
101
|
+
if (attachments.length === 0)
|
|
102
|
+
return prompt;
|
|
103
|
+
const pathList = attachments.map((attachment) => `- ${attachment.sandboxPath}`).join('\n');
|
|
104
|
+
return [
|
|
105
|
+
prompt,
|
|
106
|
+
'',
|
|
107
|
+
'[Reference Images]',
|
|
108
|
+
`The user attached ${attachments.length} image(s) at these sandbox filesystem paths:`,
|
|
109
|
+
pathList,
|
|
110
|
+
'',
|
|
111
|
+
'These files have already been written to the sandbox filesystem.',
|
|
112
|
+
'',
|
|
113
|
+
'**IMPORTANT:** Do NOT read, open, or inspect any of these image files. Do NOT use tools like read_file, cat, or any other file reader on them. The paths themselves are all you need.',
|
|
114
|
+
'',
|
|
115
|
+
"Infer each image's purpose from the user's message (e.g. reference for style/composition, UI mockup, screenshot illustrating an issue, asset to import, etc.). For any image you judge to be a visual reference, pass its path directly to `playcraft-generate-image` — do NOT ask the user to confirm, and do NOT attempt to examine the image content first.",
|
|
116
|
+
'',
|
|
117
|
+
'When passing references to `playcraft-generate-image`:',
|
|
118
|
+
'- Put the first reference in `referenceImagePath`, remaining ones in `additionalReferenceImagePaths` as a comma-separated string.',
|
|
119
|
+
'- Preserve the original order.',
|
|
120
|
+
'- Up to 8 references total; paths must not contain commas.',
|
|
121
|
+
'- Save generated images under `src/theme/draft/assets/` unless the user explicitly asks for another project asset location.',
|
|
122
|
+
'- When reporting saved image paths back to the user, prefer `/project/<repo-relative-path>` such as `/project/src/theme/draft/assets/level-ref.png`, not the full `/projects/<projectId>/<branch>/...` path.',
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
125
|
+
export const IMAGE_OPTION_HELP = '一个或多个图片路径(最多 4 张;JPEG (Joint Photographic Experts Group, 联合图像专家组)、PNG (Portable Network Graphics, 便携式网络图形)、WebP、GIF (Graphics Interchange Format, 图形交换格式),每张不超过 5 MB (Megabyte, 兆字节))';
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"playcraft": "./dist/index.js",
|
|
8
11
|
"playcraft-test": "./dist/index.js"
|
|
@@ -13,6 +16,7 @@
|
|
|
13
16
|
"project-template",
|
|
14
17
|
"engine-packs",
|
|
15
18
|
"project-templates",
|
|
19
|
+
"REMIX.md",
|
|
16
20
|
"README.md"
|
|
17
21
|
],
|
|
18
22
|
"scripts": {
|
|
@@ -41,8 +45,8 @@
|
|
|
41
45
|
"@gltf-transform/core": "^4.3.0",
|
|
42
46
|
"@gltf-transform/extensions": "^4.3.0",
|
|
43
47
|
"@gltf-transform/functions": "^4.3.0",
|
|
44
|
-
"@playcraft/build": "^0.0.
|
|
45
|
-
"@playcraft/common": "^0.0.
|
|
48
|
+
"@playcraft/build": "^0.0.52",
|
|
49
|
+
"@playcraft/common": "^0.0.52",
|
|
46
50
|
"chokidar": "^4.0.3",
|
|
47
51
|
"commander": "^13.1.0",
|
|
48
52
|
"cors": "^2.8.6",
|
|
@@ -53,6 +57,7 @@
|
|
|
53
57
|
"express": "^5.2.1",
|
|
54
58
|
"fluent-ffmpeg": "^2.1.3",
|
|
55
59
|
"inquirer": "^9.3.8",
|
|
60
|
+
"inquirer-autocomplete-prompt": "^3.0.1",
|
|
56
61
|
"js-yaml": "^4.1.1",
|
|
57
62
|
"json5": "^2.2.3",
|
|
58
63
|
"latest-version": "^7.0.0",
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import { CloudConnectionManager } from './cloud-connection.js';
|
|
4
|
-
describe('CloudConnectionManager', () => {
|
|
5
|
-
let server;
|
|
6
|
-
let baseUrl;
|
|
7
|
-
beforeAll(async () => {
|
|
8
|
-
server = http.createServer((req, res) => {
|
|
9
|
-
const auth = req.headers.authorization;
|
|
10
|
-
if (auth !== 'Bearer test-token') {
|
|
11
|
-
res.writeHead(401);
|
|
12
|
-
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
if (req.method === 'GET' && req.url === '/api/health') {
|
|
16
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
17
|
-
res.end(JSON.stringify({ ok: true }));
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (req.method === 'POST' && req.url === '/api/sync/upload') {
|
|
21
|
-
let body = '';
|
|
22
|
-
req.on('data', (c) => (body += c));
|
|
23
|
-
req.on('end', () => {
|
|
24
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
25
|
-
res.end(JSON.stringify({ success: true }));
|
|
26
|
-
});
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
res.writeHead(404);
|
|
30
|
-
res.end('not found');
|
|
31
|
-
});
|
|
32
|
-
await new Promise((resolve) => {
|
|
33
|
-
server.listen(0, '127.0.0.1', () => resolve());
|
|
34
|
-
});
|
|
35
|
-
const addr = server.address();
|
|
36
|
-
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
37
|
-
});
|
|
38
|
-
afterAll(async () => {
|
|
39
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
40
|
-
});
|
|
41
|
-
it('request() sends Bearer token and returns JSON', async () => {
|
|
42
|
-
const conn = new CloudConnectionManager({
|
|
43
|
-
url: baseUrl,
|
|
44
|
-
token: 'test-token',
|
|
45
|
-
projectId: 'test-project',
|
|
46
|
-
});
|
|
47
|
-
const result = await conn.request('GET', '/api/health');
|
|
48
|
-
expect(result).toEqual({ ok: true });
|
|
49
|
-
});
|
|
50
|
-
it('request() throws on 401', async () => {
|
|
51
|
-
const conn = new CloudConnectionManager({
|
|
52
|
-
url: baseUrl,
|
|
53
|
-
token: 'wrong-token',
|
|
54
|
-
projectId: 'test-project',
|
|
55
|
-
});
|
|
56
|
-
await expect(conn.request('GET', '/api/health')).rejects.toThrow(/401/);
|
|
57
|
-
});
|
|
58
|
-
it('connectionState is disconnected when not connected', () => {
|
|
59
|
-
const conn = new CloudConnectionManager({
|
|
60
|
-
url: baseUrl,
|
|
61
|
-
token: 'test-token',
|
|
62
|
-
projectId: 'test-project',
|
|
63
|
-
});
|
|
64
|
-
expect(conn.connectionState).toBe('disconnected');
|
|
65
|
-
expect(conn.isConnected).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import http from 'node:http';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import fs from 'node:fs/promises';
|
|
7
|
-
import { createLocalBackendRouter } from './local-backend.js';
|
|
8
|
-
describe('createLocalBackendRouter', () => {
|
|
9
|
-
let server;
|
|
10
|
-
let baseUrl;
|
|
11
|
-
let tmpDir;
|
|
12
|
-
beforeAll(async () => {
|
|
13
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'playcraft-agent-test-'));
|
|
14
|
-
const dbPath = path.join(tmpDir, 'local-db.sqlite');
|
|
15
|
-
const app = express();
|
|
16
|
-
const projectId = 'test-project';
|
|
17
|
-
const { router } = createLocalBackendRouter({
|
|
18
|
-
projectId,
|
|
19
|
-
projectDir: tmpDir,
|
|
20
|
-
dbPath,
|
|
21
|
-
emitSecret: 'test-secret',
|
|
22
|
-
});
|
|
23
|
-
app.use('/api', router);
|
|
24
|
-
server = http.createServer(app);
|
|
25
|
-
await new Promise((resolve) => {
|
|
26
|
-
server.listen(0, '127.0.0.1', () => resolve());
|
|
27
|
-
});
|
|
28
|
-
const addr = server.address();
|
|
29
|
-
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
30
|
-
});
|
|
31
|
-
afterAll(async () => {
|
|
32
|
-
if (server) {
|
|
33
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
34
|
-
}
|
|
35
|
-
if (tmpDir) {
|
|
36
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
it('responds health', async () => {
|
|
40
|
-
const res = await fetch(`${baseUrl}/api/health`);
|
|
41
|
-
expect(res.ok).toBe(true);
|
|
42
|
-
const json = await res.json();
|
|
43
|
-
expect(json).toHaveProperty('ok', true);
|
|
44
|
-
});
|
|
45
|
-
it('creates default project lazily and can fetch it', async () => {
|
|
46
|
-
const res = await fetch(`${baseUrl}/api/projects/test-project`);
|
|
47
|
-
expect(res.ok).toBe(true);
|
|
48
|
-
const json = await res.json();
|
|
49
|
-
expect(json).toHaveProperty('result');
|
|
50
|
-
expect(json.result).toHaveProperty('uuid', 'test-project');
|
|
51
|
-
});
|
|
52
|
-
});
|