@easytwin/devkit 0.1.0 → 0.1.2
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 -6
- package/dist/bin.js +991 -110
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +427 -22
- package/dist/index.js +1453 -87
- package/dist/index.js.map +1 -1
- package/dist/runtime-types/index.d.ts +8248 -0
- package/package.json +20 -5
- package/scene.example.json +2663 -0
- package/skills/easytwin-bootstrap/SKILL.md +13 -3
- package/skills/easytwin-core/SKILL.md +2 -2
- package/skills/easytwin-core/references/engine.md +5 -5
- package/skills/easytwin-develop/SKILL.md +11 -6
- package/skills/easytwin-render/SKILL.md +59 -19
- package/skills/easytwin-render/references/intro.md +11 -8
- package/skills/easytwin-render/references/scene-and-assets.md +4 -3
- package/skills/easytwin-scene/SKILL.md +35 -7
- package/skills/easytwin-upload/SKILL.md +26 -9
package/dist/bin.js
CHANGED
|
@@ -7,13 +7,23 @@ import { readFile } from "fs/promises";
|
|
|
7
7
|
import { createInterface } from "readline/promises";
|
|
8
8
|
import { stdin, stdout } from "process";
|
|
9
9
|
import { pathToFileURL } from "url";
|
|
10
|
-
import
|
|
10
|
+
import path8 from "path";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { promises as fs } from "fs";
|
|
14
14
|
import path from "path";
|
|
15
15
|
var CONFIG_FILE_NAME = "easytwin.config.json";
|
|
16
|
-
var DEFAULT_BASE_URL = "
|
|
16
|
+
var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
|
|
17
|
+
var TEST_BASE_URL = "http://172.16.125.3:10100/";
|
|
18
|
+
var TEST_OP_ACCOUNT_ID = "25";
|
|
19
|
+
var TEST_OP_USER_ID = "25";
|
|
20
|
+
var TEST_SPACE_ID = "54";
|
|
21
|
+
var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
22
|
+
var MOCK_APP_ID = "test";
|
|
23
|
+
var MOCK_APP_SECRET = "test";
|
|
24
|
+
function isMockCredentials(appId, appSecret) {
|
|
25
|
+
return appId === MOCK_APP_ID && appSecret === MOCK_APP_SECRET;
|
|
26
|
+
}
|
|
17
27
|
var ConfigError = class extends Error {
|
|
18
28
|
constructor(message) {
|
|
19
29
|
super(message);
|
|
@@ -46,10 +56,65 @@ function validateConfigShape(value) {
|
|
|
46
56
|
if (v.baseUrl !== void 0 && (typeof v.baseUrl !== "string" || v.baseUrl.length === 0)) {
|
|
47
57
|
throw new ConfigError("baseUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
48
58
|
}
|
|
59
|
+
if (v.ossUrl !== void 0 && (typeof v.ossUrl !== "string" || v.ossUrl.length === 0)) {
|
|
60
|
+
throw new ConfigError("ossUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
61
|
+
}
|
|
62
|
+
if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
|
|
63
|
+
throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
|
|
64
|
+
}
|
|
65
|
+
const opAccountId = optionalGatewayId(v.opAccountId);
|
|
66
|
+
const opUserId = optionalGatewayId(v.opUserId);
|
|
67
|
+
const spaceId = optionalGatewayId(v.spaceId);
|
|
49
68
|
const config = { appId: v.appId, appSecret: v.appSecret };
|
|
69
|
+
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
50
70
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
71
|
+
if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
|
|
72
|
+
if (opAccountId) config.opAccountId = opAccountId;
|
|
73
|
+
if (opUserId) config.opUserId = opUserId;
|
|
74
|
+
if (spaceId) config.spaceId = spaceId;
|
|
75
|
+
const scenes = parseConfigScenes(v.scenes);
|
|
76
|
+
if (scenes) config.scenes = scenes;
|
|
51
77
|
return config;
|
|
52
78
|
}
|
|
79
|
+
function optionalGatewayId(value) {
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
const t = value.trim();
|
|
82
|
+
return t.length > 0 ? t : void 0;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
85
|
+
if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
function parseConfigScenes(value) {
|
|
89
|
+
if (value === void 0) return void 0;
|
|
90
|
+
if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
91
|
+
return value.map((item, i) => {
|
|
92
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
93
|
+
throw new ConfigError(`scenes[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
94
|
+
}
|
|
95
|
+
const it = item;
|
|
96
|
+
if (typeof it.id !== "string" || it.id.length === 0) {
|
|
97
|
+
throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 id`);
|
|
98
|
+
}
|
|
99
|
+
if (typeof it.name !== "string" || it.name.length === 0) {
|
|
100
|
+
throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 name`);
|
|
101
|
+
}
|
|
102
|
+
const scene = { id: it.id, name: it.name };
|
|
103
|
+
if (it.linkedSceneId !== void 0) {
|
|
104
|
+
if (typeof it.linkedSceneId !== "string") throw new ConfigError(`scenes[${i}].linkedSceneId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
105
|
+
if (it.linkedSceneId.length > 0) scene.linkedSceneId = it.linkedSceneId;
|
|
106
|
+
}
|
|
107
|
+
if (it.snapshotUrl !== void 0) {
|
|
108
|
+
if (typeof it.snapshotUrl !== "string") throw new ConfigError(`scenes[${i}].snapshotUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
109
|
+
if (it.snapshotUrl.length > 0) scene.snapshotUrl = it.snapshotUrl;
|
|
110
|
+
}
|
|
111
|
+
if (it.defaultLoading !== void 0) {
|
|
112
|
+
if (typeof it.defaultLoading !== "boolean") throw new ConfigError(`scenes[${i}].defaultLoading \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
113
|
+
scene.defaultLoading = it.defaultLoading;
|
|
114
|
+
}
|
|
115
|
+
return scene;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
53
118
|
async function readConfigFile(cwd) {
|
|
54
119
|
const file = configFilePath(cwd);
|
|
55
120
|
let raw;
|
|
@@ -63,19 +128,50 @@ async function readConfigFile(cwd) {
|
|
|
63
128
|
function resolveConfig(file, env = process.env) {
|
|
64
129
|
const appId = env.EASYTWIN_APP_ID ?? file.appId;
|
|
65
130
|
const appSecret = env.EASYTWIN_APP_SECRET ?? file.appSecret;
|
|
66
|
-
const
|
|
131
|
+
const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file.env ?? "prod";
|
|
132
|
+
const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? (easyEnv === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL);
|
|
133
|
+
const ossUrl = env.EASYTWIN_OSS_URL ?? file.ossUrl ?? DEFAULT_OSS_URL;
|
|
67
134
|
if (!appId) throw new ConfigError("\u7F3A\u5C11 appId:\u914D\u7F6E\u6587\u4EF6\u4E2D\u672A\u63D0\u4F9B,\u4E14\u672A\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EASYTWIN_APP_ID");
|
|
68
135
|
if (!appSecret) throw new ConfigError("\u7F3A\u5C11 appSecret:\u914D\u7F6E\u6587\u4EF6\u4E2D\u672A\u63D0\u4F9B,\u4E14\u672A\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EASYTWIN_APP_SECRET");
|
|
69
136
|
const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
|
|
70
|
-
|
|
137
|
+
const useTestGateway = easyEnv === "test";
|
|
138
|
+
const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
|
|
139
|
+
const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
|
|
140
|
+
const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
|
|
141
|
+
const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
142
|
+
if (opAccountId) resolved.opAccountId = opAccountId;
|
|
143
|
+
if (opUserId) resolved.opUserId = opUserId;
|
|
144
|
+
if (spaceId) resolved.spaceId = spaceId;
|
|
145
|
+
return resolved;
|
|
71
146
|
}
|
|
72
147
|
async function loadConfig(cwd, env = process.env) {
|
|
73
|
-
|
|
148
|
+
const file = await readConfigFile(cwd);
|
|
149
|
+
const resolved = resolveConfig(file, env);
|
|
150
|
+
if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
|
|
151
|
+
if (!file.opAccountId || !file.opUserId || !file.spaceId) {
|
|
152
|
+
await writeConfigFile(cwd, {
|
|
153
|
+
...file,
|
|
154
|
+
opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
|
|
155
|
+
opUserId: file.opUserId ?? TEST_OP_USER_ID,
|
|
156
|
+
spaceId: file.spaceId ?? TEST_SPACE_ID
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return resolved;
|
|
74
161
|
}
|
|
75
162
|
async function writeConfigFile(cwd, config) {
|
|
76
163
|
const file = configFilePath(cwd);
|
|
77
164
|
const body = { appId: config.appId, appSecret: config.appSecret };
|
|
165
|
+
if (config.env) body.env = config.env;
|
|
78
166
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
167
|
+
if (config.ossUrl) body.ossUrl = config.ossUrl;
|
|
168
|
+
const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
|
|
169
|
+
const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
|
|
170
|
+
const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
|
|
171
|
+
if (opAccountId) body.opAccountId = opAccountId;
|
|
172
|
+
if (opUserId) body.opUserId = opUserId;
|
|
173
|
+
if (spaceId) body.spaceId = spaceId;
|
|
174
|
+
if (config.scenes !== void 0) body.scenes = config.scenes;
|
|
79
175
|
await fs.mkdir(cwd, { recursive: true });
|
|
80
176
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
81
177
|
return file;
|
|
@@ -103,18 +199,31 @@ async function initConfig(input, cwd) {
|
|
|
103
199
|
const gitignore = await appendGitignore(cwd);
|
|
104
200
|
return { configFile, gitignore };
|
|
105
201
|
}
|
|
202
|
+
async function writeConfigScenes(cwd, scenes) {
|
|
203
|
+
const file = await readConfigFile(cwd);
|
|
204
|
+
await writeConfigFile(cwd, { ...file, scenes });
|
|
205
|
+
}
|
|
106
206
|
|
|
107
207
|
// src/client.ts
|
|
108
208
|
import http from "http";
|
|
109
209
|
import https from "https";
|
|
110
210
|
import { URL as URL2 } from "url";
|
|
111
|
-
var AUTH_HEADER = "
|
|
112
|
-
var
|
|
113
|
-
var
|
|
211
|
+
var AUTH_HEADER = "x-app-secret";
|
|
212
|
+
var OP_ACCOUNT_ID_HEADER = "op-account-id";
|
|
213
|
+
var OP_USER_ID_HEADER = "op-user-id";
|
|
214
|
+
var SPACE_ID_HEADER = "space-id";
|
|
215
|
+
function enc(id) {
|
|
216
|
+
return encodeURIComponent(id);
|
|
217
|
+
}
|
|
114
218
|
var ENDPOINTS = {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
219
|
+
/** GET 已关联场景列表。 */
|
|
220
|
+
linkedScenes: (applicationId) => `/api/twin/v1/sdk-application-scenes/${enc(applicationId)}/scenes`,
|
|
221
|
+
/** GET 工作区全部代码文件。 */
|
|
222
|
+
workspaceCode: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}`,
|
|
223
|
+
/** GET 工作区文件约束。 */
|
|
224
|
+
workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`,
|
|
225
|
+
/** POST 新建 / PATCH 批量更新 / DELETE 批量删除。 */
|
|
226
|
+
workspaceCodeFiles: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/files`
|
|
118
227
|
};
|
|
119
228
|
var EasyTwinApiError = class extends Error {
|
|
120
229
|
status;
|
|
@@ -142,25 +251,50 @@ function parseResponseBody(buffer) {
|
|
|
142
251
|
return text;
|
|
143
252
|
}
|
|
144
253
|
}
|
|
254
|
+
function isEnvelope(value) {
|
|
255
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "success" in value;
|
|
256
|
+
}
|
|
257
|
+
function unwrapBody(parsed, status) {
|
|
258
|
+
if (!isEnvelope(parsed)) return parsed;
|
|
259
|
+
if (parsed.success === false) {
|
|
260
|
+
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? "\u8BF7\u6C42\u5931\u8D25", parsed);
|
|
261
|
+
}
|
|
262
|
+
if (parsed.success === true && "data" in parsed) return parsed.data;
|
|
263
|
+
return parsed;
|
|
264
|
+
}
|
|
145
265
|
var EasyTwinClient = class {
|
|
146
266
|
baseUrl;
|
|
267
|
+
/** twin runtime / 场景快照资产根(baseOSSUrl)。 */
|
|
268
|
+
ossUrl;
|
|
269
|
+
/** 接入凭证 App ID;同时作为场景/代码 API 的 applicationId 路径参数。 */
|
|
147
270
|
appId;
|
|
271
|
+
/** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
272
|
+
mock;
|
|
148
273
|
appSecret;
|
|
274
|
+
opAccountId;
|
|
275
|
+
opUserId;
|
|
276
|
+
spaceId;
|
|
149
277
|
constructor(config) {
|
|
150
278
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
279
|
+
this.ossUrl = config.ossUrl.replace(/\/+$/, "");
|
|
280
|
+
this.mock = config.mock;
|
|
151
281
|
this.appId = config.appId;
|
|
152
282
|
this.appSecret = config.appSecret;
|
|
283
|
+
this.opAccountId = config.opAccountId;
|
|
284
|
+
this.opUserId = config.opUserId;
|
|
285
|
+
this.spaceId = config.spaceId;
|
|
153
286
|
}
|
|
154
287
|
/** 全仓唯一认证头注入点。 */
|
|
155
288
|
authHeaders() {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
289
|
+
const headers = { [AUTH_HEADER]: this.appSecret };
|
|
290
|
+
if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
|
|
291
|
+
if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
|
|
292
|
+
if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
|
|
293
|
+
return headers;
|
|
160
294
|
}
|
|
161
295
|
/** JSON 请求(原生 fetch)。 */
|
|
162
|
-
async request(
|
|
163
|
-
const url = `${this.baseUrl}${
|
|
296
|
+
async request(path9, options = {}) {
|
|
297
|
+
const url = `${this.baseUrl}${path9}`;
|
|
164
298
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
165
299
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
166
300
|
headers["Content-Type"] = "application/json";
|
|
@@ -182,8 +316,8 @@ var EasyTwinClient = class {
|
|
|
182
316
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
183
317
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
184
318
|
*/
|
|
185
|
-
async upload(
|
|
186
|
-
const url = new URL2(`${this.baseUrl}${
|
|
319
|
+
async upload(path9, options) {
|
|
320
|
+
const url = new URL2(`${this.baseUrl}${path9}`);
|
|
187
321
|
const mod = url.protocol === "https:" ? https : http;
|
|
188
322
|
const headers = {
|
|
189
323
|
...this.authHeaders(),
|
|
@@ -216,8 +350,8 @@ var EasyTwinClient = class {
|
|
|
216
350
|
return this.handleStatus(raw.status, raw.body);
|
|
217
351
|
}
|
|
218
352
|
handleStatus(status, body) {
|
|
219
|
-
if (status >= 200 && status < 300) return parseResponseBody(body);
|
|
220
353
|
const parsed = parseResponseBody(body);
|
|
354
|
+
if (status >= 200 && status < 300) return unwrapBody(parsed, status);
|
|
221
355
|
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
|
|
222
356
|
}
|
|
223
357
|
};
|
|
@@ -225,25 +359,146 @@ var EasyTwinClient = class {
|
|
|
225
359
|
// src/scene.ts
|
|
226
360
|
import { promises as fs2 } from "fs";
|
|
227
361
|
import path2 from "path";
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
362
|
+
import { fileURLToPath } from "url";
|
|
363
|
+
function asRecord(value) {
|
|
364
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
365
|
+
}
|
|
366
|
+
function asString(value) {
|
|
367
|
+
return typeof value === "string" ? value : "";
|
|
368
|
+
}
|
|
369
|
+
function asBoolean(value) {
|
|
370
|
+
return value === true;
|
|
371
|
+
}
|
|
372
|
+
function extractSceneArray(data) {
|
|
373
|
+
if (Array.isArray(data)) return data;
|
|
374
|
+
const root = asRecord(data);
|
|
375
|
+
if (Array.isArray(root.data)) return root.data;
|
|
376
|
+
if (Array.isArray(root.list)) return root.list;
|
|
377
|
+
if (Array.isArray(root.scenes)) return root.scenes;
|
|
378
|
+
const nested = asRecord(root.data);
|
|
379
|
+
if (Array.isArray(nested.scenes)) return nested.scenes;
|
|
380
|
+
if (Array.isArray(nested.list)) return nested.list;
|
|
381
|
+
throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
382
|
+
}
|
|
383
|
+
function normalizeLinkedScenes(data) {
|
|
384
|
+
const list = extractSceneArray(data);
|
|
231
385
|
return list.map((item) => {
|
|
232
|
-
const it = item
|
|
233
|
-
|
|
386
|
+
const it = asRecord(item);
|
|
387
|
+
const sceneKey = asString(it.sceneKey);
|
|
388
|
+
const sourceProjectName = asString(it.sourceProjectName);
|
|
389
|
+
const sourceSceneName = asString(it.sourceSceneName);
|
|
390
|
+
return {
|
|
391
|
+
id: asString(it.id),
|
|
392
|
+
sceneKey,
|
|
393
|
+
name: sourceProjectName || sourceSceneName || sceneKey,
|
|
394
|
+
defaultLoading: asBoolean(it.defaultLoading),
|
|
395
|
+
sourceSceneId: asString(it.sourceSceneId),
|
|
396
|
+
sourceProjectId: asString(it.sourceProjectId),
|
|
397
|
+
sourceSceneName,
|
|
398
|
+
sourceProjectName,
|
|
399
|
+
sourceLost: asBoolean(it.sourceLost),
|
|
400
|
+
snapshotUrl: asString(it.snapshotUrl),
|
|
401
|
+
snapshotUpdatedAt: asString(it.snapshotUpdatedAt)
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function normalizeSceneList(data) {
|
|
406
|
+
return normalizeLinkedScenes(data).map((s) => ({
|
|
407
|
+
id: s.sceneKey,
|
|
408
|
+
name: s.name,
|
|
409
|
+
linkedSceneId: s.id,
|
|
410
|
+
snapshotUrl: s.snapshotUrl,
|
|
411
|
+
defaultLoading: s.defaultLoading
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
function toConfigScenes(scenes) {
|
|
415
|
+
return scenes.map((s) => {
|
|
416
|
+
const item = { id: s.id, name: s.name };
|
|
417
|
+
if (s.linkedSceneId) item.linkedSceneId = s.linkedSceneId;
|
|
418
|
+
if (s.snapshotUrl) item.snapshotUrl = s.snapshotUrl;
|
|
419
|
+
if (s.defaultLoading !== void 0) item.defaultLoading = s.defaultLoading;
|
|
420
|
+
return item;
|
|
234
421
|
});
|
|
235
422
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
423
|
+
function resolveSnapshotUrl(ossUrl, snapshotUrl) {
|
|
424
|
+
if (/^https?:\/\//i.test(snapshotUrl)) return snapshotUrl;
|
|
425
|
+
const base = ossUrl.replace(/\/+$/, "");
|
|
426
|
+
const rel = snapshotUrl.replace(/^\/+/, "");
|
|
427
|
+
return `${base}/${rel}`;
|
|
428
|
+
}
|
|
429
|
+
async function fetchSnapshotJson(ossUrl, snapshotUrl) {
|
|
430
|
+
if (snapshotUrl.length === 0) throw new Error("\u573A\u666F\u5FEB\u7167\u5730\u5740\u4E3A\u7A7A");
|
|
431
|
+
const url = resolveSnapshotUrl(ossUrl, snapshotUrl);
|
|
432
|
+
let res;
|
|
433
|
+
try {
|
|
434
|
+
res = await fetch(url);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
throw new EasyTwinApiError(0, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:${url}:${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
}
|
|
438
|
+
if (!res.ok) {
|
|
439
|
+
throw new EasyTwinApiError(res.status, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:HTTP ${res.status} ${url}`);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
return await res.json();
|
|
443
|
+
} catch {
|
|
444
|
+
throw new Error(`\u573A\u666F\u5FEB\u7167\u4E0D\u662F\u5408\u6CD5 JSON:${url}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
var EXAMPLE_SCENE_FILE = "scene.example.json";
|
|
448
|
+
var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
|
|
449
|
+
function resolveExampleScenePath() {
|
|
450
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
451
|
+
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D ${EXAMPLE_SCENE_FILE}:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 exampleFile`);
|
|
452
|
+
}
|
|
453
|
+
return path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..", EXAMPLE_SCENE_FILE);
|
|
454
|
+
}
|
|
455
|
+
async function loadExampleScene(exampleFile) {
|
|
456
|
+
const file = exampleFile ?? resolveExampleScenePath();
|
|
457
|
+
let raw;
|
|
458
|
+
try {
|
|
459
|
+
raw = await fs2.readFile(file, "utf8");
|
|
460
|
+
} catch {
|
|
461
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730\u793A\u4F8B\u573A\u666F ${file}(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F\u9700\u8981 devkit \u5305\u5185\u7684 ${EXAMPLE_SCENE_FILE})`);
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
return JSON.parse(raw);
|
|
465
|
+
} catch {
|
|
466
|
+
throw new Error(`\u672C\u5730\u793A\u4F8B\u573A\u666F\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function deriveExampleSceneId(payload) {
|
|
470
|
+
const root = payload ?? {};
|
|
471
|
+
const id = root?.objs?.map((o) => o.sceneId).find((s) => typeof s === "string" && s.length > 0);
|
|
472
|
+
return id ?? "local";
|
|
239
473
|
}
|
|
240
|
-
async function
|
|
241
|
-
|
|
242
|
-
return normalizeSceneList(data);
|
|
474
|
+
async function exampleSceneSummary(exampleFile) {
|
|
475
|
+
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
243
476
|
}
|
|
244
|
-
async function
|
|
245
|
-
const
|
|
246
|
-
|
|
477
|
+
async function listScenes(client, options = {}) {
|
|
478
|
+
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" }));
|
|
479
|
+
if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
|
|
480
|
+
return scenes;
|
|
481
|
+
}
|
|
482
|
+
async function pullScene(client, id, options = {}) {
|
|
483
|
+
if (client.mock) {
|
|
484
|
+
const payload2 = await loadExampleScene(options.exampleFile);
|
|
485
|
+
const sceneId = deriveExampleSceneId(payload2);
|
|
486
|
+
if (sceneId !== id) {
|
|
487
|
+
throw new Error(`\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F:\u793A\u4F8B\u573A\u666F id \u4E3A ${sceneId},\u6536\u5230 ${id}(\u672C\u5730\u4EC5\u63D0\u4F9B ${EXAMPLE_SCENE_FILE} \u8FD9\u4E00\u4E2A\u573A\u666F)`);
|
|
488
|
+
}
|
|
489
|
+
return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
|
|
490
|
+
}
|
|
491
|
+
const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
|
|
492
|
+
const scenes = normalizeLinkedScenes(data);
|
|
493
|
+
const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
|
|
494
|
+
if (!hit) {
|
|
495
|
+
const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
|
|
496
|
+
throw new Error(
|
|
497
|
+
available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
|
|
501
|
+
return { id: hit.sceneKey, name: hit.name, payload };
|
|
247
502
|
}
|
|
248
503
|
async function saveSceneJson(scene, out) {
|
|
249
504
|
await fs2.mkdir(path2.dirname(out), { recursive: true });
|
|
@@ -276,59 +531,389 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
276
531
|
files.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
277
532
|
return files;
|
|
278
533
|
}
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
534
|
+
function asRecord2(value) {
|
|
535
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
536
|
+
}
|
|
537
|
+
function asString2(value) {
|
|
538
|
+
return typeof value === "string" ? value : "";
|
|
539
|
+
}
|
|
540
|
+
function asNumber(value) {
|
|
541
|
+
return typeof value === "number" && Number.isFinite(value) ? value : NaN;
|
|
542
|
+
}
|
|
543
|
+
function normalizePath(relPath) {
|
|
544
|
+
return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
545
|
+
}
|
|
546
|
+
function normalizeWorkspaceConfig(data) {
|
|
547
|
+
const it = asRecord2(data);
|
|
548
|
+
if (!Array.isArray(it.allowedFileExtensions)) throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
549
|
+
const maxCodeFiles = asNumber(it.maxCodeFiles);
|
|
550
|
+
const maxCodeDirectoryDepth = asNumber(it.maxCodeDirectoryDepth);
|
|
551
|
+
if (!Number.isFinite(maxCodeFiles) || !Number.isFinite(maxCodeDirectoryDepth)) {
|
|
552
|
+
throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
allowedFileExtensions: it.allowedFileExtensions.filter((e) => typeof e === "string"),
|
|
556
|
+
maxCodeFiles,
|
|
557
|
+
maxCodeDirectoryDepth
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function normalizeWorkspaceFiles(data) {
|
|
561
|
+
if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
562
|
+
return data.map((item) => {
|
|
563
|
+
const it = asRecord2(item);
|
|
564
|
+
return { id: asString2(it.id), filePath: asString2(it.filePath), content: asString2(it.content) };
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function extensionOf(relPath) {
|
|
568
|
+
const base = relPath.split("/").pop() ?? "";
|
|
569
|
+
const i = base.lastIndexOf(".");
|
|
570
|
+
if (i <= 0) return "";
|
|
571
|
+
return base.slice(i).toLowerCase();
|
|
572
|
+
}
|
|
573
|
+
function allowedExtensionSet(list) {
|
|
574
|
+
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
575
|
+
}
|
|
576
|
+
function directoryDepth(relPath) {
|
|
577
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
578
|
+
return Math.max(0, parts.length - 1);
|
|
579
|
+
}
|
|
580
|
+
function assertUploadable(files, config) {
|
|
581
|
+
const errors = [];
|
|
582
|
+
if (files.length > config.maxCodeFiles) {
|
|
583
|
+
errors.push(`\u6587\u4EF6\u6570 ${files.length} \u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeFiles}`);
|
|
584
|
+
}
|
|
585
|
+
const tooDeep = files.filter((f) => directoryDepth(f.relPath) > config.maxCodeDirectoryDepth);
|
|
586
|
+
if (tooDeep.length > 0) {
|
|
587
|
+
errors.push(
|
|
588
|
+
`\u76EE\u5F55\u6DF1\u5EA6\u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeDirectoryDepth}:${tooDeep.map((f) => f.relPath).join(", ")}`
|
|
288
589
|
);
|
|
289
590
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
\
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
591
|
+
if (config.allowedFileExtensions.length > 0) {
|
|
592
|
+
const allowed = allowedExtensionSet(config.allowedFileExtensions);
|
|
593
|
+
const bad = files.filter((f) => !allowed.has(extensionOf(f.relPath)));
|
|
594
|
+
if (bad.length > 0) {
|
|
595
|
+
errors.push(
|
|
596
|
+
`\u6269\u5C55\u540D\u4E0D\u5728\u5141\u8BB8\u5217\u8868(${config.allowedFileExtensions.join(", ")}):${bad.map((f) => f.relPath).join(", ")}`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
|
|
601
|
+
}
|
|
602
|
+
async function mockUpload(dir, options) {
|
|
603
|
+
const ignore = options.ignore ?? (() => false);
|
|
604
|
+
const files = await collectFiles(dir, ignore);
|
|
605
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
606
|
+
options.onProgress?.({ phase: "collect", current: total, total });
|
|
607
|
+
options.onProgress?.({ phase: "upload", current: total, total });
|
|
608
|
+
return { fileCount: files.length, byteCount: total, mock: true };
|
|
305
609
|
}
|
|
306
610
|
async function uploadDirectory(client, dir, options = {}) {
|
|
611
|
+
if (client.mock) return mockUpload(dir, options);
|
|
307
612
|
const ignore = options.ignore ?? (() => false);
|
|
308
613
|
const files = await collectFiles(dir, ignore);
|
|
309
614
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
310
615
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
311
|
-
|
|
616
|
+
const appId = client.appId;
|
|
617
|
+
const wsConfig = normalizeWorkspaceConfig(await client.request(ENDPOINTS.workspaceConfig(appId), { method: "GET" }));
|
|
618
|
+
assertUploadable(files, wsConfig);
|
|
619
|
+
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
|
|
620
|
+
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
621
|
+
const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
|
|
622
|
+
const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
|
|
623
|
+
if (toDelete.length > 0) {
|
|
624
|
+
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
625
|
+
method: "DELETE",
|
|
626
|
+
body: JSON.stringify({ ids: toDelete })
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const creates = [];
|
|
630
|
+
const patches = [];
|
|
631
|
+
const unchanged = [];
|
|
312
632
|
for (const file of files) {
|
|
313
|
-
const content = await fs3.readFile(file.absPath);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await client.upload(ENDPOINTS.upload, { boundary, body });
|
|
633
|
+
const content = await fs3.readFile(file.absPath, "utf8");
|
|
634
|
+
const remoteFile = remoteByPath.get(normalizePath(file.relPath));
|
|
635
|
+
if (!remoteFile) creates.push({ file, content });
|
|
636
|
+
else if (remoteFile.content !== content) patches.push({ file, id: remoteFile.id, content });
|
|
637
|
+
else unchanged.push(file);
|
|
638
|
+
}
|
|
639
|
+
let sent = 0;
|
|
640
|
+
const bump = (file) => {
|
|
322
641
|
sent += file.size;
|
|
323
642
|
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
643
|
+
};
|
|
644
|
+
if (patches.length > 0) {
|
|
645
|
+
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
646
|
+
method: "PATCH",
|
|
647
|
+
body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
|
|
648
|
+
});
|
|
649
|
+
for (const p of patches) bump(p.file);
|
|
650
|
+
}
|
|
651
|
+
for (const c of creates) {
|
|
652
|
+
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
653
|
+
method: "POST",
|
|
654
|
+
body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
|
|
655
|
+
});
|
|
656
|
+
bump(c.file);
|
|
324
657
|
}
|
|
325
|
-
|
|
658
|
+
for (const u of unchanged) bump(u);
|
|
659
|
+
if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
|
|
660
|
+
return { fileCount: files.length, byteCount: total };
|
|
326
661
|
}
|
|
327
662
|
|
|
328
|
-
// src/
|
|
663
|
+
// src/workspace.ts
|
|
329
664
|
import { promises as fs4 } from "fs";
|
|
665
|
+
import path5 from "path";
|
|
666
|
+
|
|
667
|
+
// src/assetsPath.ts
|
|
330
668
|
import path4 from "path";
|
|
331
|
-
|
|
669
|
+
function resolveWorkspaceAssetPath(cwd, relPath) {
|
|
670
|
+
const root = path4.resolve(cwd);
|
|
671
|
+
const input = relPath.trim();
|
|
672
|
+
if (!input) throw new Error("assets \u8DEF\u5F84\u4E3A\u7A7A");
|
|
673
|
+
if (path4.isAbsolute(input)) throw new Error(`assets \u53EA\u63A5\u53D7\u5DE5\u4F5C\u533A\u76F8\u5BF9\u8DEF\u5F84,\u62D2\u7EDD\u7EDD\u5BF9\u8DEF\u5F84: ${relPath}`);
|
|
674
|
+
const resolved = path4.resolve(root, input);
|
|
675
|
+
const rel = path4.relative(root, resolved);
|
|
676
|
+
if (rel.startsWith("..") || path4.isAbsolute(rel)) {
|
|
677
|
+
throw new Error(`assets \u8DEF\u5F84\u9003\u9038\u5DE5\u4F5C\u533A: ${relPath}`);
|
|
678
|
+
}
|
|
679
|
+
return resolved;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/workspace.ts
|
|
683
|
+
var DEFAULT_ENTRY_PATH = "src/main.ts";
|
|
684
|
+
var DEFAULT_MAIN_TS = `import { TwinApp, type TwinAppContext } from "@easytwin/apps";
|
|
685
|
+
|
|
686
|
+
export default class App extends TwinApp {
|
|
687
|
+
async init(ctx: TwinAppContext) {
|
|
688
|
+
// engine / container \u5DF2\u6709;scene / camera \u4E3A null
|
|
689
|
+
void ctx;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async onSceneLoaded(ctx: TwinAppContext) {
|
|
693
|
+
// \u573A\u666F\u5B57\u6BB5\u6709\u503C;\u5728\u6B64\u52A0\u7269\u4F53 / \u7ED1\u4E8B\u4EF6,\u5E76\u7528 ctx.sceneCleanup \u5BF9\u79F0\u62C6\u9664
|
|
694
|
+
void ctx;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
onUpdate(ctx: TwinAppContext, delta: number, elapsed: number) {
|
|
698
|
+
void ctx;
|
|
699
|
+
void delta;
|
|
700
|
+
void elapsed;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
onDispose(ctx: TwinAppContext) {
|
|
704
|
+
void ctx;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
`;
|
|
708
|
+
var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
709
|
+
".git",
|
|
710
|
+
"node_modules",
|
|
711
|
+
"dist",
|
|
712
|
+
".easytwin",
|
|
713
|
+
".cursor",
|
|
714
|
+
".claude",
|
|
715
|
+
".qoder",
|
|
716
|
+
".vscode"
|
|
717
|
+
]);
|
|
718
|
+
var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
|
|
719
|
+
var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
|
|
720
|
+
function defaultPullIgnore(relPath) {
|
|
721
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
722
|
+
if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
|
|
723
|
+
const base = parts[parts.length - 1];
|
|
724
|
+
return base !== void 0 && PULL_IGNORED_FILES.has(base);
|
|
725
|
+
}
|
|
726
|
+
function isSafeRelPath(relPath) {
|
|
727
|
+
const n = normalizePath(relPath);
|
|
728
|
+
if (!n) return false;
|
|
729
|
+
if (n.toLowerCase() === "easytwin.config.json") return false;
|
|
730
|
+
if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
|
|
731
|
+
const parts = n.split("/");
|
|
732
|
+
if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
function extensionOf2(relPath) {
|
|
736
|
+
const base = relPath.split("/").pop() ?? "";
|
|
737
|
+
const i = base.lastIndexOf(".");
|
|
738
|
+
if (i <= 0) return "";
|
|
739
|
+
return base.slice(i).toLowerCase();
|
|
740
|
+
}
|
|
741
|
+
function allowedExtensionSet2(list) {
|
|
742
|
+
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
743
|
+
}
|
|
744
|
+
function normalizeEol(text) {
|
|
745
|
+
return text.replace(/\r\n/g, "\n");
|
|
746
|
+
}
|
|
747
|
+
function combineIgnore(extra) {
|
|
748
|
+
return (relPath) => defaultPullIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
749
|
+
}
|
|
750
|
+
async function readLocalText(absPath) {
|
|
751
|
+
try {
|
|
752
|
+
return await fs4.readFile(absPath, "utf8");
|
|
753
|
+
} catch (err) {
|
|
754
|
+
const code = err.code;
|
|
755
|
+
if (code === "ENOENT") return void 0;
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async function loadAllowedExtensions(client) {
|
|
760
|
+
if (client.mock) return DEFAULT_PULL_EXTENSIONS;
|
|
761
|
+
try {
|
|
762
|
+
const cfg = normalizeWorkspaceConfig(
|
|
763
|
+
await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
|
|
764
|
+
);
|
|
765
|
+
return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
|
|
766
|
+
} catch {
|
|
767
|
+
return DEFAULT_PULL_EXTENSIONS;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async function fetchRemoteFiles(client) {
|
|
771
|
+
if (client.mock) return [];
|
|
772
|
+
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(client.appId), { method: "GET" }));
|
|
773
|
+
}
|
|
774
|
+
async function planWorkspacePull(client, dir, options = {}) {
|
|
775
|
+
const ignore = combineIgnore(options.ignore);
|
|
776
|
+
const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
|
|
777
|
+
const remoteRaw = await fetchRemoteFiles(client);
|
|
778
|
+
const skippedRemotePaths = [];
|
|
779
|
+
const remoteByPath = /* @__PURE__ */ new Map();
|
|
780
|
+
for (const file of remoteRaw) {
|
|
781
|
+
const rel = normalizePath(file.filePath);
|
|
782
|
+
if (!isSafeRelPath(rel) || ignore(rel)) {
|
|
783
|
+
skippedRemotePaths.push(rel || file.filePath);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
remoteByPath.set(rel, file.content);
|
|
787
|
+
}
|
|
788
|
+
let localFiles = [];
|
|
789
|
+
try {
|
|
790
|
+
localFiles = await collectFiles(dir, ignore);
|
|
791
|
+
} catch (err) {
|
|
792
|
+
const code = err.code;
|
|
793
|
+
if (code !== "ENOENT") throw err;
|
|
794
|
+
}
|
|
795
|
+
const localByPath = /* @__PURE__ */ new Map();
|
|
796
|
+
for (const file of localFiles) {
|
|
797
|
+
const rel = normalizePath(file.relPath);
|
|
798
|
+
if (!allowed.has(extensionOf2(rel))) continue;
|
|
799
|
+
const content = await readLocalText(file.absPath);
|
|
800
|
+
if (content !== void 0) localByPath.set(rel, content);
|
|
801
|
+
}
|
|
802
|
+
const changes = [];
|
|
803
|
+
const seen = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const [rel, remoteContent] of remoteByPath) {
|
|
805
|
+
seen.add(rel);
|
|
806
|
+
const localContent = localByPath.get(rel);
|
|
807
|
+
if (localContent === void 0) {
|
|
808
|
+
changes.push({ path: rel, kind: "remote-only", remoteContent });
|
|
809
|
+
} else if (normalizeEol(localContent) === normalizeEol(remoteContent)) {
|
|
810
|
+
changes.push({ path: rel, kind: "identical", localContent, remoteContent });
|
|
811
|
+
} else {
|
|
812
|
+
changes.push({ path: rel, kind: "modified", localContent, remoteContent });
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
for (const [rel, localContent] of localByPath) {
|
|
816
|
+
if (seen.has(rel)) continue;
|
|
817
|
+
changes.push({ path: rel, kind: "local-only", localContent });
|
|
818
|
+
}
|
|
819
|
+
const remoteEmpty = remoteByPath.size === 0;
|
|
820
|
+
let seededDefaults = false;
|
|
821
|
+
if (remoteEmpty) {
|
|
822
|
+
const localMain = localByPath.get(DEFAULT_ENTRY_PATH);
|
|
823
|
+
if (localMain === void 0) {
|
|
824
|
+
changes.push({ path: DEFAULT_ENTRY_PATH, kind: "seed", remoteContent: DEFAULT_MAIN_TS });
|
|
825
|
+
seededDefaults = true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
829
|
+
return {
|
|
830
|
+
remoteEmpty,
|
|
831
|
+
seededDefaults,
|
|
832
|
+
mock: client.mock || void 0,
|
|
833
|
+
skippedRemotePaths,
|
|
834
|
+
changes
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
async function applyWorkspacePull(dir, plan, options = {}) {
|
|
838
|
+
const written = [];
|
|
839
|
+
const skippedConflicts = [];
|
|
840
|
+
for (const change of plan.changes) {
|
|
841
|
+
if (change.kind === "identical" || change.kind === "local-only") continue;
|
|
842
|
+
if (change.kind === "modified" && !options.force) {
|
|
843
|
+
skippedConflicts.push(change.path);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const content = change.remoteContent ?? "";
|
|
847
|
+
const abs = resolveWorkspaceAssetPath(dir, change.path);
|
|
848
|
+
await fs4.mkdir(path5.dirname(abs), { recursive: true });
|
|
849
|
+
await fs4.writeFile(abs, content, "utf8");
|
|
850
|
+
written.push(change.path);
|
|
851
|
+
}
|
|
852
|
+
return { written, skippedConflicts };
|
|
853
|
+
}
|
|
854
|
+
async function pullWorkspace(client, dir, options = {}) {
|
|
855
|
+
const plan = await planWorkspacePull(client, dir, { ignore: options.ignore });
|
|
856
|
+
if (options.dryRun) {
|
|
857
|
+
const skippedConflicts = plan.changes.filter((c) => c.kind === "modified" && !options.force).map((c) => c.path);
|
|
858
|
+
return { plan, written: [], skippedConflicts, dryRun: true, mock: plan.mock };
|
|
859
|
+
}
|
|
860
|
+
const applied = await applyWorkspacePull(dir, plan, { force: options.force });
|
|
861
|
+
return { plan, ...applied, mock: plan.mock };
|
|
862
|
+
}
|
|
863
|
+
var KIND_LABEL = {
|
|
864
|
+
identical: "same ",
|
|
865
|
+
"remote-only": "create",
|
|
866
|
+
"local-only": "keep ",
|
|
867
|
+
modified: "modify",
|
|
868
|
+
seed: "seed "
|
|
869
|
+
};
|
|
870
|
+
function formatWorkspacePullPlan(plan) {
|
|
871
|
+
const lines = [];
|
|
872
|
+
if (plan.mock) lines.push("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u6309\u8FDC\u7AEF\u4E3A\u7A7A\u5904\u7406");
|
|
873
|
+
const remoteCount = plan.changes.filter((c) => c.kind !== "local-only" && c.kind !== "seed").length;
|
|
874
|
+
if (plan.remoteEmpty) {
|
|
875
|
+
lines.push(
|
|
876
|
+
plan.seededDefaults ? `\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u5C06\u5199\u5165\u9ED8\u8BA4 ${DEFAULT_ENTRY_PATH}` : "\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u672C\u5730\u5DF2\u6709\u5165\u53E3,\u672A\u6539\u52A8\u9ED8\u8BA4\u6A21\u677F"
|
|
877
|
+
);
|
|
878
|
+
} else {
|
|
879
|
+
lines.push(`\u8FDC\u7AEF ${remoteCount} \u4E2A\u6587\u4EF6`);
|
|
880
|
+
}
|
|
881
|
+
for (const change of plan.changes) {
|
|
882
|
+
let extra = "";
|
|
883
|
+
if (change.kind === "modified") extra = " (\u51B2\u7A81,\u9ED8\u8BA4\u4E0D\u8986\u76D6)";
|
|
884
|
+
if (change.kind === "local-only") extra = " (\u4EC5\u672C\u5730)";
|
|
885
|
+
if (change.kind === "seed") extra = " (\u9ED8\u8BA4 TwinApp)";
|
|
886
|
+
lines.push(` ${KIND_LABEL[change.kind]} ${change.path}${extra}`);
|
|
887
|
+
}
|
|
888
|
+
if (plan.skippedRemotePaths.length > 0) {
|
|
889
|
+
lines.push(`\u8DF3\u8FC7\u975E\u6CD5/\u51ED\u636E\u8DEF\u5F84: ${plan.skippedRemotePaths.join(", ")}`);
|
|
890
|
+
}
|
|
891
|
+
return lines.join("\n");
|
|
892
|
+
}
|
|
893
|
+
function formatWorkspacePullResult(result) {
|
|
894
|
+
const lines = [formatWorkspacePullPlan(result.plan)];
|
|
895
|
+
if (result.dryRun) {
|
|
896
|
+
lines.push("dry-run:\u672A\u5199\u76D8");
|
|
897
|
+
if (result.skippedConflicts.length > 0) {
|
|
898
|
+
lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force)`);
|
|
899
|
+
}
|
|
900
|
+
return lines.join("\n");
|
|
901
|
+
}
|
|
902
|
+
if (result.written.length > 0) {
|
|
903
|
+
lines.push(`\u5DF2\u5199\u5165 ${result.written.length} \u4E2A\u6587\u4EF6: ${result.written.join(", ")}`);
|
|
904
|
+
} else {
|
|
905
|
+
lines.push("\u672A\u5199\u5165\u6587\u4EF6");
|
|
906
|
+
}
|
|
907
|
+
if (result.skippedConflicts.length > 0) {
|
|
908
|
+
lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force): ${result.skippedConflicts.join(", ")}`);
|
|
909
|
+
}
|
|
910
|
+
return lines.join("\n");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/skills.ts
|
|
914
|
+
import { existsSync, promises as fs5 } from "fs";
|
|
915
|
+
import path6 from "path";
|
|
916
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
332
917
|
var SKILL_NAMES = [
|
|
333
918
|
"easytwin-render",
|
|
334
919
|
"easytwin-core",
|
|
@@ -340,8 +925,81 @@ var SKILL_NAMES = [
|
|
|
340
925
|
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
341
926
|
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
342
927
|
var META_FILE_NAME = ".easytwin-meta.json";
|
|
928
|
+
var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
929
|
+
{
|
|
930
|
+
compilerOptions: {
|
|
931
|
+
target: "ES2022",
|
|
932
|
+
module: "ESNext",
|
|
933
|
+
moduleResolution: "bundler",
|
|
934
|
+
strict: true,
|
|
935
|
+
skipLibCheck: true,
|
|
936
|
+
noEmit: true,
|
|
937
|
+
paths: {
|
|
938
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
939
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
include: ["src/**/*.ts"]
|
|
943
|
+
},
|
|
944
|
+
null,
|
|
945
|
+
2
|
|
946
|
+
)}
|
|
947
|
+
`;
|
|
948
|
+
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
949
|
+
"skipLibCheck": true,
|
|
950
|
+
"paths": {
|
|
951
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
952
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
953
|
+
}`;
|
|
954
|
+
var APPS_TYPES_FILE = "apps.d.ts";
|
|
955
|
+
var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
|
|
956
|
+
|
|
957
|
+
export type TwinAppContext = {
|
|
958
|
+
app: {
|
|
959
|
+
id: string;
|
|
960
|
+
mode: "preview" | "publish";
|
|
961
|
+
};
|
|
962
|
+
engine: RuntimeEngine;
|
|
963
|
+
container: HTMLElement;
|
|
964
|
+
runtimeScene: RuntimeScene | null;
|
|
965
|
+
scene: RuntimeScene["sceneObject"] | null;
|
|
966
|
+
camera: RuntimeScene["camera"]["main"] | null;
|
|
967
|
+
sceneManager: {
|
|
968
|
+
currentSceneId: string;
|
|
969
|
+
runtime: SceneManager | null;
|
|
970
|
+
loadScene(sceneId: string): Promise<void>;
|
|
971
|
+
};
|
|
972
|
+
logger: {
|
|
973
|
+
log(...args: unknown[]): void;
|
|
974
|
+
info(...args: unknown[]): void;
|
|
975
|
+
warn(...args: unknown[]): void;
|
|
976
|
+
error(...args: unknown[]): void;
|
|
977
|
+
};
|
|
978
|
+
assets: {
|
|
979
|
+
text(path: string): Promise<string>;
|
|
980
|
+
json<T = unknown>(path: string): Promise<T>;
|
|
981
|
+
};
|
|
982
|
+
cleanup(fn: () => void | Promise<void>): void;
|
|
983
|
+
sceneCleanup(fn: () => void | Promise<void>): void;
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
export declare abstract class TwinApp {
|
|
987
|
+
init?(ctx: TwinAppContext): void | Promise<void>;
|
|
988
|
+
onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
|
|
989
|
+
onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
|
|
990
|
+
onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
|
|
991
|
+
onDispose?(ctx: TwinAppContext): void | Promise<void>;
|
|
992
|
+
onError?(ctx: TwinAppContext, error: unknown): boolean | void;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export declare function defineApp<T>(app: T): T;
|
|
996
|
+
`;
|
|
997
|
+
function buildRuntimeTypesContent(sourceDts) {
|
|
998
|
+
return `${sourceDts.replace(/\s+$/, "")}
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
343
1001
|
function normalizeTargets(target = "all") {
|
|
344
|
-
if (target === "all") return ["cursor", "claude", "codex"];
|
|
1002
|
+
if (target === "all") return ["cursor", "claude", "codex", "qoder"];
|
|
345
1003
|
if (Array.isArray(target)) return [...new Set(target)];
|
|
346
1004
|
return [target];
|
|
347
1005
|
}
|
|
@@ -349,58 +1007,69 @@ function resolveSkillsSourceDir() {
|
|
|
349
1007
|
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
350
1008
|
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
|
|
351
1009
|
}
|
|
352
|
-
const here =
|
|
353
|
-
return
|
|
1010
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1011
|
+
return path6.resolve(here, "..", "skills");
|
|
1012
|
+
}
|
|
1013
|
+
function resolveRuntimeTypesSourceFile() {
|
|
1014
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
1015
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
|
|
1016
|
+
}
|
|
1017
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1018
|
+
const fromDist = path6.join(here, "runtime-types", "index.d.ts");
|
|
1019
|
+
const fromSrc = path6.resolve(here, "lib", "index.d.ts");
|
|
1020
|
+
if (existsSync(fromDist)) return fromDist;
|
|
1021
|
+
if (existsSync(fromSrc)) return fromSrc;
|
|
1022
|
+
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
|
|
354
1023
|
}
|
|
355
1024
|
async function readJson(file) {
|
|
356
|
-
return JSON.parse(await
|
|
1025
|
+
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
357
1026
|
}
|
|
358
1027
|
async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
|
|
359
|
-
const marker =
|
|
1028
|
+
const marker = path6.join(skillsDir, ".easytwin-source.json");
|
|
360
1029
|
try {
|
|
361
1030
|
const meta = await readJson(marker);
|
|
362
1031
|
if (typeof meta.version === "string") return meta.version;
|
|
363
1032
|
} catch {
|
|
364
1033
|
}
|
|
365
1034
|
try {
|
|
366
|
-
const pkg = await readJson(
|
|
1035
|
+
const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
|
|
367
1036
|
return pkg.version;
|
|
368
1037
|
} catch {
|
|
369
1038
|
return "0.0.0";
|
|
370
1039
|
}
|
|
371
1040
|
}
|
|
372
1041
|
async function copyDir(src, dest) {
|
|
373
|
-
await
|
|
374
|
-
const entries = await
|
|
1042
|
+
await fs5.mkdir(dest, { recursive: true });
|
|
1043
|
+
const entries = await fs5.readdir(src, { withFileTypes: true });
|
|
375
1044
|
for (const entry of entries) {
|
|
376
|
-
const s =
|
|
377
|
-
const d =
|
|
1045
|
+
const s = path6.join(src, entry.name);
|
|
1046
|
+
const d = path6.join(dest, entry.name);
|
|
378
1047
|
if (entry.isDirectory()) await copyDir(s, d);
|
|
379
|
-
else if (entry.isFile()) await
|
|
1048
|
+
else if (entry.isFile()) await fs5.copyFile(s, d);
|
|
380
1049
|
}
|
|
381
1050
|
}
|
|
382
1051
|
async function dirMatches(src, dest) {
|
|
383
1052
|
let sourceEntries;
|
|
384
1053
|
try {
|
|
385
|
-
sourceEntries = await
|
|
1054
|
+
sourceEntries = await fs5.readdir(src);
|
|
386
1055
|
} catch {
|
|
387
1056
|
return false;
|
|
388
1057
|
}
|
|
389
1058
|
for (const name of sourceEntries) {
|
|
390
|
-
const s =
|
|
391
|
-
const d =
|
|
392
|
-
const sStat = await
|
|
1059
|
+
const s = path6.join(src, name);
|
|
1060
|
+
const d = path6.join(dest, name);
|
|
1061
|
+
const sStat = await fs5.stat(s);
|
|
393
1062
|
if (sStat.isDirectory()) {
|
|
394
1063
|
if (!await dirMatches(s, d)) return false;
|
|
395
1064
|
} else {
|
|
396
1065
|
let dStat;
|
|
397
1066
|
try {
|
|
398
|
-
dStat = await
|
|
1067
|
+
dStat = await fs5.stat(d);
|
|
399
1068
|
} catch {
|
|
400
1069
|
return false;
|
|
401
1070
|
}
|
|
402
1071
|
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
403
|
-
if (!(await
|
|
1072
|
+
if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
|
|
404
1073
|
}
|
|
405
1074
|
}
|
|
406
1075
|
return true;
|
|
@@ -411,18 +1080,18 @@ function actionFor(exists, matches) {
|
|
|
411
1080
|
}
|
|
412
1081
|
async function writeMeta(destDir, version) {
|
|
413
1082
|
const meta = { name: "@easytwin/devkit", version };
|
|
414
|
-
await
|
|
1083
|
+
await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
415
1084
|
}
|
|
416
1085
|
async function syncToDir(target, sourceRoot, targetRoot, version) {
|
|
417
1086
|
const entries = [];
|
|
418
1087
|
for (const name of SKILL_NAMES) {
|
|
419
|
-
const src =
|
|
420
|
-
const dest =
|
|
421
|
-
const exists = await
|
|
1088
|
+
const src = path6.join(sourceRoot, name);
|
|
1089
|
+
const dest = path6.join(targetRoot, name);
|
|
1090
|
+
const exists = await fs5.stat(dest).then(() => true).catch(() => false);
|
|
422
1091
|
const matches = await dirMatches(src, dest);
|
|
423
1092
|
const action = actionFor(exists, matches);
|
|
424
1093
|
if (action !== "unchanged") {
|
|
425
|
-
await
|
|
1094
|
+
await fs5.rm(dest, { recursive: true, force: true });
|
|
426
1095
|
await copyDir(src, dest);
|
|
427
1096
|
}
|
|
428
1097
|
entries.push({ name, action });
|
|
@@ -441,19 +1110,19 @@ function buildCodexSegment(version) {
|
|
|
441
1110
|
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
|
|
442
1111
|
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
443
1112
|
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
444
|
-
"- `easytwin-upload`:\
|
|
1113
|
+
"- `easytwin-upload`:\u62C9\u53D6/\u4E0A\u4F20\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5199\u9ED8\u8BA4\u5165\u53E3;\u4E0A\u4F20\u5168\u91CF\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
|
|
445
1114
|
"",
|
|
446
1115
|
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
447
1116
|
CODEX_MARKER_END
|
|
448
1117
|
].join("\n");
|
|
449
1118
|
}
|
|
450
1119
|
async function syncToCodex(sourceRoot, cwd, version) {
|
|
451
|
-
const agentsFile =
|
|
1120
|
+
const agentsFile = path6.join(cwd, "AGENTS.md");
|
|
452
1121
|
const segment = buildCodexSegment(version);
|
|
453
1122
|
let content = "";
|
|
454
1123
|
let exists = true;
|
|
455
1124
|
try {
|
|
456
|
-
content = await
|
|
1125
|
+
content = await fs5.readFile(agentsFile, "utf8");
|
|
457
1126
|
} catch {
|
|
458
1127
|
exists = false;
|
|
459
1128
|
}
|
|
@@ -471,22 +1140,201 @@ async function syncToCodex(sourceRoot, cwd, version) {
|
|
|
471
1140
|
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
472
1141
|
}
|
|
473
1142
|
if (action !== "unchanged") {
|
|
474
|
-
await
|
|
475
|
-
await
|
|
1143
|
+
await fs5.mkdir(cwd, { recursive: true });
|
|
1144
|
+
await fs5.writeFile(agentsFile, next, "utf8");
|
|
476
1145
|
}
|
|
477
1146
|
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
478
1147
|
}
|
|
1148
|
+
async function syncRuntimeTypes(cwd, typesSourceFile) {
|
|
1149
|
+
const destDir = path6.join(cwd, ".easytwin", "types");
|
|
1150
|
+
const destFile = path6.join(destDir, "index.d.ts");
|
|
1151
|
+
const appsFile = path6.join(destDir, APPS_TYPES_FILE);
|
|
1152
|
+
const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
|
|
1153
|
+
let runtimeCurrent = "";
|
|
1154
|
+
let appsCurrent = "";
|
|
1155
|
+
let runtimeExists = true;
|
|
1156
|
+
let appsExists = true;
|
|
1157
|
+
try {
|
|
1158
|
+
runtimeCurrent = await fs5.readFile(destFile, "utf8");
|
|
1159
|
+
} catch {
|
|
1160
|
+
runtimeExists = false;
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
appsCurrent = await fs5.readFile(appsFile, "utf8");
|
|
1164
|
+
} catch {
|
|
1165
|
+
appsExists = false;
|
|
1166
|
+
}
|
|
1167
|
+
const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
|
|
1168
|
+
const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
|
|
1169
|
+
const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
|
|
1170
|
+
if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
|
|
1171
|
+
await fs5.mkdir(destDir, { recursive: true });
|
|
1172
|
+
if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
|
|
1173
|
+
if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
|
|
1174
|
+
}
|
|
1175
|
+
const tsconfigPath = path6.join(cwd, "tsconfig.json");
|
|
1176
|
+
let tsconfig;
|
|
1177
|
+
let pathsHint;
|
|
1178
|
+
try {
|
|
1179
|
+
const existing = await fs5.readFile(tsconfigPath, "utf8");
|
|
1180
|
+
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
1181
|
+
else {
|
|
1182
|
+
tsconfig = "manual-paths";
|
|
1183
|
+
pathsHint = TSCONFIG_PATHS_HINT;
|
|
1184
|
+
}
|
|
1185
|
+
} catch {
|
|
1186
|
+
await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
1187
|
+
tsconfig = "created";
|
|
1188
|
+
}
|
|
1189
|
+
return { action, tsconfig, pathsHint };
|
|
1190
|
+
}
|
|
479
1191
|
async function syncSkills(options) {
|
|
480
1192
|
const targets = normalizeTargets(options.targets ?? "all");
|
|
481
1193
|
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
482
1194
|
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
483
1195
|
const summaries = [];
|
|
484
1196
|
for (const target of targets) {
|
|
485
|
-
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot,
|
|
486
|
-
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot,
|
|
1197
|
+
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
|
|
1198
|
+
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
|
|
1199
|
+
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
|
|
487
1200
|
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
488
1201
|
}
|
|
489
|
-
|
|
1202
|
+
const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
|
|
1203
|
+
return { summaries, types };
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/bundle.ts
|
|
1207
|
+
import * as esbuild from "esbuild-wasm";
|
|
1208
|
+
import { promises as fs6 } from "fs";
|
|
1209
|
+
import path7 from "path";
|
|
1210
|
+
|
|
1211
|
+
// src/apps.ts
|
|
1212
|
+
var PORTABLE_RUNTIME_EXPORTS = [
|
|
1213
|
+
"THREE",
|
|
1214
|
+
"RuntimeEngine",
|
|
1215
|
+
"SceneManager",
|
|
1216
|
+
"LoadSceneMode",
|
|
1217
|
+
"convertObjToComponentJson"
|
|
1218
|
+
];
|
|
1219
|
+
function portableRuntimeWarning(names) {
|
|
1220
|
+
const extra = [...new Set(names)].filter(
|
|
1221
|
+
(n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
|
|
1222
|
+
);
|
|
1223
|
+
const ns = [...names].includes("*");
|
|
1224
|
+
if (!ns && extra.length === 0) return void 0;
|
|
1225
|
+
const detail = ns ? "namespace import *" : extra.sort().join(", ");
|
|
1226
|
+
return `\u68C0\u6D4B\u5230 @easytwin/runtime \u5BFC\u51FA ${detail} \u4E0D\u5728\u5728\u7EBF\u7F16\u8BD1\u767D\u540D\u5355(${PORTABLE_RUNTIME_EXPORTS.join(", ")})\u5185\u3002\u672C\u5730\u9884\u89C8\u53EF\u7528,\u4E0A\u4F20\u5230\u5728\u7EBF TwinApp \u53EF\u80FD\u7F16\u4E0D\u8FC7\u3002`;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/bundle.ts
|
|
1230
|
+
var USER_ENTRY = "src/main.ts";
|
|
1231
|
+
var DEFAULT_BUNDLE_OUT = "dist/main.js";
|
|
1232
|
+
var RUNTIME_MODULE = "@easytwin/runtime";
|
|
1233
|
+
var APPS_MODULE = "@easytwin/apps";
|
|
1234
|
+
var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
|
|
1235
|
+
var BundleError = class extends Error {
|
|
1236
|
+
constructor(message) {
|
|
1237
|
+
super(message);
|
|
1238
|
+
this.name = "BundleError";
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
function isRelativeOrAbsolute(spec) {
|
|
1242
|
+
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
|
|
1243
|
+
}
|
|
1244
|
+
function whitelistPlugin() {
|
|
1245
|
+
return {
|
|
1246
|
+
name: "easytwin-whitelist",
|
|
1247
|
+
setup(build2) {
|
|
1248
|
+
build2.onResolve({ filter: /.*/ }, (args) => {
|
|
1249
|
+
if (args.kind === "entry-point") return void 0;
|
|
1250
|
+
if (isRelativeOrAbsolute(args.path)) return void 0;
|
|
1251
|
+
if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
|
|
1252
|
+
return {
|
|
1253
|
+
errors: [
|
|
1254
|
+
{
|
|
1255
|
+
text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime \u4E0E @easytwin/apps\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
|
|
1256
|
+
}
|
|
1257
|
+
]
|
|
1258
|
+
};
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function formatEsbuildMessages(messages) {
|
|
1264
|
+
return messages.map((m) => {
|
|
1265
|
+
const loc = m.location ? `${m.location.file}:${m.location.line}:${m.location.column}: ` : "";
|
|
1266
|
+
return `${loc}${m.text}`;
|
|
1267
|
+
}).join("\n");
|
|
1268
|
+
}
|
|
1269
|
+
function collectBundledRuntimeExports(code) {
|
|
1270
|
+
const names = [];
|
|
1271
|
+
if (/import\s+\*\s+as\s+[\w$]+\s+from\s*["']@easytwin\/runtime["']/.test(code)) names.push("*");
|
|
1272
|
+
const named = /import\s*\{([^}]+)\}\s*from\s*["']@easytwin\/runtime["']/g;
|
|
1273
|
+
for (const match of code.matchAll(named)) {
|
|
1274
|
+
const body = match[1] ?? "";
|
|
1275
|
+
for (const part of body.split(",")) {
|
|
1276
|
+
const token = part.replace(/\btype\b/g, "").trim();
|
|
1277
|
+
if (!token) continue;
|
|
1278
|
+
const id = token.split(/\s+as\s+/)[0]?.trim();
|
|
1279
|
+
if (id) names.push(id);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return names;
|
|
1283
|
+
}
|
|
1284
|
+
async function bundleUserCode(options) {
|
|
1285
|
+
const cwd = path7.resolve(options.cwd);
|
|
1286
|
+
const entry = path7.join(cwd, USER_ENTRY);
|
|
1287
|
+
try {
|
|
1288
|
+
await fs6.access(entry);
|
|
1289
|
+
} catch {
|
|
1290
|
+
throw new BundleError(
|
|
1291
|
+
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
let result;
|
|
1295
|
+
try {
|
|
1296
|
+
result = await esbuild.build({
|
|
1297
|
+
absWorkingDir: cwd,
|
|
1298
|
+
entryPoints: [entry],
|
|
1299
|
+
bundle: true,
|
|
1300
|
+
write: false,
|
|
1301
|
+
format: "esm",
|
|
1302
|
+
platform: "browser",
|
|
1303
|
+
target: "es2022",
|
|
1304
|
+
sourcemap: "inline",
|
|
1305
|
+
logLevel: "silent",
|
|
1306
|
+
plugins: [whitelistPlugin()]
|
|
1307
|
+
});
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
const errors = err.errors;
|
|
1310
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
1311
|
+
throw new BundleError(formatEsbuildMessages(errors));
|
|
1312
|
+
}
|
|
1313
|
+
throw err instanceof Error ? new BundleError(err.message) : err;
|
|
1314
|
+
}
|
|
1315
|
+
if (result.errors.length > 0) {
|
|
1316
|
+
throw new BundleError(formatEsbuildMessages(result.errors));
|
|
1317
|
+
}
|
|
1318
|
+
const file = result.outputFiles?.[0];
|
|
1319
|
+
if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
|
|
1320
|
+
const code = file.text;
|
|
1321
|
+
const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
|
|
1322
|
+
const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
|
|
1323
|
+
if (portable) warnings.push(portable);
|
|
1324
|
+
if (options.outFile) {
|
|
1325
|
+
const outFile = path7.isAbsolute(options.outFile) ? options.outFile : path7.join(cwd, options.outFile);
|
|
1326
|
+
await fs6.mkdir(path7.dirname(outFile), { recursive: true });
|
|
1327
|
+
await fs6.writeFile(outFile, code, "utf8");
|
|
1328
|
+
}
|
|
1329
|
+
return { code, warnings };
|
|
1330
|
+
}
|
|
1331
|
+
async function typesMissingHint(cwd) {
|
|
1332
|
+
try {
|
|
1333
|
+
await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
1334
|
+
return void 0;
|
|
1335
|
+
} catch {
|
|
1336
|
+
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
1337
|
+
}
|
|
490
1338
|
}
|
|
491
1339
|
|
|
492
1340
|
// src/bin.ts
|
|
@@ -498,6 +1346,7 @@ async function readVersion() {
|
|
|
498
1346
|
return "0.0.0";
|
|
499
1347
|
}
|
|
500
1348
|
}
|
|
1349
|
+
var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20/\u5DE5\u4F5C\u533A\u62C9\u53D6\u8D70\u672C\u5730 mock";
|
|
501
1350
|
async function prompt(question) {
|
|
502
1351
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
503
1352
|
try {
|
|
@@ -509,8 +1358,17 @@ async function prompt(question) {
|
|
|
509
1358
|
async function runInit(cwd, flags) {
|
|
510
1359
|
const appId = flags.appId ?? await prompt("App ID: ");
|
|
511
1360
|
const appSecret = flags.appSecret ?? await prompt("App Secret: ");
|
|
512
|
-
|
|
1361
|
+
let env = "prod";
|
|
1362
|
+
if (flags.env !== void 0) {
|
|
1363
|
+
env = flags.env.toLowerCase() === "test" ? "test" : "prod";
|
|
1364
|
+
} else if (flags.baseUrl === void 0) {
|
|
1365
|
+
const envRaw = (await prompt("\u73AF\u5883(prod=\u6B63\u5F0F / test=\u6D4B\u8BD5,\u7F3A\u7701 prod): ")).trim().toLowerCase();
|
|
1366
|
+
env = envRaw === "test" ? "test" : "prod";
|
|
1367
|
+
}
|
|
1368
|
+
const defaultUrl = env === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL;
|
|
1369
|
+
const baseUrlInput = flags.baseUrl ?? await prompt(`Base URL(\u7F3A\u7701 ${defaultUrl}): `);
|
|
513
1370
|
const input = { appId, appSecret };
|
|
1371
|
+
if (env === "test") input.env = "test";
|
|
514
1372
|
if (baseUrlInput.trim().length > 0) input.baseUrl = baseUrlInput.trim();
|
|
515
1373
|
const result = await initConfig(input, cwd);
|
|
516
1374
|
console.log(`\u5DF2\u751F\u6210 ${result.configFile}`);
|
|
@@ -521,28 +1379,40 @@ async function runInit(cwd, flags) {
|
|
|
521
1379
|
function buildProgram(cwd, version = "0.0.0") {
|
|
522
1380
|
const program = new Command();
|
|
523
1381
|
program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
|
|
524
|
-
program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
|
|
1382
|
+
program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--env <prod|test>", "\u670D\u52A1\u73AF\u5883(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
|
|
525
1383
|
const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
|
|
526
|
-
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F").action(async () => {
|
|
1384
|
+
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
|
|
527
1385
|
const config = await loadConfig(cwd);
|
|
1386
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
528
1387
|
const client = new EasyTwinClient(config);
|
|
529
|
-
const scenes = await listScenes(client);
|
|
1388
|
+
const scenes = await listScenes(client, { cwd });
|
|
530
1389
|
if (scenes.length === 0) {
|
|
531
1390
|
console.log("(\u65E0\u573A\u666F)");
|
|
532
|
-
|
|
1391
|
+
} else {
|
|
1392
|
+
for (const s of scenes) console.log(`${s.id} ${s.name}`);
|
|
533
1393
|
}
|
|
534
|
-
|
|
1394
|
+
console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
|
|
535
1395
|
});
|
|
536
1396
|
scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ./<id>.scene.json)").action(async (id, opts) => {
|
|
537
1397
|
const config = await loadConfig(cwd);
|
|
1398
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
538
1399
|
const client = new EasyTwinClient(config);
|
|
539
1400
|
const scene2 = await pullScene(client, id);
|
|
540
|
-
const out = opts.out ??
|
|
1401
|
+
const out = opts.out ?? path8.join(cwd, `${id}.scene.json`);
|
|
541
1402
|
const file = await saveSceneJson(scene2, out);
|
|
542
1403
|
console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
|
|
543
1404
|
});
|
|
1405
|
+
program.command("pull").description("\u62C9\u53D6\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5219\u5199\u5165\u9ED8\u8BA4 src/main.ts;\u51B2\u7A81\u9ED8\u8BA4\u4E0D\u8986\u76D6)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").option("--force", "\u7528\u8FDC\u7AEF\u5185\u5BB9\u8986\u76D6\u672C\u5730\u51B2\u7A81\u6587\u4EF6").option("--dry-run", "\u53EA\u6253\u5370 diff,\u4E0D\u5199\u76D8").action(async (dirArg, opts) => {
|
|
1406
|
+
const config = await loadConfig(cwd);
|
|
1407
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1408
|
+
const client = new EasyTwinClient(config);
|
|
1409
|
+
const target = dirArg ? path8.resolve(cwd, dirArg) : cwd;
|
|
1410
|
+
const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
|
|
1411
|
+
console.log(formatWorkspacePullResult(result));
|
|
1412
|
+
});
|
|
544
1413
|
program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
|
|
545
1414
|
const config = await loadConfig(cwd);
|
|
1415
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
546
1416
|
const client = new EasyTwinClient(config);
|
|
547
1417
|
await uploadDirectory(client, dir, {
|
|
548
1418
|
onProgress: (p) => {
|
|
@@ -553,9 +1423,17 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
553
1423
|
});
|
|
554
1424
|
process.stderr.write("\n");
|
|
555
1425
|
});
|
|
1426
|
+
program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8 @easytwin/runtime \u4E0E @easytwin/apps)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
|
|
1427
|
+
const hint = await typesMissingHint(cwd);
|
|
1428
|
+
if (hint) console.warn(hint);
|
|
1429
|
+
const outFile = path8.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
|
|
1430
|
+
const result = await bundleUserCode({ cwd, outFile });
|
|
1431
|
+
for (const warning of result.warnings) console.warn(warning);
|
|
1432
|
+
console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
|
|
1433
|
+
});
|
|
556
1434
|
const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
|
|
557
|
-
skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|all", "all").action(async (opts) => {
|
|
558
|
-
const summaries = await syncSkills({ cwd, targets: opts.target });
|
|
1435
|
+
skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex/qoder),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|qoder|all", "all").action(async (opts) => {
|
|
1436
|
+
const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
|
|
559
1437
|
for (const s of summaries) {
|
|
560
1438
|
if (s.codex) {
|
|
561
1439
|
console.log(`[codex] AGENTS.md: ${s.codex.action}`);
|
|
@@ -563,6 +1441,9 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
563
1441
|
}
|
|
564
1442
|
for (const e of s.entries) console.log(`[${s.target}] ${e.name}: ${e.action}`);
|
|
565
1443
|
}
|
|
1444
|
+
console.log(`[types] ${".easytwin/types"}: ${types.action}`);
|
|
1445
|
+
if (types.tsconfig === "created") console.log("[types] \u5DF2\u751F\u6210\u6700\u5C0F tsconfig.json");
|
|
1446
|
+
if (types.pathsHint) console.log(types.pathsHint);
|
|
566
1447
|
});
|
|
567
1448
|
return program;
|
|
568
1449
|
}
|