@easytwin/devkit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/bin.js +293 -20
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +126 -10
- package/dist/index.js +444 -17
- package/dist/index.js.map +1 -1
- package/dist/runtime-types/index.d.ts +8243 -0
- package/package.json +20 -5
- package/scene.example.json +2663 -0
- package/skills/easytwin-bootstrap/SKILL.md +7 -2
- package/skills/easytwin-core/references/engine.md +3 -3
- package/skills/easytwin-develop/SKILL.md +6 -2
- package/skills/easytwin-render/SKILL.md +20 -19
- package/skills/easytwin-render/references/intro.md +5 -6
- package/skills/easytwin-render/references/scene-and-assets.md +4 -3
- package/skills/easytwin-scene/SKILL.md +33 -5
- package/skills/easytwin-upload/SKILL.md +4 -0
package/README.md
CHANGED
|
@@ -28,11 +28,13 @@ npm i -g @easytwin/devkit # 全局安装 CLI
|
|
|
28
28
|
{
|
|
29
29
|
"appId": "必填",
|
|
30
30
|
"appSecret": "必填",
|
|
31
|
-
"
|
|
31
|
+
"env": "可选,prod=正式 / test=测试,缺省 prod",
|
|
32
|
+
"baseUrl": "可选,缺省按 env 选官方域名",
|
|
33
|
+
"ossUrl": "可选,缺省官方 OSS"
|
|
32
34
|
}
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
环境变量 `EASYTWIN_APP_ID` / `EASYTWIN_APP_SECRET` / `EASYTWIN_BASE_URL`
|
|
37
|
+
环境变量 `EASYTWIN_APP_ID` / `EASYTWIN_APP_SECRET` / `EASYTWIN_BASE_URL` / `EASYTWIN_ENV` / `EASYTWIN_OSS_URL` 优先于文件。官方 API:正式 `http://saas-twin.k8s.dtstack.cn/`、测试 `http://saas-twin-test.k8s.dtstack.cn/`,缺省按 env 选择。官方 OSS:`https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/`,供 twin runtime 加载 webp/draco/组件脚本。文件含密钥,永不入库(`init` 会处理 gitignore)。
|
|
36
38
|
|
|
37
39
|
## skills
|
|
38
40
|
|
|
@@ -50,8 +52,9 @@ npm pack --dry-run
|
|
|
50
52
|
# 3. 升版本
|
|
51
53
|
npm version patch|minor|major
|
|
52
54
|
|
|
53
|
-
# 4. 发布(
|
|
54
|
-
pnpm publish
|
|
55
|
+
# 4. 发布(必须先升版本:同版本已在 npm 上会被跳过,提示 There are no new packages that should be published)
|
|
56
|
+
# 在仓库根目录用 run,不要直接 pnpm publish(那是工作区递归发布内置命令)
|
|
57
|
+
pnpm run publish
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
- scoped 包需 publishConfig.access 为 public,已配置;
|
|
@@ -69,4 +72,4 @@ pnpm -r test
|
|
|
69
72
|
|
|
70
73
|
## 已知外部输入(TODO)
|
|
71
74
|
|
|
72
|
-
内部 swagger(API 契约)、渲染 API 文档与 d.ts
|
|
75
|
+
内部 swagger(API 契约)、渲染 API 文档与 d.ts——均未到位,代码中以 `TODO(swagger)` / `TODO(外部输入)` 标记,不臆造;官方 API 域名、官方 OSS 与场景 JSON 格式已定(见上)。
|
package/dist/bin.js
CHANGED
|
@@ -7,13 +7,20 @@ 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 path6 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://saas-twin-test.k8s.dtstack.cn/";
|
|
18
|
+
var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
19
|
+
var MOCK_APP_ID = "test";
|
|
20
|
+
var MOCK_APP_SECRET = "test";
|
|
21
|
+
function isMockCredentials(appId, appSecret) {
|
|
22
|
+
return appId === MOCK_APP_ID && appSecret === MOCK_APP_SECRET;
|
|
23
|
+
}
|
|
17
24
|
var ConfigError = class extends Error {
|
|
18
25
|
constructor(message) {
|
|
19
26
|
super(message);
|
|
@@ -46,8 +53,16 @@ function validateConfigShape(value) {
|
|
|
46
53
|
if (v.baseUrl !== void 0 && (typeof v.baseUrl !== "string" || v.baseUrl.length === 0)) {
|
|
47
54
|
throw new ConfigError("baseUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
48
55
|
}
|
|
56
|
+
if (v.ossUrl !== void 0 && (typeof v.ossUrl !== "string" || v.ossUrl.length === 0)) {
|
|
57
|
+
throw new ConfigError("ossUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
58
|
+
}
|
|
59
|
+
if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
|
|
60
|
+
throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
|
|
61
|
+
}
|
|
49
62
|
const config = { appId: v.appId, appSecret: v.appSecret };
|
|
63
|
+
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
50
64
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
65
|
+
if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
|
|
51
66
|
return config;
|
|
52
67
|
}
|
|
53
68
|
async function readConfigFile(cwd) {
|
|
@@ -63,11 +78,13 @@ async function readConfigFile(cwd) {
|
|
|
63
78
|
function resolveConfig(file, env = process.env) {
|
|
64
79
|
const appId = env.EASYTWIN_APP_ID ?? file.appId;
|
|
65
80
|
const appSecret = env.EASYTWIN_APP_SECRET ?? file.appSecret;
|
|
66
|
-
const
|
|
81
|
+
const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file.env ?? "prod";
|
|
82
|
+
const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? (easyEnv === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL);
|
|
83
|
+
const ossUrl = env.EASYTWIN_OSS_URL ?? file.ossUrl ?? DEFAULT_OSS_URL;
|
|
67
84
|
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
85
|
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
86
|
const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
|
|
70
|
-
return { appId, appSecret, baseUrl, source };
|
|
87
|
+
return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
71
88
|
}
|
|
72
89
|
async function loadConfig(cwd, env = process.env) {
|
|
73
90
|
return resolveConfig(await readConfigFile(cwd), env);
|
|
@@ -75,7 +92,9 @@ async function loadConfig(cwd, env = process.env) {
|
|
|
75
92
|
async function writeConfigFile(cwd, config) {
|
|
76
93
|
const file = configFilePath(cwd);
|
|
77
94
|
const body = { appId: config.appId, appSecret: config.appSecret };
|
|
95
|
+
if (config.env) body.env = config.env;
|
|
78
96
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
97
|
+
if (config.ossUrl) body.ossUrl = config.ossUrl;
|
|
79
98
|
await fs.mkdir(cwd, { recursive: true });
|
|
80
99
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
81
100
|
return file;
|
|
@@ -144,10 +163,13 @@ function parseResponseBody(buffer) {
|
|
|
144
163
|
}
|
|
145
164
|
var EasyTwinClient = class {
|
|
146
165
|
baseUrl;
|
|
166
|
+
/** 本地测试模式:true 时 scene/upload 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
167
|
+
mock;
|
|
147
168
|
appId;
|
|
148
169
|
appSecret;
|
|
149
170
|
constructor(config) {
|
|
150
171
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
172
|
+
this.mock = config.mock;
|
|
151
173
|
this.appId = config.appId;
|
|
152
174
|
this.appSecret = config.appSecret;
|
|
153
175
|
}
|
|
@@ -159,8 +181,8 @@ var EasyTwinClient = class {
|
|
|
159
181
|
};
|
|
160
182
|
}
|
|
161
183
|
/** JSON 请求(原生 fetch)。 */
|
|
162
|
-
async request(
|
|
163
|
-
const url = `${this.baseUrl}${
|
|
184
|
+
async request(path7, options = {}) {
|
|
185
|
+
const url = `${this.baseUrl}${path7}`;
|
|
164
186
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
165
187
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
166
188
|
headers["Content-Type"] = "application/json";
|
|
@@ -182,8 +204,8 @@ var EasyTwinClient = class {
|
|
|
182
204
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
183
205
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
184
206
|
*/
|
|
185
|
-
async upload(
|
|
186
|
-
const url = new URL2(`${this.baseUrl}${
|
|
207
|
+
async upload(path7, options) {
|
|
208
|
+
const url = new URL2(`${this.baseUrl}${path7}`);
|
|
187
209
|
const mod = url.protocol === "https:" ? https : http;
|
|
188
210
|
const headers = {
|
|
189
211
|
...this.authHeaders(),
|
|
@@ -225,6 +247,7 @@ var EasyTwinClient = class {
|
|
|
225
247
|
// src/scene.ts
|
|
226
248
|
import { promises as fs2 } from "fs";
|
|
227
249
|
import path2 from "path";
|
|
250
|
+
import { fileURLToPath } from "url";
|
|
228
251
|
function normalizeSceneList(data) {
|
|
229
252
|
const list = Array.isArray(data) ? data : data?.list;
|
|
230
253
|
if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
@@ -237,11 +260,50 @@ function normalizeSceneDetail(data) {
|
|
|
237
260
|
const it = data ?? {};
|
|
238
261
|
return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: data };
|
|
239
262
|
}
|
|
240
|
-
|
|
263
|
+
var EXAMPLE_SCENE_FILE = "scene.example.json";
|
|
264
|
+
var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
|
|
265
|
+
function resolveExampleScenePath() {
|
|
266
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
267
|
+
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D ${EXAMPLE_SCENE_FILE}:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 exampleFile`);
|
|
268
|
+
}
|
|
269
|
+
return path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..", EXAMPLE_SCENE_FILE);
|
|
270
|
+
}
|
|
271
|
+
async function loadExampleScene(exampleFile) {
|
|
272
|
+
const file = exampleFile ?? resolveExampleScenePath();
|
|
273
|
+
let raw;
|
|
274
|
+
try {
|
|
275
|
+
raw = await fs2.readFile(file, "utf8");
|
|
276
|
+
} catch {
|
|
277
|
+
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})`);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(raw);
|
|
281
|
+
} catch {
|
|
282
|
+
throw new Error(`\u672C\u5730\u793A\u4F8B\u573A\u666F\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function deriveExampleSceneId(payload) {
|
|
286
|
+
const root = payload ?? {};
|
|
287
|
+
const id = root?.objs?.map((o) => o.sceneId).find((s) => typeof s === "string" && s.length > 0);
|
|
288
|
+
return id ?? "local";
|
|
289
|
+
}
|
|
290
|
+
async function exampleSceneSummary(exampleFile) {
|
|
291
|
+
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
292
|
+
}
|
|
293
|
+
async function listScenes(client, options = {}) {
|
|
294
|
+
if (client.mock) return [await exampleSceneSummary(options.exampleFile)];
|
|
241
295
|
const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
|
|
242
296
|
return normalizeSceneList(data);
|
|
243
297
|
}
|
|
244
|
-
async function pullScene(client, id) {
|
|
298
|
+
async function pullScene(client, id, options = {}) {
|
|
299
|
+
if (client.mock) {
|
|
300
|
+
const payload = await loadExampleScene(options.exampleFile);
|
|
301
|
+
const sceneId = deriveExampleSceneId(payload);
|
|
302
|
+
if (sceneId !== id) {
|
|
303
|
+
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)`);
|
|
304
|
+
}
|
|
305
|
+
return { id: sceneId, name: MOCK_SCENE_NAME, payload };
|
|
306
|
+
}
|
|
245
307
|
const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
|
|
246
308
|
return normalizeSceneDetail(data);
|
|
247
309
|
}
|
|
@@ -303,7 +365,16 @@ Content-Type: application/octet-stream\r
|
|
|
303
365
|
`, "utf8"));
|
|
304
366
|
return Buffer.concat(parts);
|
|
305
367
|
}
|
|
368
|
+
async function mockUpload(dir, options) {
|
|
369
|
+
const ignore = options.ignore ?? (() => false);
|
|
370
|
+
const files = await collectFiles(dir, ignore);
|
|
371
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
372
|
+
options.onProgress?.({ phase: "collect", current: total, total });
|
|
373
|
+
options.onProgress?.({ phase: "upload", current: total, total });
|
|
374
|
+
return { fileCount: files.length, byteCount: total, mock: true };
|
|
375
|
+
}
|
|
306
376
|
async function uploadDirectory(client, dir, options = {}) {
|
|
377
|
+
if (client.mock) return mockUpload(dir, options);
|
|
307
378
|
const ignore = options.ignore ?? (() => false);
|
|
308
379
|
const files = await collectFiles(dir, ignore);
|
|
309
380
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
@@ -326,9 +397,9 @@ async function uploadDirectory(client, dir, options = {}) {
|
|
|
326
397
|
}
|
|
327
398
|
|
|
328
399
|
// src/skills.ts
|
|
329
|
-
import { promises as fs4 } from "fs";
|
|
400
|
+
import { existsSync, promises as fs4 } from "fs";
|
|
330
401
|
import path4 from "path";
|
|
331
|
-
import { fileURLToPath } from "url";
|
|
402
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
332
403
|
var SKILL_NAMES = [
|
|
333
404
|
"easytwin-render",
|
|
334
405
|
"easytwin-core",
|
|
@@ -340,8 +411,47 @@ var SKILL_NAMES = [
|
|
|
340
411
|
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
341
412
|
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
342
413
|
var META_FILE_NAME = ".easytwin-meta.json";
|
|
414
|
+
var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
415
|
+
{
|
|
416
|
+
compilerOptions: {
|
|
417
|
+
target: "ES2022",
|
|
418
|
+
module: "ESNext",
|
|
419
|
+
moduleResolution: "bundler",
|
|
420
|
+
strict: true,
|
|
421
|
+
skipLibCheck: true,
|
|
422
|
+
noEmit: true,
|
|
423
|
+
paths: {
|
|
424
|
+
"@easytwin/runtime": [".easytwin/types"]
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
include: ["src/**/*.ts"]
|
|
428
|
+
},
|
|
429
|
+
null,
|
|
430
|
+
2
|
|
431
|
+
)}
|
|
432
|
+
`;
|
|
433
|
+
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
434
|
+
"skipLibCheck": true,
|
|
435
|
+
"paths": {
|
|
436
|
+
"@easytwin/runtime": [".easytwin/types"]
|
|
437
|
+
}`;
|
|
438
|
+
var RUN_CONTEXT_DECL = `
|
|
439
|
+
/** \u9884\u89C8\u9875 Run \u6309\u94AE\u6CE8\u5165\u7684\u8FD0\u884C\u4E0A\u4E0B\u6587(\u89C1 D12)\u3002 */
|
|
440
|
+
export interface EasyTwinRunContext {
|
|
441
|
+
engine: RuntimeEngine;
|
|
442
|
+
runtime: typeof import("@easytwin/runtime");
|
|
443
|
+
sceneJson: SceneJson;
|
|
444
|
+
}
|
|
445
|
+
`;
|
|
446
|
+
function buildRuntimeTypesContent(sourceDts) {
|
|
447
|
+
const trimmed = sourceDts.replace(/\s+$/, "");
|
|
448
|
+
if (trimmed.includes("export interface EasyTwinRunContext")) return `${trimmed}
|
|
449
|
+
`;
|
|
450
|
+
return `${trimmed}
|
|
451
|
+
${RUN_CONTEXT_DECL}`;
|
|
452
|
+
}
|
|
343
453
|
function normalizeTargets(target = "all") {
|
|
344
|
-
if (target === "all") return ["cursor", "claude", "codex"];
|
|
454
|
+
if (target === "all") return ["cursor", "claude", "codex", "qoder"];
|
|
345
455
|
if (Array.isArray(target)) return [...new Set(target)];
|
|
346
456
|
return [target];
|
|
347
457
|
}
|
|
@@ -349,9 +459,20 @@ function resolveSkillsSourceDir() {
|
|
|
349
459
|
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
350
460
|
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
461
|
}
|
|
352
|
-
const here = path4.dirname(
|
|
462
|
+
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
353
463
|
return path4.resolve(here, "..", "skills");
|
|
354
464
|
}
|
|
465
|
+
function resolveRuntimeTypesSourceFile() {
|
|
466
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
467
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
|
|
468
|
+
}
|
|
469
|
+
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
470
|
+
const fromDist = path4.join(here, "runtime-types", "index.d.ts");
|
|
471
|
+
const fromSrc = path4.resolve(here, "lib", "index.d.ts");
|
|
472
|
+
if (existsSync(fromDist)) return fromDist;
|
|
473
|
+
if (existsSync(fromSrc)) return fromSrc;
|
|
474
|
+
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
|
|
475
|
+
}
|
|
355
476
|
async function readJson(file) {
|
|
356
477
|
return JSON.parse(await fs4.readFile(file, "utf8"));
|
|
357
478
|
}
|
|
@@ -476,6 +597,38 @@ async function syncToCodex(sourceRoot, cwd, version) {
|
|
|
476
597
|
}
|
|
477
598
|
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
478
599
|
}
|
|
600
|
+
async function syncRuntimeTypes(cwd, typesSourceFile) {
|
|
601
|
+
const destDir = path4.join(cwd, ".easytwin", "types");
|
|
602
|
+
const destFile = path4.join(destDir, "index.d.ts");
|
|
603
|
+
const content = buildRuntimeTypesContent(await fs4.readFile(typesSourceFile, "utf8"));
|
|
604
|
+
let exists = true;
|
|
605
|
+
let current = "";
|
|
606
|
+
try {
|
|
607
|
+
current = await fs4.readFile(destFile, "utf8");
|
|
608
|
+
} catch {
|
|
609
|
+
exists = false;
|
|
610
|
+
}
|
|
611
|
+
const action = actionFor(exists, current === content);
|
|
612
|
+
if (action !== "unchanged") {
|
|
613
|
+
await fs4.mkdir(destDir, { recursive: true });
|
|
614
|
+
await fs4.writeFile(destFile, content, "utf8");
|
|
615
|
+
}
|
|
616
|
+
const tsconfigPath = path4.join(cwd, "tsconfig.json");
|
|
617
|
+
let tsconfig;
|
|
618
|
+
let pathsHint;
|
|
619
|
+
try {
|
|
620
|
+
const existing = await fs4.readFile(tsconfigPath, "utf8");
|
|
621
|
+
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
622
|
+
else {
|
|
623
|
+
tsconfig = "manual-paths";
|
|
624
|
+
pathsHint = TSCONFIG_PATHS_HINT;
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
await fs4.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
628
|
+
tsconfig = "created";
|
|
629
|
+
}
|
|
630
|
+
return { action, tsconfig, pathsHint };
|
|
631
|
+
}
|
|
479
632
|
async function syncSkills(options) {
|
|
480
633
|
const targets = normalizeTargets(options.targets ?? "all");
|
|
481
634
|
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
@@ -484,9 +637,106 @@ async function syncSkills(options) {
|
|
|
484
637
|
for (const target of targets) {
|
|
485
638
|
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
|
|
486
639
|
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
|
|
640
|
+
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path4.join(options.cwd, ".qoder", "skills"), version));
|
|
487
641
|
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
488
642
|
}
|
|
489
|
-
|
|
643
|
+
const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
|
|
644
|
+
return { summaries, types };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/bundle.ts
|
|
648
|
+
import * as esbuild from "esbuild-wasm";
|
|
649
|
+
import { promises as fs5 } from "fs";
|
|
650
|
+
import path5 from "path";
|
|
651
|
+
var USER_ENTRY = "src/main.ts";
|
|
652
|
+
var DEFAULT_BUNDLE_OUT = "dist/main.js";
|
|
653
|
+
var RUNTIME_MODULE = "@easytwin/runtime";
|
|
654
|
+
var BundleError = class extends Error {
|
|
655
|
+
constructor(message) {
|
|
656
|
+
super(message);
|
|
657
|
+
this.name = "BundleError";
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
function isRelativeOrAbsolute(spec) {
|
|
661
|
+
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path5.isAbsolute(spec);
|
|
662
|
+
}
|
|
663
|
+
function whitelistPlugin() {
|
|
664
|
+
return {
|
|
665
|
+
name: "easytwin-whitelist",
|
|
666
|
+
setup(build2) {
|
|
667
|
+
build2.onResolve({ filter: /.*/ }, (args) => {
|
|
668
|
+
if (args.kind === "entry-point") return void 0;
|
|
669
|
+
if (isRelativeOrAbsolute(args.path)) return void 0;
|
|
670
|
+
if (args.path === RUNTIME_MODULE) return { path: args.path, external: true };
|
|
671
|
+
return {
|
|
672
|
+
errors: [
|
|
673
|
+
{
|
|
674
|
+
text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
};
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function formatEsbuildMessages(messages) {
|
|
683
|
+
return messages.map((m) => {
|
|
684
|
+
const loc = m.location ? `${m.location.file}:${m.location.line}:${m.location.column}: ` : "";
|
|
685
|
+
return `${loc}${m.text}`;
|
|
686
|
+
}).join("\n");
|
|
687
|
+
}
|
|
688
|
+
async function bundleUserCode(options) {
|
|
689
|
+
const cwd = path5.resolve(options.cwd);
|
|
690
|
+
const entry = path5.join(cwd, USER_ENTRY);
|
|
691
|
+
try {
|
|
692
|
+
await fs5.access(entry);
|
|
693
|
+
} catch {
|
|
694
|
+
throw new BundleError(
|
|
695
|
+
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default async function main(ctx)\`\u3002`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
let result;
|
|
699
|
+
try {
|
|
700
|
+
result = await esbuild.build({
|
|
701
|
+
absWorkingDir: cwd,
|
|
702
|
+
entryPoints: [entry],
|
|
703
|
+
bundle: true,
|
|
704
|
+
write: false,
|
|
705
|
+
format: "esm",
|
|
706
|
+
platform: "browser",
|
|
707
|
+
target: "es2022",
|
|
708
|
+
sourcemap: "inline",
|
|
709
|
+
logLevel: "silent",
|
|
710
|
+
plugins: [whitelistPlugin()]
|
|
711
|
+
});
|
|
712
|
+
} catch (err) {
|
|
713
|
+
const errors = err.errors;
|
|
714
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
715
|
+
throw new BundleError(formatEsbuildMessages(errors));
|
|
716
|
+
}
|
|
717
|
+
throw err instanceof Error ? new BundleError(err.message) : err;
|
|
718
|
+
}
|
|
719
|
+
if (result.errors.length > 0) {
|
|
720
|
+
throw new BundleError(formatEsbuildMessages(result.errors));
|
|
721
|
+
}
|
|
722
|
+
const file = result.outputFiles?.[0];
|
|
723
|
+
if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
|
|
724
|
+
const code = file.text;
|
|
725
|
+
const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
|
|
726
|
+
if (options.outFile) {
|
|
727
|
+
const outFile = path5.isAbsolute(options.outFile) ? options.outFile : path5.join(cwd, options.outFile);
|
|
728
|
+
await fs5.mkdir(path5.dirname(outFile), { recursive: true });
|
|
729
|
+
await fs5.writeFile(outFile, code, "utf8");
|
|
730
|
+
}
|
|
731
|
+
return { code, warnings };
|
|
732
|
+
}
|
|
733
|
+
async function typesMissingHint(cwd) {
|
|
734
|
+
try {
|
|
735
|
+
await fs5.access(path5.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
736
|
+
return void 0;
|
|
737
|
+
} catch {
|
|
738
|
+
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
739
|
+
}
|
|
490
740
|
}
|
|
491
741
|
|
|
492
742
|
// src/bin.ts
|
|
@@ -498,6 +748,7 @@ async function readVersion() {
|
|
|
498
748
|
return "0.0.0";
|
|
499
749
|
}
|
|
500
750
|
}
|
|
751
|
+
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\u8D70\u672C\u5730 mock";
|
|
501
752
|
async function prompt(question) {
|
|
502
753
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
503
754
|
try {
|
|
@@ -509,8 +760,17 @@ async function prompt(question) {
|
|
|
509
760
|
async function runInit(cwd, flags) {
|
|
510
761
|
const appId = flags.appId ?? await prompt("App ID: ");
|
|
511
762
|
const appSecret = flags.appSecret ?? await prompt("App Secret: ");
|
|
512
|
-
|
|
763
|
+
let env = "prod";
|
|
764
|
+
if (flags.env !== void 0) {
|
|
765
|
+
env = flags.env.toLowerCase() === "test" ? "test" : "prod";
|
|
766
|
+
} else if (flags.baseUrl === void 0) {
|
|
767
|
+
const envRaw = (await prompt("\u73AF\u5883(prod=\u6B63\u5F0F / test=\u6D4B\u8BD5,\u7F3A\u7701 prod): ")).trim().toLowerCase();
|
|
768
|
+
env = envRaw === "test" ? "test" : "prod";
|
|
769
|
+
}
|
|
770
|
+
const defaultUrl = env === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL;
|
|
771
|
+
const baseUrlInput = flags.baseUrl ?? await prompt(`Base URL(\u7F3A\u7701 ${defaultUrl}): `);
|
|
513
772
|
const input = { appId, appSecret };
|
|
773
|
+
if (env === "test") input.env = "test";
|
|
514
774
|
if (baseUrlInput.trim().length > 0) input.baseUrl = baseUrlInput.trim();
|
|
515
775
|
const result = await initConfig(input, cwd);
|
|
516
776
|
console.log(`\u5DF2\u751F\u6210 ${result.configFile}`);
|
|
@@ -521,10 +781,11 @@ async function runInit(cwd, flags) {
|
|
|
521
781
|
function buildProgram(cwd, version = "0.0.0") {
|
|
522
782
|
const program = new Command();
|
|
523
783
|
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));
|
|
784
|
+
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
785
|
const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
|
|
526
786
|
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F").action(async () => {
|
|
527
787
|
const config = await loadConfig(cwd);
|
|
788
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
528
789
|
const client = new EasyTwinClient(config);
|
|
529
790
|
const scenes = await listScenes(client);
|
|
530
791
|
if (scenes.length === 0) {
|
|
@@ -535,14 +796,16 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
535
796
|
});
|
|
536
797
|
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
798
|
const config = await loadConfig(cwd);
|
|
799
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
538
800
|
const client = new EasyTwinClient(config);
|
|
539
801
|
const scene2 = await pullScene(client, id);
|
|
540
|
-
const out = opts.out ??
|
|
802
|
+
const out = opts.out ?? path6.join(cwd, `${id}.scene.json`);
|
|
541
803
|
const file = await saveSceneJson(scene2, out);
|
|
542
804
|
console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
|
|
543
805
|
});
|
|
544
806
|
program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
|
|
545
807
|
const config = await loadConfig(cwd);
|
|
808
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
546
809
|
const client = new EasyTwinClient(config);
|
|
547
810
|
await uploadDirectory(client, dir, {
|
|
548
811
|
onProgress: (p) => {
|
|
@@ -553,9 +816,16 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
553
816
|
});
|
|
554
817
|
process.stderr.write("\n");
|
|
555
818
|
});
|
|
819
|
+
program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8\u4F9D\u8D56 @easytwin/runtime)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
|
|
820
|
+
const hint = await typesMissingHint(cwd);
|
|
821
|
+
if (hint) console.warn(hint);
|
|
822
|
+
const outFile = path6.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
|
|
823
|
+
await bundleUserCode({ cwd, outFile });
|
|
824
|
+
console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
|
|
825
|
+
});
|
|
556
826
|
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 });
|
|
827
|
+
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) => {
|
|
828
|
+
const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
|
|
559
829
|
for (const s of summaries) {
|
|
560
830
|
if (s.codex) {
|
|
561
831
|
console.log(`[codex] AGENTS.md: ${s.codex.action}`);
|
|
@@ -563,6 +833,9 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
563
833
|
}
|
|
564
834
|
for (const e of s.entries) console.log(`[${s.target}] ${e.name}: ${e.action}`);
|
|
565
835
|
}
|
|
836
|
+
console.log(`[types] ${".easytwin/types"}: ${types.action}`);
|
|
837
|
+
if (types.tsconfig === "created") console.log("[types] \u5DF2\u751F\u6210\u6700\u5C0F tsconfig.json");
|
|
838
|
+
if (types.pathsHint) console.log(types.pathsHint);
|
|
566
839
|
});
|
|
567
840
|
return program;
|
|
568
841
|
}
|