@77-zhike/app-sdk 0.2.0-d7b1.2

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.
Files changed (45) hide show
  1. package/package.json +57 -0
  2. package/src/bin.js +15 -0
  3. package/src/cli/api-client.js +245 -0
  4. package/src/cli/browser-auth.js +212 -0
  5. package/src/cli/credential-store.js +101 -0
  6. package/src/cli/development-app.js +77 -0
  7. package/src/cli/development-logs.js +180 -0
  8. package/src/cli/development-watch.js +205 -0
  9. package/src/cli/index.js +253 -0
  10. package/src/cli/local-commands.js +78 -0
  11. package/src/cli/project-init.js +371 -0
  12. package/src/cli/upload-bundle.js +73 -0
  13. package/src/cli/validate-bundle.js +36 -0
  14. package/src/client/index.d.ts +52 -0
  15. package/src/client/index.js +22 -0
  16. package/src/client/interactions.d.ts +8 -0
  17. package/src/client/interactions.js +23 -0
  18. package/src/client/points.generated.js +19 -0
  19. package/src/config.js +64 -0
  20. package/src/development-workspace.js +154 -0
  21. package/src/errors.js +20 -0
  22. package/src/forms/components.generated.js +4 -0
  23. package/src/forms/factory.js +27 -0
  24. package/src/forms/index.d.ts +54 -0
  25. package/src/forms/index.js +3 -0
  26. package/src/forms/normalize-options.js +19 -0
  27. package/src/forms/options.d.ts +25 -0
  28. package/src/forms/options.js +48 -0
  29. package/src/forms/schema.d.ts +18 -0
  30. package/src/index.d.ts +13 -0
  31. package/src/index.js +2 -0
  32. package/src/project-binding.js +84 -0
  33. package/src/server/index.d.ts +268 -0
  34. package/src/server/index.js +201 -0
  35. package/src/toolchain/canonical-json.js +17 -0
  36. package/src/toolchain/index.js +103 -0
  37. package/src/toolchain/real-build.js +371 -0
  38. package/src/toolchain/source-discovery.js +710 -0
  39. package/src/toolchain/transport-tar.js +168 -0
  40. package/src/toolchain/webhook-response.js +50 -0
  41. package/src/ui/components.generated.js +2 -0
  42. package/src/ui/factory.js +19 -0
  43. package/src/ui/icons.generated.js +2 -0
  44. package/src/ui/index.d.ts +111 -0
  45. package/src/ui/index.js +4 -0
