@easytwin/devkit 0.1.3 → 0.1.4

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
@@ -1,76 +1,78 @@
1
- # @easytwin/devkit
2
-
3
- EasyTwin 第三方应用开发工具集的核心包:lib(函数库)与 bin(`easytwin` 命令)双导出,供人类与 AI agent(如 Cursor / Claude Code / Codex)共同使用。设计蓝图见仓库根 `AGENTS.md`。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- npm i -g @easytwin/devkit # 全局安装 CLI
9
- ```
10
-
11
- 也可作为库安装:`npm i @easytwin/devkit`。
12
-
13
- ## 命令
14
-
15
- | 命令 | 行为 |
16
- | --- | --- |
17
- | `easytwin init` | 交互式生成 `easytwin.config.json`,并追加进 `.gitignore`(无则创建) |
18
- | `easytwin scene list` | 列出凭据可见的场景,并把摘要写入 `easytwin.config.json.scenes` |
19
- | `easytwin scene pull <id> [--out <path>]` | 拉取场景 JSON 到本地 |
20
- | `easytwin upload [dir]` | 对照远端全量对齐工作区(缺省 cwd;跳过插件产物) |
21
- | `easytwin skills sync [--target cursor\|claude\|codex\|all]` | 同步 skills 到用户项目,缺省 all |
22
-
23
- ## 配置
24
-
25
- 项目根 `easytwin.config.json`:
26
-
27
- ```json
28
- {
29
- "appId": "必填",
30
- "appSecret": "必填",
31
- "env": "可选,prod=正式 / test=测试,缺省 prod",
32
- "baseUrl": "可选,缺省按 env 选官方域名",
33
- "ossUrl": "可选,缺省官方 OSS",
34
- "scenes": "可选,由 scene list 写入的场景摘要"
35
- }
36
- ```
37
-
38
- 环境变量 `EASYTWIN_APP_ID` / `EASYTWIN_APP_SECRET` / `EASYTWIN_BASE_URL` / `EASYTWIN_ENV` / `EASYTWIN_OSS_URL` 优先于文件。请求带 `x-app-id`(App ID)与 `x-app-secret`(App Secret),不走 OP 网关。官方 API:正式 `http://saas-twin.k8s.dtstack.cn/`、测试 `http://172.16.125.3:10100/`,缺省按 env 选择。官方 OSS:`https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/`,供 twin runtime 加载 webp/draco/组件脚本。文件含密钥,永不入库(`init` 会处理 gitignore)。
39
-
40
- ## skills
41
-
42
- 六件套(`easytwin-render` / `-core` / `-develop` / `-bootstrap` / `-scene` / `-upload`)随包分发,通过 `easytwin skills sync` 显式写入 Cursor(`.cursor/skills/`)、Claude Code(`.claude/skills/`)与 Codex(`AGENTS.md` 标记段),重复执行幂等。
43
-
44
- ## 发布到 npm
45
-
46
- ```bash
47
- # 1. 登录(写入 ~/.npmrc 的 npmjs token;CI 用 NPM_TOKEN 环境变量)
48
- npm login --registry https://registry.npmjs.org/
49
-
50
- # 2. 确认发布产物应包含 dist/ 与 skills/
51
- npm pack --dry-run
52
-
53
- # 3. 升版本
54
- npm version patch|minor|major
55
-
56
- # 4. 发布(必须先升版本:同版本已在 npm 上会被跳过,提示 There are no new packages that should be published)
57
- # 在仓库根目录用 run,不要直接 pnpm publish(那是工作区递归发布内置命令)
58
- pnpm run publish
59
- ```
60
-
61
- - scoped 包需 publishConfig.access 为 public,已配置;
62
- - 本机默认 registry 是 npmmirror,publishConfig.registry 已固定为官方 npmjs;
63
- - 包名是否被占用可用 npm view @easytwin/devkit 确认;
64
- - repository 目前指向内网 GitLab,公网发布前建议改为公开地址(可选)。
65
-
66
- ## 开发
67
-
68
- ```bash
69
- pnpm install
70
- pnpm -r build
71
- pnpm -r test
72
- ```
73
-
74
- ## 已知外部输入(TODO)
75
-
76
- 渲染 API 文档与 d.ts 仍未到位,skills 中以 `TODO(外部输入)` 标记,不臆造。场景列表/拉取与工作区代码上传已按内部 swagger(SDK 应用场景 / SDK 应用代码)落地;官方 API 域名、官方 OSS 与场景 JSON 格式已定(见上)。
1
+ # @easytwin/devkit
2
+
3
+ EasyTwin 第三方应用开发工具集的核心包:lib(函数库)与 bin(`easytwin` 命令)双导出,供人类与 AI agent(如 Cursor / Claude Code / Codex)共同使用。设计蓝图见仓库根 `AGENTS.md`。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm i -g @easytwin/devkit # 全局安装 CLI
9
+ ```
10
+
11
+ 也可作为库安装:`npm i @easytwin/devkit`。
12
+
13
+ ## 命令
14
+
15
+ | 命令 | 行为 |
16
+ | --- | --- |
17
+ | `easytwin init` | 交互式生成 `.easytwin/easytwin.config.json`,并将凭据与 `.easytwin/scenes/` 追加进 `.gitignore`(无则创建) |
18
+ | `easytwin scene list` | 列出凭据可见的场景,并把摘要写入 `.easytwin/easytwin.config.json.scenes` |
19
+ | `easytwin scene pull <id> [--out <path>]` | 拉取场景 JSON:写入 `.easytwin/scenes/<id>.scene.json`;`--out` 再额外写一份 |
20
+ | `easytwin scene inspect <id> [--name] [--type]` | 只读本地快照,打印对象树(id/name/type);超过 200 节点截断 |
21
+ | `easytwin upload [dir]` | 对照远端全量对齐工作区(缺省 cwd;跳过插件产物) |
22
+ | `easytwin skills sync [--target cursor\|claude\|codex\|all]` | 同步 skills 到用户项目,缺省 all |
23
+
24
+ ## 配置
25
+
26
+ `.easytwin/easytwin.config.json`(项目级):
27
+
28
+ ```json
29
+ {
30
+ "appId": "必填",
31
+ "appSecret": "必填",
32
+ "env": "可选,prod=正式 / test=测试,缺省 prod",
33
+ "baseUrl": "可选,缺省按 env 选官方域名",
34
+ "ossUrl": "可选,缺省官方 OSS",
35
+ "scenes": "可选,由 scene list 写入的场景摘要"
36
+ }
37
+ ```
38
+
39
+ 环境变量 `EASYTWIN_APP_ID` / `EASYTWIN_APP_SECRET` / `EASYTWIN_BASE_URL` / `EASYTWIN_ENV` / `EASYTWIN_OSS_URL` 优先于文件。请求带 `x-app-id`(App ID)与 `x-app-secret`(App Secret),不走 OP 网关。官方 API:正式 `http://saas-twin.k8s.dtstack.cn/`、测试 `http://172.16.125.3:10100/`,缺省按 env 选择。官方 OSS 资产根:`https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/`,供 twin runtime 加载 webp/draco/组件脚本。runtime 类型:`env=prod` 用正式桶 `https://dt-easyv-prod.oss-cn-hangzhou.aliyuncs.com/easytwin/system/libs/runtime/types/`,其余用测试桶同路径。文件含密钥,永不入库(`init` 会处理 gitignore)。
40
+
41
+ ## skills
42
+
43
+ 六件套(`easytwin-render` / `-core` / `-develop` / `-bootstrap` / `-scene` / `-upload`)随包分发,通过 `easytwin skills sync` 显式写入 Cursor(`.cursor/skills/`)、Claude Code(`.claude/skills/`)与 Codex(`AGENTS.md` 标记段),重复执行幂等。
44
+
45
+ ## 发布到 npm
46
+
47
+ ```bash
48
+ # 1. 登录(写入 ~/.npmrc 的 npmjs token;CI 用 NPM_TOKEN 环境变量)
49
+ npm login --registry https://registry.npmjs.org/
50
+
51
+ # 2. 确认发布产物应包含 dist/ 与 skills/
52
+ npm pack --dry-run
53
+
54
+ # 3. 升版本
55
+ npm version patch|minor|major
56
+
57
+ # 4. 发布(必须先升版本:同版本已在 npm 上会被跳过,提示 There are no new packages that should be published)
58
+ # 在仓库根目录用 run,不要直接 pnpm publish(那是工作区递归发布内置命令)
59
+ pnpm run publish
60
+ ```
61
+
62
+ - scoped 包需 publishConfig.access public,已配置;
63
+ - 本机默认 registry npmmirror,publishConfig.registry 已固定为官方 npmjs;
64
+ - `pnpm run publish` 只在本次进程清掉 `proxy` / `HTTP_PROXY`,不改 `~/.npmrc`;Clash 日常代理可保持 `http://127.0.0.1:7890`;
65
+ - 包名是否被占用可用 npm view @easytwin/devkit 确认;
66
+ - repository 目前指向内网 GitLab,公网发布前建议改为公开地址(可选)。
67
+
68
+ ## 开发
69
+
70
+ ```bash
71
+ pnpm install
72
+ pnpm -r build
73
+ pnpm -r test
74
+ ```
75
+
76
+ ## 已知外部输入(TODO)
77
+
78
+ 渲染 API 文档与 d.ts 仍未到位,skills 中以 `TODO(外部输入)` 标记,不臆造。场景列表/拉取与工作区代码上传已按内部 swagger(SDK 应用场景 / SDK 应用代码)落地;官方 API 域名、官方 OSS 与场景 JSON 格式已定(见上)。
package/dist/bin.js CHANGED
@@ -12,10 +12,15 @@ import path10 from "path";
12
12
  // src/config.ts
