@77-zhike/app-sdk 0.2.0-d7b1.10
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 +251 -0
- package/package.json +71 -0
- package/src/bin.js +15 -0
- package/src/cli/api-client.js +249 -0
- package/src/cli/browser-auth.js +212 -0
- package/src/cli/credential-store.js +101 -0
- package/src/cli/development-app.js +77 -0
- package/src/cli/development-logs.js +183 -0
- package/src/cli/development-watch.js +205 -0
- package/src/cli/index.js +256 -0
- package/src/cli/local-commands.js +80 -0
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +259 -0
- package/src/cli/upload-bundle.js +73 -0
- package/src/client/analysis.d.ts +4 -0
- package/src/client/analysis.js +12 -0
- package/src/client/destinations.d.ts +25 -0
- package/src/client/destinations.js +70 -0
- package/src/client/external-url.js +9 -0
- package/src/client/index.d.ts +249 -0
- package/src/client/index.js +72 -0
- package/src/client/interactions.d.ts +23 -0
- package/src/client/interactions.js +62 -0
- package/src/client/points.generated.js +67 -0
- package/src/config.js +68 -0
- package/src/development-workspace.js +154 -0
- package/src/errors.js +20 -0
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/components.generated.js +4 -0
- package/src/forms/factory.js +41 -0
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +118 -0
- package/src/forms/index.js +5 -0
- package/src/forms/normalize-options.js +19 -0
- package/src/forms/options.d.ts +30 -0
- package/src/forms/options.js +55 -0
- package/src/forms/record-options.d.ts +28 -0
- package/src/forms/record-options.js +92 -0
- package/src/forms/schema.d.ts +82 -0
- package/src/forms/schema.js +128 -0
- package/src/forms/state.d.ts +30 -0
- package/src/forms/state.js +82 -0
- package/src/forms/validation.d.ts +9 -0
- package/src/forms/validation.js +81 -0
- package/src/forms/values.d.ts +5 -0
- package/src/forms/values.js +38 -0
- package/src/index.d.ts +13 -0
- package/src/index.js +2 -0
- package/src/jsx-dev-runtime.d.ts +2 -0
- package/src/jsx-dev-runtime.js +1 -0
- package/src/jsx-runtime.d.ts +2 -0
- package/src/jsx-runtime.js +1 -0
- package/src/project-binding.js +84 -0
- package/src/server/index.d.ts +318 -0
- package/src/server/index.js +242 -0
- package/src/server/steps/index.d.ts +71 -0
- package/src/server/steps/index.js +15 -0
- package/src/server/triggers/index.d.ts +68 -0
- package/src/server/triggers/index.js +27 -0
- package/src/server/workflow-blocks/index.d.ts +72 -0
- package/src/server/workflow-blocks/index.js +32 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/canonical-json.js +17 -0
- package/src/toolchain/dependency-policy.js +181 -0
- package/src/toolchain/index.js +15 -0
- package/src/toolchain/real-build.js +459 -0
- package/src/toolchain/source-discovery.js +919 -0
- package/src/toolchain/transport-tar.js +168 -0
- package/src/toolchain/webhook-response.js +50 -0
- package/src/toolchain/workflow-blocks/discovery.js +133 -0
- package/src/toolchain/workflow-steps/manifest.js +99 -0
- package/src/toolchain/workflow-triggers/manifest.js +47 -0
- package/src/ui/components.generated.js +5 -0
- package/src/ui/events.d.ts +10 -0
- package/src/ui/factory.js +70 -0
- package/src/ui/icons.generated.js +2 -0
- package/src/ui/index.d.ts +343 -0
- package/src/ui/index.js +4 -0
- package/src/ui/jsx.generated.d.ts +359 -0
- package/src/ui/native-dom.d.ts +49 -0
- package/src/ui/native-dom.js +44 -0
- package/src/ui/style.d.ts +5 -0
- package/src/ui/style.generated.js +2 -0
- package/src/ui/style.js +15 -0
- package/src/ui/validation.js +86 -0
|
@@ -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
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { loadCliConfig } from "../config.js";
|
|
4
|
+
import { asSafeCliError, ZhikeCliError } from "../errors.js";
|
|
5
|
+
import { buildAppBundle } from "../toolchain/index.js";
|
|
6
|
+
import { authenticateCli, openSystemBrowser } from "./browser-auth.js";
|
|
7
|
+
import { createSystemCredentialStore } from "./credential-store.js";
|
|
8
|
+
import {
|
|
9
|
+
assertEmptyTarget,
|
|
10
|
+
assertExistingProjectTarget,
|
|
11
|
+
initializeExistingProject,
|
|
12
|
+
initializeProject,
|
|
13
|
+
validateAppSlug,
|
|
14
|
+
} from "./project-init.js";
|
|
15
|
+
import { uploadAppBundle } from "./upload-bundle.js";
|
|
16
|
+
import { watchDevelopmentApp } from "./development-watch.js";
|
|
17
|
+
import { runLocalCommand } from "./local-commands.js";
|
|
18
|
+
import { readProjectBinding } from "../project-binding.js";
|
|
19
|
+
import { parseLogsArguments, queryAppLogs } from "./logs.js";
|
|
20
|
+
|
|
21
|
+
export async function runCli(
|
|
22
|
+
args,
|
|
23
|
+
{
|
|
24
|
+
environment = process.env,
|
|
25
|
+
cwd = process.cwd(),
|
|
26
|
+
output = process.stdout,
|
|
27
|
+
credentialStore = null,
|
|
28
|
+
authenticate = authenticateCli,
|
|
29
|
+
scaffold,
|
|
30
|
+
fetchImpl = globalThis.fetch,
|
|
31
|
+
openBrowser,
|
|
32
|
+
selectWorkspace,
|
|
33
|
+
isInteractive = Boolean(process.stdin.isTTY),
|
|
34
|
+
developmentRunner = watchDevelopmentApp,
|
|
35
|
+
} = {},
|
|
36
|
+
) {
|
|
37
|
+
const [command, ...rest] = args;
|
|
38
|
+
const localCommand = !command || ["-h", "--help"].includes(command) ? "help" : command;
|
|
39
|
+
if (["help", "version", "doctor"].includes(localCommand)) {
|
|
40
|
+
if (rest.filter((arg) => arg === "--json").length > 1) {
|
|
41
|
+
throw new ZhikeCliError("zhike_arguments_invalid", "--json 不可重复");
|
|
42
|
+
}
|
|
43
|
+
const result = await runLocalCommand(localCommand, rest.filter((arg) => arg !== "--json"), { cwd, environment });
|
|
44
|
+
output.write(rest.includes("--json") ? `${JSON.stringify(result)}\n`
|
|
45
|
+
: localCommand === "help" ? Object.values(result.data.commands).map((usage) => ` zhike ${usage}\n`).join("")
|
|
46
|
+
: `${JSON.stringify(result.data, null, 2)}\n`);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
if (command !== "logs" && rest.includes("--json")) {
|
|
50
|
+
throw new ZhikeCliError("zhike_json_not_supported", "此命令暂不支持 --json,请用 help --json 查询支持范围");
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (command === "build") {
|
|
54
|
+
_rejectArguments(rest);
|
|
55
|
+
const result = await buildAppBundle({
|
|
56
|
+
projectDirectory: cwd,
|
|
57
|
+
writeToDist: true,
|
|
58
|
+
});
|
|
59
|
+
output.write(
|
|
60
|
+
`App Bundle Build 完成:${result.artifacts.length} Artifacts,传输包 ${result.sha256} (${result.size} bytes)\n`,
|
|
61
|
+
);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
if (command === "validate") {
|
|
65
|
+
_rejectArguments(rest);
|
|
66
|
+
const result = await buildAppBundle({
|
|
67
|
+
projectDirectory: cwd,
|
|
68
|
+
});
|
|
69
|
+
output.write(
|
|
70
|
+
`真实 App 源码校验通过:${result.artifacts.length} Artifacts,Bundle ${result.sha256} (${result.size} bytes);未写入 dist\n`,
|
|
71
|
+
);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const onProgress = (message) => output.write(`${message}\n`);
|
|
76
|
+
let remoteContext = null;
|
|
77
|
+
const getRemoteContext = () => {
|
|
78
|
+
remoteContext ??= {
|
|
79
|
+
config: loadCliConfig(environment),
|
|
80
|
+
store: credentialStore || createSystemCredentialStore(),
|
|
81
|
+
};
|
|
82
|
+
return remoteContext;
|
|
83
|
+
};
|
|
84
|
+
if (command === "logs") {
|
|
85
|
+
const parsed = parseLogsArguments(rest);
|
|
86
|
+
const { config, store } = getRemoteContext();
|
|
87
|
+
const binding = await readProjectBinding(cwd, config.apiBaseUrl);
|
|
88
|
+
const api = await authenticate({ ...config, credentialStore: store, fetchImpl, openBrowser,
|
|
89
|
+
onProgress: parsed.json ? () => {} : onProgress });
|
|
90
|
+
return await queryAppLogs({ api, binding, ...parsed, output });
|
|
91
|
+
}
|
|
92
|
+
if (command === "login") {
|
|
93
|
+
_rejectArguments(rest);
|
|
94
|
+
const { config, store } = getRemoteContext();
|
|
95
|
+
await authenticate({
|
|
96
|
+
...config,
|
|
97
|
+
credentialStore: store,
|
|
98
|
+
fetchImpl,
|
|
99
|
+
openBrowser,
|
|
100
|
+
onProgress,
|
|
101
|
+
allowBrowserAuthorization: true,
|
|
102
|
+
});
|
|
103
|
+
output.write("Developer CLI 授权成功。\n");
|
|
104
|
+
return { command: "login" };
|
|
105
|
+
}
|
|
106
|
+
if (command === "init") {
|
|
107
|
+
const here = rest.includes("--here");
|
|
108
|
+
const slugArgs = rest.filter((arg) => arg !== "--here");
|
|
109
|
+
if (slugArgs.length !== 1 || slugArgs[0].startsWith("-") ||
|
|
110
|
+
rest.length !== (here ? 2 : 1)) {
|
|
111
|
+
throw new ZhikeCliError(
|
|
112
|
+
"zhike_init_usage_invalid",
|
|
113
|
+
"用法:zhike init <app-slug> [--here]",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const appSlug = validateAppSlug(slugArgs[0]);
|
|
117
|
+
const targetDirectory = here ? cwd : resolve(cwd, appSlug);
|
|
118
|
+
if (here) await assertExistingProjectTarget(targetDirectory);
|
|
119
|
+
else await assertEmptyTarget(targetDirectory);
|
|
120
|
+
const { config, store } = getRemoteContext();
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const authenticatedApi = await authenticate({
|
|
123
|
+
...config,
|
|
124
|
+
credentialStore: store,
|
|
125
|
+
fetchImpl,
|
|
126
|
+
openBrowser,
|
|
127
|
+
onProgress,
|
|
128
|
+
allowBrowserAuthorization: true,
|
|
129
|
+
});
|
|
130
|
+
output.write(
|
|
131
|
+
`Developer CLI 登录成功(${_formatElapsed(startedAt)}),正在${here ? "绑定当前项目" : "创建 App 和本地项目"}...\n`,
|
|
132
|
+
);
|
|
133
|
+
const result = here
|
|
134
|
+
? await initializeExistingProject({ projectDirectory: targetDirectory, appSlug,
|
|
135
|
+
apiBaseUrl: config.apiBaseUrl, authenticatedApi })
|
|
136
|
+
: await initializeProject({ targetDirectory, appSlug, apiBaseUrl: config.apiBaseUrl,
|
|
137
|
+
authenticatedApi, ...(scaffold ? { scaffold } : {}) });
|
|
138
|
+
output.write(
|
|
139
|
+
`${result.created ? "已创建" : "已复用"} App ${result.app.slug},项目位于 ${here ? "当前目录" : `${appSlug}/`}(总耗时 ${_formatElapsed(startedAt)})\n`,
|
|
140
|
+
);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
if (command === "upload") {
|
|
144
|
+
_rejectArguments(rest);
|
|
145
|
+
const { config, store } = getRemoteContext();
|
|
146
|
+
const authenticatedApi = await authenticate({
|
|
147
|
+
...config,
|
|
148
|
+
credentialStore: store,
|
|
149
|
+
fetchImpl,
|
|
150
|
+
openBrowser,
|
|
151
|
+
onProgress,
|
|
152
|
+
});
|
|
153
|
+
const result = await uploadAppBundle({
|
|
154
|
+
projectDirectory: cwd,
|
|
155
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
156
|
+
authenticatedApi,
|
|
157
|
+
});
|
|
158
|
+
output.write(
|
|
159
|
+
`Bundle 已受理并更新当前 Development Manifest:revision ${result.developmentManifest.revision},${result.upload.id} (${result.artifact.artifacts.length} Artifacts)\n`,
|
|
160
|
+
);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
if (command === "dev") {
|
|
164
|
+
const requestedWorkspaceId = _parseDevArguments(rest);
|
|
165
|
+
if (!requestedWorkspaceId && !selectWorkspace && !isInteractive) {
|
|
166
|
+
throw new ZhikeCliError(
|
|
167
|
+
"zhike_workspace_selection_required",
|
|
168
|
+
"非交互环境必须显式指定 Workspace:zhike dev --workspace <workspace-id>",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const { config, store } = getRemoteContext();
|
|
172
|
+
const authenticatedApi = await authenticate({
|
|
173
|
+
...config,
|
|
174
|
+
credentialStore: store,
|
|
175
|
+
fetchImpl,
|
|
176
|
+
openBrowser,
|
|
177
|
+
onProgress,
|
|
178
|
+
});
|
|
179
|
+
let ready = false;
|
|
180
|
+
const onReady = async (result) => {
|
|
181
|
+
ready = true;
|
|
182
|
+
output.write(_developmentResultMessage(result));
|
|
183
|
+
try {
|
|
184
|
+
await (openBrowser || openSystemBrowser)(result.workspaceAppUrl);
|
|
185
|
+
} catch {
|
|
186
|
+
output.write(`无法自动打开浏览器,请手动访问:${result.workspaceAppUrl}\n`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const result = await developmentRunner({
|
|
190
|
+
projectDirectory: cwd,
|
|
191
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
192
|
+
workspaceBaseUrl: config.workspaceBaseUrl,
|
|
193
|
+
authenticatedApi,
|
|
194
|
+
requestedWorkspaceId,
|
|
195
|
+
selectWorkspace,
|
|
196
|
+
onProgress,
|
|
197
|
+
onReady,
|
|
198
|
+
});
|
|
199
|
+
if (result && !ready) await onReady(result);
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
throw new ZhikeCliError(
|
|
203
|
+
"zhike_command_unknown",
|
|
204
|
+
`未知命令:${command}`,
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw asSafeCliError(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _rejectArguments(args) {
|
|
212
|
+
if (args.length > 0) {
|
|
213
|
+
throw new ZhikeCliError("zhike_arguments_invalid", "当前命令不接受额外参数");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _parseDevArguments(args) {
|
|
218
|
+
if (args.length === 0) return null;
|
|
219
|
+
if (args.length === 2 && args[0] === "--workspace" && args[1]) {
|
|
220
|
+
return args[1];
|
|
221
|
+
}
|
|
222
|
+
throw new ZhikeCliError(
|
|
223
|
+
"zhike_dev_usage_invalid",
|
|
224
|
+
"用法:zhike dev [--workspace <workspace-id>]",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _developmentResultMessage(result) {
|
|
229
|
+
const revision = result.uploaded.developmentManifest.revision;
|
|
230
|
+
const workspace = `Workspace「${result.workspace.name}」`;
|
|
231
|
+
if (result.application.status === "applied") {
|
|
232
|
+
return `Development Manifest revision ${revision} 已更新;${workspace}已应用 generation ${result.application.generation}。\n`;
|
|
233
|
+
}
|
|
234
|
+
if (result.application.status === "not_installed") {
|
|
235
|
+
return `Development Manifest revision ${revision} 已更新;${workspace}尚未安装,请在标准Apps页面完成安装。\n`;
|
|
236
|
+
}
|
|
237
|
+
throw new ZhikeCliError("zhike_dev_server_upgrade_required", "服务端未完成开发同步,请更新 Developer/Main 服务后重试");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _formatElapsed(startedAt) {
|
|
241
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
242
|
+
return elapsedMs < 1_000 ? "不到 1 秒" : `${(elapsedMs / 1_000).toFixed(1)} 秒`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { authenticateCli, authorizeCli } from "./browser-auth.js";
|
|
246
|
+
export {
|
|
247
|
+
MacOSKeychainCredentialStore,
|
|
248
|
+
MemoryCredentialStore,
|
|
249
|
+
createSystemCredentialStore,
|
|
250
|
+
} from "./credential-store.js";
|
|
251
|
+
export {
|
|
252
|
+
assertEmptyTarget,
|
|
253
|
+
initializeProject,
|
|
254
|
+
scaffoldProject,
|
|
255
|
+
} from "./project-init.js";
|
|
256
|
+
export { uploadAppBundle } from "./upload-bundle.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { loadCliConfig } from "../config.js";
|
|
6
|
+
import { ZhikeCliError } from "../errors.js";
|
|
7
|
+
import { readProjectBinding } from "../project-binding.js";
|
|
8
|
+
|
|
9
|
+
const metadata = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
10
|
+
export const commandHelp = {
|
|
11
|
+
help: "help [command] [--json]",
|
|
12
|
+
version: "version [--json]",
|
|
13
|
+
doctor: "doctor [--json]",
|
|
14
|
+
login: "login",
|
|
15
|
+
init: "init <app-slug> [--here]",
|
|
16
|
+
validate: "validate",
|
|
17
|
+
build: "build",
|
|
18
|
+
upload: "upload",
|
|
19
|
+
dev: "dev [--workspace <workspace-id>]",
|
|
20
|
+
logs: "logs --workspace <id> [--since <time|1h|1d>] [--until <time>] [--level log|warn|error] [--keyword <text>] [--version <id>] [--limit 1..100] [--cursor <cursor>] [--json]",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function localEnvelope(data, status = "completed") {
|
|
24
|
+
return { schemaVersion: 1, status, target: null, data,
|
|
25
|
+
effects: { business: "none", authentication: "none" }, error: null, nextAction: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 本地诊断不调用认证、网络或项目脚本;依赖仅解析路径,不执行包代码。 */
|
|
29
|
+
export async function runLocalCommand(command, args, { cwd, environment }) {
|
|
30
|
+
if (command === "help") {
|
|
31
|
+
if (args.length > 1 || (args.length && !Object.hasOwn(commandHelp, args[0]))) {
|
|
32
|
+
throw new ZhikeCliError("zhike_help_usage_invalid", "用法:help [command] [--json]");
|
|
33
|
+
}
|
|
34
|
+
return localEnvelope({ commands: args.length ? { [args[0]]: commandHelp[args[0]] } : commandHelp,
|
|
35
|
+
jsonCommands: ["help", "version", "doctor", "logs"] });
|
|
36
|
+
}
|
|
37
|
+
if (args.length) throw new ZhikeCliError("zhike_arguments_invalid", "当前命令不接受额外参数");
|
|
38
|
+
if (command === "version") return localEnvelope({ package: metadata.name, version: metadata.version,
|
|
39
|
+
node: process.version, requiredNode: metadata.engines.node });
|
|
40
|
+
|
|
41
|
+
const checks = [];
|
|
42
|
+
checks.push({ name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20,
|
|
43
|
+
required: metadata.engines.node, actual: process.version });
|
|
44
|
+
let config;
|
|
45
|
+
try {
|
|
46
|
+
config = loadCliConfig(environment);
|
|
47
|
+
checks.push({ name: "environment", ok: true, source: "configuration", ...config });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
checks.push({ name: "environment", ok: false, code: error.code });
|
|
50
|
+
}
|
|
51
|
+
let binding;
|
|
52
|
+
try {
|
|
53
|
+
binding = await readProjectBinding(cwd, config?.apiBaseUrl);
|
|
54
|
+
checks.push({ name: "binding", ok: Boolean(config), appId: binding.appId });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
checks.push({ name: "binding", ok: false, code: error.code });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(await readFile(resolve(cwd, "package.json"), "utf8"));
|
|
60
|
+
const require = createRequire(resolve(cwd, "package.json"));
|
|
61
|
+
const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
62
|
+
const sdkDeclared = Boolean(dependencies["@77-zhike/app-sdk"]);
|
|
63
|
+
checks.push({ name: "sdkDeclaration", ok: sdkDeclared });
|
|
64
|
+
for (const name of Object.keys(dependencies)) {
|
|
65
|
+
try {
|
|
66
|
+
require.resolve(name);
|
|
67
|
+
checks.push({ name: "dependency", package: name, ok: true });
|
|
68
|
+
} catch {
|
|
69
|
+
checks.push({ name: "dependency", package: name, ok: false, code: "dependency_unresolved" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
checks.push({ name: "package", ok: false, code: "package_missing_or_invalid" });
|
|
74
|
+
}
|
|
75
|
+
checks.push({ name: "secureStore", ok: process.platform === "darwin", platform: process.platform });
|
|
76
|
+
const result = localEnvelope({ localReady: checks.every((check) => check.ok),
|
|
77
|
+
authentication: "not_checked", installation: "not_checked", checks });
|
|
78
|
+
result.target = config ? { environment: config.apiBaseUrl, appId: binding?.appId ?? null } : null;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
package/src/cli/logs.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../errors.js";
|
|
2
|
+
import { formatAppLog } from "./development-logs.js";
|
|
3
|
+
|
|
4
|
+
const fields = { "--workspace": "workspace_id", "--since": "from", "--until": "to",
|
|
5
|
+
"--level": "level", "--keyword": "query", "--version": "app_version_id",
|
|
6
|
+
"--cursor": "cursor", "--limit": "limit" };
|
|
7
|
+
|
|
8
|
+
/** 查询一页历史日志;不订阅、不构建,也不改变安装状态。 */
|
|
9
|
+
export function parseLogsArguments(args, now = Date.now()) {
|
|
10
|
+
const query = {}, seen = new Set();
|
|
11
|
+
let json = false;
|
|
12
|
+
const invalid = () => { throw new ZhikeCliError("zhike_logs_usage_invalid", "用法:zhike logs --workspace <id> [--since <带时区时间或 1h/1d>] [--until <带时区时间>] [--level log|warn|error] [--keyword <关键词>] [--version <id>] [--limit 1..100] [--cursor <游标>] [--json];最多查询最近三天"); };
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const flag = args[i];
|
|
15
|
+
if (seen.has(flag)) invalid();
|
|
16
|
+
seen.add(flag);
|
|
17
|
+
if (flag === "--json") { json = true; continue; }
|
|
18
|
+
if (!Object.hasOwn(fields, flag) || !args[i + 1] || args[i + 1].startsWith("--")) invalid();
|
|
19
|
+
query[fields[flag]] = args[++i];
|
|
20
|
+
}
|
|
21
|
+
if (!query.workspace_id || query.workspace_id.length > 128) invalid();
|
|
22
|
+
if (query.level && !["log", "warn", "error"].includes(query.level)) invalid();
|
|
23
|
+
if (query.limit && (!/^\d+$/.test(query.limit) || +query.limit < 1 || +query.limit > 100)) invalid();
|
|
24
|
+
if ((query.query?.length ?? 0) > 200 || (query.cursor?.length ?? 0) > 180 || (query.app_version_id?.length ?? 0) > 128) invalid();
|
|
25
|
+
const timestamp = (value, relative) => {
|
|
26
|
+
const match = relative && /^(\d+)(m|h|d)$/.exec(value);
|
|
27
|
+
const time = match ? now - +match[1] * { m: 60_000, h: 3_600_000, d: 86_400_000 }[match[2]]
|
|
28
|
+
: /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value) ? Date.parse(value) : NaN;
|
|
29
|
+
if (!Number.isFinite(time)) invalid();
|
|
30
|
+
return time;
|
|
31
|
+
};
|
|
32
|
+
const end = query.to ? timestamp(query.to, false) : now;
|
|
33
|
+
const start = query.from ? timestamp(query.from, true) : end - 3_600_000;
|
|
34
|
+
if (start >= end || start < now - 3 * 86_400_000 || end > now) invalid();
|
|
35
|
+
query.from = new Date(start).toISOString();
|
|
36
|
+
query.to = new Date(end).toISOString();
|
|
37
|
+
return { query, json };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function queryAppLogs({ api, binding, query, json, output }) {
|
|
41
|
+
const page = await api.queryAppLogs(binding.appId, query);
|
|
42
|
+
if (!page || !Array.isArray(page.items) || !(page.next_cursor === null || typeof page.next_cursor === "string")) {
|
|
43
|
+
throw new ZhikeCliError("zhike_logs_response_invalid", "日志响应格式错误,请检查服务端版本");
|
|
44
|
+
}
|
|
45
|
+
const result = { schemaVersion: 1, status: "completed",
|
|
46
|
+
target: { environment: binding.platformEnvironment, appId: binding.appId, workspaceId: query.workspace_id },
|
|
47
|
+
data: { ...page, from: query.from, to: query.to },
|
|
48
|
+
effects: { business: "none", authentication: "session" }, error: null, nextAction: null };
|
|
49
|
+
if (json) output.write(`${JSON.stringify(result)}\n`);
|
|
50
|
+
else {
|
|
51
|
+
for (const entry of page.items) output.write(`${formatAppLog(binding.appSlug, entry)}\n`);
|
|
52
|
+
if (!page.items.length) output.write("当前条件下没有日志。\n");
|
|
53
|
+
if (page.next_cursor) output.write(`还有更早日志:保持筛选条件并使用 --since ${query.from} --until ${query.to} --cursor ${JSON.stringify(page.next_cursor)}\n`);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|