@easytwin/devkit 0.1.0
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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/bin.js +586 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +181 -0
- package/dist/index.js +551 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/skills/easytwin-bootstrap/SKILL.md +26 -0
- package/skills/easytwin-core/SKILL.md +25 -0
- package/skills/easytwin-core/references/base-classes.md +32 -0
- package/skills/easytwin-core/references/camera.md +33 -0
- package/skills/easytwin-core/references/engine.md +88 -0
- package/skills/easytwin-core/references/physics.md +30 -0
- package/skills/easytwin-core/references/virtual-components.md +169 -0
- package/skills/easytwin-develop/SKILL.md +25 -0
- package/skills/easytwin-render/SKILL.md +42 -0
- package/skills/easytwin-render/references/intro.md +81 -0
- package/skills/easytwin-render/references/lifecycle-events.md +79 -0
- package/skills/easytwin-render/references/rendering-apis.md +116 -0
- package/skills/easytwin-render/references/scene-and-assets.md +94 -0
- package/skills/easytwin-scene/SKILL.md +26 -0
- package/skills/easytwin-upload/SKILL.md +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var CONFIG_FILE_NAME = "easytwin.config.json";
|
|
5
|
+
var DEFAULT_BASE_URL = "https://api.easytwin.example.com";
|
|
6
|
+
var ConfigError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ConfigError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function configFilePath(cwd) {
|
|
13
|
+
return path.join(cwd, CONFIG_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
function parseConfig(raw) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
throw new ConfigError(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON:${CONFIG_FILE_NAME}`);
|
|
21
|
+
}
|
|
22
|
+
return validateConfigShape(parsed);
|
|
23
|
+
}
|
|
24
|
+
function validateConfigShape(value) {
|
|
25
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
26
|
+
throw new ConfigError("\u914D\u7F6E\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
|
|
27
|
+
}
|
|
28
|
+
const v = value;
|
|
29
|
+
if (typeof v.appId !== "string" || v.appId.length === 0) {
|
|
30
|
+
throw new ConfigError("\u914D\u7F6E\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 appId");
|
|
31
|
+
}
|
|
32
|
+
if (typeof v.appSecret !== "string" || v.appSecret.length === 0) {
|
|
33
|
+
throw new ConfigError("\u914D\u7F6E\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 appSecret");
|
|
34
|
+
}
|
|
35
|
+
if (v.baseUrl !== void 0 && (typeof v.baseUrl !== "string" || v.baseUrl.length === 0)) {
|
|
36
|
+
throw new ConfigError("baseUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
37
|
+
}
|
|
38
|
+
const config = { appId: v.appId, appSecret: v.appSecret };
|
|
39
|
+
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
async function readConfigFile(cwd) {
|
|
43
|
+
const file = configFilePath(cwd);
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = await fs.readFile(file, "utf8");
|
|
47
|
+
} catch {
|
|
48
|
+
throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
|
|
49
|
+
}
|
|
50
|
+
return parseConfig(raw);
|
|
51
|
+
}
|
|
52
|
+
function resolveConfig(file, env = process.env) {
|
|
53
|
+
const appId = env.EASYTWIN_APP_ID ?? file.appId;
|
|
54
|
+
const appSecret = env.EASYTWIN_APP_SECRET ?? file.appSecret;
|
|
55
|
+
const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? DEFAULT_BASE_URL;
|
|
56
|
+
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");
|
|
57
|
+
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");
|
|
58
|
+
const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
|
|
59
|
+
return { appId, appSecret, baseUrl, source };
|
|
60
|
+
}
|
|
61
|
+
async function loadConfig(cwd, env = process.env) {
|
|
62
|
+
return resolveConfig(await readConfigFile(cwd), env);
|
|
63
|
+
}
|
|
64
|
+
async function writeConfigFile(cwd, config) {
|
|
65
|
+
const file = configFilePath(cwd);
|
|
66
|
+
const body = { appId: config.appId, appSecret: config.appSecret };
|
|
67
|
+
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
68
|
+
await fs.mkdir(cwd, { recursive: true });
|
|
69
|
+
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
70
|
+
return file;
|
|
71
|
+
}
|
|
72
|
+
var GITIGNORE_FILE_NAME = ".gitignore";
|
|
73
|
+
var GITIGNORE_ENTRY = "easytwin.config.json";
|
|
74
|
+
async function appendGitignore(cwd) {
|
|
75
|
+
const file = path.join(cwd, GITIGNORE_FILE_NAME);
|
|
76
|
+
let content = "";
|
|
77
|
+
let created = false;
|
|
78
|
+
try {
|
|
79
|
+
content = await fs.readFile(file, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
created = true;
|
|
82
|
+
}
|
|
83
|
+
const lines = content.split(/\r?\n/);
|
|
84
|
+
if (lines.includes(GITIGNORE_ENTRY)) return { created, added: false };
|
|
85
|
+
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
86
|
+
await fs.writeFile(file, content + prefix + GITIGNORE_ENTRY + "\n", "utf8");
|
|
87
|
+
return { created, added: true };
|
|
88
|
+
}
|
|
89
|
+
async function initConfig(input, cwd) {
|
|
90
|
+
const validated = validateConfigShape(input);
|
|
91
|
+
const configFile = await writeConfigFile(cwd, validated);
|
|
92
|
+
const gitignore = await appendGitignore(cwd);
|
|
93
|
+
return { configFile, gitignore };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/client.ts
|
|
97
|
+
import http from "http";
|
|
98
|
+
import https from "https";
|
|
99
|
+
import { URL } from "url";
|
|
100
|
+
var AUTH_HEADER = "Authorization";
|
|
101
|
+
var BEARER_PREFIX = "Bearer";
|
|
102
|
+
var APP_ID_HEADER = "X-Easytwin-App-Id";
|
|
103
|
+
var ENDPOINTS = {
|
|
104
|
+
scenes: "/api/scenes",
|
|
105
|
+
scene: (id) => `/api/scenes/${encodeURIComponent(id)}`,
|
|
106
|
+
upload: "/api/upload"
|
|
107
|
+
};
|
|
108
|
+
var EasyTwinApiError = class extends Error {
|
|
109
|
+
status;
|
|
110
|
+
body;
|
|
111
|
+
constructor(status, message, body) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "EasyTwinApiError";
|
|
114
|
+
this.status = status;
|
|
115
|
+
this.body = body;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function messageFromBody(body) {
|
|
119
|
+
if (typeof body === "object" && body !== null) {
|
|
120
|
+
const m = body.message;
|
|
121
|
+
if (typeof m === "string") return m;
|
|
122
|
+
}
|
|
123
|
+
return void 0;
|
|
124
|
+
}
|
|
125
|
+
function parseResponseBody(buffer) {
|
|
126
|
+
const text = buffer.toString("utf8");
|
|
127
|
+
if (text.length === 0) return void 0;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(text);
|
|
130
|
+
} catch {
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
var EasyTwinClient = class {
|
|
135
|
+
baseUrl;
|
|
136
|
+
appId;
|
|
137
|
+
appSecret;
|
|
138
|
+
constructor(config) {
|
|
139
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
140
|
+
this.appId = config.appId;
|
|
141
|
+
this.appSecret = config.appSecret;
|
|
142
|
+
}
|
|
143
|
+
/** 全仓唯一认证头注入点。 */
|
|
144
|
+
authHeaders() {
|
|
145
|
+
return {
|
|
146
|
+
[AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
|
|
147
|
+
[APP_ID_HEADER]: this.appId
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** JSON 请求(原生 fetch)。 */
|
|
151
|
+
async request(path5, options = {}) {
|
|
152
|
+
const url = `${this.baseUrl}${path5}`;
|
|
153
|
+
const headers = { ...this.authHeaders(), ...options.headers };
|
|
154
|
+
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
155
|
+
headers["Content-Type"] = "application/json";
|
|
156
|
+
}
|
|
157
|
+
let res;
|
|
158
|
+
try {
|
|
159
|
+
res = await fetch(url, {
|
|
160
|
+
method: options.method ?? "GET",
|
|
161
|
+
headers,
|
|
162
|
+
body: options.body,
|
|
163
|
+
signal: options.signal
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new EasyTwinApiError(0, `\u8BF7\u6C42\u5931\u8D25:${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
}
|
|
168
|
+
return this.handleStatus(res.status, Buffer.from(await res.arrayBuffer()));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
172
|
+
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
173
|
+
*/
|
|
174
|
+
async upload(path5, options) {
|
|
175
|
+
const url = new URL(`${this.baseUrl}${path5}`);
|
|
176
|
+
const mod = url.protocol === "https:" ? https : http;
|
|
177
|
+
const headers = {
|
|
178
|
+
...this.authHeaders(),
|
|
179
|
+
"Content-Type": `multipart/form-data; boundary=${options.boundary}`,
|
|
180
|
+
"Content-Length": String(options.body.length)
|
|
181
|
+
};
|
|
182
|
+
const raw = await new Promise((resolve, reject) => {
|
|
183
|
+
const req = mod.request(url, { method: "POST", headers }, (res) => {
|
|
184
|
+
const chunks = [];
|
|
185
|
+
res.on("data", (c) => chunks.push(c));
|
|
186
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks) }));
|
|
187
|
+
});
|
|
188
|
+
req.on("error", reject);
|
|
189
|
+
const CHUNK = 64 * 1024;
|
|
190
|
+
let offset = 0;
|
|
191
|
+
const sendNext = () => {
|
|
192
|
+
if (offset >= options.body.length) {
|
|
193
|
+
req.end();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const chunk = options.body.subarray(offset, offset + CHUNK);
|
|
197
|
+
offset += chunk.length;
|
|
198
|
+
req.write(chunk, () => {
|
|
199
|
+
options.onBytes?.(chunk.length);
|
|
200
|
+
sendNext();
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
sendNext();
|
|
204
|
+
});
|
|
205
|
+
return this.handleStatus(raw.status, raw.body);
|
|
206
|
+
}
|
|
207
|
+
handleStatus(status, body) {
|
|
208
|
+
if (status >= 200 && status < 300) return parseResponseBody(body);
|
|
209
|
+
const parsed = parseResponseBody(body);
|
|
210
|
+
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/scene.ts
|
|
215
|
+
import { promises as fs2 } from "fs";
|
|
216
|
+
import path2 from "path";
|
|
217
|
+
function normalizeSceneList(data) {
|
|
218
|
+
const list = Array.isArray(data) ? data : data?.list;
|
|
219
|
+
if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
220
|
+
return list.map((item) => {
|
|
221
|
+
const it = item ?? {};
|
|
222
|
+
return { id: String(it.id ?? ""), name: String(it.name ?? it.id ?? "") };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function normalizeSceneDetail(data) {
|
|
226
|
+
const it = data ?? {};
|
|
227
|
+
return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: data };
|
|
228
|
+
}
|
|
229
|
+
async function listScenes(client) {
|
|
230
|
+
const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
|
|
231
|
+
return normalizeSceneList(data);
|
|
232
|
+
}
|
|
233
|
+
async function pullScene(client, id) {
|
|
234
|
+
const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
|
|
235
|
+
return normalizeSceneDetail(data);
|
|
236
|
+
}
|
|
237
|
+
async function saveSceneJson(scene, out) {
|
|
238
|
+
await fs2.mkdir(path2.dirname(out), { recursive: true });
|
|
239
|
+
await fs2.writeFile(out, JSON.stringify(scene.payload ?? scene, null, 2) + "\n", "utf8");
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/upload.ts
|
|
244
|
+
import { promises as fs3 } from "fs";
|
|
245
|
+
import path3 from "path";
|
|
246
|
+
var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([".git"]);
|
|
247
|
+
async function collectFiles(dir, ignore = () => false) {
|
|
248
|
+
const files = [];
|
|
249
|
+
async function walk(current, rel) {
|
|
250
|
+
const entries = await fs3.readdir(current, { withFileTypes: true });
|
|
251
|
+
for (const entry of entries) {
|
|
252
|
+
const relPath = path3.posix.join(rel, entry.name);
|
|
253
|
+
if (ignore(relPath)) continue;
|
|
254
|
+
if (entry.isDirectory()) {
|
|
255
|
+
if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
|
|
256
|
+
await walk(path3.join(current, entry.name), relPath);
|
|
257
|
+
} else if (entry.isFile()) {
|
|
258
|
+
const absPath = path3.join(current, entry.name);
|
|
259
|
+
const stat = await fs3.stat(absPath);
|
|
260
|
+
files.push({ relPath, absPath, size: stat.size });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await walk(dir, "");
|
|
265
|
+
files.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
266
|
+
return files;
|
|
267
|
+
}
|
|
268
|
+
function buildMultipartBody(boundary, fields, file) {
|
|
269
|
+
const parts = [];
|
|
270
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
271
|
+
parts.push(
|
|
272
|
+
Buffer.from(`--${boundary}\r
|
|
273
|
+
Content-Disposition: form-data; name="${key}"\r
|
|
274
|
+
\r
|
|
275
|
+
${value}\r
|
|
276
|
+
`, "utf8")
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
parts.push(
|
|
280
|
+
Buffer.from(
|
|
281
|
+
`--${boundary}\r
|
|
282
|
+
Content-Disposition: form-data; name="file"; filename="${file.name}"\r
|
|
283
|
+
Content-Type: application/octet-stream\r
|
|
284
|
+
\r
|
|
285
|
+
`,
|
|
286
|
+
"utf8"
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
parts.push(file.content);
|
|
290
|
+
parts.push(Buffer.from(`\r
|
|
291
|
+
--${boundary}--\r
|
|
292
|
+
`, "utf8"));
|
|
293
|
+
return Buffer.concat(parts);
|
|
294
|
+
}
|
|
295
|
+
async function uploadDirectory(client, dir, options = {}) {
|
|
296
|
+
const ignore = options.ignore ?? (() => false);
|
|
297
|
+
const files = await collectFiles(dir, ignore);
|
|
298
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
299
|
+
options.onProgress?.({ phase: "collect", current: total, total });
|
|
300
|
+
let sent = 0;
|
|
301
|
+
for (const file of files) {
|
|
302
|
+
const content = await fs3.readFile(file.absPath);
|
|
303
|
+
const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
304
|
+
const body = buildMultipartBody(
|
|
305
|
+
boundary,
|
|
306
|
+
// TODO(swagger): multipart 字段名/结构待定,当前为占位约定。
|
|
307
|
+
{ path: file.relPath },
|
|
308
|
+
{ name: path3.basename(file.relPath), content }
|
|
309
|
+
);
|
|
310
|
+
await client.upload(ENDPOINTS.upload, { boundary, body });
|
|
311
|
+
sent += file.size;
|
|
312
|
+
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
313
|
+
}
|
|
314
|
+
return { fileCount: files.length, byteCount: sent };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/skills.ts
|
|
318
|
+
import { promises as fs4 } from "fs";
|
|
319
|
+
import path4 from "path";
|
|
320
|
+
import { fileURLToPath } from "url";
|
|
321
|
+
var SKILL_NAMES = [
|
|
322
|
+
"easytwin-render",
|
|
323
|
+
"easytwin-core",
|
|
324
|
+
"easytwin-develop",
|
|
325
|
+
"easytwin-bootstrap",
|
|
326
|
+
"easytwin-scene",
|
|
327
|
+
"easytwin-upload"
|
|
328
|
+
];
|
|
329
|
+
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
330
|
+
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
331
|
+
var META_FILE_NAME = ".easytwin-meta.json";
|
|
332
|
+
function normalizeTargets(target = "all") {
|
|
333
|
+
if (target === "all") return ["cursor", "claude", "codex"];
|
|
334
|
+
if (Array.isArray(target)) return [...new Set(target)];
|
|
335
|
+
return [target];
|
|
336
|
+
}
|
|
337
|
+
function resolveSkillsSourceDir() {
|
|
338
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
339
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
|
|
340
|
+
}
|
|
341
|
+
const here = path4.dirname(fileURLToPath(import.meta.url));
|
|
342
|
+
return path4.resolve(here, "..", "skills");
|
|
343
|
+
}
|
|
344
|
+
async function readJson(file) {
|
|
345
|
+
return JSON.parse(await fs4.readFile(file, "utf8"));
|
|
346
|
+
}
|
|
347
|
+
async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
|
|
348
|
+
const marker = path4.join(skillsDir, ".easytwin-source.json");
|
|
349
|
+
try {
|
|
350
|
+
const meta = await readJson(marker);
|
|
351
|
+
if (typeof meta.version === "string") return meta.version;
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const pkg = await readJson(path4.resolve(skillsDir, "..", "package.json"));
|
|
356
|
+
return pkg.version;
|
|
357
|
+
} catch {
|
|
358
|
+
return "0.0.0";
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function copyDir(src, dest) {
|
|
362
|
+
await fs4.mkdir(dest, { recursive: true });
|
|
363
|
+
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
const s = path4.join(src, entry.name);
|
|
366
|
+
const d = path4.join(dest, entry.name);
|
|
367
|
+
if (entry.isDirectory()) await copyDir(s, d);
|
|
368
|
+
else if (entry.isFile()) await fs4.copyFile(s, d);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function dirMatches(src, dest) {
|
|
372
|
+
let sourceEntries;
|
|
373
|
+
try {
|
|
374
|
+
sourceEntries = await fs4.readdir(src);
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
for (const name of sourceEntries) {
|
|
379
|
+
const s = path4.join(src, name);
|
|
380
|
+
const d = path4.join(dest, name);
|
|
381
|
+
const sStat = await fs4.stat(s);
|
|
382
|
+
if (sStat.isDirectory()) {
|
|
383
|
+
if (!await dirMatches(s, d)) return false;
|
|
384
|
+
} else {
|
|
385
|
+
let dStat;
|
|
386
|
+
try {
|
|
387
|
+
dStat = await fs4.stat(d);
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
392
|
+
if (!(await fs4.readFile(s)).equals(await fs4.readFile(d))) return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
function actionFor(exists, matches) {
|
|
398
|
+
if (!exists) return "created";
|
|
399
|
+
return matches ? "unchanged" : "updated";
|
|
400
|
+
}
|
|
401
|
+
async function writeMeta(destDir, version) {
|
|
402
|
+
const meta = { name: "@easytwin/devkit", version };
|
|
403
|
+
await fs4.writeFile(path4.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
404
|
+
}
|
|
405
|
+
async function syncToDir(target, sourceRoot, targetRoot, version) {
|
|
406
|
+
const entries = [];
|
|
407
|
+
for (const name of SKILL_NAMES) {
|
|
408
|
+
const src = path4.join(sourceRoot, name);
|
|
409
|
+
const dest = path4.join(targetRoot, name);
|
|
410
|
+
const exists = await fs4.stat(dest).then(() => true).catch(() => false);
|
|
411
|
+
const matches = await dirMatches(src, dest);
|
|
412
|
+
const action = actionFor(exists, matches);
|
|
413
|
+
if (action !== "unchanged") {
|
|
414
|
+
await fs4.rm(dest, { recursive: true, force: true });
|
|
415
|
+
await copyDir(src, dest);
|
|
416
|
+
}
|
|
417
|
+
entries.push({ name, action });
|
|
418
|
+
}
|
|
419
|
+
await writeMeta(targetRoot, version);
|
|
420
|
+
return { target, entries };
|
|
421
|
+
}
|
|
422
|
+
function buildCodexSegment(version) {
|
|
423
|
+
return [
|
|
424
|
+
CODEX_MARKER_BEGIN,
|
|
425
|
+
"<!-- \u672C\u6BB5\u7531 `easytwin skills sync` \u7BA1\u7406,\u53EF\u6574\u6BB5\u66FF\u6362;\u624B\u52A8\u4FEE\u6539\u4F1A\u88AB\u4E0B\u6B21\u540C\u6B65\u8986\u76D6\u3002 -->",
|
|
426
|
+
`EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
|
|
427
|
+
"",
|
|
428
|
+
"- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
|
|
429
|
+
"- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
|
|
430
|
+
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
|
|
431
|
+
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
432
|
+
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
433
|
+
"- `easytwin-upload`:\u5168\u91CF\u4E0A\u4F20\u5F00\u53D1\u4EA7\u7269\u76EE\u5F55(\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
|
|
434
|
+
"",
|
|
435
|
+
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
436
|
+
CODEX_MARKER_END
|
|
437
|
+
].join("\n");
|
|
438
|
+
}
|
|
439
|
+
async function syncToCodex(sourceRoot, cwd, version) {
|
|
440
|
+
const agentsFile = path4.join(cwd, "AGENTS.md");
|
|
441
|
+
const segment = buildCodexSegment(version);
|
|
442
|
+
let content = "";
|
|
443
|
+
let exists = true;
|
|
444
|
+
try {
|
|
445
|
+
content = await fs4.readFile(agentsFile, "utf8");
|
|
446
|
+
} catch {
|
|
447
|
+
exists = false;
|
|
448
|
+
}
|
|
449
|
+
const start = content.indexOf(CODEX_MARKER_BEGIN);
|
|
450
|
+
const end = content.indexOf(CODEX_MARKER_END);
|
|
451
|
+
let action;
|
|
452
|
+
let next;
|
|
453
|
+
if (start === -1 || end === -1 || end < start) {
|
|
454
|
+
action = exists ? "updated" : "created";
|
|
455
|
+
next = content.length > 0 && !content.endsWith("\n") ? content + "\n\n" : content + (content.length > 0 ? "\n" : "");
|
|
456
|
+
next += segment + "\n";
|
|
457
|
+
} else {
|
|
458
|
+
const current = content.slice(start, end + CODEX_MARKER_END.length);
|
|
459
|
+
action = current === segment ? "unchanged" : "updated";
|
|
460
|
+
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
461
|
+
}
|
|
462
|
+
if (action !== "unchanged") {
|
|
463
|
+
await fs4.mkdir(cwd, { recursive: true });
|
|
464
|
+
await fs4.writeFile(agentsFile, next, "utf8");
|
|
465
|
+
}
|
|
466
|
+
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
467
|
+
}
|
|
468
|
+
async function syncSkills(options) {
|
|
469
|
+
const targets = normalizeTargets(options.targets ?? "all");
|
|
470
|
+
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
471
|
+
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
472
|
+
const summaries = [];
|
|
473
|
+
for (const target of targets) {
|
|
474
|
+
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
|
|
475
|
+
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
|
|
476
|
+
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
477
|
+
}
|
|
478
|
+
return summaries;
|
|
479
|
+
}
|
|
480
|
+
async function detectSkillsStatus(cwd, sourceDir) {
|
|
481
|
+
const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
|
|
482
|
+
const version = await readDevkitVersion(sourceRoot);
|
|
483
|
+
const targets = [];
|
|
484
|
+
for (const target of ["cursor", "claude"]) {
|
|
485
|
+
const root = path4.join(cwd, target === "cursor" ? ".cursor/skills" : ".claude/skills");
|
|
486
|
+
const missing = [];
|
|
487
|
+
for (const name of SKILL_NAMES) {
|
|
488
|
+
if (!await fs4.stat(path4.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
|
|
489
|
+
}
|
|
490
|
+
let stale = false;
|
|
491
|
+
try {
|
|
492
|
+
const meta = await readJson(path4.join(root, META_FILE_NAME));
|
|
493
|
+
stale = meta.version !== version;
|
|
494
|
+
} catch {
|
|
495
|
+
stale = missing.length === 0;
|
|
496
|
+
}
|
|
497
|
+
targets.push({
|
|
498
|
+
target,
|
|
499
|
+
synced: missing.length === 0 && !stale,
|
|
500
|
+
reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
let agentsContent = "";
|
|
504
|
+
try {
|
|
505
|
+
agentsContent = await fs4.readFile(path4.join(cwd, "AGENTS.md"), "utf8");
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
|
|
509
|
+
targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
|
|
510
|
+
return { cwd, targets };
|
|
511
|
+
}
|
|
512
|
+
export {
|
|
513
|
+
APP_ID_HEADER,
|
|
514
|
+
AUTH_HEADER,
|
|
515
|
+
BEARER_PREFIX,
|
|
516
|
+
CODEX_MARKER_BEGIN,
|
|
517
|
+
CODEX_MARKER_END,
|
|
518
|
+
CONFIG_FILE_NAME,
|
|
519
|
+
ConfigError,
|
|
520
|
+
DEFAULT_BASE_URL,
|
|
521
|
+
ENDPOINTS,
|
|
522
|
+
EasyTwinApiError,
|
|
523
|
+
EasyTwinClient,
|
|
524
|
+
GITIGNORE_ENTRY,
|
|
525
|
+
GITIGNORE_FILE_NAME,
|
|
526
|
+
META_FILE_NAME,
|
|
527
|
+
SKILL_NAMES,
|
|
528
|
+
appendGitignore,
|
|
529
|
+
buildMultipartBody,
|
|
530
|
+
collectFiles,
|
|
531
|
+
configFilePath,
|
|
532
|
+
detectSkillsStatus,
|
|
533
|
+
initConfig,
|
|
534
|
+
listScenes,
|
|
535
|
+
loadConfig,
|
|
536
|
+
normalizeSceneDetail,
|
|
537
|
+
normalizeSceneList,
|
|
538
|
+
normalizeTargets,
|
|
539
|
+
parseConfig,
|
|
540
|
+
pullScene,
|
|
541
|
+
readConfigFile,
|
|
542
|
+
readDevkitVersion,
|
|
543
|
+
resolveConfig,
|
|
544
|
+
resolveSkillsSourceDir,
|
|
545
|
+
saveSceneJson,
|
|
546
|
+
syncSkills,
|
|
547
|
+
uploadDirectory,
|
|
548
|
+
validateConfigShape,
|
|
549
|
+
writeConfigFile
|
|
550
|
+
};
|
|
551
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/client.ts","../src/scene.ts","../src/upload.ts","../src/skills.ts"],"sourcesContent":["import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\n/** 配置文件名(项目根)。 */\nexport const CONFIG_FILE_NAME = \"easytwin.config.json\";\n\n/**\n * 官方缺省域名。\n * TODO(外部输入): 域名值待定。该常量是全仓唯一收口点,只允许写在这里。\n */\nexport const DEFAULT_BASE_URL = \"https://api.easytwin.example.com\";\n\n/** 磁盘上的配置结构(4.3 配置契约)。 */\nexport interface EasyTwinConfig {\n appId: string;\n appSecret: string;\n baseUrl?: string;\n}\n\n/** 解析 + 环境变量覆盖后的最终配置。 */\nexport interface ResolvedConfig {\n appId: string;\n appSecret: string;\n baseUrl: string;\n source: \"file\" | \"env\";\n}\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\nexport function configFilePath(cwd: string): string {\n return path.join(cwd, CONFIG_FILE_NAME);\n}\n\nexport function parseConfig(raw: string): EasyTwinConfig {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new ConfigError(`配置文件不是合法 JSON:${CONFIG_FILE_NAME}`);\n }\n return validateConfigShape(parsed);\n}\n\nexport function validateConfigShape(value: unknown): EasyTwinConfig {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new ConfigError(\"配置必须是 JSON 对象\");\n }\n const v = value as Record<string, unknown>;\n if (typeof v.appId !== \"string\" || v.appId.length === 0) {\n throw new ConfigError(\"配置缺少必填字段 appId\");\n }\n if (typeof v.appSecret !== \"string\" || v.appSecret.length === 0) {\n throw new ConfigError(\"配置缺少必填字段 appSecret\");\n }\n if (v.baseUrl !== undefined && (typeof v.baseUrl !== \"string\" || v.baseUrl.length === 0)) {\n throw new ConfigError(\"baseUrl 必须是非空字符串\");\n }\n const config: EasyTwinConfig = { appId: v.appId, appSecret: v.appSecret };\n if (typeof v.baseUrl === \"string\") config.baseUrl = v.baseUrl;\n return config;\n}\n\nexport async function readConfigFile(cwd: string): Promise<EasyTwinConfig> {\n const file = configFilePath(cwd);\n let raw: string;\n try {\n raw = await fs.readFile(file, \"utf8\");\n } catch {\n throw new ConfigError(`未找到配置文件 ${file},请先运行 \\`easytwin init\\``);\n }\n return parseConfig(raw);\n}\n\n/** 环境变量优先于文件(CI 与不落盘场景)。 */\nexport function resolveConfig(file: EasyTwinConfig, env: NodeJS.ProcessEnv = process.env): ResolvedConfig {\n const appId = env.EASYTWIN_APP_ID ?? file.appId;\n const appSecret = env.EASYTWIN_APP_SECRET ?? file.appSecret;\n const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? DEFAULT_BASE_URL;\n if (!appId) throw new ConfigError(\"缺少 appId:配置文件中未提供,且未设置环境变量 EASYTWIN_APP_ID\");\n if (!appSecret) throw new ConfigError(\"缺少 appSecret:配置文件中未提供,且未设置环境变量 EASYTWIN_APP_SECRET\");\n const source: ResolvedConfig[\"source\"] =\n env.EASYTWIN_APP_ID !== undefined || env.EASYTWIN_APP_SECRET !== undefined ? \"env\" : \"file\";\n return { appId, appSecret, baseUrl, source };\n}\n\nexport async function loadConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): Promise<ResolvedConfig> {\n return resolveConfig(await readConfigFile(cwd), env);\n}\n\nexport async function writeConfigFile(cwd: string, config: EasyTwinConfig): Promise<string> {\n const file = configFilePath(cwd);\n const body: EasyTwinConfig = { appId: config.appId, appSecret: config.appSecret };\n if (config.baseUrl) body.baseUrl = config.baseUrl;\n await fs.mkdir(cwd, { recursive: true });\n await fs.writeFile(file, JSON.stringify(body, null, 2) + \"\\n\", \"utf8\");\n return file;\n}\n\nexport const GITIGNORE_FILE_NAME = \".gitignore\";\nexport const GITIGNORE_ENTRY = \"easytwin.config.json\";\n\nexport interface GitignoreResult {\n created: boolean;\n added: boolean;\n}\n\n/** 把配置文件名追加进 .gitignore(无则创建),幂等:已存在条目时不重复追加。 */\nexport async function appendGitignore(cwd: string): Promise<GitignoreResult> {\n const file = path.join(cwd, GITIGNORE_FILE_NAME);\n let content = \"\";\n let created = false;\n try {\n content = await fs.readFile(file, \"utf8\");\n } catch {\n created = true;\n }\n const lines = content.split(/\\r?\\n/);\n if (lines.includes(GITIGNORE_ENTRY)) return { created, added: false };\n const prefix = content.length > 0 && !content.endsWith(\"\\n\") ? \"\\n\" : \"\";\n await fs.writeFile(file, content + prefix + GITIGNORE_ENTRY + \"\\n\", \"utf8\");\n return { created, added: true };\n}\n\nexport interface InitResult {\n configFile: string;\n gitignore: GitignoreResult;\n}\n\n/** init 的 lib 实现:生成配置文件 + 追加 gitignore(与插件配置向导共用同一实现)。 */\nexport async function initConfig(input: EasyTwinConfig, cwd: string): Promise<InitResult> {\n const validated = validateConfigShape(input);\n const configFile = await writeConfigFile(cwd, validated);\n const gitignore = await appendGitignore(cwd);\n return { configFile, gitignore };\n}","import http from \"node:http\";\nimport https from \"node:https\";\nimport { URL } from \"node:url\";\nimport type { ResolvedConfig } from \"./config\";\n\n/**\n * TODO(swagger): 认证头名称、endpoint 路径、请求/响应结构一律以内部 swagger 为准。\n * 以下常量为占位约定;swagger 到位后在 client.ts / scene.ts / upload.ts 集中修订,不扩散到其它文件。\n */\nexport const AUTH_HEADER = \"Authorization\";\nexport const BEARER_PREFIX = \"Bearer\";\n/** TODO(swagger): 应用标识头名待定(上传目标空间由 App ID 决定)。 */\nexport const APP_ID_HEADER = \"X-Easytwin-App-Id\";\n\nexport const ENDPOINTS = {\n scenes: \"/api/scenes\",\n scene: (id: string) => `/api/scenes/${encodeURIComponent(id)}`,\n upload: \"/api/upload\",\n} as const;\n\nexport class EasyTwinApiError extends Error {\n readonly status: number;\n readonly body: unknown;\n constructor(status: number, message: string, body?: unknown) {\n super(message);\n this.name = \"EasyTwinApiError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\n headers?: Record<string, string>;\n body?: string | null;\n signal?: AbortSignal;\n}\n\ninterface RawResponse {\n status: number;\n body: Buffer;\n}\n\nfunction messageFromBody(body: unknown): string | undefined {\n if (typeof body === \"object\" && body !== null) {\n const m = (body as Record<string, unknown>).message;\n if (typeof m === \"string\") return m;\n }\n return undefined;\n}\n\nfunction parseResponseBody(buffer: Buffer): unknown {\n const text = buffer.toString(\"utf8\");\n if (text.length === 0) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nexport class EasyTwinClient {\n readonly baseUrl: string;\n private readonly appId: string;\n private readonly appSecret: string;\n\n constructor(config: ResolvedConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n this.appId = config.appId;\n this.appSecret = config.appSecret;\n }\n\n /** 全仓唯一认证头注入点。 */\n private authHeaders(): Record<string, string> {\n return {\n [AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,\n [APP_ID_HEADER]: this.appId,\n };\n }\n\n /** JSON 请求(原生 fetch)。 */\n async request(path: string, options: RequestOptions = {}): Promise<unknown> {\n const url = `${this.baseUrl}${path}`;\n const headers: Record<string, string> = { ...this.authHeaders(), ...options.headers };\n if (options.body !== undefined && options.body !== null && !(\"Content-Type\" in headers)) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n let res: Response;\n try {\n res = await fetch(url, {\n method: options.method ?? \"GET\",\n headers,\n body: options.body,\n signal: options.signal,\n });\n } catch (err) {\n throw new EasyTwinApiError(0, `请求失败:${err instanceof Error ? err.message : String(err)}`);\n }\n return this.handleStatus(res.status, Buffer.from(await res.arrayBuffer()));\n }\n\n /**\n * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。\n * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。\n */\n async upload(\n path: string,\n options: { boundary: string; body: Buffer; onBytes?: (bytes: number) => void }\n ): Promise<unknown> {\n const url = new URL(`${this.baseUrl}${path}`);\n const mod = url.protocol === \"https:\" ? https : http;\n const headers: Record<string, string> = {\n ...this.authHeaders(),\n \"Content-Type\": `multipart/form-data; boundary=${options.boundary}`,\n \"Content-Length\": String(options.body.length),\n };\n const raw = await new Promise<RawResponse>((resolve, reject) => {\n const req = mod.request(url, { method: \"POST\", headers }, (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks) }));\n });\n req.on(\"error\", reject);\n const CHUNK = 64 * 1024;\n let offset = 0;\n const sendNext = (): void => {\n if (offset >= options.body.length) {\n req.end();\n return;\n }\n const chunk = options.body.subarray(offset, offset + CHUNK);\n offset += chunk.length;\n req.write(chunk, () => {\n options.onBytes?.(chunk.length);\n sendNext();\n });\n };\n sendNext();\n });\n return this.handleStatus(raw.status, raw.body);\n }\n\n private handleStatus(status: number, body: Buffer): unknown {\n if (status >= 200 && status < 300) return parseResponseBody(body);\n const parsed = parseResponseBody(body);\n throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);\n }\n}","import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { ENDPOINTS, type EasyTwinClient } from \"./client\";\n\n// TODO(swagger): 场景列表/详情响应结构待定,当前类型为占位,以内部 swagger 为准。\nexport interface SceneSummary {\n id: string;\n name: string;\n}\n\nexport interface SceneDetail {\n id: string;\n name: string;\n /** 服务端返回的场景 JSON 原始内容。 */\n payload: unknown;\n}\n\nexport function normalizeSceneList(data: unknown): SceneSummary[] {\n // TODO(swagger): 兼容 \"数组\" 与 \"{ list: [...] }\" 两种占位形态,swagger 到位后收敛为一种。\n const list = Array.isArray(data) ? data : (data as { list?: unknown } | null)?.list;\n if (!Array.isArray(list)) throw new Error(\"scene list 响应结构无法识别\");\n return list.map((item) => {\n const it = (item ?? {}) as Record<string, unknown>;\n return { id: String(it.id ?? \"\"), name: String(it.name ?? it.id ?? \"\") };\n });\n}\n\nexport function normalizeSceneDetail(data: unknown): SceneDetail {\n const it = (data ?? {}) as Record<string, unknown>;\n return { id: String(it.id ?? \"\"), name: String(it.name ?? \"\"), payload: data };\n}\n\nexport async function listScenes(client: EasyTwinClient): Promise<SceneSummary[]> {\n const data = await client.request(ENDPOINTS.scenes, { method: \"GET\" });\n return normalizeSceneList(data);\n}\n\nexport async function pullScene(client: EasyTwinClient, id: string): Promise<SceneDetail> {\n const data = await client.request(ENDPOINTS.scene(id), { method: \"GET\" });\n return normalizeSceneDetail(data);\n}\n\nexport async function saveSceneJson(scene: SceneDetail, out: string): Promise<string> {\n await fs.mkdir(path.dirname(out), { recursive: true });\n await fs.writeFile(out, JSON.stringify(scene.payload ?? scene, null, 2) + \"\\n\", \"utf8\");\n return out;\n}","import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { ENDPOINTS, type EasyTwinClient } from \"./client\";\n\nexport interface UploadProgress {\n phase: \"collect\" | \"upload\";\n /** collect 阶段为已统计字节;upload 阶段为已发送字节。 */\n current: number;\n total: number;\n /** upload 阶段当前文件相对路径。 */\n file?: string;\n}\n\nexport interface UploadOptions {\n onProgress?: (progress: UploadProgress) => void;\n /** 自定义忽略规则,默认只跳过 .git。 */\n ignore?: (relPath: string) => boolean;\n}\n\nexport interface UploadResult {\n fileCount: number;\n byteCount: number;\n}\n\nconst DEFAULT_IGNORED_DIRS = new Set([\".git\"]);\n\nexport interface CollectedFile {\n relPath: string;\n absPath: string;\n size: number;\n}\n\n/** 递归收集目录下全部文件(默认跳过 .git),按相对路径升序排列,保证顺序稳定。 */\nexport async function collectFiles(dir: string, ignore: (relPath: string) => boolean = () => false): Promise<CollectedFile[]> {\n const files: CollectedFile[] = [];\n async function walk(current: string, rel: string): Promise<void> {\n const entries = await fs.readdir(current, { withFileTypes: true });\n for (const entry of entries) {\n const relPath = path.posix.join(rel, entry.name);\n if (ignore(relPath)) continue;\n if (entry.isDirectory()) {\n if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;\n await walk(path.join(current, entry.name), relPath);\n } else if (entry.isFile()) {\n const absPath = path.join(current, entry.name);\n const stat = await fs.stat(absPath);\n files.push({ relPath, absPath, size: stat.size });\n }\n }\n }\n await walk(dir, \"\");\n files.sort((a, b) => a.relPath.localeCompare(b.relPath));\n return files;\n}\n\nexport function buildMultipartBody(\n boundary: string,\n fields: Record<string, string>,\n file: { name: string; content: Buffer }\n): Buffer {\n const parts: Buffer[] = [];\n for (const [key, value] of Object.entries(fields)) {\n parts.push(\n Buffer.from(`--${boundary}\\r\\nContent-Disposition: form-data; name=\"${key}\"\\r\\n\\r\\n${value}\\r\\n`, \"utf8\")\n );\n }\n parts.push(\n Buffer.from(\n `--${boundary}\\r\\nContent-Disposition: form-data; name=\"file\"; filename=\"${file.name}\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n`,\n \"utf8\"\n )\n );\n parts.push(file.content);\n parts.push(Buffer.from(`\\r\\n--${boundary}--\\r\\n`, \"utf8\"));\n return Buffer.concat(parts);\n}\n\n/** 全量覆盖上传目录:逐文件 multipart 上传,带进度回调(供插件进度条)。 */\nexport async function uploadDirectory(client: EasyTwinClient, dir: string, options: UploadOptions = {}): Promise<UploadResult> {\n const ignore = options.ignore ?? (() => false);\n const files = await collectFiles(dir, ignore);\n const total = files.reduce((sum, f) => sum + f.size, 0);\n options.onProgress?.({ phase: \"collect\", current: total, total });\n\n // 进度以原始文件字节计(multipart 头开销不计入),逐文件上报,供进度条/进度输出使用。\n let sent = 0;\n for (const file of files) {\n const content = await fs.readFile(file.absPath);\n const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;\n const body = buildMultipartBody(\n boundary,\n // TODO(swagger): multipart 字段名/结构待定,当前为占位约定。\n { path: file.relPath },\n { name: path.basename(file.relPath), content }\n );\n await client.upload(ENDPOINTS.upload, { boundary, body });\n sent += file.size;\n options.onProgress?.({ phase: \"upload\", current: sent, total, file: file.relPath });\n }\n return { fileCount: files.length, byteCount: sent };\n}","import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type SyncTarget = \"cursor\" | \"claude\" | \"codex\";\nexport type SyncTargetArg = SyncTarget | \"all\";\n\n/** 六件套清单(5.1),顺序即输出顺序。 */\nexport const SKILL_NAMES = [\n \"easytwin-render\",\n \"easytwin-core\",\n \"easytwin-develop\",\n \"easytwin-bootstrap\",\n \"easytwin-scene\",\n \"easytwin-upload\",\n] as const;\n\nexport type SyncAction = \"created\" | \"updated\" | \"unchanged\";\n\nexport interface SyncEntrySummary {\n name: string;\n action: SyncAction;\n}\n\nexport interface SyncSummary {\n target: SyncTarget;\n entries: SyncEntrySummary[];\n codex?: SyncEntrySummary;\n}\n\nexport interface SyncOptions {\n cwd: string;\n targets?: SyncTargetArg | SyncTarget[];\n version?: string;\n /** skills 源目录;缺省按 devkit 包内 skills/ 解析(CLI)。插件 bundle 场景显式传入插件内目录。 */\n sourceDir?: string;\n}\n\nexport const CODEX_MARKER_BEGIN = \"<!-- EASYTWIN-SKILLS:BEGIN -->\";\nexport const CODEX_MARKER_END = \"<!-- EASYTWIN-SKILLS:END -->\";\nexport const META_FILE_NAME = \".easytwin-meta.json\";\n\nexport function normalizeTargets(target: SyncTargetArg | SyncTarget[] = \"all\"): SyncTarget[] {\n if (target === \"all\") return [\"cursor\", \"claude\", \"codex\"];\n if (Array.isArray(target)) return [...new Set(target)];\n return [target];\n}\n\n/** skills 源目录默认解析:CLI 下为 devkit 包内 skills/。插件 bundle 到 CJS 后 import.meta.url 为空,\n * 必须通过 SyncOptions.sourceDir / detectSkillsStatus 的 sourceDir 显式传入(见 esbuild.config.mjs 注释)。 */\nexport function resolveSkillsSourceDir(): string {\n if (typeof import.meta.url !== \"string\" || import.meta.url.length === 0) {\n throw new Error(\"无法定位 skills 源目录:插件(bundle/CJS)场景必须显式传入 sourceDir\");\n }\n const here = path.dirname(fileURLToPath(import.meta.url));\n return path.resolve(here, \"..\", \"skills\");\n}\n\nasync function readJson<T>(file: string): Promise<T> {\n return JSON.parse(await fs.readFile(file, \"utf8\")) as T;\n}\n\nexport async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()): Promise<string> {\n const marker = path.join(skillsDir, \".easytwin-source.json\");\n try {\n const meta = await readJson<{ version: string }>(marker);\n if (typeof meta.version === \"string\") return meta.version;\n } catch {\n // 无构建期标记,退回读包内 package.json\n }\n try {\n const pkg = await readJson<{ version: string }>(path.resolve(skillsDir, \"..\", \"package.json\"));\n return pkg.version;\n } catch {\n return \"0.0.0\";\n }\n}\n\nasync function copyDir(src: string, dest: string): Promise<void> {\n await fs.mkdir(dest, { recursive: true });\n const entries = await fs.readdir(src, { withFileTypes: true });\n for (const entry of entries) {\n const s = path.join(src, entry.name);\n const d = path.join(dest, entry.name);\n if (entry.isDirectory()) await copyDir(s, d);\n else if (entry.isFile()) await fs.copyFile(s, d);\n }\n}\n\n/** 比较源目录的每个文件与目标是否逐字节一致(忽略目标里额外的文件,如版本标记)。 */\nasync function dirMatches(src: string, dest: string): Promise<boolean> {\n let sourceEntries: string[];\n try {\n sourceEntries = await fs.readdir(src);\n } catch {\n return false;\n }\n for (const name of sourceEntries) {\n const s = path.join(src, name);\n const d = path.join(dest, name);\n const sStat = await fs.stat(s);\n if (sStat.isDirectory()) {\n if (!(await dirMatches(s, d))) return false;\n } else {\n let dStat;\n try {\n dStat = await fs.stat(d);\n } catch {\n return false;\n }\n if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;\n if (!(await fs.readFile(s)).equals(await fs.readFile(d))) return false;\n }\n }\n return true;\n}\n\nfunction actionFor(exists: boolean, matches: boolean): SyncAction {\n if (!exists) return \"created\";\n return matches ? \"unchanged\" : \"updated\";\n}\n\nasync function writeMeta(destDir: string, version: string): Promise<void> {\n const meta = { name: \"@easytwin/devkit\", version };\n await fs.writeFile(path.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + \"\\n\", \"utf8\");\n}\n\n/** cursor / claude:复制技能目录到用户项目,按内容覆盖,写入版本标记。 */\nasync function syncToDir(target: SyncTarget, sourceRoot: string, targetRoot: string, version: string): Promise<SyncSummary> {\n const entries: SyncEntrySummary[] = [];\n for (const name of SKILL_NAMES) {\n const src = path.join(sourceRoot, name);\n const dest = path.join(targetRoot, name);\n const exists = await fs\n .stat(dest)\n .then(() => true)\n .catch(() => false);\n const matches = await dirMatches(src, dest);\n const action = actionFor(exists, matches);\n if (action !== \"unchanged\") {\n await fs.rm(dest, { recursive: true, force: true });\n await copyDir(src, dest);\n }\n entries.push({ name, action });\n }\n await writeMeta(targetRoot, version);\n return { target, entries };\n}\n\n/** codex:在用户项目 AGENTS.md 注入带标记的引用段(整段替换,幂等)。 */\nfunction buildCodexSegment(version: string): string {\n return [\n CODEX_MARKER_BEGIN,\n \"<!-- 本段由 `easytwin skills sync` 管理,可整段替换;手动修改会被下次同步覆盖。 -->\",\n `EasyTwin 开发技能(由 @easytwin/devkit v${version} 同步):`,\n \"\",\n \"- `easytwin-develop`:工作流总纲,开始 EasyTwin 开发前必读。\",\n \"- `easytwin-bootstrap`:初始化应用级凭证(easytwin.config.json)。\",\n \"- `easytwin-scene`:列出场景、拉取场景 JSON。\",\n \"- `easytwin-render`:用 twin runtime 开发渲染功能(主力)。\",\n \"- `easytwin-core`:引擎内核 API(RuntimeEngine/基类/Time/相机/物理)。\",\n \"- `easytwin-upload`:全量上传开发产物目录(覆盖不可逆)。\",\n \"\",\n \"详细用法见各技能;重新同步运行 `easytwin skills sync --target codex`。\",\n CODEX_MARKER_END,\n ].join(\"\\n\");\n}\n\nasync function syncToCodex(sourceRoot: string, cwd: string, version: string): Promise<SyncSummary> {\n const agentsFile = path.join(cwd, \"AGENTS.md\");\n const segment = buildCodexSegment(version);\n let content = \"\";\n let exists = true;\n try {\n content = await fs.readFile(agentsFile, \"utf8\");\n } catch {\n exists = false;\n }\n const start = content.indexOf(CODEX_MARKER_BEGIN);\n const end = content.indexOf(CODEX_MARKER_END);\n let action: SyncAction;\n let next: string;\n if (start === -1 || end === -1 || end < start) {\n action = exists ? \"updated\" : \"created\";\n next = content.length > 0 && !content.endsWith(\"\\n\") ? content + \"\\n\\n\" : content + (content.length > 0 ? \"\\n\" : \"\");\n next += segment + \"\\n\";\n } else {\n const current = content.slice(start, end + CODEX_MARKER_END.length);\n action = current === segment ? \"unchanged\" : \"updated\";\n next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);\n }\n if (action !== \"unchanged\") {\n await fs.mkdir(cwd, { recursive: true });\n await fs.writeFile(agentsFile, next, \"utf8\");\n }\n return { target: \"codex\", entries: [], codex: { name: \"AGENTS.md\", action } };\n}\n\nexport async function syncSkills(options: SyncOptions): Promise<SyncSummary[]> {\n const targets = normalizeTargets(options.targets ?? \"all\");\n const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();\n const version = options.version ?? (await readDevkitVersion(sourceRoot));\n const summaries: SyncSummary[] = [];\n for (const target of targets) {\n if (target === \"cursor\") summaries.push(await syncToDir(\"cursor\", sourceRoot, path.join(options.cwd, \".cursor\", \"skills\"), version));\n else if (target === \"claude\") summaries.push(await syncToDir(\"claude\", sourceRoot, path.join(options.cwd, \".claude\", \"skills\"), version));\n else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));\n }\n return summaries;\n}\n\nexport interface SkillsTargetStatus {\n target: SyncTarget;\n synced: boolean;\n reason?: string;\n}\n\nexport interface SkillsStatus {\n cwd: string;\n targets: SkillsTargetStatus[];\n}\n\n/** 供插件\"缺失/过期时提示一次\"使用:检测各目标当前同步状态。 */\nexport async function detectSkillsStatus(cwd: string, sourceDir?: string): Promise<SkillsStatus> {\n const sourceRoot = sourceDir ?? resolveSkillsSourceDir();\n const version = await readDevkitVersion(sourceRoot);\n const targets: SkillsTargetStatus[] = [];\n\n for (const target of [\"cursor\", \"claude\"] as const) {\n const root = path.join(cwd, target === \"cursor\" ? \".cursor/skills\" : \".claude/skills\");\n const missing = [];\n for (const name of SKILL_NAMES) {\n if (!(await fs.stat(path.join(root, name)).then(() => true).catch(() => false))) missing.push(name);\n }\n let stale = false;\n try {\n const meta = await readJson<{ version: string }>(path.join(root, META_FILE_NAME));\n stale = meta.version !== version;\n } catch {\n stale = missing.length === 0; // 目录存在但无版本标记\n }\n targets.push({\n target,\n synced: missing.length === 0 && !stale,\n reason: missing.length > 0 ? `缺少 ${missing.join(\", \")}` : stale ? \"版本过期\" : undefined,\n });\n }\n\n let agentsContent = \"\";\n try {\n agentsContent = await fs.readFile(path.join(cwd, \"AGENTS.md\"), \"utf8\");\n } catch {\n // AGENTS.md 不存在视为未同步\n }\n const hasCodex =\n agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);\n targets.push({ target: \"codex\", synced: hasCodex, reason: hasCodex ? undefined : \"AGENTS.md 缺少同步标记段\" });\n\n return { cwd, targets };\n}\n"],"mappings":"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","names":["path","fs","path","fs","path","fs","path","fs","path","fs","path"]}
|