@demogo-cn/cli 0.4.0 → 0.5.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/README.md +95 -78
- package/bin/demogo.js +117 -24
- package/lib/core.js +147 -84
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,82 +1,99 @@
|
|
|
1
|
-
# DemoGo CLI
|
|
2
|
-
|
|
3
|
-
DemoGo CLI lets AI coding tools publish a local project to DemoGo and return a shareable trial link.
|
|
4
|
-
|
|
5
|
-
## Local Install
|
|
6
|
-
|
|
7
|
-
Current MVP delivery uses a local install package.
|
|
8
|
-
|
|
9
|
-
1. Unzip `demogo-cli-v0.
|
|
10
|
-
2. Open a terminal in the extracted folder.
|
|
11
|
-
3. Run:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install -g .
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
4. Verify:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
demogo --version
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Configure
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
demogo config set --api https://demogo.cn --token <DemoGo AI publish token>
|
|
27
|
-
demogo doctor
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The token is reusable. Do not reset it for every deployment.
|
|
31
|
-
Reset it only when it is lost, invalid, or exposed in chat/logs.
|
|
32
|
-
|
|
33
|
-
## Deploy
|
|
34
|
-
|
|
35
|
-
Run the command from a clean project folder. Do not run it directly from Desktop, Downloads, Documents, or the user home folder.
|
|
36
|
-
|
|
37
|
-
Use without installing:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npx --yes @demogo-cn/cli deploy --api https://demogo.cn --token <DemoGo AI publish token>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Or use the installed command:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
demogo deploy
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
For a specific folder or project name:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
demogo deploy --dir <project-folder> --name <project-name>
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
# DemoGo CLI
|
|
2
|
+
|
|
3
|
+
DemoGo CLI lets AI coding tools publish a local project to DemoGo and return a shareable trial link.
|
|
4
|
+
|
|
5
|
+
## Local Install
|
|
6
|
+
|
|
7
|
+
Current MVP delivery uses a local install package.
|
|
8
|
+
|
|
9
|
+
1. Unzip `demogo-cli-v0.5.0.zip`.
|
|
10
|
+
2. Open a terminal in the extracted folder.
|
|
11
|
+
3. Run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
4. Verify:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
demogo --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configure
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
demogo config set --api https://demogo.cn --token <DemoGo AI publish token>
|
|
27
|
+
demogo doctor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The token is reusable. Do not reset it for every deployment.
|
|
31
|
+
Reset it only when it is lost, invalid, or exposed in chat/logs.
|
|
32
|
+
|
|
33
|
+
## Deploy
|
|
34
|
+
|
|
35
|
+
Run the command from a clean project folder. Do not run it directly from Desktop, Downloads, Documents, or the user home folder.
|
|
36
|
+
|
|
37
|
+
Use without installing:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx --yes @demogo-cn/cli deploy --api https://demogo.cn --token <DemoGo AI publish token>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or use the installed command:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
demogo deploy
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For a specific folder or project name:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
demogo deploy --dir <project-folder> --name <project-name>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
After a successful first publish, the CLI stores a small local record in `.demogo/project.json`. If the same project folder is published again with `demogo deploy`, DemoGo updates the original trial link by default. This does not consume a new Demo slot, but it does consume one publish/update count.
|
|
56
|
+
|
|
57
|
+
To force a brand-new trial link from the same folder:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
demogo deploy --new
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
To update an existing link from another folder or another machine:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
demogo update --id https://demogo.cn/d/try-xxxxxx/
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If you only have one HTML file, put that file in a clean folder and publish that folder. You do not need to rename the file to `index.html`.
|
|
70
|
+
|
|
71
|
+
Project names are used for display in the DemoGo workbench. Every first publish receives an automatically assigned trial link path. Lite and Pro users can later customize the `/d/...` path in the workbench. Pro users can apply for a `xxx.demogo.cn` subdomain.
|
|
72
|
+
|
|
73
|
+
## Current Boundary
|
|
74
|
+
|
|
61
75
|
DemoGo supports static pages, single HTML pages, built frontend output, frontend source projects that can build static output, and Node.js single-service trial projects when the platform reports that the Node runtime is available. Node.js projects must provide a start command and listen on `process.env.PORT`.
|
|
62
76
|
|
|
63
|
-
DemoGo can
|
|
64
|
-
|
|
65
|
-
## npm / npx Status
|
|
66
|
-
|
|
67
|
-
AI coding tools can run:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npx --yes @demogo-cn/cli deploy --api https://demogo.cn --token <DemoGo AI publish token>
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Or install it globally:
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm install -g @demogo-cn/cli
|
|
77
|
-
demogo deploy
|
|
78
|
-
```
|
|
77
|
+
DemoGo can classify common frontend, backend, database, and environment-variable signals, including Next.js, TanStack Start, Nuxt, SvelteKit, Astro, Express, Fastify, Hono, Supabase, Postgres, MySQL, Prisma, and Drizzle.
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
DemoGo can allocate an empty MySQL trial database for supported Node.js single-service projects. It does not currently run Redis, MongoDB, PostgreSQL, multi-service apps, WebSocket, or SSR runtimes such as Next/Nuxt/Remix/TanStack Start/SvelteKit/Astro server mode.
|
|
80
|
+
|
|
81
|
+
## npm / npx Status
|
|
82
|
+
|
|
83
|
+
AI coding tools can run:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx --yes @demogo-cn/cli deploy --api https://demogo.cn --token <DemoGo AI publish token>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or install it globally:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install -g @demogo-cn/cli
|
|
93
|
+
demogo deploy
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The package name is `@demogo-cn/cli`; the installed command remains `demogo`.
|
|
97
|
+
If an AI coding tool cannot run `demogo` or `npx @demogo-cn/cli`, it should explain why and then use DemoGo MCP or the Agent API fallback.
|
|
98
|
+
|
|
82
99
|
|
package/bin/demogo.js
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
checkApiHealth,
|
|
17
17
|
normalizeApiBase,
|
|
18
18
|
safeArchiveName,
|
|
19
|
-
summarizeProject
|
|
19
|
+
summarizeProject,
|
|
20
|
+
updateArchive
|
|
20
21
|
} from "../lib/core.js";
|
|
21
22
|
|
|
22
23
|
const CONFIG_DIR = path.join(os.homedir(), ".demogo");
|
|
@@ -51,6 +52,11 @@ async function main() {
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
if (command === "update") {
|
|
56
|
+
await handleUpdate(rest);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
if (command === "doctor") {
|
|
55
61
|
await handleDoctor(rest);
|
|
56
62
|
return;
|
|
@@ -100,6 +106,62 @@ async function handleConfig(args) {
|
|
|
100
106
|
|
|
101
107
|
async function handleDeploy(args) {
|
|
102
108
|
const options = parseOptions(args);
|
|
109
|
+
const { apiBase, token, projectDir, projectName } = await resolvePublishContext(options, "deploy");
|
|
110
|
+
const existingProject = await readProjectRecord(projectDir);
|
|
111
|
+
if (existingProject?.demoRef && !options.new) {
|
|
112
|
+
console.log("DemoGo 发现当前项目目录已有发布记录,本次将更新原试用链接。");
|
|
113
|
+
console.log(`原访问链接:${existingProject.publicUrl || existingProject.demoRef}`);
|
|
114
|
+
await publishUpdate({
|
|
115
|
+
apiBase,
|
|
116
|
+
token,
|
|
117
|
+
projectDir,
|
|
118
|
+
projectName,
|
|
119
|
+
demoRef: existingProject.demoRef,
|
|
120
|
+
source: "cli"
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const archivePath = path.join(await mkdtemp(path.join(os.tmpdir(), "demogo-cli-")), `${safeArchiveName(projectName)}.tar.gz`);
|
|
126
|
+
try {
|
|
127
|
+
const archiveStats = await packProject(projectDir, archivePath);
|
|
128
|
+
console.log(`项目包已生成:${formatBytes(archiveStats.size)}`);
|
|
129
|
+
console.log("DemoGo 正在生成试用链接...");
|
|
130
|
+
const result = await deployArchive({ apiBase, token, archivePath, projectName, source: "cli" });
|
|
131
|
+
printDeployResult(result);
|
|
132
|
+
await writeProjectRecord(projectDir, result);
|
|
133
|
+
} finally {
|
|
134
|
+
await rm(path.dirname(archivePath), { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function handleUpdate(args) {
|
|
139
|
+
const options = parseOptions(args);
|
|
140
|
+
const demoRef = String(options.id || options.demo || options.slug || options.url || "").trim();
|
|
141
|
+
if (!demoRef) {
|
|
142
|
+
fail("缺少要更新的项目。请使用:demogo update --id <Demo ID 或原试用链接>");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { apiBase, token, projectDir, projectName } = await resolvePublishContext(options, "update");
|
|
146
|
+
await publishUpdate({ apiBase, token, projectDir, projectName, demoRef, source: "cli" });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function publishUpdate({ apiBase, token, projectDir, projectName, demoRef, source }) {
|
|
150
|
+
const archivePath = path.join(await mkdtemp(path.join(os.tmpdir(), "demogo-cli-")), `${safeArchiveName(projectName || demoRef)}.tar.gz`);
|
|
151
|
+
try {
|
|
152
|
+
console.log("DemoGo 正在准备更新已有试用链接...");
|
|
153
|
+
const archiveStats = await packProject(projectDir, archivePath);
|
|
154
|
+
console.log(`项目包已生成:${formatBytes(archiveStats.size)}`);
|
|
155
|
+
console.log("DemoGo 正在更新试用项目,原链接会保持不变...");
|
|
156
|
+
const result = await updateArchive({ apiBase, token, archivePath, demoRef, source });
|
|
157
|
+
printDeployResult(result, { action: "update" });
|
|
158
|
+
await writeProjectRecord(projectDir, result);
|
|
159
|
+
} finally {
|
|
160
|
+
await rm(path.dirname(archivePath), { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function resolvePublishContext(options, action = "deploy") {
|
|
103
165
|
const config = await readConfig();
|
|
104
166
|
const apiBase = normalizeApiBase(options.api || config.apiBase || process.env.DEMOGO_API_BASE);
|
|
105
167
|
const token = String(options.token || config.token || process.env.DEMOGO_AGENT_TOKEN || "").trim();
|
|
@@ -110,7 +172,7 @@ async function handleDeploy(args) {
|
|
|
110
172
|
fail([
|
|
111
173
|
"缺少 DemoGo 平台地址。",
|
|
112
174
|
"可以直接运行:",
|
|
113
|
-
|
|
175
|
+
`demogo ${action} --api <DemoGo平台地址> --token <你的AI发布口令>`
|
|
114
176
|
].join("\n"));
|
|
115
177
|
}
|
|
116
178
|
|
|
@@ -118,35 +180,31 @@ async function handleDeploy(args) {
|
|
|
118
180
|
fail([
|
|
119
181
|
"缺少 DemoGo AI 发布口令。",
|
|
120
182
|
"可以直接运行:",
|
|
121
|
-
`demogo
|
|
183
|
+
`demogo ${action} --api ${apiBase || "<DemoGo平台地址>"} --token <你的AI发布口令>`
|
|
122
184
|
].join("\n"));
|
|
123
185
|
}
|
|
124
186
|
|
|
125
187
|
await assertDirectory(projectDir);
|
|
126
188
|
assertSafeProjectDirectory(projectDir);
|
|
189
|
+
return { apiBase, token, projectDir, projectName };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function packProject(projectDir, archivePath) {
|
|
127
193
|
const fileList = await collectFiles(projectDir);
|
|
128
194
|
if (!fileList.length) fail("当前目录没有可发布文件。");
|
|
129
195
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"你可能在错误目录执行了发布。请切换到真正的项目目录,或使用 --dir 指定干净项目文件夹。"
|
|
141
|
-
].join("\n"));
|
|
142
|
-
}
|
|
143
|
-
console.log(`项目包已生成:${formatBytes(archiveStats.size)}`);
|
|
144
|
-
console.log("DemoGo 正在生成试用链接...");
|
|
145
|
-
const result = await deployArchive({ apiBase, token, archivePath, projectName, source: "cli" });
|
|
146
|
-
printDeployResult(result);
|
|
147
|
-
} finally {
|
|
148
|
-
await rm(path.dirname(archivePath), { recursive: true, force: true });
|
|
196
|
+
console.log("DemoGo 正在准备当前项目...");
|
|
197
|
+
printProjectSummary(summarizeProject(projectDir, fileList));
|
|
198
|
+
console.log("DemoGo 正在打包项目...");
|
|
199
|
+
await createProjectArchive(projectDir, fileList, archivePath);
|
|
200
|
+
const archiveStats = await stat(archivePath);
|
|
201
|
+
if (archiveStats.size > MAX_BYTES) {
|
|
202
|
+
fail([
|
|
203
|
+
`打包后文件超过 50MB,当前约 ${formatBytes(archiveStats.size)}。`,
|
|
204
|
+
"你可能在错误目录执行了发布。请切换到真正的项目目录,或使用 --dir 指定干净项目文件夹。"
|
|
205
|
+
].join("\n"));
|
|
149
206
|
}
|
|
207
|
+
return archiveStats;
|
|
150
208
|
}
|
|
151
209
|
|
|
152
210
|
async function handleDoctor(args) {
|
|
@@ -213,11 +271,41 @@ async function clearConfig() {
|
|
|
213
271
|
});
|
|
214
272
|
}
|
|
215
273
|
|
|
216
|
-
function
|
|
274
|
+
async function readProjectRecord(projectDir) {
|
|
275
|
+
try {
|
|
276
|
+
const record = JSON.parse(await readFile(projectRecordPath(projectDir), "utf8"));
|
|
277
|
+
const demoRef = String(record.demoId || record.id || record.publicUrl || record.slug || "").trim();
|
|
278
|
+
return demoRef ? { ...record, demoRef } : null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function writeProjectRecord(projectDir, result) {
|
|
285
|
+
if (!result?.id && !result?.publicUrl) return;
|
|
286
|
+
const record = {
|
|
287
|
+
demoId: result.id || "",
|
|
288
|
+
slug: result.slug || "",
|
|
289
|
+
publicUrl: result.publicUrl || "",
|
|
290
|
+
projectName: result.projectName || result.name || "",
|
|
291
|
+
version: result.version || null,
|
|
292
|
+
updatedAt: new Date().toISOString()
|
|
293
|
+
};
|
|
294
|
+
const recordPath = projectRecordPath(projectDir);
|
|
295
|
+
await mkdir(path.dirname(recordPath), { recursive: true });
|
|
296
|
+
await writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function projectRecordPath(projectDir) {
|
|
300
|
+
return path.join(projectDir, ".demogo", "project.json");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function printDeployResult(result, options = {}) {
|
|
217
304
|
console.log("");
|
|
218
|
-
console.log("DemoGo 试用链接已生成。");
|
|
305
|
+
console.log(options.action === "update" ? "DemoGo 试用项目已更新,原链接保持不变。" : "DemoGo 试用链接已生成。");
|
|
219
306
|
console.log(`项目名称:${result.projectName || result.name || result.slug || "-"}`);
|
|
220
307
|
console.log(`访问链接:${result.publicUrl || "-"}`);
|
|
308
|
+
if (result.version) console.log(`当前版本:v${result.version}`);
|
|
221
309
|
console.log(`发布方式:${result.deploySourceLabel || "DemoGo CLI"}`);
|
|
222
310
|
console.log(`页面类型:${result.detectedType || "-"}`);
|
|
223
311
|
console.log(`内容检查:${result.contentReview?.statusLabel || result.contentReviewStatus || "已通过"}`);
|
|
@@ -247,8 +335,11 @@ DemoGo CLI ${VERSION}
|
|
|
247
335
|
demogo config clear
|
|
248
336
|
demogo doctor
|
|
249
337
|
demogo deploy
|
|
338
|
+
demogo deploy --new
|
|
339
|
+
demogo update --id <Demo ID 或原试用链接>
|
|
250
340
|
demogo deploy --api <DemoGo地址> --token <AI发布口令>
|
|
251
341
|
demogo deploy --name <项目名称> --dir <项目目录>
|
|
342
|
+
demogo update --id <Demo ID 或原试用链接> --dir <项目目录>
|
|
252
343
|
|
|
253
344
|
本地安装:
|
|
254
345
|
解压 dist/demogo-cli-v${VERSION}.zip 后,在解压目录执行 npm install -g .
|
|
@@ -261,6 +352,8 @@ DemoGo CLI ${VERSION}
|
|
|
261
352
|
示例:
|
|
262
353
|
demogo config set --api https://your-demogo.example.com --token dmg_xxx_xxx
|
|
263
354
|
demogo deploy --api https://your-demogo.example.com --token dmg_xxx_xxx
|
|
355
|
+
demogo deploy --new
|
|
356
|
+
demogo update --id https://your-demogo.example.com/d/try-xxxxxx/ --dir ./my-project
|
|
264
357
|
demogo deploy --name 我的报名页
|
|
265
358
|
`.trim());
|
|
266
359
|
}
|
package/lib/core.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { gzipSync } from "node:zlib";
|
|
2
2
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
|
-
export const VERSION =
|
|
6
|
-
export const MAX_FILES = 800;
|
|
7
|
-
export const MAX_BYTES = 50 * 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
export const unsafeProjectDirectoryNames = new Set([
|
|
10
|
-
"desktop",
|
|
11
|
-
"downloads",
|
|
12
|
-
"documents",
|
|
13
|
-
"onedrive",
|
|
14
|
-
"桌面",
|
|
15
|
-
"下载",
|
|
16
|
-
"文档"
|
|
17
|
-
]);
|
|
6
|
+
export const VERSION = readVersion();
|
|
7
|
+
export const MAX_FILES = 800;
|
|
8
|
+
export const MAX_BYTES = 50 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export const unsafeProjectDirectoryNames = new Set([
|
|
11
|
+
"desktop",
|
|
12
|
+
"downloads",
|
|
13
|
+
"documents",
|
|
14
|
+
"onedrive",
|
|
15
|
+
"桌面",
|
|
16
|
+
"下载",
|
|
17
|
+
"文档"
|
|
18
|
+
]);
|
|
18
19
|
|
|
19
20
|
const EXCLUDED_DIRS = new Set([
|
|
21
|
+
".demogo",
|
|
20
22
|
".git",
|
|
21
23
|
".hg",
|
|
22
24
|
".svn",
|
|
@@ -52,36 +54,53 @@ const SENSITIVE_EXTENSIONS = [
|
|
|
52
54
|
".cer"
|
|
53
55
|
];
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
function readVersion() {
|
|
58
|
+
const candidates = [
|
|
59
|
+
path.resolve(process.cwd(), "VERSION"),
|
|
60
|
+
path.resolve(process.cwd(), "..", "VERSION"),
|
|
61
|
+
path.resolve(process.cwd(), "..", "..", "VERSION"),
|
|
62
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "VERSION")
|
|
63
|
+
];
|
|
64
|
+
for (const filePath of candidates) {
|
|
65
|
+
try {
|
|
66
|
+
return readFileSync(filePath, "utf8").trim();
|
|
67
|
+
} catch {
|
|
68
|
+
// Try the next packaged or repository-level VERSION file.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return "0.5.0";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function assertDirectory(dir) {
|
|
75
|
+
try {
|
|
76
|
+
const stats = await stat(dir);
|
|
77
|
+
if (!stats.isDirectory()) throw new Error(`不是项目目录:${dir}`);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(`找不到项目目录:${dir}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function assertSafeProjectDirectory(dir) {
|
|
84
|
+
const resolved = path.resolve(dir);
|
|
85
|
+
const home = path.resolve(process.env.USERPROFILE || process.env.HOME || "");
|
|
86
|
+
const baseName = path.basename(resolved).toLowerCase();
|
|
87
|
+
|
|
88
|
+
if (home && resolved === home) {
|
|
89
|
+
throw new Error([
|
|
90
|
+
"当前目录像是用户主目录,不适合直接发布。",
|
|
91
|
+
"请新建一个干净文件夹,只放要发布的项目文件,然后在该文件夹里运行发布命令。",
|
|
92
|
+
"也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
|
|
93
|
+
].join("\n"));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (unsafeProjectDirectoryNames.has(baseName)) {
|
|
97
|
+
throw new Error([
|
|
98
|
+
`当前目录是 ${path.basename(resolved)},通常会包含很多无关文件,不适合直接发布。`,
|
|
99
|
+
"请新建一个干净文件夹,只放要发布的项目文件;如果只有一个 HTML 文件,也可以让 AI 先创建临时目录再发布。",
|
|
100
|
+
"也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
|
|
101
|
+
].join("\n"));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
85
104
|
|
|
86
105
|
export async function collectFiles(rootDir) {
|
|
87
106
|
const files = [];
|
|
@@ -101,24 +120,24 @@ export async function collectFiles(rootDir) {
|
|
|
101
120
|
if (!entry.isFile()) continue;
|
|
102
121
|
const stats = await stat(fullPath);
|
|
103
122
|
totalBytes += stats.size;
|
|
104
|
-
if (files.length >= MAX_FILES) throw new Error(projectTooLargeMessage(`项目文件超过 ${MAX_FILES} 个。`));
|
|
105
|
-
if (totalBytes > MAX_BYTES) throw new Error(projectTooLargeMessage("项目文件超过 50MB。"));
|
|
123
|
+
if (files.length >= MAX_FILES) throw new Error(projectTooLargeMessage(`项目文件超过 ${MAX_FILES} 个。`));
|
|
124
|
+
if (totalBytes > MAX_BYTES) throw new Error(projectTooLargeMessage("项目文件超过 50MB。"));
|
|
106
125
|
files.push({ fullPath, relativePath, size: stats.size });
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
await walk(rootDir);
|
|
111
|
-
return files;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function projectTooLargeMessage(reason) {
|
|
115
|
-
return [
|
|
116
|
-
`${reason}你可能在桌面、下载、文档或用户根目录执行了发布。`,
|
|
117
|
-
"请切换到真正的项目目录,或新建一个干净文件夹,只放要发布的项目文件。",
|
|
118
|
-
"如果只有一个 HTML 文件,不需要改名为 index.html,可以把它单独放进干净目录后发布。",
|
|
119
|
-
"也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
|
|
120
|
-
].join("\n");
|
|
121
|
-
}
|
|
129
|
+
await walk(rootDir);
|
|
130
|
+
return files;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function projectTooLargeMessage(reason) {
|
|
134
|
+
return [
|
|
135
|
+
`${reason}你可能在桌面、下载、文档或用户根目录执行了发布。`,
|
|
136
|
+
"请切换到真正的项目目录,或新建一个干净文件夹,只放要发布的项目文件。",
|
|
137
|
+
"如果只有一个 HTML 文件,不需要改名为 index.html,可以把它单独放进干净目录后发布。",
|
|
138
|
+
"也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
122
141
|
|
|
123
142
|
export function summarizeProject(projectDir, files) {
|
|
124
143
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
@@ -206,7 +225,7 @@ export async function deployArchive({ apiBase, token, archivePath, projectName,
|
|
|
206
225
|
return payload;
|
|
207
226
|
}
|
|
208
227
|
|
|
209
|
-
export async function checkApiHealth(apiBase) {
|
|
228
|
+
export async function checkApiHealth(apiBase) {
|
|
210
229
|
let response;
|
|
211
230
|
try {
|
|
212
231
|
response = await fetch(`${normalizeApiBase(apiBase)}/api/health`, {
|
|
@@ -221,31 +240,74 @@ export async function checkApiHealth(apiBase) {
|
|
|
221
240
|
if (!response.ok || !payload?.ok) {
|
|
222
241
|
throw new Error(`DemoGo 平台地址不可用:HTTP ${response.status}`);
|
|
223
242
|
}
|
|
224
|
-
return payload;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
243
|
+
return payload;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function updateArchive({ apiBase, token, archivePath, demoRef, source = "cli", userAgent = `demogo-cli/${VERSION}` }) {
|
|
247
|
+
const formData = new FormData();
|
|
248
|
+
formData.set("source", source);
|
|
249
|
+
formData.set("demoId", demoRef);
|
|
250
|
+
formData.set(
|
|
251
|
+
"project",
|
|
252
|
+
new Blob([await readFile(archivePath)], { type: "application/gzip" }),
|
|
253
|
+
path.basename(archivePath)
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
let response;
|
|
257
|
+
try {
|
|
258
|
+
response = await fetch(`${normalizeApiBase(apiBase)}/api/agent/update`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: {
|
|
261
|
+
Authorization: `Bearer ${token}`,
|
|
262
|
+
"User-Agent": userAgent,
|
|
263
|
+
"X-DemoGo-Deploy-Source": source
|
|
264
|
+
},
|
|
265
|
+
body: formData
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查 API 地址是否正确,或稍后再试。`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return parseDeploymentResponse(response);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function parseDeploymentResponse(response) {
|
|
275
|
+
const text = await response.text();
|
|
276
|
+
const payload = parseJson(text);
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
const message = payload?.error || readableHttpError(response.status, text);
|
|
279
|
+
const shouldShowFixPrompt = response.status === 400 || payload?.inspection?.canPublish === false;
|
|
280
|
+
const fixPrompt = shouldShowFixPrompt
|
|
281
|
+
? payload?.inspection?.fixPrompt || payload?.inspection?.ruleReport?.fixPrompt || ""
|
|
282
|
+
: "";
|
|
283
|
+
const details = fixPrompt ? `\n\n给 AI 工具的修改建议:\n${fixPrompt}` : "";
|
|
284
|
+
throw new Error(`${message}${details}`);
|
|
285
|
+
}
|
|
286
|
+
return payload;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function checkAgentToken(apiBase, token, userAgent = `demogo-cli/${VERSION}`) {
|
|
290
|
+
let response;
|
|
291
|
+
try {
|
|
292
|
+
response = await fetch(`${normalizeApiBase(apiBase)}/api/agent/token-check`, {
|
|
293
|
+
method: "GET",
|
|
294
|
+
headers: {
|
|
295
|
+
Authorization: `Bearer ${token}`,
|
|
296
|
+
"User-Agent": userAgent
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
} catch {
|
|
300
|
+
throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查平台地址。`);
|
|
301
|
+
}
|
|
302
|
+
const text = await response.text();
|
|
303
|
+
const payload = parseJson(text);
|
|
304
|
+
if (!response.ok || !payload?.ok) {
|
|
305
|
+
throw new Error(payload?.error || readableHttpError(response.status, text));
|
|
306
|
+
}
|
|
307
|
+
return payload;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function normalizeApiBase(value) {
|
|
249
311
|
return String(value || "").trim().replace(/\/+$/, "");
|
|
250
312
|
}
|
|
251
313
|
|
|
@@ -373,5 +435,6 @@ function readableHttpError(status, text) {
|
|
|
373
435
|
if (status === 400) return "项目暂时不能发布,请根据 DemoGo 返回的提示修改后再试。";
|
|
374
436
|
if (text?.trim().startsWith("<")) return "服务器返回了异常页面,本次发布没有完成。";
|
|
375
437
|
return `DemoGo 发布失败:HTTP ${status}`;
|
|
376
|
-
}
|
|
438
|
+
}
|
|
439
|
+
|
|
377
440
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demogo-cn/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "DemoGo CLI for generating shareable trial links from AI-built pages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,4 +14,5 @@
|
|
|
14
14
|
"node": ">=18.0.0"
|
|
15
15
|
},
|
|
16
16
|
"license": "UNLICENSED"
|
|
17
|
-
}
|
|
17
|
+
}
|
|
18
|
+
|