zerocut-cli 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/.eslintrc.js +11 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +6 -0
- package/README.md +205 -0
- package/dist/bin/zerocut.d.ts +2 -0
- package/dist/bin/zerocut.js +5 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +24 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +129 -0
- package/dist/commands/ffmpeg.d.ts +4 -0
- package/dist/commands/ffmpeg.js +32 -0
- package/dist/commands/foo.d.ts +4 -0
- package/dist/commands/foo.js +14 -0
- package/dist/commands/help.d.ts +4 -0
- package/dist/commands/help.js +14 -0
- package/dist/commands/image.d.ts +4 -0
- package/dist/commands/image.js +149 -0
- package/dist/commands/music.d.ts +4 -0
- package/dist/commands/music.js +74 -0
- package/dist/commands/pandoc.d.ts +4 -0
- package/dist/commands/pandoc.js +32 -0
- package/dist/commands/skill.d.ts +4 -0
- package/dist/commands/skill.js +24 -0
- package/dist/commands/tts.d.ts +4 -0
- package/dist/commands/tts.js +74 -0
- package/dist/commands/video.d.ts +4 -0
- package/dist/commands/video.js +166 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/services/cerevox.d.ts +34 -0
- package/dist/services/cerevox.js +256 -0
- package/dist/services/commandLoader.d.ts +3 -0
- package/dist/services/commandLoader.js +80 -0
- package/dist/services/config.d.ts +15 -0
- package/dist/services/config.js +235 -0
- package/dist/skill/SKILL.md +133 -0
- package/dist/types/command.d.ts +6 -0
- package/dist/types/command.js +2 -0
- package/dist/utils/progress.d.ts +1 -0
- package/dist/utils/progress.js +13 -0
- package/eslint.config.js +30 -0
- package/package.json +52 -0
- package/scripts/copy-skill-md.cjs +8 -0
- package/src/bin/zerocut.ts +3 -0
- package/src/cli.ts +25 -0
- package/src/commands/config.ts +130 -0
- package/src/commands/ffmpeg.ts +37 -0
- package/src/commands/help.ts +13 -0
- package/src/commands/image.ts +194 -0
- package/src/commands/music.ts +78 -0
- package/src/commands/pandoc.ts +37 -0
- package/src/commands/skill.ts +20 -0
- package/src/commands/tts.ts +80 -0
- package/src/commands/video.ts +202 -0
- package/src/index.ts +1 -0
- package/src/services/cerevox.ts +296 -0
- package/src/services/commandLoader.ts +42 -0
- package/src/services/config.ts +230 -0
- package/src/skill/SKILL.md +209 -0
- package/src/types/command.ts +7 -0
- package/src/utils/progress.ts +10 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SESSION_SYMBOL = void 0;
|
|
4
|
+
exports.attachSessionToCommand = attachSessionToCommand;
|
|
5
|
+
exports.getSessionFromCommand = getSessionFromCommand;
|
|
6
|
+
exports.getApiKey = getApiKey;
|
|
7
|
+
exports.getRegion = getRegion;
|
|
8
|
+
exports.openSession = openSession;
|
|
9
|
+
exports.closeSession = closeSession;
|
|
10
|
+
exports.getMaterialUri = getMaterialUri;
|
|
11
|
+
exports.syncToTOS = syncToTOS;
|
|
12
|
+
exports.runFFMpegCommand = runFFMpegCommand;
|
|
13
|
+
exports.runPandocCommand = runPandocCommand;
|
|
14
|
+
const cerevox_1 = require("cerevox");
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const config_1 = require("./config");
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const promises_1 = require("node:fs/promises");
|
|
20
|
+
exports.SESSION_SYMBOL = Symbol("zerocut.session");
|
|
21
|
+
function attachSessionToCommand(cmd, session) {
|
|
22
|
+
cmd[exports.SESSION_SYMBOL] = session;
|
|
23
|
+
}
|
|
24
|
+
function getSessionFromCommand(cmd) {
|
|
25
|
+
return cmd[exports.SESSION_SYMBOL];
|
|
26
|
+
}
|
|
27
|
+
function getApiKey() {
|
|
28
|
+
const v = (0, config_1.getConfigValueSync)("apiKey");
|
|
29
|
+
return typeof v === "string" ? v : "";
|
|
30
|
+
}
|
|
31
|
+
function getRegion() {
|
|
32
|
+
const v = (0, config_1.getConfigValueSync)("region");
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
async function openSession() {
|
|
36
|
+
let apiKey = getApiKey();
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error("apiKey is not set");
|
|
39
|
+
}
|
|
40
|
+
const region = getRegion();
|
|
41
|
+
apiKey = region + apiKey.slice(2);
|
|
42
|
+
const cerevox = new cerevox_1.Cerevox({
|
|
43
|
+
apiKey,
|
|
44
|
+
});
|
|
45
|
+
let sandboxId = (0, config_1.getConfigValueSync)("sandboxId");
|
|
46
|
+
if (sandboxId) {
|
|
47
|
+
try {
|
|
48
|
+
return await cerevox.connect(sandboxId, 300000);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
sandboxId = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const session = await cerevox.launch({ timeout: 60, region });
|
|
55
|
+
sandboxId = session.sandbox.sandboxId;
|
|
56
|
+
(0, config_1.setConfigValueSync)("sandboxId", sandboxId);
|
|
57
|
+
return session;
|
|
58
|
+
}
|
|
59
|
+
async function closeSession(session) {
|
|
60
|
+
await session.close();
|
|
61
|
+
}
|
|
62
|
+
async function computeSha256(filePath) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
65
|
+
const stream = (0, node_fs_1.createReadStream)(filePath);
|
|
66
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
67
|
+
stream.on("error", reject);
|
|
68
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function getMaterialUri(session, fileName, options = {}) {
|
|
72
|
+
const resolvedOptions = {
|
|
73
|
+
fileSizeLimit: options.fileSizeLimit ?? -1,
|
|
74
|
+
};
|
|
75
|
+
const localPath = (0, node_path_1.resolve)(fileName);
|
|
76
|
+
const hash = await computeSha256(localPath);
|
|
77
|
+
const url = session.sandbox.getUrl(`/zerocut/${session.terminal.id}/materials/${(0, node_path_1.basename)(fileName)}`);
|
|
78
|
+
// check url avaliable,用 HEAD 请求检查是否存在
|
|
79
|
+
const res = await fetch(url, {
|
|
80
|
+
method: "HEAD",
|
|
81
|
+
});
|
|
82
|
+
if (res.status === 404 || hash !== res.headers.get("X-Content-Hash")) {
|
|
83
|
+
if (resolvedOptions.fileSizeLimit > 0) {
|
|
84
|
+
const stats = await (0, promises_1.stat)(localPath);
|
|
85
|
+
const fileSizeMb = stats.size / (1024 * 1024);
|
|
86
|
+
if (fileSizeMb > resolvedOptions.fileSizeLimit) {
|
|
87
|
+
throw new Error(`文件太大:${fileName} (${fileSizeMb.toFixed(2)}MB),限制为 ${resolvedOptions.fileSizeLimit}MB`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const saveToPath = `/home/user/cerevox-zerocut/projects/${session.terminal.id}/materials/${fileName}`;
|
|
91
|
+
const files = session.files;
|
|
92
|
+
await files.upload(localPath, saveToPath);
|
|
93
|
+
}
|
|
94
|
+
else if (res.status > 299) {
|
|
95
|
+
throw new Error(`Failed to get material from ${url}. Details: ${res.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
const node_fs_2 = require("node:fs");
|
|
100
|
+
const node_os_1 = require("node:os");
|
|
101
|
+
const node_path_2 = require("node:path");
|
|
102
|
+
const promises_2 = require("node:stream/promises");
|
|
103
|
+
const node_fs_3 = require("node:fs");
|
|
104
|
+
async function syncToTOS(url) {
|
|
105
|
+
const api = "https://api.zerocut.cn/api/v1/upload/material";
|
|
106
|
+
const apiKey = getApiKey();
|
|
107
|
+
const fileRes = await fetch(url);
|
|
108
|
+
if (!fileRes.ok || !fileRes.body) {
|
|
109
|
+
throw new Error("Failed to fetch source file");
|
|
110
|
+
}
|
|
111
|
+
const contentType = fileRes.headers.get("content-type") || "application/octet-stream";
|
|
112
|
+
const map = {
|
|
113
|
+
"audio/mpeg": "mp3",
|
|
114
|
+
"audio/mpga": "mp3",
|
|
115
|
+
"audio/wav": "wav",
|
|
116
|
+
"audio/x-wav": "wav",
|
|
117
|
+
"audio/flac": "flac",
|
|
118
|
+
"audio/ogg": "ogg",
|
|
119
|
+
"audio/webm": "webm",
|
|
120
|
+
"video/mp4": "mp4",
|
|
121
|
+
"video/mpeg": "mp4",
|
|
122
|
+
"video/quicktime": "mov",
|
|
123
|
+
"video/webm": "webm",
|
|
124
|
+
"image/png": "png",
|
|
125
|
+
"image/jpeg": "jpg",
|
|
126
|
+
"image/jpg": "jpg",
|
|
127
|
+
"image/webp": "webp",
|
|
128
|
+
"image/gif": "gif",
|
|
129
|
+
"application/pdf": "pdf",
|
|
130
|
+
};
|
|
131
|
+
let ext = map[contentType] || "";
|
|
132
|
+
if (!ext) {
|
|
133
|
+
try {
|
|
134
|
+
const u = new URL(url);
|
|
135
|
+
const p = u.pathname;
|
|
136
|
+
const i = p.lastIndexOf(".");
|
|
137
|
+
if (i !== -1 && i < p.length - 1) {
|
|
138
|
+
const e = p.substring(i + 1).toLowerCase();
|
|
139
|
+
if (/^[a-z0-9]{2,5}$/.test(e))
|
|
140
|
+
ext = e;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { }
|
|
144
|
+
}
|
|
145
|
+
if (!ext)
|
|
146
|
+
ext = "bin";
|
|
147
|
+
const tempPath = (0, node_path_2.join)((0, node_os_1.tmpdir)(), `upload-${Date.now()}.${ext}`);
|
|
148
|
+
try {
|
|
149
|
+
await (0, promises_2.pipeline)(fileRes.body, (0, node_fs_3.createWriteStream)(tempPath));
|
|
150
|
+
const fileBuffer = await node_fs_2.promises.readFile(tempPath);
|
|
151
|
+
const fileName = `file.${ext}`;
|
|
152
|
+
const file = new File([fileBuffer], fileName, { type: contentType });
|
|
153
|
+
const formData = new FormData();
|
|
154
|
+
formData.append("file", file);
|
|
155
|
+
const res = await fetch(api, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${apiKey}`,
|
|
159
|
+
},
|
|
160
|
+
body: formData,
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new Error(`Failed to upload to TOS: ${res.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
const result = await res.json();
|
|
166
|
+
return result?.data?.url;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await node_fs_2.promises.unlink(tempPath).catch(() => { });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function runFFMpegCommand(session, command, resources = []) {
|
|
173
|
+
// 验证命令只能是 ffmpeg 或 ffprobe
|
|
174
|
+
const trimmedCommand = command.trim();
|
|
175
|
+
if (!trimmedCommand.startsWith("ffmpeg") && !trimmedCommand.startsWith("ffprobe")) {
|
|
176
|
+
throw new Error("Only ffmpeg and ffprobe commands are allowed");
|
|
177
|
+
}
|
|
178
|
+
const terminal = session.terminal;
|
|
179
|
+
// 自动添加 -y 参数以避免交互式确认导致命令卡住
|
|
180
|
+
let finalCommand = trimmedCommand;
|
|
181
|
+
if (trimmedCommand.startsWith("ffmpeg") && !trimmedCommand.includes(" -y")) {
|
|
182
|
+
// 在 ffmpeg 后面插入 -y 参数
|
|
183
|
+
finalCommand = trimmedCommand.replace(/^ffmpeg/, "ffmpeg -y");
|
|
184
|
+
}
|
|
185
|
+
// 将 resources 中的文件同步到沙箱 materials 目录
|
|
186
|
+
await Promise.all(resources.map((resource) => {
|
|
187
|
+
return getMaterialUri(session, resource);
|
|
188
|
+
}));
|
|
189
|
+
// 构建工作目录路径 - materials 目录
|
|
190
|
+
const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials`;
|
|
191
|
+
// 执行命令,用一个独立的命令行以免影响当前会话的cwd
|
|
192
|
+
const response = await terminal.create().run(finalCommand, {
|
|
193
|
+
cwd: workDir,
|
|
194
|
+
});
|
|
195
|
+
const outputFilePath = trimmedCommand.startsWith("ffmpeg")
|
|
196
|
+
? finalCommand.split(" ").pop() || ""
|
|
197
|
+
: "";
|
|
198
|
+
const sandboxFilePath = (0, node_path_2.join)(workDir, outputFilePath);
|
|
199
|
+
// 等待命令完成
|
|
200
|
+
const result = await response.json();
|
|
201
|
+
if (result.exitCode === 0 && outputFilePath) {
|
|
202
|
+
const savePath = (0, node_path_2.join)(process.cwd(), (0, node_path_1.basename)(outputFilePath));
|
|
203
|
+
console.log(sandboxFilePath, savePath);
|
|
204
|
+
const files = session.files;
|
|
205
|
+
await files.download(sandboxFilePath, savePath);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
exitCode: result.exitCode,
|
|
209
|
+
outputFilePath,
|
|
210
|
+
data: {
|
|
211
|
+
stdout: result.stdout || (!result.exitCode && result.stderr) || "",
|
|
212
|
+
stderr: result.exitCode ? result.stderr : undefined,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function getPandocOutputFilePath(command) {
|
|
217
|
+
const inlineMatch = command.match(/(?:^|\s)--output=("[^"]+"|'[^']+'|[^\s]+)/);
|
|
218
|
+
if (inlineMatch?.[1]) {
|
|
219
|
+
return inlineMatch[1].replace(/^["']|["']$/g, "");
|
|
220
|
+
}
|
|
221
|
+
const outputMatch = command.match(/(?:^|\s)(?:-o|--output)\s+("[^"]+"|'[^']+'|[^\s]+)/);
|
|
222
|
+
if (outputMatch?.[1]) {
|
|
223
|
+
return outputMatch[1].replace(/^["']|["']$/g, "");
|
|
224
|
+
}
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
227
|
+
async function runPandocCommand(session, command, resources = []) {
|
|
228
|
+
const trimmedCommand = command.trim();
|
|
229
|
+
if (!trimmedCommand.startsWith("pandoc")) {
|
|
230
|
+
throw new Error("Only pandoc command is allowed");
|
|
231
|
+
}
|
|
232
|
+
const terminal = session.terminal;
|
|
233
|
+
await Promise.all(resources.map((resource) => {
|
|
234
|
+
return getMaterialUri(session, resource);
|
|
235
|
+
}));
|
|
236
|
+
const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials`;
|
|
237
|
+
const response = await terminal.create().run(trimmedCommand, {
|
|
238
|
+
cwd: workDir,
|
|
239
|
+
});
|
|
240
|
+
const outputFilePath = getPandocOutputFilePath(trimmedCommand);
|
|
241
|
+
const sandboxFilePath = outputFilePath ? (0, node_path_2.join)(workDir, outputFilePath) : "";
|
|
242
|
+
const result = await response.json();
|
|
243
|
+
if (result.exitCode === 0 && outputFilePath) {
|
|
244
|
+
const savePath = (0, node_path_2.join)(process.cwd(), (0, node_path_1.basename)(outputFilePath));
|
|
245
|
+
const files = session.files;
|
|
246
|
+
await files.download(sandboxFilePath, savePath);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
exitCode: result.exitCode,
|
|
250
|
+
outputFilePath,
|
|
251
|
+
data: {
|
|
252
|
+
stdout: result.stdout || (!result.exitCode && result.stderr) || "",
|
|
253
|
+
stderr: result.exitCode ? result.stderr : undefined,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.loadBuiltInCommands = loadBuiltInCommands;
|
|
40
|
+
exports.loadExternalCommandsAsync = loadExternalCommandsAsync;
|
|
41
|
+
const help_1 = require("../commands/help");
|
|
42
|
+
const image_1 = require("../commands/image");
|
|
43
|
+
const config_1 = require("../commands/config");
|
|
44
|
+
const video_1 = require("../commands/video");
|
|
45
|
+
const music_1 = require("../commands/music");
|
|
46
|
+
const tts_1 = require("../commands/tts");
|
|
47
|
+
const ffmpeg_1 = require("../commands/ffmpeg");
|
|
48
|
+
const pandoc_1 = require("../commands/pandoc");
|
|
49
|
+
const skill_1 = require("../commands/skill");
|
|
50
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
51
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
52
|
+
function loadBuiltInCommands(program) {
|
|
53
|
+
(0, help_1.register)(program);
|
|
54
|
+
(0, config_1.register)(program);
|
|
55
|
+
(0, image_1.register)(program);
|
|
56
|
+
(0, video_1.register)(program);
|
|
57
|
+
(0, music_1.register)(program);
|
|
58
|
+
(0, tts_1.register)(program);
|
|
59
|
+
(0, ffmpeg_1.register)(program);
|
|
60
|
+
(0, pandoc_1.register)(program);
|
|
61
|
+
(0, skill_1.register)(program);
|
|
62
|
+
}
|
|
63
|
+
async function loadExternalCommandsAsync(program, dir) {
|
|
64
|
+
const d = dir ?? process.env.ZEROCUT_COMMANDS_DIR;
|
|
65
|
+
if (!d)
|
|
66
|
+
return;
|
|
67
|
+
if (!node_fs_1.default.existsSync(d))
|
|
68
|
+
return;
|
|
69
|
+
const files = node_fs_1.default.readdirSync(d).filter((f) => f.endsWith(".js") || f.endsWith(".cjs"));
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
const full = node_path_1.default.join(d, f);
|
|
72
|
+
try {
|
|
73
|
+
const mod = (await Promise.resolve(`${full}`).then(s => __importStar(require(s))));
|
|
74
|
+
const fn = mod.register ?? mod.default;
|
|
75
|
+
if (typeof fn === "function")
|
|
76
|
+
fn(program);
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
export interface ZerocutConfig {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
region?: "us" | "cn";
|
|
5
|
+
}
|
|
6
|
+
export declare function configPath(): string;
|
|
7
|
+
export declare function readConfig(): Promise<Partial<ZerocutConfig>>;
|
|
8
|
+
export declare function readConfigSync(): Partial<ZerocutConfig>;
|
|
9
|
+
export declare function writeConfig(update: Partial<ZerocutConfig>): Promise<Partial<ZerocutConfig>>;
|
|
10
|
+
export declare function getConfigValueSync(key: string): unknown;
|
|
11
|
+
export declare function getConfigValue(key: string): Promise<unknown>;
|
|
12
|
+
export declare function setConfigValueSync(key: string, value: unknown): void;
|
|
13
|
+
export declare function setConfigValue(key: string, value: unknown): Promise<void>;
|
|
14
|
+
export declare function ensureConfig(): Promise<boolean>;
|
|
15
|
+
export declare function applyConfigInterceptor(program: Command): void;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.configPath = configPath;
|
|
7
|
+
exports.readConfig = readConfig;
|
|
8
|
+
exports.readConfigSync = readConfigSync;
|
|
9
|
+
exports.writeConfig = writeConfig;
|
|
10
|
+
exports.getConfigValueSync = getConfigValueSync;
|
|
11
|
+
exports.getConfigValue = getConfigValue;
|
|
12
|
+
exports.setConfigValueSync = setConfigValueSync;
|
|
13
|
+
exports.setConfigValue = setConfigValue;
|
|
14
|
+
exports.ensureConfig = ensureConfig;
|
|
15
|
+
exports.applyConfigInterceptor = applyConfigInterceptor;
|
|
16
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
20
|
+
const cerevox_1 = require("./cerevox");
|
|
21
|
+
function configPath() {
|
|
22
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".zerocut", "config.json");
|
|
23
|
+
}
|
|
24
|
+
function configFallbackPath() {
|
|
25
|
+
return node_path_1.default.join(process.cwd(), ".zerocut", "config.json");
|
|
26
|
+
}
|
|
27
|
+
async function readJson(file) {
|
|
28
|
+
try {
|
|
29
|
+
const buf = await promises_1.default.readFile(file, "utf8");
|
|
30
|
+
return JSON.parse(buf);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function readJsonSync(file) {
|
|
37
|
+
try {
|
|
38
|
+
const buf = node_fs_1.default.readFileSync(file, "utf8");
|
|
39
|
+
return JSON.parse(buf);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function writeJson(file, data) {
|
|
46
|
+
const dir = node_path_1.default.dirname(file);
|
|
47
|
+
try {
|
|
48
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
49
|
+
await promises_1.default.writeFile(file, JSON.stringify(data, null, 2), "utf8");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
const code = e.code;
|
|
54
|
+
if (code !== "EPERM" && code !== "EACCES")
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
const fb = configFallbackPath();
|
|
58
|
+
const fdir = node_path_1.default.dirname(fb);
|
|
59
|
+
await promises_1.default.mkdir(fdir, { recursive: true });
|
|
60
|
+
await promises_1.default.writeFile(fb, JSON.stringify(data, null, 2), "utf8");
|
|
61
|
+
}
|
|
62
|
+
async function readConfig() {
|
|
63
|
+
const primary = configPath();
|
|
64
|
+
try {
|
|
65
|
+
await promises_1.default.access(primary);
|
|
66
|
+
return (await readJson(primary));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
const fb = configFallbackPath();
|
|
70
|
+
try {
|
|
71
|
+
await promises_1.default.access(fb);
|
|
72
|
+
return (await readJson(fb));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function readConfigSync() {
|
|
80
|
+
const primary = configPath();
|
|
81
|
+
if (node_fs_1.default.existsSync(primary))
|
|
82
|
+
return readJsonSync(primary);
|
|
83
|
+
const fb = configFallbackPath();
|
|
84
|
+
if (node_fs_1.default.existsSync(fb))
|
|
85
|
+
return readJsonSync(fb);
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
async function writeConfig(update) {
|
|
89
|
+
const file = configPath();
|
|
90
|
+
const current = (await readJson(file));
|
|
91
|
+
const next = { ...current, ...update };
|
|
92
|
+
await writeJson(file, next);
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
function splitKeyPath(key) {
|
|
96
|
+
return key
|
|
97
|
+
.split(".")
|
|
98
|
+
.map((s) => s.trim())
|
|
99
|
+
.filter((s) => s.length > 0);
|
|
100
|
+
}
|
|
101
|
+
function deepGet(obj, segments) {
|
|
102
|
+
let cur = obj;
|
|
103
|
+
for (const seg of segments) {
|
|
104
|
+
if (typeof cur !== "object" || cur === null)
|
|
105
|
+
return undefined;
|
|
106
|
+
const rec = cur;
|
|
107
|
+
cur = rec[seg];
|
|
108
|
+
}
|
|
109
|
+
return cur;
|
|
110
|
+
}
|
|
111
|
+
function deepSet(obj, segments, value) {
|
|
112
|
+
let cur = obj;
|
|
113
|
+
for (let i = 0; i < segments.length; i++) {
|
|
114
|
+
const seg = segments[i];
|
|
115
|
+
const isLast = i === segments.length - 1;
|
|
116
|
+
if (isLast) {
|
|
117
|
+
cur[seg] = value;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const next = cur[seg];
|
|
121
|
+
if (typeof next !== "object" || next === null) {
|
|
122
|
+
const created = {};
|
|
123
|
+
cur[seg] = created;
|
|
124
|
+
cur = created;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
cur = next;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function getConfigValueSync(key) {
|
|
133
|
+
const primary = configPath();
|
|
134
|
+
const fb = configFallbackPath();
|
|
135
|
+
const data = node_fs_1.default.existsSync(primary)
|
|
136
|
+
? readJsonSync(primary)
|
|
137
|
+
: node_fs_1.default.existsSync(fb)
|
|
138
|
+
? readJsonSync(fb)
|
|
139
|
+
: {};
|
|
140
|
+
return deepGet(data, splitKeyPath(key));
|
|
141
|
+
}
|
|
142
|
+
async function getConfigValue(key) {
|
|
143
|
+
const primary = configPath();
|
|
144
|
+
try {
|
|
145
|
+
await promises_1.default.access(primary);
|
|
146
|
+
const data = (await readJson(primary));
|
|
147
|
+
return deepGet(data, splitKeyPath(key));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
const fb = configFallbackPath();
|
|
151
|
+
try {
|
|
152
|
+
await promises_1.default.access(fb);
|
|
153
|
+
const data = (await readJson(fb));
|
|
154
|
+
return deepGet(data, splitKeyPath(key));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function writeJsonSync(file, data) {
|
|
162
|
+
const dir = node_path_1.default.dirname(file);
|
|
163
|
+
try {
|
|
164
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
165
|
+
node_fs_1.default.writeFileSync(file, JSON.stringify(data, null, 2), "utf8");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
const code = e.code;
|
|
170
|
+
if (code !== "EPERM" && code !== "EACCES")
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
const fb = configFallbackPath();
|
|
174
|
+
const fdir = node_path_1.default.dirname(fb);
|
|
175
|
+
node_fs_1.default.mkdirSync(fdir, { recursive: true });
|
|
176
|
+
node_fs_1.default.writeFileSync(fb, JSON.stringify(data, null, 2), "utf8");
|
|
177
|
+
}
|
|
178
|
+
function setConfigValueSync(key, value) {
|
|
179
|
+
const file = configPath();
|
|
180
|
+
const data = readJsonSync(file);
|
|
181
|
+
deepSet(data, splitKeyPath(key), value);
|
|
182
|
+
writeJsonSync(file, data);
|
|
183
|
+
}
|
|
184
|
+
async function setConfigValue(key, value) {
|
|
185
|
+
const file = configPath();
|
|
186
|
+
const data = (await readJson(file));
|
|
187
|
+
deepSet(data, splitKeyPath(key), value);
|
|
188
|
+
await writeJson(file, data);
|
|
189
|
+
}
|
|
190
|
+
async function ensureConfig() {
|
|
191
|
+
const apiKey = getConfigValueSync("apiKey");
|
|
192
|
+
const region = getConfigValueSync("region");
|
|
193
|
+
const missing = [];
|
|
194
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0)
|
|
195
|
+
missing.push("apiKey");
|
|
196
|
+
if (missing.length > 0) {
|
|
197
|
+
process.stderr.write(`Missing required configuration: ${missing.join(", ")}\nConfigure using:\n zerocut config key <key>\n or:\n zerocut config --ott <token> --region <cn|us>\n`);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (region !== "us" && region !== "cn") {
|
|
201
|
+
setConfigValueSync("region", "us");
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function applyConfigInterceptor(program) {
|
|
206
|
+
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
207
|
+
const current = (actionCommand ?? thisCommand);
|
|
208
|
+
const name = current?.name?.();
|
|
209
|
+
const parentName = current?.parent?.name?.();
|
|
210
|
+
if (name === "help" || name === "skill" || name === "config" || parentName === "config")
|
|
211
|
+
return;
|
|
212
|
+
const ok = await ensureConfig();
|
|
213
|
+
if (!ok) {
|
|
214
|
+
process.exit(1);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const session = await (0, cerevox_1.openSession)();
|
|
218
|
+
if (actionCommand) {
|
|
219
|
+
(0, cerevox_1.attachSessionToCommand)(actionCommand, session);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
program.hook("postAction", async (thisCommand, actionCommand) => {
|
|
223
|
+
const name = actionCommand?.name?.() ?? thisCommand?.name?.();
|
|
224
|
+
if (name === "help" || name === "skill")
|
|
225
|
+
return;
|
|
226
|
+
try {
|
|
227
|
+
const cmd = (actionCommand ?? thisCommand);
|
|
228
|
+
const session = cmd?.[cerevox_1.SESSION_SYMBOL];
|
|
229
|
+
if (session)
|
|
230
|
+
await (0, cerevox_1.closeSession)(session);
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
});
|
|
235
|
+
}
|