@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 CHANGED
@@ -28,11 +28,13 @@ npm i -g @easytwin/devkit # 全局安装 CLI
28
28
  {
29
29
  "appId": "必填",
30
30
  "appSecret": "必填",
31
- "baseUrl": "可选,缺省官方固定域名"
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` 优先于文件。文件含密钥,永不入库(`init` 会处理 gitignore)。
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. 发布(发布前自动 build + test;publishConfig 已固定 public + npmjs)
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、场景 JSON 格式、官方缺省域名——均未到位,代码中以 `TODO(swagger)` / `TODO(外部输入)` 标记,不臆造。
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 path5 from "path";
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 = "https://api.easytwin.example.com";
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 baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? DEFAULT_BASE_URL;
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(path6, options = {}) {
163
- const url = `${this.baseUrl}${path6}`;
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(path6, options) {
186
- const url = new URL2(`${this.baseUrl}${path6}`);
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
- async function listScenes(client) {
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(fileURLToPath(import.meta.url));
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
- return summaries;
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
- const baseUrlInput = flags.baseUrl ?? await prompt(`Base URL(\u7F3A\u7701 ${DEFAULT_BASE_URL}): `);
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 ?? path5.join(cwd, `${id}.scene.json`);
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
  }