@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 +78 -76
- package/dist/bin.js +262 -49
- package/dist/index.d.ts +82 -8
- package/dist/index.js +220 -35
- package/package.json +2 -2
- package/skills/easytwin-bootstrap/SKILL.md +37 -33
- package/skills/easytwin-develop/SKILL.md +32 -32
- package/skills/easytwin-render/SKILL.md +1 -1
- package/skills/easytwin-render/references/intro.md +1 -1
- package/skills/easytwin-scene/SKILL.md +18 -38
- package/skills/easytwin-upload/SKILL.md +1 -1
- package/dist/runtime-types/index.d.ts +0 -8248
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` | 交互式生成
|
|
18
|
-
| `easytwin scene list` | 列出凭据可见的场景,并把摘要写入
|
|
19
|
-
| `easytwin scene pull <id> [--out <path>]` | 拉取场景 JSON
|
|
20
|
-
| `easytwin
|
|
21
|
-
| `easytwin
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
pnpm
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
101
|
-
const file = configFilePath(cwd);
|
|
102
|
-
let raw;
|
|
128
|
+
async function tryReadText(file) {
|
|
103
129
|
try {
|
|
104
|
-
|
|
130
|
+
return await fs.readFile(file, "utf8");
|
|
105
131
|
} catch {
|
|
106
|
-
|
|
132
|
+
return void 0;
|
|
107
133
|
}
|
|
108
|
-
|
|
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(
|
|
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
|
-
|
|
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 +
|
|
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
|
|
433
|
-
const sceneId = deriveExampleSceneId(
|
|
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
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
449
|
-
return
|
|
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
|
-
|
|
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
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (
|
|
963
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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 ${
|
|
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\
|
|
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
|
|
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);
|