@petercjl/tuda 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +11 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/UPDATE.md +11 -0
- package/capabilities.json +452 -0
- package/dist/board-plugin.LICENSES.txt +207 -0
- package/dist/board-plugin.js +17452 -0
- package/dist/main.js +1067 -0
- package/guard/command-source-tuda.v1.json +1144 -0
- package/guard/command.tuda.json +584 -0
- package/guard/command.tuda.v1.json +5 -0
- package/package.json +56 -4
- package/release-manifest.json +5 -0
- package/scripts/validate-package.mjs +22 -0
- package/skills/using-tuda/SKILL.md +81 -0
- package/skills/using-tuda/adapters/generic.json +21 -0
- package/skills/using-tuda/agents/openai.yaml +6 -0
- package/skills/using-tuda/capabilities.json +44 -0
- package/skills/using-tuda/references/cli.md +63 -0
- package/skills/using-tuda/references/visual-qa.md +59 -0
- package/skills/using-tuda/scripts/validate-image-batch.mjs +146 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/main.ts
|
|
4
|
+
import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3, stat } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
|
+
import path6 from "node:path";
|
|
7
|
+
import os3 from "node:os";
|
|
8
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9
|
+
|
|
10
|
+
// src/management.ts
|
|
11
|
+
import { access as access2, cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
import path2 from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
// src/paths.ts
|
|
19
|
+
import { access } from "node:fs/promises";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
function defaultUserDataPath() {
|
|
23
|
+
if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "Tuda");
|
|
24
|
+
if (process.platform === "win32") return path.join(process.env.APPDATA || os.homedir(), "Tuda");
|
|
25
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "Tuda");
|
|
26
|
+
}
|
|
27
|
+
function candidateTudaAppRoots() {
|
|
28
|
+
if (process.platform === "darwin") return ["/Applications/Tuda.app", path.join(os.homedir(), "Applications/Tuda.app")];
|
|
29
|
+
if (process.platform === "win32") return [path.join(process.env.LOCALAPPDATA || os.homedir(), "Programs/Tuda"), "C:\\Program Files\\Tuda"];
|
|
30
|
+
return ["/opt/Tuda", "/usr/lib/tuda"];
|
|
31
|
+
}
|
|
32
|
+
async function resolveTudaApp(explicitRoot) {
|
|
33
|
+
for (const root2 of explicitRoot ? [explicitRoot] : candidateTudaAppRoots()) {
|
|
34
|
+
const commandPath = process.platform === "darwin" ? path.join(root2, "Contents/MacOS/Tuda") : path.join(root2, process.platform === "win32" ? "Tuda.exe" : "Tuda");
|
|
35
|
+
try {
|
|
36
|
+
await access(commandPath);
|
|
37
|
+
return { root: root2, commandPath };
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/management.ts
|
|
45
|
+
var root = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..");
|
|
46
|
+
var skillName = "using-tuda";
|
|
47
|
+
var owner = "@petercjl/tuda";
|
|
48
|
+
var receiptName = ".tuda-skill.json";
|
|
49
|
+
var exists = (p) => access2(p).then(() => true, () => false);
|
|
50
|
+
var readJson = async (p) => JSON.parse(await readFile(p, "utf8"));
|
|
51
|
+
var json = (value) => process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, ...value }, null, 2) + "\n");
|
|
52
|
+
var source = path2.join(root, "skills", skillName);
|
|
53
|
+
async function digest(directory) {
|
|
54
|
+
const hash = createHash("sha256");
|
|
55
|
+
async function visit(dir, prefix = "") {
|
|
56
|
+
for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
57
|
+
if ([receiptName, ".runtime", "agents"].includes(entry.name)) continue;
|
|
58
|
+
if (entry.isSymbolicLink()) throw new Error("UNSAFE_SKILL_LINK");
|
|
59
|
+
const name = path2.posix.join(prefix, entry.name);
|
|
60
|
+
if (entry.isDirectory()) await visit(path2.join(dir, entry.name), name);
|
|
61
|
+
else if (entry.isFile()) hash.update(name + "\0").update(await readFile(path2.join(dir, entry.name)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await visit(directory);
|
|
65
|
+
return hash.digest("hex");
|
|
66
|
+
}
|
|
67
|
+
function selectedTarget(args) {
|
|
68
|
+
const at = args.indexOf("--target");
|
|
69
|
+
const raw = at >= 0 ? args[at + 1] : void 0;
|
|
70
|
+
if (!raw || raw.startsWith("--") || !path2.isAbsolute(raw)) throw new Error("TARGET_REQUIRED: --target must be the current Agent's absolute, exact Skill directory.");
|
|
71
|
+
const target = path2.resolve(raw);
|
|
72
|
+
if (path2.basename(target) !== skillName || target === path2.parse(target).root || target === source) throw new Error("UNSAFE_TARGET: select a separate using-tuda directory.");
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
async function installSkill(target, activation = {}) {
|
|
76
|
+
const renameDirectory = activation.rename ?? rename;
|
|
77
|
+
const pkg = await readJson(path2.join(root, "package.json"));
|
|
78
|
+
const expected = await digest(source);
|
|
79
|
+
let backup;
|
|
80
|
+
if (await exists(target)) {
|
|
81
|
+
if ((await lstat(target)).isSymbolicLink()) throw new Error("UNMANAGED_TARGET: links require an explicit migration.");
|
|
82
|
+
const receipt = await readJson(path2.join(target, receiptName)).catch(() => null);
|
|
83
|
+
if (receipt?.package !== owner) throw new Error("UNMANAGED_TARGET: existing directory was preserved.");
|
|
84
|
+
if (await digest(target) !== receipt.digest) throw new Error("LOCAL_EDITS: existing Skill was preserved.");
|
|
85
|
+
const installedRuntime = await readFile(path2.join(target, ".runtime", "dist", "main.js")).catch(() => null);
|
|
86
|
+
const installedHash = installedRuntime ? createHash("sha256").update(installedRuntime).digest("hex") : null;
|
|
87
|
+
const currentHash = createHash("sha256").update(await readFile(path2.join(root, "dist", "main.js"))).digest("hex");
|
|
88
|
+
if (installedHash && installedHash !== receipt.runtimeSha256) throw new Error("LOCAL_EDITS: target-owned runtime was preserved.");
|
|
89
|
+
if (receipt.version === pkg.version && receipt.digest === expected && installedHash === currentHash) {
|
|
90
|
+
return { target, installed: true, managed: true, current: true, version: pkg.version, digest: expected, changed: false, hostDiscovery: "pending" };
|
|
91
|
+
}
|
|
92
|
+
backup = `${target}.backup-${randomUUID()}`;
|
|
93
|
+
}
|
|
94
|
+
await mkdir(path2.dirname(target), { recursive: true });
|
|
95
|
+
const staging = `${target}.install-${randomUUID()}`;
|
|
96
|
+
try {
|
|
97
|
+
await cp(source, staging, { recursive: true, errorOnExist: true });
|
|
98
|
+
await mkdir(path2.join(staging, ".runtime"));
|
|
99
|
+
for (const name of ["dist", "guard", "skills", "package.json", "capabilities.json", "release-manifest.json", "INSTALL.md", "UPDATE.md", "LICENSE", "THIRD_PARTY_NOTICES.md"]) {
|
|
100
|
+
if (await exists(path2.join(root, name))) await cp(path2.join(root, name), path2.join(staging, ".runtime", name), { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
if (backup && await exists(path2.join(target, "agents"))) await cp(path2.join(target, "agents"), path2.join(staging, "agents"), { recursive: true, force: true });
|
|
103
|
+
await writeFile(path2.join(staging, receiptName), JSON.stringify({ schemaVersion: 1, package: owner, version: pkg.version, digest: expected, runtimeSha256: createHash("sha256").update(await readFile(path2.join(staging, ".runtime", "dist", "main.js"))).digest("hex") }, null, 2), { flag: "wx" });
|
|
104
|
+
let inPlace = false;
|
|
105
|
+
let oldFiles = [];
|
|
106
|
+
const files = async (at, prefix = "") => {
|
|
107
|
+
const result = [];
|
|
108
|
+
for (const entry of await readdir(at, { withFileTypes: true })) {
|
|
109
|
+
if (entry.isSymbolicLink()) throw new Error("UNSAFE_SKILL_LINK");
|
|
110
|
+
const relative = path2.join(prefix, entry.name);
|
|
111
|
+
if (entry.isDirectory()) result.push(...await files(path2.join(at, entry.name), relative));
|
|
112
|
+
else result.push(relative);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
116
|
+
if (backup) {
|
|
117
|
+
try {
|
|
118
|
+
await renameDirectory(target, backup);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!["EBUSY", "EPERM", "EACCES"].includes(error.code ?? "")) throw error;
|
|
121
|
+
oldFiles = await files(target);
|
|
122
|
+
await cp(target, backup, { recursive: true, errorOnExist: true });
|
|
123
|
+
inPlace = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const stagedFiles = await files(staging);
|
|
127
|
+
try {
|
|
128
|
+
if (inPlace) {
|
|
129
|
+
await cp(staging, target, { recursive: true, force: true });
|
|
130
|
+
for (const name of oldFiles) if (!stagedFiles.includes(name)) await rm(path2.join(target, name), { force: true });
|
|
131
|
+
if (await digest(target) !== expected) throw new Error("ACTIVATION_INTEGRITY_FAILED");
|
|
132
|
+
} else await renameDirectory(staging, target);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (backup) {
|
|
135
|
+
if (inPlace) {
|
|
136
|
+
await cp(backup, target, { recursive: true, force: true });
|
|
137
|
+
for (const name of stagedFiles) if (!oldFiles.includes(name)) await rm(path2.join(target, name), { force: true });
|
|
138
|
+
} else await rename(backup, target);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
return { target, installed: true, managed: true, current: true, version: pkg.version, digest: expected, changed: true, backup, hostDiscovery: "pending" };
|
|
143
|
+
} finally {
|
|
144
|
+
await rm(staging, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function registry(tag, timeout = 4e3) {
|
|
148
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(owner)}/${encodeURIComponent(tag)}`, { signal: AbortSignal.timeout(timeout) });
|
|
149
|
+
if (response.status === 404) return null;
|
|
150
|
+
if (!response.ok) throw new Error(`REGISTRY_ERROR: ${response.status}`);
|
|
151
|
+
return response.json();
|
|
152
|
+
}
|
|
153
|
+
async function automaticUpdateCheck() {
|
|
154
|
+
if (process.env.TUDA_NO_UPDATE_CHECK === "1" || await exists(path2.join(root, "..", ".git"))) return;
|
|
155
|
+
const cache = path2.join(defaultUserDataPath(), "cli-update-check.json");
|
|
156
|
+
try {
|
|
157
|
+
const previous = await readJson(cache).catch(() => null);
|
|
158
|
+
if (previous && Date.now() - previous.checkedAt < 864e5) return;
|
|
159
|
+
const own = await readJson(path2.join(root, "package.json"));
|
|
160
|
+
const latest = await registry("latest", 600);
|
|
161
|
+
await mkdir(path2.dirname(cache), { recursive: true });
|
|
162
|
+
const temporary = `${cache}.${randomUUID()}`;
|
|
163
|
+
await writeFile(temporary, JSON.stringify({ checkedAt: Date.now(), latest: latest?.version ?? null }), { flag: "wx" });
|
|
164
|
+
await rename(temporary, cache);
|
|
165
|
+
if (latest && latest.version !== own.version) process.stderr.write(`Tuda ${latest.version} is available; use tuda update check --json.
|
|
166
|
+
`);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function handleManagement(args) {
|
|
171
|
+
const [group, action] = args;
|
|
172
|
+
if (args.includes("--help") && args.includes("--json")) {
|
|
173
|
+
const manifest = await readJson(path2.join(root, "capabilities.json"));
|
|
174
|
+
json({ ...manifest, options: manifest.commandContracts[`${group} ${action}`]?.options ?? {} });
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (!["version", "--version", "capabilities", "skill", "update", "doctor"].includes(group)) return false;
|
|
178
|
+
const allowed = /* @__PURE__ */ new Set(["--json", "--version", "--target", "--agent", "--name", "--channel", "--user-data-path", "--no-launch", "--app-path"]);
|
|
179
|
+
for (const arg of args.filter((arg2) => arg2.startsWith("--"))) if (!allowed.has(arg)) throw new Error(`UNKNOWN_OPTION: ${arg}`);
|
|
180
|
+
const option = (name) => {
|
|
181
|
+
const i = args.indexOf(name);
|
|
182
|
+
return i < 0 ? void 0 : args[i + 1];
|
|
183
|
+
};
|
|
184
|
+
const agent = option("--agent");
|
|
185
|
+
if (agent && !/^[a-z][a-z0-9-]{0,63}$/.test(agent)) throw new Error("INVALID_AGENT");
|
|
186
|
+
const pkg = await readJson(path2.join(root, "package.json"));
|
|
187
|
+
if (group === "version" || group === "--version") {
|
|
188
|
+
if (args.includes("--json")) json({ package: pkg.name, version: pkg.version });
|
|
189
|
+
else process.stdout.write(pkg.version + "\n");
|
|
190
|
+
} else if (group === "capabilities") json(await readJson(path2.join(root, "capabilities.json")));
|
|
191
|
+
else if (group === "doctor") {
|
|
192
|
+
const checks = { node: Number(process.versions.node.split(".")[0]) >= 22, skill: await exists(path2.join(source, "SKILL.md")), runtime: await exists(path2.join(root, "dist", "main.js")) };
|
|
193
|
+
json({ package: pkg.name, version: pkg.version, checks, runtimeReady: Object.values(checks).every(Boolean), desktopRequiredForCanvas: true, platforms: { darwin: "development", win32: "customer" } });
|
|
194
|
+
if (!Object.values(checks).every(Boolean)) process.exitCode = 1;
|
|
195
|
+
} else if (group === "skill") {
|
|
196
|
+
if (option("--name") && option("--name") !== skillName) throw new Error("UNKNOWN_SKILL");
|
|
197
|
+
if (action === "source") {
|
|
198
|
+
json({ ...await readJson(path2.join(root, "capabilities.json")), package: pkg.name, version: pkg.version, name: skillName, sourcePath: source, skills: [{ ok: true, name: skillName, package: pkg.name, version: pkg.version, sourcePath: source }], complete: true });
|
|
199
|
+
} else if (action === "status") {
|
|
200
|
+
const target = selectedTarget(args);
|
|
201
|
+
const receipt = await readJson(path2.join(target, receiptName)).catch(() => null);
|
|
202
|
+
const installed = await exists(path2.join(target, "SKILL.md"));
|
|
203
|
+
const actual = installed ? await digest(target) : void 0;
|
|
204
|
+
const runtime = await readFile(path2.join(target, ".runtime", "dist", "main.js")).catch(() => null);
|
|
205
|
+
const runtimeCurrent = Boolean(runtime && receipt?.runtimeSha256 === createHash("sha256").update(runtime).digest("hex") && receipt.runtimeSha256 === createHash("sha256").update(await readFile(path2.join(root, "dist", "main.js"))).digest("hex"));
|
|
206
|
+
json({ name: skillName, package: pkg.name, edited: Boolean(installed && receipt && actual !== receipt.digest), verification: { verified: Boolean(actual && actual === receipt?.digest && runtimeCurrent), algorithm: "sha256" }, recovery: { required: false }, target, installed, managed: receipt?.package === owner, current: receipt?.package === owner && receipt.version === pkg.version && actual === receipt.digest && actual === await digest(source) && runtimeCurrent, version: receipt?.version ?? pkg.version, digest: actual, hostDiscovery: "pending" });
|
|
207
|
+
} else if (["install", "update"].includes(action)) json(await installSkill(selectedTarget(args)));
|
|
208
|
+
else throw new Error("UNKNOWN_SKILL_COMMAND");
|
|
209
|
+
} else if (group === "update") {
|
|
210
|
+
const tag = option("--channel") ?? "latest";
|
|
211
|
+
if (!["latest", "next"].includes(tag)) throw new Error("INVALID_CHANNEL");
|
|
212
|
+
const target = action === "install" ? selectedTarget(args) : void 0;
|
|
213
|
+
if (action === "install" && await exists(path2.join(root, "..", ".git"))) throw new Error("GIT_OWNED_INSTALL: update the checkout through Git.");
|
|
214
|
+
const latest = await registry(tag);
|
|
215
|
+
if (action === "check") json({ package: pkg.name, currentVersion: pkg.version, latestVersion: latest?.version ?? null, available: Boolean(latest && latest.version !== pkg.version), channel: tag, automaticInstall: false });
|
|
216
|
+
else if (action === "install") {
|
|
217
|
+
if (!latest || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(latest.version)) throw new Error("RELEASE_UNAVAILABLE");
|
|
218
|
+
const stage = path2.join(path2.dirname(target), `.tuda-update-${randomUUID()}`);
|
|
219
|
+
await mkdir(stage);
|
|
220
|
+
const npmEntry = process.platform === "win32" ? path2.join(path2.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js") : void 0;
|
|
221
|
+
if (npmEntry && !await exists(npmEntry)) throw new Error("NPM_RUNTIME_REQUIRED: run this management operation with the installed Node.js runtime.");
|
|
222
|
+
await promisify(execFile)(npmEntry ? process.execPath : "npm", [...npmEntry ? [npmEntry] : [], "install", "--prefix", stage, "--ignore-scripts", "--no-audit", "--no-fund", `${owner}@${latest.version}`], { timeout: 18e4, windowsHide: true });
|
|
223
|
+
const entry = path2.join(stage, "node_modules", "@petercjl", "tuda", "dist", "main.js");
|
|
224
|
+
const result = await promisify(execFile)(process.execPath, [entry, "skill", "update", "--target", target, "--json"], { timeout: 6e4 });
|
|
225
|
+
const receipt = JSON.parse(result.stdout);
|
|
226
|
+
if (!receipt.ok || receipt.version !== latest.version || !receipt.current) throw new Error("SKILL_SYNC_INCOMPLETE");
|
|
227
|
+
await rm(stage, { recursive: true, force: true });
|
|
228
|
+
json({ ...receipt, package: owner, runtimeEntry: path2.join(target, ".runtime", "dist", "main.js"), bootstrapReplaced: false });
|
|
229
|
+
} else throw new Error("UNKNOWN_UPDATE_COMMAND");
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ../electron/agent-integration.ts
|
|
235
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
236
|
+
import { access as access3, cp as cp2, mkdir as mkdir2, readFile as readFile2, readdir as readdir2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
237
|
+
import path3 from "node:path";
|
|
238
|
+
import os2 from "node:os";
|
|
239
|
+
var MANAGED_SKILL_FILE = ".tuda-managed.json";
|
|
240
|
+
var SKILL_AUTO_INSTALL_POLICY_FILE = "mcp-skill-policy.json";
|
|
241
|
+
async function getAgentIntegrationStatus(homeDir) {
|
|
242
|
+
return { clients: await Promise.all(SUPPORTED_AGENT_IDS.map((client) => agentStatus(client, homeDir))) };
|
|
243
|
+
}
|
|
244
|
+
var AGENT_SKILL_NAMES = ["using-tuda"];
|
|
245
|
+
async function bundledAgentSkills(bundledSkillPath) {
|
|
246
|
+
const bundles = AGENT_SKILL_NAMES.map((name) => ({
|
|
247
|
+
name,
|
|
248
|
+
source: bundledSkillPath
|
|
249
|
+
}));
|
|
250
|
+
for (const { source: source2 } of bundles) {
|
|
251
|
+
try {
|
|
252
|
+
await access3(path3.join(source2, "SKILL.md"));
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error("Tuda \u5185\u7F6E Skill \u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5 App \u540E\u91CD\u8BD5\u3002");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return Promise.all(bundles.map(async (bundle) => ({ ...bundle, snapshot: await snapshotSkill(bundle.source) })));
|
|
258
|
+
}
|
|
259
|
+
async function installAgentSkill(client, homeDir, bundledSkillPath) {
|
|
260
|
+
const bundles = await bundledAgentSkills(bundledSkillPath);
|
|
261
|
+
for (const { name } of bundles) {
|
|
262
|
+
const target = getSkillPath(client, homeDir, name);
|
|
263
|
+
if (await pathExists(target) && !await readManagedSkillDigest(target)) {
|
|
264
|
+
throw new Error(`\u5DF2\u6709\u81EA\u884C\u7EF4\u62A4\u7684\u540C\u540D Skill\uFF08${name}\uFF09\uFF0C\u8BF7\u5148\u5904\u7406\u8BE5\u76EE\u5F55\u518D\u5B89\u88C5\u3002`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const { name, source: source2, snapshot } of bundles) {
|
|
268
|
+
const target = getSkillPath(client, homeDir, name);
|
|
269
|
+
if (target !== source2) await replaceManagedSkill(target, source2, snapshot.digest);
|
|
270
|
+
}
|
|
271
|
+
await removeManagedWorkspaceSkill(client, homeDir);
|
|
272
|
+
return getAgentIntegrationStatus(homeDir);
|
|
273
|
+
}
|
|
274
|
+
async function detectCodingAgents(homeDir) {
|
|
275
|
+
const status = await getAgentIntegrationStatus(homeDir);
|
|
276
|
+
return status.clients.filter((client) => client.detected).map((client) => client.id);
|
|
277
|
+
}
|
|
278
|
+
function skillAutoInstallPolicyPath(userDataPath) {
|
|
279
|
+
return path3.join(userDataPath, SKILL_AUTO_INSTALL_POLICY_FILE);
|
|
280
|
+
}
|
|
281
|
+
async function readSkillAutoInstallPolicy(userDataPath) {
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(await readOptional(skillAutoInstallPolicyPath(userDataPath)));
|
|
284
|
+
if (isRecord(parsed) && isRecord(parsed.declined)) {
|
|
285
|
+
const declined = {};
|
|
286
|
+
for (const id of SUPPORTED_AGENT_IDS) if (parsed.declined[id] === true) declined[id] = true;
|
|
287
|
+
return { schemaVersion: 1, declined };
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
return { schemaVersion: 1, declined: {} };
|
|
292
|
+
}
|
|
293
|
+
async function setSkillAutoInstallDeclined(userDataPath, client, declined) {
|
|
294
|
+
const policy = await readSkillAutoInstallPolicy(userDataPath);
|
|
295
|
+
if (declined) policy.declined[client] = true;
|
|
296
|
+
else delete policy.declined[client];
|
|
297
|
+
await writeWithParent(skillAutoInstallPolicyPath(userDataPath), `${JSON.stringify(policy, null, 2)}
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
async function uninstallAgentSkill(client, homeDir) {
|
|
301
|
+
const targets = AGENT_SKILL_NAMES.map((name) => getSkillPath(client, homeDir, name));
|
|
302
|
+
for (const target of targets) {
|
|
303
|
+
if (await pathExists(target) && !await readManagedSkillDigest(target) && !await readOptional(path3.join(target, ".tuda-skill.json")).then((text) => {
|
|
304
|
+
try {
|
|
305
|
+
return JSON.parse(text).package === "@petercjl/tuda";
|
|
306
|
+
} catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
})) throw new Error("\u6B64 Skill \u672A\u7531 Tuda \u7BA1\u7406\uFF0C\u672A\u79FB\u9664\u3002");
|
|
310
|
+
}
|
|
311
|
+
for (const target of targets) await rm2(target, { recursive: true, force: true });
|
|
312
|
+
await removeManagedWorkspaceSkill(client, homeDir);
|
|
313
|
+
return getAgentIntegrationStatus(homeDir);
|
|
314
|
+
}
|
|
315
|
+
async function removeManagedWorkspaceSkill(client, homeDir) {
|
|
316
|
+
const legacyPath = path3.join(path3.dirname(getSkillPath(client, homeDir)), "tuda-workspace");
|
|
317
|
+
if (await readManagedSkillDigest(legacyPath)) await rm2(legacyPath, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
async function removeLegacyIntegration(client, homeDir) {
|
|
320
|
+
if (client !== "codex" && client !== "claude-code") return;
|
|
321
|
+
const configPath = path3.join(homeDir, client === "codex" ? ".codex/config.toml" : ".claude.json");
|
|
322
|
+
const current = await readOptional(configPath);
|
|
323
|
+
if (!current.trim()) return;
|
|
324
|
+
let next = current;
|
|
325
|
+
if (client === "codex") {
|
|
326
|
+
next = removeTomlTable(current, "mcp_servers.tuda");
|
|
327
|
+
} else {
|
|
328
|
+
let config;
|
|
329
|
+
try {
|
|
330
|
+
config = JSON.parse(current);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error("Claude Code \u914D\u7F6E\u4E0D\u662F\u6709\u6548 JSON\uFF0C\u672A\u4FEE\u6539\u8BE5\u6587\u4EF6\u3002");
|
|
333
|
+
}
|
|
334
|
+
if (!isRecord(config)) throw new Error("Claude Code \u914D\u7F6E\u683C\u5F0F\u65E0\u6548\uFF0C\u672A\u4FEE\u6539\u8BE5\u6587\u4EF6\u3002");
|
|
335
|
+
if (!isRecord(config.mcpServers) || !("tuda" in config.mcpServers)) return;
|
|
336
|
+
delete config.mcpServers.tuda;
|
|
337
|
+
next = JSON.stringify(config, null, 2) + "\n";
|
|
338
|
+
}
|
|
339
|
+
if (next !== current) await writeWithParent(configPath, next);
|
|
340
|
+
}
|
|
341
|
+
async function agentStatus(client, homeDir) {
|
|
342
|
+
const executable = AGENT_EXECUTABLES[client];
|
|
343
|
+
const directories = [...(process.env.PATH ?? "").split(path3.delimiter), path3.join(homeDir, ".local/bin"), path3.join(homeDir, ".npm-global/bin"), ...homeDir === os2.homedir() ? ["/opt/homebrew/bin", "/usr/local/bin"] : []];
|
|
344
|
+
let executablePath;
|
|
345
|
+
for (const directory of directories.filter(Boolean)) {
|
|
346
|
+
const candidate = path3.join(directory, executable + (process.platform === "win32" ? ".cmd" : ""));
|
|
347
|
+
try {
|
|
348
|
+
await access3(candidate, 1);
|
|
349
|
+
executablePath = candidate;
|
|
350
|
+
break;
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const desktop = client === "codex" && process.platform === "darwin" && (await pathExists("/Applications/Codex.app") || await pathExists(path3.join(homeDir, "Applications/Codex.app")));
|
|
355
|
+
const configPath = path3.join(homeDir, client === "codex" ? ".codex/config.toml" : client === "claude-code" ? ".claude.json" : AGENT_CONFIG_PATHS[client]);
|
|
356
|
+
const content = await readOptional(configPath);
|
|
357
|
+
let legacyConfigured = false;
|
|
358
|
+
if (client === "codex") legacyConfigured = /\[mcp_servers\.tuda\]/u.test(content);
|
|
359
|
+
else {
|
|
360
|
+
try {
|
|
361
|
+
legacyConfigured = Boolean(JSON.parse(content).mcpServers?.tuda);
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const skillPath = getSkillPath(client, homeDir);
|
|
366
|
+
const skillPaths = AGENT_SKILL_NAMES.map((name) => getSkillPath(client, homeDir, name));
|
|
367
|
+
const skillInstalled = (await Promise.all(skillPaths.map((target, index) => isSkillInstalled(target, AGENT_SKILL_NAMES[index])))).every(Boolean);
|
|
368
|
+
const skillManaged = (await Promise.all(skillPaths.map(async (target) => await readManagedSkillDigest(target) || await readOptional(path3.join(target, ".tuda-skill.json")).then((text) => {
|
|
369
|
+
try {
|
|
370
|
+
return JSON.parse(text).package === "@petercjl/tuda";
|
|
371
|
+
} catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
})))).every(Boolean);
|
|
375
|
+
const detected = Boolean(executablePath || desktop);
|
|
376
|
+
return { id: client, detected, detection: detected ? "installed" : content.trim() ? "config-only" : "not-found", executablePath, skillInstalled, skillManaged, skillPath, legacyConfigured };
|
|
377
|
+
}
|
|
378
|
+
var SUPPORTED_AGENT_IDS = ["codex", "claude-code", "workbuddy", "cursor", "windsurf", "cline", "roo-cline", "gemini-cli", "opencode"];
|
|
379
|
+
var AGENT_EXECUTABLES = { codex: "codex", "claude-code": "claude", workbuddy: "workbuddy", cursor: "cursor", windsurf: "windsurf", cline: "cline", "roo-cline": "roo", "gemini-cli": "gemini", opencode: "opencode" };
|
|
380
|
+
var AGENT_SKILL_DIRS = { codex: ".codex/skills", "claude-code": ".claude/skills", workbuddy: ".workbuddy/skills", cursor: ".cursor/skills", windsurf: ".codeium/windsurf/skills", cline: ".cline/skills", "roo-cline": ".roo/skills", "gemini-cli": ".gemini/skills", opencode: ".config/opencode/skills" };
|
|
381
|
+
var AGENT_CONFIG_PATHS = { workbuddy: ".workbuddy/config.json", cursor: ".cursor/config.json", windsurf: ".codeium/windsurf/config.json", cline: ".cline/config.json", "roo-cline": ".roo/config.json", "gemini-cli": ".gemini/settings.json", opencode: ".config/opencode/config.json" };
|
|
382
|
+
function getSkillPath(client, homeDir, name = "using-tuda") {
|
|
383
|
+
return path3.join(homeDir, AGENT_SKILL_DIRS[client], name);
|
|
384
|
+
}
|
|
385
|
+
async function isSkillInstalled(skillPath, name = "using-tuda") {
|
|
386
|
+
try {
|
|
387
|
+
const content = await readFile2(path3.join(skillPath, "SKILL.md"), "utf8");
|
|
388
|
+
return new RegExp(`^name:\\s*${name}\\s*$`, "mu").test(content);
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function replaceManagedSkill(skillPath, bundledSkillPath, bundleDigest) {
|
|
394
|
+
const temporaryPath = `${skillPath}.update-${process.pid}-${randomUUID2()}`;
|
|
395
|
+
const backupPath = `${skillPath}.backup-${process.pid}-${randomUUID2()}`;
|
|
396
|
+
let movedExisting = false;
|
|
397
|
+
let installedNew = false;
|
|
398
|
+
await mkdir2(path3.dirname(skillPath), { recursive: true });
|
|
399
|
+
try {
|
|
400
|
+
await cp2(bundledSkillPath, temporaryPath, { recursive: true, force: true });
|
|
401
|
+
await writeFile2(path3.join(temporaryPath, MANAGED_SKILL_FILE), `${JSON.stringify({ schemaVersion: 1, bundleDigest }, null, 2)}
|
|
402
|
+
`, "utf8");
|
|
403
|
+
try {
|
|
404
|
+
await rename2(skillPath, backupPath);
|
|
405
|
+
movedExisting = true;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (!isNodeError(error, "ENOENT")) throw error;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
await rename2(temporaryPath, skillPath);
|
|
411
|
+
installedNew = true;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (movedExisting) await rename2(backupPath, skillPath);
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
} finally {
|
|
417
|
+
await rm2(temporaryPath, { recursive: true, force: true });
|
|
418
|
+
if (installedNew) await rm2(backupPath, { recursive: true, force: true });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function readManagedSkillDigest(skillPath) {
|
|
422
|
+
try {
|
|
423
|
+
const value = JSON.parse(await readFile2(path3.join(skillPath, MANAGED_SKILL_FILE), "utf8"));
|
|
424
|
+
return typeof value.bundleDigest === "string" ? value.bundleDigest : null;
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function snapshotSkill(directory) {
|
|
430
|
+
const files = /* @__PURE__ */ new Map();
|
|
431
|
+
await collectSkillFiles(directory, "", files);
|
|
432
|
+
const hash = createHash2("sha256");
|
|
433
|
+
for (const [relativePath, fileDigest] of files) {
|
|
434
|
+
hash.update(relativePath).update("\0").update(fileDigest).update("\0");
|
|
435
|
+
}
|
|
436
|
+
return { digest: hash.digest("hex"), files };
|
|
437
|
+
}
|
|
438
|
+
async function collectSkillFiles(directory, relativeDirectory, files) {
|
|
439
|
+
const entries = await readdir2(directory, { withFileTypes: true });
|
|
440
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
441
|
+
if (entry.name === MANAGED_SKILL_FILE) continue;
|
|
442
|
+
const relativePath = path3.posix.join(relativeDirectory, entry.name);
|
|
443
|
+
const sourcePath = path3.join(directory, entry.name);
|
|
444
|
+
if (entry.isDirectory()) {
|
|
445
|
+
await collectSkillFiles(sourcePath, relativePath, files);
|
|
446
|
+
} else if (entry.isFile()) {
|
|
447
|
+
files.set(relativePath, createHash2("sha256").update(await readFile2(sourcePath)).digest("hex"));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function removeTomlTable(content, table) {
|
|
452
|
+
return content.replace(tomlTablePattern(table), "$1");
|
|
453
|
+
}
|
|
454
|
+
function tomlTablePattern(table) {
|
|
455
|
+
const escapedTable = table.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
456
|
+
return new RegExp(`(^|\\n)(?:#.*\\n)*\\[${escapedTable}\\][\\s\\S]*?(?=\\n(?:#.*\\n)*\\[(?!${escapedTable}\\.env\\])[^\\]]+\\]|$)`, "u");
|
|
457
|
+
}
|
|
458
|
+
function isRecord(value) {
|
|
459
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
460
|
+
}
|
|
461
|
+
async function readOptional(filePath) {
|
|
462
|
+
try {
|
|
463
|
+
return await readFile2(filePath, "utf8");
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (isNodeError(error, "ENOENT")) return "";
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function pathExists(targetPath) {
|
|
470
|
+
try {
|
|
471
|
+
await access3(targetPath);
|
|
472
|
+
return true;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (isNodeError(error, "ENOENT")) return false;
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function writeWithParent(filePath, content) {
|
|
479
|
+
const { mkdir: mkdir4 } = await import("node:fs/promises");
|
|
480
|
+
await mkdir4(path3.dirname(filePath), { recursive: true });
|
|
481
|
+
await writeFile2(filePath, content, "utf8");
|
|
482
|
+
}
|
|
483
|
+
function isNodeError(error, code) {
|
|
484
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === code;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/commands.ts
|
|
488
|
+
var CliError = class extends Error {
|
|
489
|
+
};
|
|
490
|
+
async function runAdd(options) {
|
|
491
|
+
const targets = options.agents ?? await detectCodingAgents(options.homeDir);
|
|
492
|
+
if (!targets.length) throw new CliError("\u672A\u68C0\u6D4B\u5230 Coding Agent\u3002\u4F7F\u7528 --agent workbuddy \u7B49\u6307\u5B9A\u76EE\u6807\u3002");
|
|
493
|
+
const results = [];
|
|
494
|
+
for (const client of targets) {
|
|
495
|
+
await installAgentSkill(client, options.homeDir, options.bundledSkillPath);
|
|
496
|
+
await removeLegacyIntegration(client, options.homeDir);
|
|
497
|
+
await setSkillAutoInstallDeclined(options.userDataPath, client, false);
|
|
498
|
+
results.push({ client, skill: "installed" });
|
|
499
|
+
}
|
|
500
|
+
return { results };
|
|
501
|
+
}
|
|
502
|
+
async function runRemove(options) {
|
|
503
|
+
const targets = options.agents ?? await detectCodingAgents(options.homeDir);
|
|
504
|
+
for (const client of targets) {
|
|
505
|
+
await uninstallAgentSkill(client, options.homeDir);
|
|
506
|
+
await removeLegacyIntegration(client, options.homeDir);
|
|
507
|
+
await setSkillAutoInstallDeclined(options.userDataPath, client, true);
|
|
508
|
+
}
|
|
509
|
+
return { results: targets.map((client) => ({ client, skill: "removed" })) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/runtime.ts
|
|
513
|
+
import { spawn } from "node:child_process";
|
|
514
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
515
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
516
|
+
import path5 from "node:path";
|
|
517
|
+
|
|
518
|
+
// ../electron/local-api-client.ts
|
|
519
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
520
|
+
import path4 from "node:path";
|
|
521
|
+
var BRIDGE_FILE = "local-api.json";
|
|
522
|
+
async function callLocalApi(userDataPath, method, args) {
|
|
523
|
+
const bridgePath = path4.join(userDataPath, BRIDGE_FILE);
|
|
524
|
+
const info = JSON.parse(await readFile3(bridgePath, "utf8"));
|
|
525
|
+
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535 || typeof info.token !== "string" || !/^[A-Za-z0-9_-]{20,100}$/u.test(info.token)) throw new Error("Tuda \u672C\u5730\u670D\u52A1\u53D1\u73B0\u4FE1\u606F\u65E0\u6548\u3002");
|
|
526
|
+
const response = await fetch(`http://127.0.0.1:${info.port}/call`, {
|
|
527
|
+
method: "POST",
|
|
528
|
+
headers: { authorization: `Bearer ${info.token}`, "content-type": "application/json" },
|
|
529
|
+
signal: AbortSignal.timeout(3e4),
|
|
530
|
+
body: JSON.stringify({ method, args })
|
|
531
|
+
});
|
|
532
|
+
const body = await response.json();
|
|
533
|
+
if (!response.ok) throw new Error(body.error || "Tuda \u672A\u80FD\u5B8C\u6210 CLI \u8BF7\u6C42\u3002");
|
|
534
|
+
return body.result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ../electron/background-session.ts
|
|
538
|
+
var CLI_BACKGROUND_FLAG = "--tuda-background";
|
|
539
|
+
|
|
540
|
+
// src/runtime.ts
|
|
541
|
+
async function desktopInstance(userDataPath) {
|
|
542
|
+
let info;
|
|
543
|
+
try {
|
|
544
|
+
info = JSON.parse(await readFile4(path5.join(userDataPath, "local-api.json"), "utf8"));
|
|
545
|
+
} catch {
|
|
546
|
+
throw new Error("INVALID_LOCAL_BRIDGE");
|
|
547
|
+
}
|
|
548
|
+
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535 || typeof info.token !== "string" || !/^[A-Za-z0-9_-]{20,100}$/.test(info.token)) throw new Error("INVALID_LOCAL_BRIDGE");
|
|
549
|
+
return createHash3("sha256").update(`${info.port}\0${info.token}`).digest("hex");
|
|
550
|
+
}
|
|
551
|
+
async function prepareDesktop(options) {
|
|
552
|
+
const call = await connect(options);
|
|
553
|
+
const state = await call("status");
|
|
554
|
+
return { ready: true, instanceId: await desktopInstance(options.userDataPath), protocolVersion: state.protocolVersion, desktopVersion: state.desktopVersion, boardPlugin: state.boardPlugin };
|
|
555
|
+
}
|
|
556
|
+
async function connect(options) {
|
|
557
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? Number(process.env.TUDA_STARTUP_TIMEOUT_MS ?? 3e4);
|
|
558
|
+
if (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs < 1e3 || startupTimeoutMs > 18e4) throw new Error("INVALID_STARTUP_TIMEOUT: expected 1000..180000 milliseconds");
|
|
559
|
+
const call = (method, args = {}) => callLocalApi(options.userDataPath, method, args);
|
|
560
|
+
try {
|
|
561
|
+
await call("status");
|
|
562
|
+
return call;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (options.noLaunch) throw error;
|
|
565
|
+
}
|
|
566
|
+
const app = await resolveTudaApp(options.appRoot);
|
|
567
|
+
if (!app) throw new Error("\u672A\u627E\u5230 Tuda App\uFF0C\u8BF7\u5148\u5B89\u88C5\u5E76\u6253\u5F00 App\uFF0C\u6216\u6307\u5B9A --app-path\u3002");
|
|
568
|
+
const child = spawn(app.commandPath, [CLI_BACKGROUND_FLAG], { detached: true, stdio: "ignore", windowsHide: true, env: Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== "ELECTRON_RUN_AS_NODE")) });
|
|
569
|
+
await new Promise((resolve, reject) => {
|
|
570
|
+
child.once("spawn", resolve);
|
|
571
|
+
child.once("error", reject);
|
|
572
|
+
});
|
|
573
|
+
child.unref();
|
|
574
|
+
const startupDeadline = Date.now() + startupTimeoutMs;
|
|
575
|
+
while (Date.now() < startupDeadline) {
|
|
576
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
577
|
+
try {
|
|
578
|
+
await call("status");
|
|
579
|
+
return call;
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
throw new Error("Tuda \u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5C31\u7EEA\uFF0C\u8BF7\u68C0\u67E5\u8FD0\u884C\u65E5\u5FD7\uFF0C\u6216\u624B\u52A8\u6253\u5F00 App \u540E\u91CD\u8BD5\u3002");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ../shared/media-import.ts
|
|
587
|
+
var MAX_INLINE_MEDIA_BYTES = 20 * 1024 * 1024;
|
|
588
|
+
var MAX_INLINE_MEDIA_DATA_URL_LENGTH = Math.ceil(MAX_INLINE_MEDIA_BYTES / 3) * 4 + 128;
|
|
589
|
+
function assertInlineMediaByteLength(bytes, label = "\u7D20\u6750") {
|
|
590
|
+
if (bytes === 0) throw new Error(`${label}\u4E3A\u7A7A\uFF0C\u8BF7\u9009\u62E9\u975E\u7A7A\u7D20\u6750\u6587\u4EF6\u3002`);
|
|
591
|
+
if (bytes > MAX_INLINE_MEDIA_BYTES) {
|
|
592
|
+
throw new Error(`${label}\u539F\u59CB\u6587\u4EF6\u5927\u5C0F\u8D85\u8FC7 20 MiB\uFF08${MAX_INLINE_MEDIA_BYTES} \u5B57\u8282\uFF09\uFF0C\u5F53\u524D ${bytes} \u5B57\u8282\u3002\u8BF7\u538B\u7F29\u540E\u91CD\u8BD5\uFF0C\u6216\u901A\u8FC7 App \u5BFC\u5165\u3002`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ../electron/workspace-tools.ts
|
|
597
|
+
var MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
598
|
+
function listWorkspaceTools() {
|
|
599
|
+
return [
|
|
600
|
+
tool("status", "Read workspace status and protocol version."),
|
|
601
|
+
tool("get_space", "Read selected personal/enterprise space, authenticated member, collaboration and pending-save state. No tokens returned."),
|
|
602
|
+
tool("select_space", "Switch the desktop selected space after flushing changes. Enterprise requires DingTalk login.", { target: { type: "string", enum: ["personal", "enterprise"] } }, ["target"]),
|
|
603
|
+
tool("list_providers", "List saved providers without credentials.", { kind: { type: "string", enum: ["image", "video", "text", "audio"] } }),
|
|
604
|
+
tool("get_provider", "Read saved Provider configuration without credentials.", { providerId: stringField("Provider ID") }, ["providerId"]),
|
|
605
|
+
tool("configure_provider", "Atomically configure a video or TTS provider. Does not submit generation. Omitted fields and credentials are preserved; empty apiKey clears it.", {
|
|
606
|
+
kind: { type: "string", enum: ["video", "audio"] },
|
|
607
|
+
action: { type: "string", enum: ["upsert", "default", "remove"] },
|
|
608
|
+
providerId: stringField("Existing ID or optional new ID"),
|
|
609
|
+
name: stringField("Display name"),
|
|
610
|
+
type: { type: "string", enum: ["ark", "fal", "openrouter", "minimax", "openai-compatible"] },
|
|
611
|
+
baseUrl: stringField("HTTP(S) service URL"),
|
|
612
|
+
model: stringField("Default model"),
|
|
613
|
+
models: stringArrayField("Saved models", 0),
|
|
614
|
+
voice: stringField("TTS voice ID"),
|
|
615
|
+
speechEndpoint: stringField("TTS relative endpoint; defaults to /v1/audio/speech"),
|
|
616
|
+
connectionProviderId: stringField("Video image connection ID; empty detaches"),
|
|
617
|
+
apiKey: stringField("Credential to encrypt; empty clears; never returned"),
|
|
618
|
+
setDefault: { type: "boolean" }
|
|
619
|
+
}, ["kind"]),
|
|
620
|
+
tool("list_models", "Read saved models for a provider.", { providerId: stringField("Provider ID") }, ["providerId"]),
|
|
621
|
+
tool("list_tasks", "Read generation history."),
|
|
622
|
+
tool("cancel_generation", "Cancel a locally queued image task before provider submission. Running requests are not cancelled and remote billing cannot be stopped or confirmed. Ctrl+C in the CLI only stops waiting.", { generationId: stringField("Image client task/generation ID") }, ["generationId"]),
|
|
623
|
+
tool("list_modification_requests", "Read saved annotation/mask edit briefs. No generation is submitted. Only ready requests may be claimed.", { boardId: stringField("Optional board ID") }),
|
|
624
|
+
tool("get_modification_request", "Read a brief with immutable sourcePath/sourceVersionId, relative regions, annotationPath/maskPath and required clarification. Use read_asset to inspect its managed images.", { requestId: stringField("Request ID") }, ["requestId"]),
|
|
625
|
+
tool("claim_modification_request", "Atomically claim a ready brief. Other agents cannot claim it. Retain the returned claim.token; use the fixed sourcePath when editing. No model call is submitted.", { requestId: stringField("Request ID"), owner: stringField("Agent session identifier") }, ["requestId", "owner"]),
|
|
626
|
+
tool("clarify_modification_request", "Supply explicit user clarification to a needs-input/failed brief. Never invent missing instructions. Does not generate images.", { requestId: stringField("Request ID"), instruction: stringField("Clarified user instruction") }, ["requestId", "instruction"]),
|
|
627
|
+
tool("finish_modification_request", "Link a claimed brief to a succeeded edit task and its managed outputs, or record an error. Requires claim token. Results are appended to the original node once.", { requestId: stringField("Request ID"), claimToken: stringField("Claim token"), resultJobId: stringField("Succeeded edit generation job ID"), resultPaths: stringArrayField("Managed output paths", 1), error: stringField("Failure description") }, ["requestId", "claimToken"]),
|
|
628
|
+
tool("create_canvas", "Create a persistent HTML/React/Vue code Canvas on the current board. Code is stored, not executed; user clicks Run in its isolated preview. React accepts JSX with default export or App; Vue accepts SFC script setup/template/style. Only built-in react, react-dom/client, vue imports are supported; no npm installation or network requests. Read get_board before editing existing nodes.", { boardId: stringField("Current board ID"), framework: { type: "string", enum: ["html", "react", "vue"] }, code: { type: "string", maxLength: 1e5 }, title: stringField("Canvas title") }, ["boardId", "framework", "code"]),
|
|
629
|
+
tool("update_canvas", "Update a Canvas node after reading get_board. Send full replacement code and expectedCode equal to the last read code to avoid overwriting manual edits. Does not execute code.", { boardId: stringField("Current board ID"), nodeId: stringField("Canvas node ID"), framework: { type: "string", enum: ["html", "react", "vue"] }, code: { type: "string", maxLength: 1e5 }, expectedCode: stringField("Exact previous code read from get_board"), title: stringField("Optional title") }, ["boardId", "nodeId", "framework", "code", "expectedCode"]),
|
|
630
|
+
tool("list_templates", "List reusable custom templates shared with the App. Legacy local templates migrate when the App workspace opens."),
|
|
631
|
+
tool("get_template", "Read a saved reusable template.", { templateId: stringField("Template ID") }, ["templateId"]),
|
|
632
|
+
...["create_template", "update_template"].map((name) => tool(name, "Save reusable rules in the App template library. No generation or model call. Updates preserve omitted fields.", {
|
|
633
|
+
templateId: stringField("Existing template ID for update"),
|
|
634
|
+
name: stringField("Template name"),
|
|
635
|
+
description: stringField("Description"),
|
|
636
|
+
prompt: stringField("Fixed reusable rules"),
|
|
637
|
+
category: stringField("marketing/product/social/illustration/portrait/brand/concept/editing/other"),
|
|
638
|
+
mode: { type: "string", enum: ["generate", "edit"] },
|
|
639
|
+
useCase: stringField("Image use case; defaults to other"),
|
|
640
|
+
expectedPrompt: stringField("Optional previous prompt for optimistic concurrency"),
|
|
641
|
+
extractionScenario: { type: "string", enum: ["design-language", "layout-skeleton", "series-template", "replica"] },
|
|
642
|
+
fields: { type: "array", items: { type: "object", required: ["id", "label", "value"], properties: { id: stringField("Field ID"), label: stringField("Label"), value: stringField("Rule") } } }
|
|
643
|
+
}, name === "create_template" ? ["name", "prompt"] : ["templateId"])),
|
|
644
|
+
tool("get_board_library", "Read shared folder tree, board locations and directory revision in the selected space."),
|
|
645
|
+
tool("manage_board_library", "Create/rename folders, copy or move boards or folder trees, or delete an empty folder. Requires the latest expectedRevision from get_board_library. Null parentId means root; moves retain contents and asset paths.", {
|
|
646
|
+
expectedRevision: { type: "integer", minimum: 0 },
|
|
647
|
+
action: { type: "object", properties: {
|
|
648
|
+
type: { type: "string", enum: ["createFolder", "renameFolder", "move", "copy", "deleteEmptyFolder"] },
|
|
649
|
+
name: stringField("Folder name"),
|
|
650
|
+
id: stringField("Existing folder ID"),
|
|
651
|
+
parentId: { type: ["string", "null"] },
|
|
652
|
+
items: { type: "array", minItems: 1, maxItems: 1e3, items: { type: "object", properties: { kind: { type: "string", enum: ["board", "folder"] }, id: stringField("Item ID") }, required: ["kind", "id"] } }
|
|
653
|
+
}, required: ["type"], additionalProperties: false }
|
|
654
|
+
}, ["expectedRevision", "action"]),
|
|
655
|
+
tool("list_boards", "List Tuda boards."),
|
|
656
|
+
tool("resolve_generation_board", "Resolve the configured CLI default board, or reuse/create CLI \u521B\u4F5C when unset. An explicit boardId takes priority. Missing configured boards fail before generation.", { boardId: stringField("Optional target board ID") }),
|
|
657
|
+
tool("get_active_board", "Read the board currently active in the Tuda workspace, if any."),
|
|
658
|
+
tool("get_board", "Read a board including its nodes and image URLs.", { boardId: stringField("Board ID") }, ["boardId"]),
|
|
659
|
+
tool("get_board_layout", "Inspect a board's rendered bounds and report overlapping image nodes.", { boardId: stringField("Board ID") }, ["boardId"]),
|
|
660
|
+
tool("open_board", "Open a board in Tuda without changing its viewport.", { boardId: stringField("Board ID") }, ["boardId"]),
|
|
661
|
+
tool("fit_board_view", "Open a board in Tuda and fit all of its image nodes into the visible canvas.", { boardId: stringField("Board ID") }, ["boardId"]),
|
|
662
|
+
tool("focus_nodes", "Open a board and focus selected nodes in the visible canvas.", { boardId: stringField("Board ID"), nodeIds: stringArrayField("Board node IDs", 1) }, ["boardId", "nodeIds"]),
|
|
663
|
+
tool("move_image", "Move an image node to absolute canvas coordinates.", { boardId: stringField("Board ID"), nodeId: stringField("Image node ID"), x: numberField("Absolute canvas X coordinate"), y: numberField("Absolute canvas Y coordinate") }, ["boardId", "nodeId", "x", "y"]),
|
|
664
|
+
tool("resize_image", "Resize an image node by display width while preserving its image aspect ratio.", { boardId: stringField("Board ID"), nodeId: stringField("Image node ID"), displayWidth: { type: "number", minimum: 120, maximum: 600, description: "Display width in canvas units" } }, ["boardId", "nodeId", "displayWidth"]),
|
|
665
|
+
tool("arrange_images", "Arrange image nodes around their current center.", { boardId: stringField("Board ID"), nodeIds: stringArrayField("Image node IDs", 2), mode: { type: "string", enum: ["horizontal", "vertical", "grid"] }, gap: { type: "number", minimum: 0, maximum: 1e3, description: "Gap between images; defaults to 40" } }, ["boardId", "nodeIds", "mode"]),
|
|
666
|
+
tool("duplicate_images", "Duplicate image nodes, including their version history.", { boardId: stringField("Board ID"), nodeIds: stringArrayField("Image node IDs", 1), offset: { type: "number", minimum: -1e4, maximum: 1e4, description: "X and Y offset; defaults to 24" } }, ["boardId", "nodeIds"]),
|
|
667
|
+
tool("trash_images", "Move image nodes to Tuda's recoverable image trash.", { boardId: stringField("Board ID"), nodeIds: stringArrayField("Image node IDs", 1) }, ["boardId", "nodeIds"]),
|
|
668
|
+
tool("restore_image", "Restore an image from Tuda's recoverable image trash.", { trashId: stringField("Trashed image record ID returned by trash_images") }, ["trashId"]),
|
|
669
|
+
tool("set_image_version", "Switch an image node to a zero-based version index.", { boardId: stringField("Board ID"), nodeId: stringField("Image node ID"), versionIndex: { type: "integer", minimum: 0, description: "Zero-based image version index" } }, ["boardId", "nodeId", "versionIndex"]),
|
|
670
|
+
tool("create_board", "Create a new Tuda board.", { name: stringField("Board name") }),
|
|
671
|
+
tool("list_assets", "List searchable image assets.", { limit: { type: "number", minimum: 1, maximum: 100 } }),
|
|
672
|
+
tool("read_asset", "Read a managed image as a data URL. Use the CLI assets export command to save a source, annotation or mask locally for visual inspection.", { path: stringField("Managed Tuda image path") }, ["path"]),
|
|
673
|
+
tool("get_generation_status", "Read an image generation or edit task. Poll this after generate_image or edit_image returns a generationId.", { generationId: stringField("Generation ID") }, ["generationId"]),
|
|
674
|
+
tool("generate_image", "Start image generation using Tuda's configured provider. Reference paths are managed Tuda asset paths and remain visible on the board. Set background to transparent for a transparent PNG. Check the Provider capabilities with list_providers first; unsupported transparency requests fail instead of falling back to an opaque image. Returns immediately with a generationId; poll get_generation_status until terminal. Successful images are added to the board. Omit boardId to use the configured CLI default board.", { boardId: stringField("Target board ID"), prompt: stringField("Image prompt"), templateId: stringField("Optional saved template; prompt supplies this run content"), providerId: stringField("Provider ID"), quality: { type: "string", enum: ["auto", "low", "medium", "high"] }, style: stringField("Style"), referencePaths: { type: "array", items: { type: "string" }, description: "Managed Tuda reference image paths" }, n: { type: "number", minimum: 1, maximum: 4 }, model: stringField("Optional model"), size: stringField("Optional size"), background: { type: "string", enum: ["auto", "opaque", "transparent"], description: "Requested output background; support depends on the active Provider" } }, ["prompt"]),
|
|
675
|
+
tool("edit_image", "Edit a managed image. For a claimed annotation brief, supply modificationRequestId and claimToken: the app validates its fixed source, uses its mask/references and binds one execution key. Finish the request to append results once. Set background to transparent for a transparent PNG; check Provider capabilities with list_providers first. Unsupported transparency requests fail. This operation may incur provider charges.", { boardId: stringField("Target board ID"), sourcePath: stringField("Managed source image path"), prompt: stringField("Reviewed edit instruction"), templateId: stringField("Optional saved edit template"), providerId: stringField("Provider ID"), model: stringField("Model ID"), background: { type: "string", enum: ["auto", "opaque", "transparent"], description: "Requested output background; transparent produces PNG and requires Provider support" }, modificationRequestId: stringField("Optional claimed modification request ID"), claimToken: stringField("Token returned only by claim_modification_request"), referencePaths: stringArrayField("Additional managed references when no modification request", 0) }, ["boardId", "sourcePath", "prompt"]),
|
|
676
|
+
tool("import_image", "Import a data URL image into Tuda managed assets and optionally append it to a board. Maximum original file size: 20 MiB; independent of text limits.", { dataUrl: { ...stringField("Base64 image data URL; decoded file must be non-empty and at most 20 MiB"), maxLength: MAX_INLINE_MEDIA_DATA_URL_LENGTH }, boardId: stringField("Optional target board ID") }, ["dataUrl"]),
|
|
677
|
+
tool("generate_video", "Start an asynchronous video generation task. Returns a videoGenerationId immediately; poll get_video_generation_status until terminal. Final results are managed local files, never remote provider URLs.", {
|
|
678
|
+
boardId: stringField("Target board ID"),
|
|
679
|
+
prompt: stringField("Video prompt"),
|
|
680
|
+
mode: { type: "string", enum: ["generate", "remix", "extend"] },
|
|
681
|
+
model: stringField("Optional video model"),
|
|
682
|
+
aspectRatio: stringField("Optional aspect ratio, for example 16:9"),
|
|
683
|
+
resolution: stringField("Optional resolution, for example 720p"),
|
|
684
|
+
durationSeconds: { type: "number", minimum: 1, maximum: 60 },
|
|
685
|
+
referenceImagePaths: { type: "array", items: { type: "string" }, description: "Managed reference image paths. Empty or omitted uses the requested board image or latest board image; set textOnly=true for text-only." },
|
|
686
|
+
textOnly: { type: "boolean", description: "Explicitly generate without board image references" },
|
|
687
|
+
referenceNodeIds: stringArrayField("Board image node IDs to animate; used when referenceImagePaths is omitted", 0),
|
|
688
|
+
firstFramePath: stringField("Optional managed first-frame image path"),
|
|
689
|
+
lastFramePath: stringField("Optional managed last-frame image path"),
|
|
690
|
+
sourceVideoPath: stringField("Managed source video path for remix or extend"),
|
|
691
|
+
referenceVideoPaths: { type: "array", items: { type: "string" }, description: "Managed reference video paths" },
|
|
692
|
+
referenceAudioPaths: { type: "array", items: { type: "string" }, description: "Managed reference audio paths" },
|
|
693
|
+
generationNodeId: stringField("Video generation node ID for a board workflow"),
|
|
694
|
+
targetVideoNodeId: stringField("Target video node ID for remix or extend"),
|
|
695
|
+
generateAudio: { type: "boolean" },
|
|
696
|
+
watermark: { type: "boolean" },
|
|
697
|
+
providerId: stringField("Optional provider ID")
|
|
698
|
+
}, ["prompt"]),
|
|
699
|
+
tool("get_video_generation_status", "Read a persisted video task. Poll until completed, failed, or cancelled. Completed results expose managed local paths only.", { videoGenerationId: stringField("Video generation ID") }, ["videoGenerationId"]),
|
|
700
|
+
tool("stop_video_polling", "Stop this App session's local polling for a video task. This does not cancel the remote provider task.", { videoGenerationId: stringField("Video generation ID") }, ["videoGenerationId"]),
|
|
701
|
+
tool("import_video", "Import a video data URL into Tuda managed storage and optionally append it to a board. Maximum original file size: 20 MiB; independent of text limits. Remote URLs are not accepted as final results.", { dataUrl: { ...stringField("Base64 MP4/MOV/WebM data URL; decoded file must be non-empty and at most 20 MiB"), maxLength: MAX_INLINE_MEDIA_DATA_URL_LENGTH }, boardId: stringField("Optional target board ID") }, ["dataUrl"]),
|
|
702
|
+
tool("slice_image", "Locally slice a managed image into PNG assets and append to a board. No model call. Specify crops OR rows/columns (1-8); margin/gap are source pixels. Preserves the original. Returns images, crops and nodeIds in reading order.", {
|
|
703
|
+
boardId: stringField("Target board ID"),
|
|
704
|
+
nodeId: stringField("Source image node ID; alternatively sourcePath"),
|
|
705
|
+
sourcePath: stringField("Managed source image path"),
|
|
706
|
+
rows: { type: "integer", minimum: 1, maximum: 8 },
|
|
707
|
+
columns: { type: "integer", minimum: 1, maximum: 8 },
|
|
708
|
+
margin: { type: "integer", minimum: 0 },
|
|
709
|
+
gap: { type: "integer", minimum: 0 },
|
|
710
|
+
crops: { type: "array", minItems: 1, maxItems: 64, items: { type: "object", required: ["x", "y", "width", "height"], additionalProperties: false, properties: { x: { type: "integer", minimum: 0 }, y: { type: "integer", minimum: 0 }, width: { type: "integer", minimum: 1 }, height: { type: "integer", minimum: 1 } } } }
|
|
711
|
+
}, ["boardId"]),
|
|
712
|
+
tool("export_assets", "Export selected managed image paths as a ZIP to Tuda's download directory.", { paths: { type: "array", items: { type: "string" } } }, ["paths"]),
|
|
713
|
+
tool("open_tuda", "Open Tuda visibly for the user.")
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
function tool(name, description, properties = {}, required = []) {
|
|
717
|
+
return { name, description, inputSchema: { type: "object", properties: { space: { type: "string", enum: ["personal", "enterprise"], description: "Pin this operation to a space; omitted uses the selected desktop space at task start." }, ...properties }, ...required.length ? { required } : {} } };
|
|
718
|
+
}
|
|
719
|
+
function stringField(description) {
|
|
720
|
+
return { type: "string", description };
|
|
721
|
+
}
|
|
722
|
+
function numberField(description) {
|
|
723
|
+
return { type: "number", description };
|
|
724
|
+
}
|
|
725
|
+
function stringArrayField(description, minItems) {
|
|
726
|
+
return { type: "array", items: { type: "string" }, minItems, uniqueItems: true, description };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/main.ts
|
|
730
|
+
var HELP = `Tuda CLI \u2014 \u4F7F\u7528 App \u4FDD\u5B58\u7684\u6A21\u578B\u521B\u4F5C
|
|
731
|
+
|
|
732
|
+
tuda status \u5DE5\u4F5C\u533A\u72B6\u6001
|
|
733
|
+
tuda desktop prepare \u51C6\u5907\u684C\u9762\u540E\u53F0\u5E76\u8FD4\u56DE\u4E0D\u542B\u51ED\u8BC1\u7684\u5B9E\u4F8B\u8EAB\u4EFD
|
|
734
|
+
tuda providers list [--kind image] \u5DF2\u4FDD\u5B58\u7684\u6A21\u578B\u670D\u52A1\uFF08\u4E0D\u5305\u542B\u5BC6\u94A5\uFF09
|
|
735
|
+
tuda providers show <id> \u67E5\u770B\u914D\u7F6E\uFF08\u4E0D\u5305\u542B\u5BC6\u94A5\uFF09
|
|
736
|
+
tuda providers set --kind video|audio --provider <id> --model <id> [\u914D\u7F6E\u9009\u9879]
|
|
737
|
+
tuda providers default|remove <id> --kind video|audio
|
|
738
|
+
tuda models list --provider <id> \u670D\u52A1\u5DF2\u4FDD\u5B58\u7684\u6A21\u578B
|
|
739
|
+
tuda models set --kind video|audio --provider <id> --model <id> [--saved-model <id>]
|
|
740
|
+
\u914D\u7F6E\uFF1A--type --name --base-url --voice --speech-endpoint --connection-provider
|
|
741
|
+
--saved-model <id>\uFF08\u53EF\u91CD\u590D\uFF09 --default --api-key-env <\u53D8\u91CF\u540D> | --api-key-stdin
|
|
742
|
+
--clear-api-key \u6E05\u9664\u5BC6\u94A5\uFF1B\u7701\u7565\u5BC6\u94A5\u53C2\u6570\u5219\u4FDD\u7559\u539F\u51ED\u8BC1\u3002audio \u4E3A\u72EC\u7ACB TTS \u914D\u7F6E\u3002
|
|
743
|
+
tuda image generate --prompt <text> [--reference <file>] [--out <file>]
|
|
744
|
+
tuda image slice --path <managed-path> --rows 3 --columns 4 [--out <directory>]
|
|
745
|
+
tuda image edit --input <file> --prompt <text> [--out <file>]
|
|
746
|
+
tuda video generate --prompt <text> [--board <id>]
|
|
747
|
+
tuda tasks list|get|wait|cancel [task-id] [--kind video] [--out <file>]
|
|
748
|
+
tuda boards list|create|show|open [id] [--name <name>]
|
|
749
|
+
tuda assets list|import|export [--input <file>] [--path <managed-path>] [--out <file>]
|
|
750
|
+
tuda templates list|show [id] \u67E5\u8BE2\u53EF\u590D\u7528\u6A21\u677F
|
|
751
|
+
tuda templates create --name <name> --prompt <rules>
|
|
752
|
+
tuda templates update <id> --prompt <rules>
|
|
753
|
+
\u6A21\u677F\u5B57\u6BB5\uFF1A--description --category --mode --use-case\uFF1B--input <JSON\u6587\u4EF6>
|
|
754
|
+
\u56FE\u7247\u751F\u6210/\u7F16\u8F91\uFF1A--template <id> --prompt <\u672C\u6B21\u5185\u5BB9>
|
|
755
|
+
tuda tools list \u5B8C\u6574\u64CD\u4F5C\u76EE\u5F55\u53CA JSON schema
|
|
756
|
+
tuda guard descriptor \u79BB\u7EBF\u8F93\u51FA Guard \u547D\u4EE4\u63CF\u8FF0\uFF08\u4E0D\u4F1A\u5B89\u88C5\u6216\u542F\u7528 Guard\uFF09
|
|
757
|
+
tuda call <operation> --input '<JSON>' \u8C03\u7528\u4EFB\u610F\u5DE5\u4F5C\u53F0\u64CD\u4F5C
|
|
758
|
+
tuda add [--agent <id>] \u5B89\u88C5 CLI Skill\uFF0C\u8FC1\u79FB\u65E7\u96C6\u6210
|
|
759
|
+
tuda remove --agent <id> \u79FB\u9664 Skill
|
|
760
|
+
|
|
761
|
+
\u901A\u7528\u9009\u9879\uFF1A--json --async --provider <id> --model <id> --board <id>
|
|
762
|
+
--user-data-path <path> --app-path <path> --no-launch --timeout <seconds> --startup-timeout <seconds>
|
|
763
|
+
\u9ED8\u8BA4\u81EA\u52A8\u542F\u52A8\u9690\u85CF\u540E\u53F0\u670D\u52A1\uFF0C\u4E0D\u663E\u793A App \u7A97\u53E3\uFF1B--no-launch \u4EC5\u8FDE\u63A5\u5DF2\u6709\u670D\u52A1\u3002
|
|
764
|
+
\u751F\u6210\uFF1A--size auto --n 1 --quality high --background transparent
|
|
765
|
+
\u672A\u6307\u5B9A --board \u65F6\u4F7F\u7528\u8BBE\u7F6E\u4E2D\u7684 CLI \u9ED8\u8BA4\u753B\u677F\uFF1B\u672A\u914D\u7F6E\u65F6\u4F7F\u7528\u300CCLI \u521B\u4F5C\u300D\u3002
|
|
766
|
+
\u9ED8\u8BA4\u7B49\u5F85\u4EFB\u52A1\u5B8C\u6210\uFF1BCtrl+C \u53EA\u7ED3\u675F\u7B49\u5F85\u3002\u5DF2\u6709\u8F93\u51FA\u6587\u4EF6\u4E0D\u4F1A\u8986\u76D6\uFF0C\u9664\u975E --overwrite\u3002`;
|
|
767
|
+
function parse(argv) {
|
|
768
|
+
const words = [], flags = {};
|
|
769
|
+
const bools = /* @__PURE__ */ new Set(["json", "async", "no-launch", "overwrite", "help", "version", "default", "api-key-stdin", "clear-api-key"]);
|
|
770
|
+
const values = /* @__PURE__ */ new Set(["template", "description", "category", "mode", "use-case", "expected-prompt", "agent", "home", "kind", "provider", "model", "prompt", "input", "out", "path", "board", "name", "size", "quality", "style", "background", "n", "reference", "ratio", "duration", "resolution", "timeout", "startup-timeout", "request-id", "app-path", "user-data-path", "type", "base-url", "voice", "speech-endpoint", "connection-provider", "saved-model", "api-key-env", "rows", "columns", "margin", "gap", "crops", "node"]);
|
|
771
|
+
for (let i = 0; i < argv.length; i++) {
|
|
772
|
+
const arg = argv[i];
|
|
773
|
+
if (!arg.startsWith("--")) {
|
|
774
|
+
words.push(arg);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const key = arg.slice(2);
|
|
778
|
+
if (bools.has(key)) {
|
|
779
|
+
flags[key] = ["true"];
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (!values.has(key)) throw new Error(`\u672A\u77E5\u53C2\u6570 --${key}\u3002`);
|
|
783
|
+
const value = argv[++i];
|
|
784
|
+
if (!value || value.startsWith("--")) throw new Error(`\u53C2\u6570 --${key} \u7F3A\u5C11\u503C\u3002`);
|
|
785
|
+
(flags[key] ??= []).push(value);
|
|
786
|
+
}
|
|
787
|
+
return { words, flags };
|
|
788
|
+
}
|
|
789
|
+
function record(value) {
|
|
790
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
791
|
+
}
|
|
792
|
+
var moduleDir = path6.dirname(fileURLToPath2(import.meta.url));
|
|
793
|
+
async function importFile(call, file, boardId) {
|
|
794
|
+
const mime = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" };
|
|
795
|
+
const mimeType = mime[path6.extname(file).toLowerCase()];
|
|
796
|
+
if (!mimeType) throw new Error("\u4E0D\u652F\u6301\u7684\u7D20\u6750\u6587\u4EF6\u683C\u5F0F\u3002");
|
|
797
|
+
const label = `\u7D20\u6750\u300C${path6.basename(file)}\u300D`;
|
|
798
|
+
const info = await stat(file);
|
|
799
|
+
if (!info.isFile()) throw new Error(`${label}\u4E0D\u662F\u666E\u901A\u6587\u4EF6\u3002`);
|
|
800
|
+
assertInlineMediaByteLength(info.size, label);
|
|
801
|
+
const bytes = await readFile5(file);
|
|
802
|
+
assertInlineMediaByteLength(bytes.length, label);
|
|
803
|
+
const result = await call(mimeType.startsWith("video/") ? "import_video" : "import_image", { dataUrl: `data:${mimeType};base64,${bytes.toString("base64")}`, ...boardId ? { boardId } : {} });
|
|
804
|
+
if (typeof result === "string") return result;
|
|
805
|
+
const r = record(result);
|
|
806
|
+
const managed = r.path ?? r.videoPath ?? r.imagePath;
|
|
807
|
+
if (typeof managed !== "string") throw new Error("\u7D20\u6750\u5BFC\u5165\u672A\u8FD4\u56DE\u53D7\u7BA1\u8DEF\u5F84\u3002");
|
|
808
|
+
return managed;
|
|
809
|
+
}
|
|
810
|
+
async function exportFiles(call, paths, destination, overwrite) {
|
|
811
|
+
if (!paths.length) throw new Error("\u6CA1\u6709\u53EF\u5BFC\u51FA\u7684\u7D20\u6750\u3002");
|
|
812
|
+
const directory = paths.length > 1 || destination.endsWith(path6.sep) || (await stat(destination).catch(() => null))?.isDirectory();
|
|
813
|
+
const outputs = [];
|
|
814
|
+
for (let i = 0; i < paths.length; i++) {
|
|
815
|
+
const target = path6.resolve(directory ? path6.join(destination, `${i + 1}-${path6.basename(paths[i])}`) : destination);
|
|
816
|
+
const data = record(await call("read_asset", { path: paths[i] })).dataUrl;
|
|
817
|
+
if (typeof data !== "string" || !/^data:[^;]+;base64,/u.test(data)) throw new Error("\u7D20\u6750\u6570\u636E\u683C\u5F0F\u65E0\u6548\u3002");
|
|
818
|
+
await mkdir3(path6.dirname(target), { recursive: true });
|
|
819
|
+
await writeFile3(target, Buffer.from(data.slice(data.indexOf(",") + 1), "base64"), { flag: overwrite ? "w" : "wx" });
|
|
820
|
+
outputs.push(target);
|
|
821
|
+
}
|
|
822
|
+
return outputs;
|
|
823
|
+
}
|
|
824
|
+
async function main() {
|
|
825
|
+
if (await handleManagement(process.argv.slice(2))) return;
|
|
826
|
+
const { words, flags } = parse(process.argv.slice(2));
|
|
827
|
+
if (!(words[0] === "desktop" && words[1] === "prepare")) await automaticUpdateCheck();
|
|
828
|
+
const get = (key) => flags[key]?.at(-1);
|
|
829
|
+
if (flags.help || words[0] === "help" || !words.length && !flags.version) {
|
|
830
|
+
process.stdout.write(HELP + "\n");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (flags.version || words[0] === "version") {
|
|
834
|
+
const pkg = JSON.parse(await readFile5(path6.resolve(moduleDir, "../package.json"), "utf8"));
|
|
835
|
+
process.stdout.write(pkg.version + "\n");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const timeout = Number(get("timeout") ?? "600");
|
|
839
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("--timeout \u5FC5\u987B\u662F\u6B63\u6570\u3002");
|
|
840
|
+
const userDataPath = get("user-data-path") ?? process.env.TUDA_USER_DATA_PATH ?? defaultUserDataPath();
|
|
841
|
+
const [group, action, id] = words;
|
|
842
|
+
const output = (data) => process.stdout.write(JSON.stringify(flags.json ? { schemaVersion: 1, ok: true, data } : data, (key, value) => key === "previewImageDataUrls" ? void 0 : value, 2) + "\n");
|
|
843
|
+
if (group === "desktop" && action === "prepare") {
|
|
844
|
+
output(await prepareDesktop({
|
|
845
|
+
userDataPath,
|
|
846
|
+
appRoot: get("app-path"),
|
|
847
|
+
noLaunch: Boolean(flags["no-launch"]),
|
|
848
|
+
startupTimeoutMs: get("startup-timeout") === void 0 ? Number(process.env.TUDA_STARTUP_TIMEOUT_MS ?? 12e4) : Number(get("startup-timeout")) * 1e3
|
|
849
|
+
}));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (group === "guard" && action === "descriptor") {
|
|
853
|
+
output(JSON.parse(await readFile5(path6.resolve(moduleDir, "../guard/command.tuda.json"), "utf8")));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (group === "add" || group === "remove") {
|
|
857
|
+
const agents = flags.agent;
|
|
858
|
+
const supported = /* @__PURE__ */ new Set(["codex", "claude-code", "workbuddy", "cursor", "windsurf", "cline", "roo-cline", "gemini-cli", "opencode"]);
|
|
859
|
+
if (agents?.some((a) => !supported.has(a))) throw new Error("\u672A\u77E5\u7684 Coding Agent\u3002");
|
|
860
|
+
const options = { homeDir: get("home") ?? os3.homedir(), userDataPath, agents: agents ?? null, bundledSkillPath: path6.resolve(moduleDir, "../skills/using-tuda") };
|
|
861
|
+
output(await (group === "add" ? runAdd(options) : runRemove(options)));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (group === "tools" && action === "list") {
|
|
865
|
+
output(listWorkspaceTools());
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const call = await connect({ ...get("startup-timeout") === void 0 ? {} : { startupTimeoutMs: Number(get("startup-timeout")) * 1e3 }, userDataPath, appRoot: get("app-path"), noLaunch: Boolean(flags["no-launch"]) || Boolean(process.env.TUDA_USER_DATA_PATH) || Boolean(get("user-data-path")) });
|
|
869
|
+
if (group === "status" || group === "doctor") {
|
|
870
|
+
output(await call("status"));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (group === "providers" && action === "list") {
|
|
874
|
+
output(await call("list_providers", get("kind") ? { kind: get("kind") } : {}));
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (group === "providers" && action === "show") {
|
|
878
|
+
output(await call("get_provider", { providerId: id ?? get("provider") }));
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (group === "providers" && ["set", "default", "remove"].includes(action) || group === "models" && action === "set") {
|
|
882
|
+
if (!["video", "audio"].includes(get("kind") ?? "")) throw new Error("\u914D\u7F6E\u5FC5\u987B\u6307\u5B9A --kind video \u6216 audio\u3002");
|
|
883
|
+
const input = { kind: get("kind"), action: action === "set" ? "upsert" : action, providerId: id ?? get("provider") };
|
|
884
|
+
if (action !== "set" && !input.providerId) throw new Error("\u7F3A\u5C11 Provider ID\u3002");
|
|
885
|
+
const fields = { name: "name", type: "type", "base-url": "baseUrl", model: "model", voice: "voice", "speech-endpoint": "speechEndpoint", "connection-provider": "connectionProviderId" };
|
|
886
|
+
for (const [flag, field] of Object.entries(fields)) if (get(flag) !== void 0) input[field] = get(flag) === "none" && flag === "connection-provider" ? "" : get(flag);
|
|
887
|
+
if (flags["saved-model"]) input.models = flags["saved-model"];
|
|
888
|
+
if (flags.default) input.setDefault = true;
|
|
889
|
+
const keySources = [Boolean(get("api-key-env")), Boolean(flags["api-key-stdin"]), Boolean(flags["clear-api-key"])].filter(Boolean).length;
|
|
890
|
+
if (keySources > 1) throw new Error("\u5BC6\u94A5\u53C2\u6570\u4E0D\u80FD\u540C\u65F6\u4F7F\u7528\u3002");
|
|
891
|
+
if (action !== "set" && (keySources || Object.keys(input).some((key) => !["kind", "action", "providerId"].includes(key)))) throw new Error("\u9ED8\u8BA4/\u5220\u9664\u64CD\u4F5C\u4E0D\u63A5\u53D7\u5176\u5B83\u914D\u7F6E\u5B57\u6BB5\u3002");
|
|
892
|
+
if (get("api-key-env")) {
|
|
893
|
+
const key = process.env[get("api-key-env")];
|
|
894
|
+
if (!key?.trim()) throw new Error("\u6307\u5B9A\u7684 API Key \u73AF\u5883\u53D8\u91CF\u4E3A\u7A7A\u6216\u4E0D\u5B58\u5728\u3002");
|
|
895
|
+
input.apiKey = key.trim();
|
|
896
|
+
}
|
|
897
|
+
if (flags["api-key-stdin"]) {
|
|
898
|
+
if (process.stdin.isTTY) throw new Error("\u8BF7\u901A\u8FC7\u7BA1\u9053\u5411 stdin \u63D0\u4F9B\u5BC6\u94A5\u3002");
|
|
899
|
+
const chunks = [];
|
|
900
|
+
let bytes = 0;
|
|
901
|
+
for await (const chunk of process.stdin) {
|
|
902
|
+
const buffer = Buffer.from(chunk);
|
|
903
|
+
bytes += buffer.length;
|
|
904
|
+
if (bytes > 1e5) throw new Error("API Key \u8F93\u5165\u8FC7\u957F\u3002");
|
|
905
|
+
chunks.push(buffer);
|
|
906
|
+
}
|
|
907
|
+
const key = Buffer.concat(chunks).toString("utf8").trim();
|
|
908
|
+
if (!key) throw new Error("API Key \u8F93\u5165\u4E3A\u7A7A\uFF1B\u6E05\u9664\u8BF7\u4F7F\u7528 --clear-api-key\u3002");
|
|
909
|
+
input.apiKey = key;
|
|
910
|
+
}
|
|
911
|
+
if (flags["clear-api-key"]) input.apiKey = "";
|
|
912
|
+
output(await call("configure_provider", input));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (group === "models" && action === "list") {
|
|
916
|
+
output(await call("list_models", { providerId: get("provider") }));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (group === "call") {
|
|
920
|
+
if (!action || !listWorkspaceTools().some((tool2) => tool2.name === action)) throw new Error("\u672A\u77E5\u7684\u5DE5\u4F5C\u53F0\u64CD\u4F5C\uFF0C\u8BF7\u8FD0\u884C tuda tools list\u3002");
|
|
921
|
+
const args = JSON.parse(get("input") ?? "{}");
|
|
922
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("--input \u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3002");
|
|
923
|
+
output(await call(action, args));
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
let result;
|
|
927
|
+
const video = group === "video" || get("kind") === "video";
|
|
928
|
+
if (group === "templates") {
|
|
929
|
+
const methods = { list: "list_templates", show: "get_template", create: "create_template", update: "update_template" };
|
|
930
|
+
if (!methods[action]) throw new Error("\u672A\u77E5\u6A21\u677F\u64CD\u4F5C\u3002");
|
|
931
|
+
if (["show", "update"].includes(action) && !id) throw new Error("\u7F3A\u5C11\u6A21\u677F ID\u3002");
|
|
932
|
+
let input = {};
|
|
933
|
+
if (get("input")) {
|
|
934
|
+
const parsed = JSON.parse(await readFile5(get("input"), "utf8"));
|
|
935
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("\u6A21\u677F\u6587\u4EF6\u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3002");
|
|
936
|
+
input = parsed;
|
|
937
|
+
}
|
|
938
|
+
for (const [flag, field] of Object.entries({ name: "name", prompt: "prompt", description: "description", category: "category", mode: "mode", "use-case": "useCase", "expected-prompt": "expectedPrompt" })) {
|
|
939
|
+
if (get(flag) !== void 0) input[field] = get(flag);
|
|
940
|
+
}
|
|
941
|
+
if (id) input.templateId = id;
|
|
942
|
+
output(await call(methods[action], input));
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (group === "boards") {
|
|
946
|
+
const methods = { list: "list_boards", create: "create_board", show: "get_board", open: "open_board", active: "get_active_board" };
|
|
947
|
+
if (!methods[action]) throw new Error("\u672A\u77E5\u753B\u677F\u64CD\u4F5C\u3002");
|
|
948
|
+
output(await call(methods[action], { boardId: id, name: get("name") }));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (group === "assets") {
|
|
952
|
+
if (action === "list") {
|
|
953
|
+
output(await call("list_assets"));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (action === "import" && get("input")) {
|
|
957
|
+
output({ path: await importFile(call, get("input"), get("board")) });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (action === "export" && get("path") && get("out")) {
|
|
961
|
+
output({ paths: await exportFiles(call, [get("path")], get("out"), Boolean(flags.overwrite)) });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
throw new Error("\u7D20\u6750\u64CD\u4F5C\u7F3A\u5C11\u53C2\u6570\u3002\u8FD0\u884C tuda help \u67E5\u770B\u7528\u6CD5\u3002");
|
|
965
|
+
}
|
|
966
|
+
if (group === "image" && action === "slice") {
|
|
967
|
+
if ([get("input"), get("path"), get("node")].filter(Boolean).length !== 1) throw new Error("\u5207\u56FE\u987B\u6307\u5B9A\u4E14\u4EC5\u6307\u5B9A --input\u3001--path \u6216 --node\u3002");
|
|
968
|
+
const args = {};
|
|
969
|
+
for (const key of ["rows", "columns", "margin", "gap"]) if (get(key) !== void 0) args[key] = Number(get(key));
|
|
970
|
+
if (get("crops")) args.crops = JSON.parse(get("crops"));
|
|
971
|
+
if (args.crops === void 0 && (!get("rows") || !get("columns"))) throw new Error("\u8BF7\u6307\u5B9A --rows \u548C --columns\uFF0C\u6216 --crops JSON\u3002");
|
|
972
|
+
const boardId = get("board") ?? String(record(await call("resolve_generation_board")).boardId);
|
|
973
|
+
args.boardId = boardId;
|
|
974
|
+
if (get("node")) args.nodeId = get("node");
|
|
975
|
+
else args.sourcePath = get("path") ?? await importFile(call, get("input"), boardId);
|
|
976
|
+
const sliced = record(await call("slice_image", args));
|
|
977
|
+
if (get("out")) {
|
|
978
|
+
try {
|
|
979
|
+
sliced.exportedPaths = await exportFiles(call, sliced.images.map((image) => image.path), path6.resolve(get("out")) + path6.sep, Boolean(flags.overwrite));
|
|
980
|
+
} catch (error) {
|
|
981
|
+
throw new Error(`\u5207\u56FE\u5DF2\u4FDD\u5B58\u5230\u753B\u677F ${boardId}\uFF1B\u5BFC\u51FA\u5931\u8D25\uFF0C\u53EF\u7528 assets export \u91CD\u8BD5\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
output(sliced);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (group === "tasks" && action === "list") {
|
|
988
|
+
output(await call("list_tasks"));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (group === "tasks" && action === "cancel") {
|
|
992
|
+
if (!id) throw new Error("\u7F3A\u5C11\u4EFB\u52A1 ID\u3002");
|
|
993
|
+
if (video) throw new Error("\u89C6\u9891\u8FDC\u7AEF\u53D6\u6D88\u4E0D\u5728\u6B64\u64CD\u4F5C\u8303\u56F4\uFF1B\u4EC5\u505C\u6B62\u7B49\u5F85\u4E0D\u4F1A\u505C\u6B62\u8FDC\u7AEF\u4EFB\u52A1\u3002");
|
|
994
|
+
output(await call("cancel_generation", { generationId: id }));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (group === "tasks" && (action === "get" || action === "wait")) {
|
|
998
|
+
if (!id) throw new Error("\u7F3A\u5C11\u4EFB\u52A1 ID\u3002");
|
|
999
|
+
result = await call(video ? "get_video_generation_status" : "get_generation_status", video ? { videoGenerationId: id } : { generationId: id });
|
|
1000
|
+
if (action === "get") {
|
|
1001
|
+
const r = record(result);
|
|
1002
|
+
delete r.previewImageDataUrls;
|
|
1003
|
+
output(r);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
} else if (group === "image" && (action === "generate" || action === "edit") || group === "video" && action === "generate") {
|
|
1007
|
+
if (video && get("template")) throw new Error("--template \u76EE\u524D\u53EA\u652F\u6301\u56FE\u7247\u751F\u6210\u548C\u7F16\u8F91\u3002");
|
|
1008
|
+
if (!get("prompt")?.trim()) throw new Error("\u7F3A\u5C11 --prompt\u3002");
|
|
1009
|
+
if (action === "edit" && !get("input")) throw new Error("\u7F3A\u5C11 --input\u3002");
|
|
1010
|
+
let boardId = get("board");
|
|
1011
|
+
if (!boardId) {
|
|
1012
|
+
boardId = String(record(await call("resolve_generation_board")).boardId);
|
|
1013
|
+
}
|
|
1014
|
+
const args = { templateId: get("template"), boardId, prompt: get("prompt"), providerId: get("provider"), model: get("model"), size: get("size"), quality: get("quality"), background: get("background"), style: get("style"), n: get("n") ? Number(get("n")) : 1 };
|
|
1015
|
+
if (flags.reference) args[video ? "referenceImagePaths" : "referencePaths"] = await Promise.all(flags.reference.map((file) => importFile(call, file, boardId)));
|
|
1016
|
+
if (action === "edit") {
|
|
1017
|
+
if (!get("input")) throw new Error("\u7F3A\u5C11 --input\u3002");
|
|
1018
|
+
args.sourcePath = await importFile(call, get("input"), boardId);
|
|
1019
|
+
}
|
|
1020
|
+
if (video) {
|
|
1021
|
+
args.aspectRatio = get("ratio");
|
|
1022
|
+
args.durationSeconds = get("duration") ? Number(get("duration")) : void 0;
|
|
1023
|
+
args.resolution = get("resolution");
|
|
1024
|
+
}
|
|
1025
|
+
args.requestId = get("request-id") ?? randomUUID3();
|
|
1026
|
+
if (!video) process.stderr.write(`\u63D0\u4EA4\u8BF7\u6C42 ${String(args.requestId)}
|
|
1027
|
+
`);
|
|
1028
|
+
try {
|
|
1029
|
+
result = await call(video ? "generate_video" : action === "edit" ? "edit_image" : "generate_image", args);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}${video ? "" : `\uFF1B\u8BF7\u6C42 ID ${String(args.requestId)}\uFF0C\u8BF7\u5148\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001\u518D\u51B3\u5B9A\u662F\u5426\u91CD\u65B0\u751F\u6210\u3002`}`);
|
|
1032
|
+
}
|
|
1033
|
+
if (flags.async) {
|
|
1034
|
+
output(result);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
} else throw new Error("\u672A\u77E5\u547D\u4EE4\u3002\u8FD0\u884C tuda help \u67E5\u770B\u7528\u6CD5\u3002");
|
|
1038
|
+
let task = record(result);
|
|
1039
|
+
const taskId = task.generationId ?? task.videoGenerationId;
|
|
1040
|
+
if (typeof taskId !== "string") throw new Error("\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u672A\u8FD4\u56DE\u4EFB\u52A1 ID\u3002");
|
|
1041
|
+
process.stderr.write(`\u4EFB\u52A1 ${taskId}\uFF1BCtrl+C \u7ED3\u675F\u7B49\u5F85\uFF0C\u53EF\u7528 tasks wait ${taskId}${video ? " --kind video" : ""} \u7EE7\u7EED\u3002
|
|
1042
|
+
`);
|
|
1043
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
1044
|
+
while (!["succeeded", "completed", "failed", "cancelled", "interrupted", "unknown"].includes(String(task.status))) {
|
|
1045
|
+
if (Date.now() >= deadline) throw new Error(`\u7B49\u5F85\u8D85\u65F6\uFF0C\u4EFB\u52A1\u4ECD\u53EF\u67E5\u8BE2\uFF1A${taskId}`);
|
|
1046
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1047
|
+
task = record(await call(video ? "get_video_generation_status" : "get_generation_status", video ? { videoGenerationId: taskId } : { generationId: taskId }));
|
|
1048
|
+
}
|
|
1049
|
+
delete task.previewImageDataUrls;
|
|
1050
|
+
if (["failed", "cancelled", "interrupted", "unknown"].includes(String(task.status))) throw new Error(`\u4EFB\u52A1 ${taskId}\uFF1A${String(task.error ?? task.status)}`);
|
|
1051
|
+
if (get("out")) {
|
|
1052
|
+
const paths = video ? [String(record(task.video).path)] : (task.images ?? []).map((image) => image.path);
|
|
1053
|
+
try {
|
|
1054
|
+
task.exportedPaths = await exportFiles(call, paths, get("out"), Boolean(flags.overwrite));
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
throw new Error(`\u751F\u6210\u5DF2\u5B8C\u6210\uFF0C\u4EFB\u52A1 ${taskId}\uFF1B\u5BFC\u51FA\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}\u3002\u53EF\u518D\u6B21 tasks wait \u5BFC\u51FA\u3002`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
output(task);
|
|
1060
|
+
}
|
|
1061
|
+
void main().catch((error) => {
|
|
1062
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1063
|
+
if (process.argv.includes("--json")) process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: { code: /^([A-Z][A-Z0-9_]+):/.exec(message)?.[1] ?? "TUDA_ERROR", message } }) + "\n");
|
|
1064
|
+
else process.stderr.write(`tuda\uFF1A${message}
|
|
1065
|
+
`);
|
|
1066
|
+
process.exitCode = 1;
|
|
1067
|
+
});
|