@@ -0,0 +1,180 @@
1
+ import { createHash } from "node:crypto";
2
+ import { stripVTControlCharacters } from "node:util";
3
+
4
+ const colors = {
5
+ dim: "\x1b[2m",
6
+ cyan: "\x1b[36m",
7
+ yellow: "\x1b[33m",
8
+ red: "\x1b[31m",
9
+ bold: "\x1b[1m",
10
+ reset: "\x1b[0m",
11
+ };
12
+
13
+ function formatTimestamp(value) {
14
+ const date = new Date(value);
15
+ if (Number.isNaN(date.getTime())) return "时间未知";
16
+ const pad = (part, width = 2) => String(part).padStart(width, "0");
17
+ const offset = -date.getTimezoneOffset();
18
+ const sign = offset >= 0 ? "+" : "-";
19
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
20
+ + ` ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
21
+ + ` ${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`;
22
+ }
23
+
24
+ /** 日志不可信:过滤控制指令,多行逐行带 App 标识,不影响 CLI 自己的颜色。 */
25
+ export function formatAppLog(app, entry, {
26
+ color = Boolean(process.stdout.isTTY && !process.env.NO_COLOR),
27
+ } = {}) {
28
+ const safe = (value) => stripVTControlCharacters(String(value))
29
+ .replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, "");
30
+ const label = safe(app).replace(/\n/g, " ").slice(0, 128);
31
+ const level = ["log", "warn", "error"].includes(entry.level) ? entry.level : "log";
32
+ const levelLabel = { log: "INFO ", warn: "WARN ", error: "ERROR" }[level];
33
+ const levelColor = { log: colors.cyan, warn: colors.yellow, error: colors.red }[level];
34
+ const timestamp = formatTimestamp(entry.received_at);
35
+ const prefix = color
36
+ ? `${colors.dim}${timestamp}${colors.reset} ${levelColor}${levelLabel}${colors.reset} ${colors.bold}${label}${colors.reset}`
37
+ : `${timestamp} ${levelLabel} ${label}`;
38
+ return safe(entry.message).split("\n").map((line) => `${prefix} ${line}`).join("\n");
39
+ }
40
+
41
+ /** 增量 UTF-8/SSE;只保留一个有界帧,取消时主动释放 reader。 */
42
+ export async function* parseLogEvents(body, { signal, onActivity = () => {} } = {}) {
43
+ const reader = body.getReader();
44
+ const decoder = new TextDecoder();
45
+ let buffer = "", event = "message", id, data = [];
46
+ let frameSize = 0;
47
+ const cancel = () => { void reader.cancel().catch(() => {}); };
48
+ signal?.addEventListener("abort", cancel, { once: true });
49
+ try {
50
+ if (signal?.aborted) return;
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done) break; // 不展示未完整接收的尾帧,也不推进其游标。
54
+ onActivity();
55
+ buffer += decoder.decode(value, { stream: true });
56
+ let match;
57
+ while ((match = /\r\n|\r|\n/.exec(buffer))) {
58
+ if (match[0] === "\r" && match.index === buffer.length - 1) break;
59
+ const line = buffer.slice(0, match.index);
60
+ buffer = buffer.slice(match.index + match[0].length);
61
+ frameSize += line.length;
62
+ if (frameSize > 70_000) throw new Error("developer_log_frame_too_large");
63
+ if (!line) {
64
+ if (data.length) yield { event, id, data: JSON.parse(data.join("\n")) };
65
+ event = "message"; id = undefined; data = []; frameSize = 0;
66
+ } else if (!line.startsWith(":")) {
67
+ const separator = line.indexOf(":");
68
+ const field = separator < 0 ? line : line.slice(0, separator);
69
+ const text = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
70
+ if (field === "event") event = text;
71
+ if (field === "id" && !text.includes("\0")) id = text;
72
+ if (field === "data") data.push(text);
73
+ }
74
+ }
75
+ if (buffer.length + frameSize > 70_000) throw new Error("developer_log_frame_too_large");
76
+ }
77
+ } finally {
78
+ signal?.removeEventListener("abort", cancel);
79
+ await reader.cancel().catch(() => {});
80
+ reader.releaseLock();
81
+ }
82
+ }
83
+
84
+ /** 一个 dev 的日志订阅;ready 有界等待,重连与 code sync 各自取消。 */
85
+ export function startDevelopmentLogs({ api, appId, workspaceId, appLabel = appId,
86
+ onLog = () => {}, onStatus = () => {}, signal, readyTimeoutMs = 3_000, reconnectMs = 500 }) {
87
+ const controller = new AbortController();
88
+ const stop = () => controller.abort();
89
+ signal?.addEventListener("abort", stop, { once: true });
90
+ if (signal?.aborted) stop();
91
+ const stream = createHash("sha256").update(JSON.stringify([workspaceId, appId])).digest("hex");
92
+ let cursor, position = -1n, settled = false, warning = false, idleTimer, delayTimer;
93
+ let resolveReady;
94
+ const ready = new Promise((resolve) => { resolveReady = resolve; });
95
+ const settle = () => { settled = true; clearTimeout(readyTimer); resolveReady(); };
96
+ const unavailable = () => {
97
+ if (!warning) onStatus(cursor
98
+ ? "App 日志连接中断,恢复后从已显示位置补读;代码同步继续。"
99
+ : "App 日志尚未就绪,代码同步继续;首次连接前的日志可能存在缺口。");
100
+ warning = true;
101
+ settle();
102
+ };
103
+ const readyTimer = setTimeout(unavailable, readyTimeoutMs);
104
+ const parseCursor = (value) => {
105
+ const match = typeof value === "string" && /^1\.([a-f0-9]{64})\.(0|[1-9][0-9]{0,18})\.([0-9]{1,12})$/.exec(value);
106
+ if (!match || match[1] !== stream || BigInt(match[2]) > 2n ** 63n - 1n) throw new Error("developer_log_cursor_invalid");
107
+ return BigInt(match[2]);
108
+ };
109
+ const done = (async () => {
110
+ let attempt = 0;
111
+ try {
112
+ while (!controller.signal.aborted) {
113
+ const connection = new AbortController();
114
+ const connectionSignal = AbortSignal.any([controller.signal, connection.signal]);
115
+ const activity = () => {
116
+ clearTimeout(idleTimer);
117
+ idleTimer = setTimeout(() => connection.abort(), 45_000);
118
+ };
119
+ idleTimer = setTimeout(() => connection.abort(), 10_000);
120
+ try {
121
+ const response = await api.openLogStream(appId, workspaceId, { cursor, signal: connectionSignal });
122
+ if (!response.headers.get("content-type")?.startsWith("text/event-stream") || !response.body) {
123
+ await response.body?.cancel();
124
+ throw new Error("developer_log_stream_invalid");
125
+ }
126
+ activity();
127
+ for await (const item of parseLogEvents(response.body, { signal: connectionSignal, onActivity: activity })) {
128
+ if (controller.signal.aborted) break;
129
+ if (item.event === "gap") {
130
+ onStatus("App 日志游标超过七天查询窗口,存在缺口;正在补读仍可查询的日志。");
131
+ cursor = undefined; position = -1n;
132
+ } else if (item.event === "ready") {
133
+ const next = parseCursor(item.id);
134
+ if (next < position) throw new Error("developer_log_cursor_regressed");
135
+ position = next; cursor = item.id;
136
+ if (!settled || warning) onStatus("App 实时日志已连接。");
137
+ settle(); warning = false; attempt = 0;
138
+ } else if (item.event === "log") {
139
+ const next = parseCursor(item.id);
140
+ if (next <= position) continue;
141
+ if (typeof item.data?.message !== "string" || item.data.message.length > 32_768
142
+ || String(item.data.position) !== String(next)) throw new Error("developer_log_entry_invalid");
143
+ await onLog(formatAppLog(appLabel, item.data));
144
+ position = next; cursor = item.id; // 完整解析、展示成功后才推进。
145
+ } else if (item.event === "error") {
146
+ const error = new Error("developer_log_stream_closed");
147
+ error.statusCode = item.data?.status;
148
+ throw error;
149
+ }
150
+ }
151
+ if (!controller.signal.aborted) unavailable();
152
+ } catch (error) {
153
+ if (controller.signal.aborted) break;
154
+ unavailable();
155
+ if (error.statusCode === 403 || error.statusCode === 404) {
156
+ onStatus("当前已无权读取此 App/Workspace 日志,停止日志订阅。");
157
+ break;
158
+ }
159
+ if (error.statusCode === 400) {
160
+ onStatus("App 日志游标已失效,重新连接时可能存在日志缺口。");
161
+ cursor = undefined; position = -1n;
162
+ }
163
+ } finally {
164
+ clearTimeout(idleTimer);
165
+ connection.abort();
166
+ }
167
+ if (controller.signal.aborted) break;
168
+ await new Promise((resolve) => {
169
+ const finish = () => { clearTimeout(delayTimer); controller.signal.removeEventListener("abort", finish); resolve(); };
170
+ delayTimer = setTimeout(finish, Math.min(reconnectMs * 2 ** Math.min(attempt++, 5), 10_000) * (0.8 + Math.random() * 0.4));
171
+ controller.signal.addEventListener("abort", finish, { once: true });
172
+ });
173
+ }
174
+ } finally {
175
+ settle(); clearTimeout(idleTimer); clearTimeout(delayTimer);
176
+ signal?.removeEventListener("abort", stop);
177
+ }
178
+ })();
179
+ return { ready, done, stop };
180
+ }
@@ -0,0 +1,205 @@
1
+ import { watch } from "node:fs";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import { resolve } from "node:path";
5
+
6
+ import { ZhikeCliError } from "../errors.js";
7
+ import { readProjectBinding } from "../project-binding.js";
8
+ import { runDevelopmentApp } from "./development-app.js";
9
+ import { uploadAppBundle } from "./upload-bundle.js";
10
+ import { startDevelopmentLogs } from "./development-logs.js";
11
+
12
+ /** 一个 dev 进程只拥有一个 Workspace;watch、upload 和远端更新串行处理。 */
13
+ export async function watchDevelopmentApp({
14
+ projectDirectory, apiBaseUrl, authenticatedApi, onReady = async () => {},
15
+ onProgress = () => {}, signal, pollIntervalMs = 2_000, debounceMs = 200,
16
+ upload = uploadAppBundle, ...options
17
+ }) {
18
+ const controller = new AbortController();
19
+ const stop = () => controller.abort();
20
+ const watchSignal = controller.signal;
21
+ let changes = 0;
22
+ let changedAt = 0;
23
+ let wake;
24
+ let timer;
25
+ let watcherError;
26
+ let logSubscription;
27
+ const watchers = [];
28
+ const previousSignal = authenticatedApi.signal;
29
+ authenticatedApi.signal = watchSignal;
30
+ const changed = () => {
31
+ changes += 1;
32
+ changedAt = Date.now();
33
+ wake?.();
34
+ };
35
+ const wait = (milliseconds) => new Promise((resolveWait) => {
36
+ const finish = () => {
37
+ clearTimeout(timer);
38
+ watchSignal.removeEventListener("abort", finish);
39
+ wake = null;
40
+ resolveWait();
41
+ };
42
+ if (watchSignal.aborted) return resolveWait();
43
+ wake = finish;
44
+ timer = setTimeout(finish, milliseconds);
45
+ watchSignal.addEventListener("abort", finish, { once: true });
46
+ });
47
+ signal?.addEventListener("abort", stop, { once: true });
48
+ if (signal?.aborted) stop();
49
+ if (!signal) {
50
+ process.on("SIGINT", stop);
51
+ process.on("SIGTERM", stop);
52
+ }
53
+ try {
54
+ if (watchSignal.aborted) return null;
55
+ const binding = await readProjectBinding(projectDirectory, apiBaseUrl);
56
+ watchers.push(watch(resolve(projectDirectory, "src"), { recursive: true }, changed));
57
+ watchers.push(watch(projectDirectory, (_event, name) => {
58
+ if (name && /^(package(?:-lock)?\.json|pnpm-lock\.yaml|tsconfig(?:\.[\w-]+)?\.json)$/.test(String(name))) changed();
59
+ }));
60
+ for (const watcher of watchers) watcher.on("error", (error) => {
61
+ watcherError = error;
62
+ stop();
63
+ });
64
+ const current = await authenticatedApi.getDevelopmentManifest(binding.appId);
65
+ const scopeRevision = current.scope_configuration_revision;
66
+ if (!Number.isSafeInteger(scopeRevision) || scopeRevision < 0) {
67
+ throw new ZhikeCliError("zhike_dev_server_upgrade_required", "Developer Server 尚不支持 dev watch,请更新服务后重试");
68
+ }
69
+ let syncedChanges = changes;
70
+ let sourceDigest = await fingerprintSources(projectDirectory);
71
+ onProgress("正在构建、上传并同步到开发 Workspace...");
72
+ let result = await runDevelopmentApp({
73
+ ...options, projectDirectory, apiBaseUrl, authenticatedApi, upload,
74
+ expectedScopeRevision: scopeRevision,
75
+ onWorkspaceReady: async ({ binding: selected, workspace }) => {
76
+ logSubscription = startDevelopmentLogs({
77
+ api: authenticatedApi, appId: selected.appId, workspaceId: workspace.id,
78
+ appLabel: selected.appSlug || selected.appId, signal: watchSignal,
79
+ onLog: onProgress, onStatus: onProgress,
80
+ });
81
+ await logSubscription.ready;
82
+ },
83
+ });
84
+ if (watchSignal.aborted) return result;
85
+ await onReady(result);
86
+ onProgress("正在监听代码变化。按 Ctrl+C 结束开发;已安装的 App 会保留。");
87
+ let lastDigest = result.uploaded.developmentManifest.manifest_digest;
88
+ let warning = null;
89
+ let pendingUpload = false;
90
+ while (!watchSignal.aborted) {
91
+ const remainingDebounce = changedAt + debounceMs - Date.now();
92
+ if (remainingDebounce > 0) {
93
+ await wait(remainingDebounce);
94
+ continue;
95
+ }
96
+ try {
97
+ const latest = await authenticatedApi.getDevelopmentManifest(binding.appId);
98
+ if (latest.scope_configuration_revision !== scopeRevision) throw scopeChanged();
99
+ if (watchSignal.aborted) break;
100
+ let manifest = latest;
101
+ if (changes !== syncedChanges) {
102
+ syncedChanges = changes;
103
+ const digest = await fingerprintSources(projectDirectory);
104
+ pendingUpload ||= digest !== sourceDigest;
105
+ sourceDigest = digest;
106
+ }
107
+ const codeChanged = pendingUpload;
108
+ if (codeChanged) {
109
+ const currentBinding = await readProjectBinding(projectDirectory, apiBaseUrl);
110
+ if (currentBinding.appId !== binding.appId) {
111
+ throw new ZhikeCliError("zhike_dev_binding_changed", "项目绑定已变化,请重新运行 npm run dev");
112
+ }
113
+ onProgress("检测到代码变化,正在构建并上传...");
114
+ const uploaded = await upload({
115
+ projectDirectory, apiBaseUrl, authenticatedApi, expectedScopeRevision: scopeRevision,
116
+ });
117
+ manifest = uploaded.developmentManifest;
118
+ pendingUpload = false;
119
+ }
120
+ if (watchSignal.aborted) break;
121
+ if (codeChanged || manifest.manifest_digest !== lastDigest || result.application.status === "not_installed") {
122
+ if (manifest.scope_configuration_revision !== scopeRevision) throw scopeChanged();
123
+ const application = await authenticatedApi.applyDevelopmentManifest(
124
+ binding.appId, result.workspace.id, {
125
+ development_revision: manifest.revision,
126
+ manifest_digest: manifest.manifest_digest,
127
+ expected_scope_configuration_revision: scopeRevision,
128
+ },
129
+ );
130
+ if (watchSignal.aborted) break;
131
+ if (manifest.manifest_digest !== lastDigest || application.status !== result.application.status || codeChanged) {
132
+ onProgress(application.status === "applied"
133
+ ? `Revision ${manifest.revision} 已应用到 Workspace「${result.workspace.name}」。`
134
+ : `Revision ${manifest.revision} 已上传;请在 Workspace「${result.workspace.name}」安装 App。`);
135
+ }
136
+ result = { ...result, application, uploaded: { developmentManifest: manifest } };
137
+ lastDigest = manifest.manifest_digest;
138
+ }
139
+ warning = null;
140
+ } catch (error) {
141
+ if (watchSignal.aborted) break;
142
+ if (hasCode(error, "developer_scope_configuration_changed")) throw scopeChanged();
143
+ if (isAuthorizationError(error) || error.code === "zhike_dev_binding_changed") throw error;
144
+ // 构建错误等到源码变化再试;网络错误保留待上传内容,下一轮继续。
145
+ if (!error.retryable) pendingUpload = false;
146
+ const message = `[${error.code || "zhike_dev_sync_failed"}] ${error.message}`;
147
+ if (message !== warning) onProgress(`${message}\n本次同步未完成;保留原运行版本,继续等待代码修复或服务恢复。`);
148
+ warning = message;
149
+ }
150
+ if (changes === syncedChanges) await wait(pollIntervalMs);
151
+ }
152
+ if (watcherError) throw watcherError;
153
+ onProgress("已停止监听,未卸载 App。");
154
+ return result;
155
+ } catch (error) {
156
+ if (hasCode(error, "developer_scope_configuration_changed")) throw scopeChanged();
157
+ if (watchSignal.aborted && !watcherError) return null;
158
+ throw error;
159
+ } finally {
160
+ stop();
161
+ logSubscription?.stop();
162
+ await logSubscription?.done;
163
+ clearTimeout(timer);
164
+ for (const watcher of watchers) watcher.close();
165
+ signal?.removeEventListener("abort", stop);
166
+ if (!signal) {
167
+ process.off("SIGINT", stop);
168
+ process.off("SIGTERM", stop);
169
+ }
170
+ authenticatedApi.signal = previousSignal;
171
+ }
172
+ }
173
+
174
+ function scopeChanged() {
175
+ return new ZhikeCliError("developer_scope_configuration_changed", "检测到 App Scope 已更新。请重新运行 npm run dev,使新的权限配置生效。");
176
+ }
177
+
178
+ function hasCode(error, code) {
179
+ return error?.code === code || (error?.cause && hasCode(error.cause, code));
180
+ }
181
+
182
+ function isAuthorizationError(error) {
183
+ return error?.statusCode === 401 || error?.statusCode === 403
184
+ || hasCode(error, "zhike_cli_auth_required")
185
+ || (error?.cause && isAuthorizationError(error.cause));
186
+ }
187
+
188
+ async function fingerprintSources(root) {
189
+ const hash = createHash("sha256");
190
+ async function include(path) {
191
+ const entries = await readdir(resolve(root, path), { withFileTypes: true });
192
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
193
+ const file = `${path}/${entry.name}`;
194
+ if (entry.isDirectory()) await include(file);
195
+ else if (entry.isFile()) hash.update(file).update("\0").update(await readFile(resolve(root, file))).update("\0");
196
+ }
197
+ }
198
+ await include("src");
199
+ for (const file of (await readdir(root)).sort()) {
200
+ if (/^(package(?:-lock)?\.json|pnpm-lock\.yaml|tsconfig(?:\.[\w-]+)?\.json)$/.test(file)) {
201
+ hash.update(file).update("\0").update(await readFile(resolve(root, file))).update("\0");
202
+ }
203
+ }
204
+ return hash.digest("hex");
205
+ }
@@ -0,0 +1,253 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import { loadCliConfig } from "../config.js";
4
+ import { asSafeCliError, ZhikeCliError } from "../errors.js";
5
+ import { buildAppBundle, buildMockArtifact } from "../toolchain/index.js";
6
+ import { authenticateCli, openSystemBrowser } from "./browser-auth.js";
7
+ import { createSystemCredentialStore } from "./credential-store.js";
8
+ import {
9
+ assertEmptyTarget,
10
+ initializeProject,
11
+ validateAppSlug,
12
+ } from "./project-init.js";
13
+ import { validateBundleUpload } from "./validate-bundle.js";
14
+ import { uploadAppBundle } from "./upload-bundle.js";
15
+ import { watchDevelopmentApp } from "./development-watch.js";
16
+ import { runLocalCommand } from "./local-commands.js";
17
+
18
+ export async function runCli(
19
+ args,
20
+ {
21
+ environment = process.env,
22
+ cwd = process.cwd(),
23
+ output = process.stdout,
24
+ credentialStore = null,
25
+ authenticate = authenticateCli,
26
+ scaffold,
27
+ fetchImpl = globalThis.fetch,
28
+ openBrowser,
29
+ selectWorkspace,
30
+ isInteractive = Boolean(process.stdin.isTTY),
31
+ developmentRunner = watchDevelopmentApp,
32
+ } = {},
33
+ ) {
34
+ const [command, ...rest] = args;
35
+ const localCommand = !command || ["-h", "--help"].includes(command) ? "help" : command;
36
+ if (["help", "version", "doctor"].includes(localCommand)) {
37
+ if (rest.filter((arg) => arg === "--json").length > 1) {
38
+ throw new ZhikeCliError("zhike_arguments_invalid", "--json 不可重复");
39
+ }
40
+ const result = await runLocalCommand(localCommand, rest.filter((arg) => arg !== "--json"), { cwd, environment });
41
+ output.write(rest.includes("--json") ? `${JSON.stringify(result)}\n`
42
+ : localCommand === "help" ? Object.values(result.data.commands).map((usage) => ` zhike ${usage}\n`).join("")
43
+ : `${JSON.stringify(result.data, null, 2)}\n`);
44
+ return result;
45
+ }
46
+ if (rest.includes("--json")) {
47
+ throw new ZhikeCliError("zhike_json_not_supported", "此命令暂不支持 --json,请用 help --json 查询支持范围");
48
+ }
49
+ try {
50
+ if (command === "build") {
51
+ _rejectArguments(rest);
52
+ const result = await buildAppBundle({
53
+ projectDirectory: cwd,
54
+ writeToDist: true,
55
+ });
56
+ output.write(
57
+ `App Bundle Build 完成:${result.artifacts.length} Artifacts,传输包 ${result.sha256} (${result.size} bytes)\n`,
58
+ );
59
+ return result;
60
+ }
61
+
62
+ const onProgress = (message) => output.write(`${message}\n`);
63
+ let remoteContext = null;
64
+ const getRemoteContext = () => {
65
+ remoteContext ??= {
66
+ config: loadCliConfig(environment),
67
+ store: credentialStore || createSystemCredentialStore(),
68
+ };
69
+ return remoteContext;
70
+ };
71
+ if (command === "login") {
72
+ _rejectArguments(rest);
73
+ const { config, store } = getRemoteContext();
74
+ await authenticate({
75
+ ...config,
76
+ credentialStore: store,
77
+ fetchImpl,
78
+ openBrowser,
79
+ onProgress,
80
+ allowBrowserAuthorization: true,
81
+ });
82
+ output.write("Developer CLI 授权成功。\n");
83
+ return { command: "login" };
84
+ }
85
+ if (command === "init") {
86
+ if (rest.length !== 1 || rest[0].startsWith("-")) {
87
+ throw new ZhikeCliError(
88
+ "zhike_init_usage_invalid",
89
+ "用法:zhike init <app-slug>",
90
+ );
91
+ }
92
+ const appSlug = validateAppSlug(rest[0]);
93
+ const targetDirectory = resolve(cwd, appSlug);
94
+ await assertEmptyTarget(targetDirectory);
95
+ const { config, store } = getRemoteContext();
96
+ const startedAt = Date.now();
97
+ const authenticatedApi = await authenticate({
98
+ ...config,
99
+ credentialStore: store,
100
+ fetchImpl,
101
+ openBrowser,
102
+ onProgress,
103
+ });
104
+ output.write(
105
+ `Developer CLI 登录成功(${_formatElapsed(startedAt)}),正在创建 App 和本地项目...\n`,
106
+ );
107
+ const result = await initializeProject({
108
+ targetDirectory,
109
+ appSlug,
110
+ apiBaseUrl: config.apiBaseUrl,
111
+ authenticatedApi,
112
+ ...(scaffold ? { scaffold } : {}),
113
+ });
114
+ output.write(
115
+ `${result.created ? "已创建" : "已复用"} App ${result.app.slug},项目位于 ${appSlug}/(总耗时 ${_formatElapsed(startedAt)})\n`,
116
+ );
117
+ return result;
118
+ }
119
+ if (command === "upload") {
120
+ _rejectArguments(rest);
121
+ const { config, store } = getRemoteContext();
122
+ const authenticatedApi = await authenticate({
123
+ ...config,
124
+ credentialStore: store,
125
+ fetchImpl,
126
+ openBrowser,
127
+ onProgress,
128
+ });
129
+ const result = await uploadAppBundle({
130
+ projectDirectory: cwd,
131
+ apiBaseUrl: config.apiBaseUrl,
132
+ authenticatedApi,
133
+ });
134
+ output.write(
135
+ `Bundle 已受理并更新当前 Development Manifest:revision ${result.developmentManifest.revision},${result.upload.id} (${result.artifact.artifacts.length} Artifacts)\n`,
136
+ );
137
+ return result;
138
+ }
139
+ if (command === "dev") {
140
+ const requestedWorkspaceId = _parseDevArguments(rest);
141
+ if (!requestedWorkspaceId && !selectWorkspace && !isInteractive) {
142
+ throw new ZhikeCliError(
143
+ "zhike_workspace_selection_required",
144
+ "非交互环境必须显式指定 Workspace:zhike dev --workspace <workspace-id>",
145
+ );
146
+ }
147
+ const { config, store } = getRemoteContext();
148
+ const authenticatedApi = await authenticate({
149
+ ...config,
150
+ credentialStore: store,
151
+ fetchImpl,
152
+ openBrowser,
153
+ onProgress,
154
+ });
155
+ let ready = false;
156
+ const onReady = async (result) => {
157
+ ready = true;
158
+ output.write(_developmentResultMessage(result));
159
+ try {
160
+ await (openBrowser || openSystemBrowser)(result.workspaceAppUrl);
161
+ } catch {
162
+ output.write(`无法自动打开浏览器,请手动访问:${result.workspaceAppUrl}\n`);
163
+ }
164
+ };
165
+ const result = await developmentRunner({
166
+ projectDirectory: cwd,
167
+ apiBaseUrl: config.apiBaseUrl,
168
+ workspaceBaseUrl: config.workspaceBaseUrl,
169
+ authenticatedApi,
170
+ requestedWorkspaceId,
171
+ selectWorkspace,
172
+ onProgress,
173
+ onReady,
174
+ });
175
+ if (result && !ready) await onReady(result);
176
+ return result;
177
+ }
178
+ if (command === "experimental" && rest[0] === "validate-bundle") {
179
+ _rejectArguments(rest.slice(1));
180
+ const { config, store } = getRemoteContext();
181
+ const authenticatedApi = await authenticate({
182
+ ...config,
183
+ credentialStore: store,
184
+ fetchImpl,
185
+ openBrowser,
186
+ onProgress,
187
+ });
188
+ const result = await validateBundleUpload({
189
+ projectDirectory: cwd,
190
+ apiBaseUrl: config.apiBaseUrl,
191
+ authenticatedApi,
192
+ });
193
+ output.write(
194
+ `${result.upload.status === "accepted" ? "Bundle 已受理" : "Bundle 校验失败"}:${result.upload.id}\n`,
195
+ );
196
+ return result;
197
+ }
198
+ throw new ZhikeCliError(
199
+ "zhike_command_unknown",
200
+ `未知命令:${command}`,
201
+ );
202
+ } catch (error) {
203
+ throw asSafeCliError(error);
204
+ }
205
+ }
206
+
207
+ function _rejectArguments(args) {
208
+ if (args.length > 0) {
209
+ throw new ZhikeCliError("zhike_arguments_invalid", "当前命令不接受额外参数");
210
+ }
211
+ }
212
+
213
+ function _parseDevArguments(args) {
214
+ if (args.length === 0) return null;
215
+ if (args.length === 2 && args[0] === "--workspace" && args[1]) {
216
+ return args[1];
217
+ }
218
+ throw new ZhikeCliError(
219
+ "zhike_dev_usage_invalid",
220
+ "用法:zhike dev [--workspace <workspace-id>]",
221
+ );
222
+ }
223
+
224
+ function _developmentResultMessage(result) {
225
+ const revision = result.uploaded.developmentManifest.revision;
226
+ const workspace = `Workspace「${result.workspace.name}」`;
227
+ if (result.application.status === "applied") {
228
+ return `Development Manifest revision ${revision} 已更新;${workspace}已应用 generation ${result.application.generation}。\n`;
229
+ }
230
+ if (result.application.status === "not_installed") {
231
+ return `Development Manifest revision ${revision} 已更新;${workspace}尚未安装,请在标准Apps页面完成安装。\n`;
232
+ }
233
+ throw new ZhikeCliError("zhike_dev_server_upgrade_required", "服务端未完成开发同步,请更新 Developer/Main 服务后重试");
234
+ }
235
+
236
+ function _formatElapsed(startedAt) {
237
+ const elapsedMs = Math.max(0, Date.now() - startedAt);
238
+ return elapsedMs < 1_000 ? "不到 1 秒" : `${(elapsedMs / 1_000).toFixed(1)} 秒`;
239
+ }
240
+
241
+ export { authenticateCli, authorizeCli } from "./browser-auth.js";
242
+ export {
243
+ MacOSKeychainCredentialStore,
244
+ MemoryCredentialStore,
245
+ createSystemCredentialStore,
246
+ } from "./credential-store.js";
247
+ export {
248
+ assertEmptyTarget,
249
+ initializeProject,
250
+ scaffoldProject,
251
+ } from "./project-init.js";
252
+ export { validateBundleUpload } from "./validate-bundle.js";
253
+ export { uploadAppBundle } from "./upload-bundle.js";