13
13
  import { promises as fs } from "fs";
14
14
  import path from "path";
15
+ var EASYTWIN_DIR = ".easytwin";
15
16
  var CONFIG_FILE_NAME = "easytwin.config.json";
17
+ var EASYTWIN_SCENES_DIRNAME = "scenes";
16
18
  var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
17
19
  var TEST_BASE_URL = "http://172.16.125.3:10100/";
18
- var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
20
+ var TEST_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
21
+ var PROD_OSS_URL = "https://dt-easyv-prod.oss-cn-hangzhou.aliyuncs.com/";
22
+ var DEFAULT_OSS_URL = TEST_OSS_URL;
23
+ var RUNTIME_TYPES_RELPATH = "easytwin/system/libs/runtime/types/index.d.ts";
19
24
  var MOCK_APP_ID = "test";
20
25
  var MOCK_APP_SECRET = "test";
21
26
  function isMockCredentials(appId, appSecret) {
@@ -28,8 +33,31 @@ var ConfigError = class extends Error {
28
33
  }
29
34
  };
30
35
  function configFilePath(cwd) {
36
+ return path.join(cwd, EASYTWIN_DIR, CONFIG_FILE_NAME);
37
+ }
38
+ function joinOssPath(ossUrl, relPath) {
39
+ return `${ossUrl.replace(/\/+$/, "")}/${relPath.replace(/^\/+/, "")}`;
40
+ }
41
+ function resolveRuntimeTypesUrl(options = {}) {
42
+ const root = options.ossUrl ?? (options.env === "prod" ? PROD_OSS_URL : TEST_OSS_URL);
43
+ return joinOssPath(root, RUNTIME_TYPES_RELPATH);
44
+ }
45
+ async function resolveRuntimeTypesUrlForCwd(cwd, env = process.env) {
46
+ let file;
47
+ try {
48
+ file = await readConfigFile(cwd);
49
+ } catch {
50
+ }
51
+ const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file?.env;
52
+ const ossUrl = env.EASYTWIN_OSS_URL ?? file?.ossUrl;
53
+ return resolveRuntimeTypesUrl({ env: easyEnv, ossUrl });
54
+ }
55
+ function legacyConfigFilePath(cwd) {
31
56
  return path.join(cwd, CONFIG_FILE_NAME);
32
57
  }
58
+ function defaultSceneOutPath(cwd, id) {
59
+ return path.join(cwd, EASYTWIN_DIR, EASYTWIN_SCENES_DIRNAME, `${path.basename(id)}.scene.json`);
60
+ }
33
61
  function parseConfig(raw) {
34
62
  let parsed;
35
63
  try {
@@ -97,15 +125,39 @@ function parseConfigScenes(value) {
97
125
  return scene;
98
126
  });
99
127
  }
100
- async function readConfigFile(cwd) {
101
- const file = configFilePath(cwd);
102
- let raw;
128
+ async function tryReadText(file) {
103
129
  try {
104
- raw = await fs.readFile(file, "utf8");
130
+ return await fs.readFile(file, "utf8");
105
131
  } catch {
106
- throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
132
+ return void 0;
107
133
  }
108
- return parseConfig(raw);
134
+ }
135
+ async function removeLegacyConfigFile(cwd) {
136
+ try {
137
+ await fs.unlink(legacyConfigFilePath(cwd));
138
+ } catch {
139
+ }
140
+ }
141
+ async function migrateLegacyConfigIfPresent(cwd) {
142
+ const raw = await tryReadText(legacyConfigFilePath(cwd));
143
+ if (raw === void 0) return void 0;
144
+ const config = parseConfig(raw);
145
+ await writeConfigFile(cwd, config);
146
+ await removeLegacyConfigFile(cwd);
147
+ await appendGitignore(cwd);
148
+ return config;
149
+ }
150
+ async function readConfigFile(cwd) {
151
+ const file = configFilePath(cwd);
152
+ const raw = await tryReadText(file);
153
+ if (raw !== void 0) {
154
+ const config = parseConfig(raw);
155
+ await removeLegacyConfigFile(cwd);
156
+ return config;
157
+ }
158
+ const migrated = await migrateLegacyConfigIfPresent(cwd);
159
+ if (migrated) return migrated;
160
+ throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
109
161
  }
110
162
  function resolveConfig(file, env = process.env) {
111
163
  const appId = env.EASYTWIN_APP_ID ?? file.appId;
@@ -129,12 +181,14 @@ async function writeConfigFile(cwd, config) {
129
181
  if (config.baseUrl) body.baseUrl = config.baseUrl;
130
182
  if (config.ossUrl) body.ossUrl = config.ossUrl;
131
183
  if (config.scenes !== void 0) body.scenes = config.scenes;
132
- await fs.mkdir(cwd, { recursive: true });
184
+ await fs.mkdir(path.dirname(file), { recursive: true });
133
185
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
186
+ await removeLegacyConfigFile(cwd);
134
187
  return file;
135
188
  }
136
189
  var GITIGNORE_FILE_NAME = ".gitignore";
137
- var GITIGNORE_ENTRY = "easytwin.config.json";
190
+ var GITIGNORE_ENTRY = ".easytwin/easytwin.config.json";
191
+ var GITIGNORE_ENTRIES = [GITIGNORE_ENTRY, ".easytwin/scenes/"];
138
192
  async function appendGitignore(cwd) {
139
193
  const file = path.join(cwd, GITIGNORE_FILE_NAME);
140
194
  let content = "";
@@ -145,9 +199,10 @@ async function appendGitignore(cwd) {
145
199
  created = true;
146
200
  }
147
201
  const lines = content.split(/\r?\n/);
148
- if (lines.includes(GITIGNORE_ENTRY)) return { created, added: false };
202
+ const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
203
+ if (missing.length === 0) return { created, added: false };
149
204
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
150
- await fs.writeFile(file, content + prefix + GITIGNORE_ENTRY + "\n", "utf8");
205
+ await fs.writeFile(file, content + prefix + missing.join("\n") + "\n", "utf8");
151
206
  return { created, added: true };
152
207
  }
153
208
  async function initConfig(input, cwd) {
@@ -428,31 +483,165 @@ async function listScenes(client, options = {}) {
428
483
  return scenes;
429
484
  }
430
485
  async function pullScene(client, id, options = {}) {
486
+ let scene;
431
487
  if (client.mock) {
432
- const payload2 = await loadExampleScene(options.exampleFile);
433
- const sceneId = deriveExampleSceneId(payload2);
488
+ const payload = await loadExampleScene(options.exampleFile);
489
+ const sceneId = deriveExampleSceneId(payload);
434
490
  if (sceneId !== id) {
435
491
  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)`);
436
492
  }
437
- return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
438
- }
439
- const data = await fetchLinkedScenes(client);
440
- const scenes = normalizeLinkedScenes(data);
441
- const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
442
- if (!hit) {
443
- const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
444
- throw new Error(
445
- available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
446
- );
493
+ scene = { id: sceneId, name: MOCK_SCENE_NAME, payload };
494
+ } else {
495
+ const data = await fetchLinkedScenes(client);
496
+ const scenes = normalizeLinkedScenes(data);
497
+ const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
498
+ if (!hit) {
499
+ const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
500
+ throw new Error(
501
+ available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
502
+ );
503
+ }
504
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
505
+ scene = { id: hit.sceneKey, name: hit.name, payload };
447
506
  }
448
- const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
449
- return { id: hit.sceneKey, name: hit.name, payload };
507
+ if (options.cwd) await saveSceneJson(scene, defaultSceneOutPath(options.cwd, scene.id));
508
+ return scene;
450
509
  }
451
510
  async function saveSceneJson(scene, out) {
452
511
  await fs2.mkdir(path2.dirname(out), { recursive: true });
453
512
  await fs2.writeFile(out, JSON.stringify(scene.payload ?? scene, null, 2) + "\n", "utf8");
454
513
  return out;
455
514
  }
515
+ function parseSceneStructure(scene) {
516
+ let data = scene;
517
+ if (typeof data === "string") {
518
+ try {
519
+ data = JSON.parse(data);
520
+ } catch {
521
+ return [];
522
+ }
523
+ }
524
+ const root = data ?? {};
525
+ const objs = Array.isArray(root.objs) ? root.objs : [];
526
+ const hierarchy = Array.isArray(root.hierarchy) ? root.hierarchy : [];
527
+ const items = objs.length > 0 ? objs : hierarchy;
528
+ const typeById = /* @__PURE__ */ new Map();
529
+ for (const o of objs) {
530
+ if (typeof o.id === "string" && typeof o.type === "string") typeById.set(o.id, o.type);
531
+ }
532
+ const nodes = /* @__PURE__ */ new Map();
533
+ const order = [];
534
+ for (const item of items) {
535
+ const id = typeof item.id === "string" ? item.id : "";
536
+ if (id.length === 0 || nodes.has(id)) continue;
537
+ const name = typeof item.name === "string" && item.name.length > 0 ? item.name : id;
538
+ nodes.set(id, { id, name, type: typeById.get(id), children: [] });
539
+ order.push(id);
540
+ }
541
+ const childrenOf = /* @__PURE__ */ new Map();
542
+ for (const item of items) {
543
+ const id = typeof item.id === "string" ? item.id : "";
544
+ if (id.length === 0 || !nodes.has(id)) continue;
545
+ const parentId = typeof item.parentObjId === "string" && item.parentObjId.length > 0 ? item.parentObjId : "";
546
+ if (parentId.length === 0 || parentId === id || !nodes.has(parentId)) continue;
547
+ const kids = childrenOf.get(parentId) ?? [];
548
+ kids.push(id);
549
+ childrenOf.set(parentId, kids);
550
+ }
551
+ const isChild = /* @__PURE__ */ new Set();
552
+ for (const kids of childrenOf.values()) for (const k of kids) isChild.add(k);
553
+ const build2 = (id, seen) => {
554
+ const node = nodes.get(id);
555
+ const children = [];
556
+ if (!seen.has(id)) {
557
+ seen.add(id);
558
+ for (const kid of childrenOf.get(id) ?? []) children.push(build2(kid, seen));
559
+ seen.delete(id);
560
+ }
561
+ return { ...node, children };
562
+ };
563
+ return order.filter((id) => !isChild.has(id)).map((id) => build2(id, /* @__PURE__ */ new Set()));
564
+ }
565
+ var INSPECT_NODE_LIMIT = 200;
566
+ var INSPECT_INDENT = " ";
567
+ function nodeMatchesInspect(node, filter) {
568
+ if (filter.name !== void 0 && filter.name.length > 0) {
569
+ if (!node.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
570
+ }
571
+ if (filter.type !== void 0 && filter.type.length > 0) {
572
+ if (node.type !== filter.type) return false;
573
+ }
574
+ return true;
575
+ }
576
+ function filterSceneTree(roots, filter = {}) {
577
+ const hasName = filter.name !== void 0 && filter.name.length > 0;
578
+ const hasType = filter.type !== void 0 && filter.type.length > 0;
579
+ if (!hasName && !hasType) return roots;
580
+ const walk = (node) => {
581
+ const children = [];
582
+ for (const child of node.children) {
583
+ const kept = walk(child);
584
+ if (kept) children.push(kept);
585
+ }
586
+ if (nodeMatchesInspect(node, filter) || children.length > 0) {
587
+ return { ...node, children };
588
+ }
589
+ return void 0;
590
+ };
591
+ return roots.flatMap((node) => {
592
+ const kept = walk(node);
593
+ return kept ? [kept] : [];
594
+ });
595
+ }
596
+ function countSceneTreeNodes(roots) {
597
+ let n = 0;
598
+ const walk = (nodes) => {
599
+ for (const node of nodes) {
600
+ n += 1;
601
+ walk(node.children);
602
+ }
603
+ };
604
+ walk(roots);
605
+ return n;
606
+ }
607
+ function formatSceneTree(roots, limit = INSPECT_NODE_LIMIT) {
608
+ const total = countSceneTreeNodes(roots);
609
+ if (total === 0) {
610
+ return { text: "(\u65E0\u5BF9\u8C61\u7ED3\u6784)\n", printed: 0, total: 0, truncated: false };
611
+ }
612
+ const lines = [];
613
+ let printed = 0;
614
+ const walk = (nodes, depth) => {
615
+ for (const node of nodes) {
616
+ if (printed >= limit) return;
617
+ lines.push(`${INSPECT_INDENT.repeat(depth)}${node.id} ${node.name} ${node.type ?? ""}`);
618
+ printed += 1;
619
+ walk(node.children, depth + 1);
620
+ }
621
+ };
622
+ walk(roots, 0);
623
+ const truncated = printed < total;
624
+ if (truncated) lines.push(`\u5176\u4F59 ${total - printed} \u4E2A,\u8BF7\u52A0 --name/--type`);
625
+ return { text: `${lines.join("\n")}
626
+ `, printed, total, truncated };
627
+ }
628
+ async function inspectScene(options) {
629
+ const file = defaultSceneOutPath(options.cwd, options.id);
630
+ let raw;
631
+ try {
632
+ raw = await fs2.readFile(file, "utf8");
633
+ } catch {
634
+ throw new Error(`\u672A\u627E\u5230\u672C\u5730\u573A\u666F\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin scene pull ${options.id}\``);
635
+ }
636
+ let payload;
637
+ try {
638
+ payload = JSON.parse(raw);
639
+ } catch {
640
+ throw new Error(`\u573A\u666F\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
641
+ }
642
+ const roots = filterSceneTree(parseSceneStructure(payload), { name: options.name, type: options.type });
643
+ return { file, roots, ...formatSceneTree(roots, options.limit ?? INSPECT_NODE_LIMIT) };
644
+ }
456
645
 
457
646
  // src/upload.ts
458
647
  import { promises as fs3 } from "fs";
@@ -685,7 +874,9 @@ function defaultPullIgnore(relPath) {
685
874
  function isSafeRelPath(relPath) {
686
875
  const n = normalizePath(relPath);
687
876
  if (!n) return false;
688
- if (n.toLowerCase() === "easytwin.config.json") return false;
877
+ const lower = n.toLowerCase();
878
+ if (lower === "easytwin.config.json" || lower.endsWith("/easytwin.config.json")) return false;
879
+ if (lower.split("/")[0] === ".easytwin") return false;
689
880
  const base = n.split("/").pop() ?? "";
690
881
  if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
691
882
  if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
@@ -939,6 +1130,12 @@ function buildRuntimeTypesContent(sourceDts) {
939
1130
  return `${sourceDts.replace(/\s+$/, "")}
940
1131
  `;
941
1132
  }
1133
+ var SkillsError = class extends Error {
1134
+ constructor(message) {
1135
+ super(message);
1136
+ this.name = "SkillsError";
1137
+ }
1138
+ };
942
1139
  function normalizeTargets(target = "all") {
943
1140
  if (target === "all") return ["cursor", "claude", "codex", "qoder"];
944
1141
  if (Array.isArray(target)) return [...new Set(target)];
@@ -951,16 +1148,23 @@ function resolveSkillsSourceDir() {
951
1148
  const here = path6.dirname(fileURLToPath2(import.meta.url));
952
1149
  return path6.resolve(here, "..", "skills");
953
1150
  }
954
- function resolveRuntimeTypesSourceFile() {
955
- if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
956
- throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
1151
+ async function fetchRuntimeTypesDts(url, fetchImpl) {
1152
+ let res;
1153
+ try {
1154
+ res = await fetchImpl(url);
1155
+ } catch (err) {
1156
+ throw new SkillsError(
1157
+ `\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:${url} (${err instanceof Error ? err.message : String(err)})`
1158
+ );
957
1159
  }
958
- const here = path6.dirname(fileURLToPath2(import.meta.url));
959
- const fromDist = path6.join(here, "runtime-types", "index.d.ts");
960
- const fromSrc = path6.resolve(here, "lib", "index.d.ts");
961
- if (existsSync(fromDist)) return fromDist;
962
- if (existsSync(fromSrc)) return fromSrc;
963
- throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
1160
+ if (!res.ok) {
1161
+ throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:HTTP ${res.status} ${url}`);
1162
+ }
1163
+ const text = await res.text();
1164
+ if (text.trim().length === 0) {
1165
+ throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:\u7A7A\u54CD\u5E94 ${url}`);
1166
+ }
1167
+ return text;
964
1168
  }
965
1169
  async function readJson(file) {
966
1170
  return JSON.parse(await fs5.readFile(file, "utf8"));
@@ -1047,8 +1251,8 @@ function buildCodexSegment(version) {
1047
1251
  `EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
1048
1252
  "",
1049
1253
  "- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
1050
- "- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
1051
- "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
1254
+ "- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(`.easytwin/easytwin.config.json`)\u3002",
1255
+ "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3001inspect \u672C\u5730\u5BF9\u8C61\u6811\u3002",
1052
1256
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
1053
1257
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
1054
1258
  "- `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",
@@ -1087,11 +1291,11 @@ async function syncToCodex(sourceRoot, cwd, version) {
1087
1291
  }
1088
1292
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
1089
1293
  }
1090
- async function syncRuntimeTypes(cwd, typesSourceFile) {
1294
+ async function syncRuntimeTypes(cwd, sourceDts) {
1091
1295
  const destDir = path6.join(cwd, ".easytwin", "types");
1092
1296
  const destFile = path6.join(destDir, "index.d.ts");
1093
1297
  const appsFile = path6.join(destDir, APPS_TYPES_FILE);
1094
- const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
1298
+ const content = buildRuntimeTypesContent(sourceDts);
1095
1299
  let runtimeCurrent = "";
1096
1300
  let appsCurrent = "";
1097
1301
  let runtimeExists = true;
@@ -1141,7 +1345,11 @@ async function syncSkills(options) {
1141
1345
  else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
1142
1346
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
1143
1347
  }
1144
- const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
1348
+ const typesSource = options.typesSourceFile ? await fs5.readFile(options.typesSourceFile, "utf8") : await fetchRuntimeTypesDts(
1349
+ await resolveRuntimeTypesUrlForCwd(options.cwd, options.env ?? process.env),
1350
+ options.fetch ?? fetch
1351
+ );
1352
+ const types = await syncRuntimeTypes(options.cwd, typesSource);
1145
1353
  return { summaries, types };
1146
1354
  }
1147
1355
 
@@ -2291,7 +2499,7 @@ async function startPreviewServer(options) {
2291
2499
  }
2292
2500
  }
2293
2501
  const client = new EasyTwinClient(config);
2294
- const scene = await pullScene(client, sceneId);
2502
+ const scene = await pullScene(client, sceneId, { cwd });
2295
2503
  const tests = await listWorkspaceTests(cwd);
2296
2504
  const hostname = options.hostname ?? DEFAULT_PREVIEW_HOST;
2297
2505
  const log = options.log ?? ((line) => process.stderr.write(`${line}
@@ -2458,16 +2666,17 @@ async function runInit(cwd, flags) {
2458
2666
  if (baseUrlInput.trim().length > 0) input.baseUrl = baseUrlInput.trim();
2459
2667
  const result = await initConfig(input, cwd);
2460
2668
  console.log(`\u5DF2\u751F\u6210 ${result.configFile}`);
2669
+ const gitignoreHint = GITIGNORE_ENTRIES.join("\u3001");
2461
2670
  console.log(
2462
- result.gitignore.added ? `\u5DF2\u628A ${CONFIG_FILE_NAME} \u8FFD\u52A0\u8FDB .gitignore(\u9632\u6B62\u5BC6\u94A5\u5165\u5E93)` : `.gitignore \u5DF2\u5305\u542B ${CONFIG_FILE_NAME}`
2671
+ result.gitignore.added ? `\u5DF2\u628A ${gitignoreHint} \u8FFD\u52A0\u8FDB .gitignore(\u9632\u6B62\u5BC6\u94A5\u5165\u5E93)` : `.gitignore \u5DF2\u5305\u542B ${gitignoreHint}`
2463
2672
  );
2464
2673
  }
2465
2674
  function buildProgram(cwd, version = "0.0.0") {
2466
2675
  const program = new Command();
2467
2676
  program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
2468
- 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));
2677
+ program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 .easytwin/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));
2469
2678
  const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
2470
- scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
2679
+ scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 .easytwin/easytwin.config.json").action(async () => {
2471
2680
  const config = await loadConfig(cwd);
2472
2681
  if (config.mock) console.log(MOCK_MODE_HINT);
2473
2682
  const client = new EasyTwinClient(config);
@@ -2477,17 +2686,21 @@ function buildProgram(cwd, version = "0.0.0") {
2477
2686
  } else {
2478
2687
  for (const s of scenes) console.log(`${s.id} ${s.name}`);
2479
2688
  }
2480
- console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
2689
+ console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${EASYTWIN_DIR}/${CONFIG_FILE_NAME}`);
2481
2690
  });
2482
- 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) => {
2691
+ scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u989D\u5916\u8F93\u51FA\u8DEF\u5F84(\u4ECD\u4F1A\u5199\u5165 .easytwin/scenes/<id>.scene.json)").action(async (id, opts) => {
2483
2692
  const config = await loadConfig(cwd);
2484
2693
  if (config.mock) console.log(MOCK_MODE_HINT);
2485
2694
  const client = new EasyTwinClient(config);
2486
- const scene2 = await pullScene(client, id);
2487
- const out = opts.out ?? path10.join(cwd, `${id}.scene.json`);
2488
- const file = await saveSceneJson(scene2, out);
2695
+ const scene2 = await pullScene(client, id, { cwd });
2696
+ const out = opts.out;
2697
+ const file = out ? await saveSceneJson(scene2, out) : defaultSceneOutPath(cwd, scene2.id);
2489
2698
  console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
2490
2699
  });
2700
+ scene.command("inspect <id>").description("\u6253\u5370\u672C\u5730\u573A\u666F\u5BF9\u8C61\u6811(id/name/type),\u4E0D\u8BFB\u5B8C\u6574 JSON").option("--name <substr>", "\u6309\u540D\u79F0\u5B50\u4E32\u8FC7\u6EE4(\u4E0D\u533A\u5206\u5927\u5C0F\u5199)").option("--type <type>", "\u6309\u7EC4\u4EF6 type \u7CBE\u786E\u8FC7\u6EE4").action(async (id, opts) => {
2701
+ const result = await inspectScene({ cwd, id, name: opts.name, type: opts.type });
2702
+ process.stdout.write(result.text);
2703
+ });
2491
2704
  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) => {
2492
2705
  const config = await loadConfig(cwd);
2493
2706
  if (config.mock) console.log(MOCK_MODE_HINT);