@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.
- package/package.json +57 -0
- package/src/bin.js +15 -0
- package/src/cli/api-client.js +245 -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 +180 -0
- package/src/cli/development-watch.js +205 -0
- package/src/cli/index.js +253 -0
- package/src/cli/local-commands.js +78 -0
- package/src/cli/project-init.js +371 -0
- package/src/cli/upload-bundle.js +73 -0
- package/src/cli/validate-bundle.js +36 -0
- package/src/client/index.d.ts +52 -0
- package/src/client/index.js +22 -0
- package/src/client/interactions.d.ts +8 -0
- package/src/client/interactions.js +23 -0
- package/src/client/points.generated.js +19 -0
- package/src/config.js +64 -0
- package/src/development-workspace.js +154 -0
- package/src/errors.js +20 -0
- package/src/forms/components.generated.js +4 -0
- package/src/forms/factory.js +27 -0
- package/src/forms/index.d.ts +54 -0
- package/src/forms/index.js +3 -0
- package/src/forms/normalize-options.js +19 -0
- package/src/forms/options.d.ts +25 -0
- package/src/forms/options.js +48 -0
- package/src/forms/schema.d.ts +18 -0
- package/src/index.d.ts +13 -0
- package/src/index.js +2 -0
- package/src/project-binding.js +84 -0
- package/src/server/index.d.ts +268 -0
- package/src/server/index.js +201 -0
- package/src/toolchain/canonical-json.js +17 -0
- package/src/toolchain/index.js +103 -0
- package/src/toolchain/real-build.js +371 -0
- package/src/toolchain/source-discovery.js +710 -0
- package/src/toolchain/transport-tar.js +168 -0
- package/src/toolchain/webhook-response.js +50 -0
- package/src/ui/components.generated.js +2 -0
- package/src/ui/factory.js +19 -0
- package/src/ui/icons.generated.js +2 -0
- package/src/ui/index.d.ts +111 -0
- package/src/ui/index.js +4 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@77-zhike/app-sdk",
|
|
3
|
+
"version": "0.2.0-d7b1.2",
|
|
4
|
+
"description": "\u667a\u5ba2 Integration App \u4e00\u671f SDK \u4e0e\u672c\u5730 Toolchain",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"zhike": "src/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"default": "./src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./src/client/index.d.ts",
|
|
19
|
+
"default": "./src/client/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./server": {
|
|
22
|
+
"types": "./src/server/index.d.ts",
|
|
23
|
+
"default": "./src/server/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./cli": "./src/cli/index.js",
|
|
26
|
+
"./toolchain": "./src/toolchain/index.js",
|
|
27
|
+
"./ui": {
|
|
28
|
+
"types": "./src/ui/index.d.ts",
|
|
29
|
+
"default": "./src/ui/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./forms": {
|
|
32
|
+
"types": "./src/forms/index.d.ts",
|
|
33
|
+
"default": "./src/forms/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"generate:components": "node scripts/generate-components.mjs",
|
|
41
|
+
"check:components": "node scripts/generate-components.mjs --check",
|
|
42
|
+
"test": "node --test test/*.test.js"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@types/react": "19.1.0",
|
|
46
|
+
"esbuild": "0.25.12",
|
|
47
|
+
"typescript": "5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "19.2.5"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"react": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/bin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from "./cli/index.js";
|
|
4
|
+
|
|
5
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
if (process.argv.slice(2).includes("--json")) {
|
|
7
|
+
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, status: "failed", target: null,
|
|
8
|
+
data: null, effects: { business: "none", authentication: "none" },
|
|
9
|
+
error: { code: error?.code || "zhike_error" }, nextAction: null })}\n`);
|
|
10
|
+
process.exitCode = error?.exitCode || 1;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
process.stderr.write(`[${error?.code || "zhike_error"}] ${error?.message || "操作失败"}\n`);
|
|
14
|
+
process.exitCode = error?.exitCode || 1;
|
|
15
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { ZhikeCliError } from "../errors.js";
|
|
4
|
+
|
|
5
|
+
export class DeveloperApiClient {
|
|
6
|
+
constructor({ apiBaseUrl, fetchImpl = globalThis.fetch, accessToken = null }) {
|
|
7
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
8
|
+
this.fetchImpl = fetchImpl;
|
|
9
|
+
this.accessToken = accessToken;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
withAccessToken(accessToken) {
|
|
13
|
+
return new DeveloperApiClient({
|
|
14
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
15
|
+
fetchImpl: this.fetchImpl,
|
|
16
|
+
accessToken,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async exchangeCliAuthorization(payload) {
|
|
21
|
+
return this.request("/api/developer/cli-auth/exchange", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: payload,
|
|
24
|
+
authenticated: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async refreshCliSession(refreshCredential) {
|
|
29
|
+
return this.request("/api/developer/cli-auth/refresh", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: { refresh_credential: refreshCredential },
|
|
32
|
+
authenticated: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async logoutCliSession(refreshCredential) {
|
|
37
|
+
return this.request("/api/developer/cli-auth/logout", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: { refresh_credential: refreshCredential },
|
|
40
|
+
authenticated: false,
|
|
41
|
+
expectEmpty: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async listOrganizations() {
|
|
46
|
+
return this.request("/api/developer/organizations");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getAppBySlug(organizationId, slug) {
|
|
50
|
+
const path = `/api/developer/organizations/${encodeURIComponent(
|
|
51
|
+
organizationId,
|
|
52
|
+
)}/apps/by-slug/${encodeURIComponent(slug)}`;
|
|
53
|
+
return this.request(path, { allowNotFound: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async createApp(organizationId, { name, slug, idempotencyKey }) {
|
|
57
|
+
return this.request(
|
|
58
|
+
`/api/developer/organizations/${encodeURIComponent(organizationId)}/apps`,
|
|
59
|
+
{
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Idempotency-Key": idempotencyKey,
|
|
63
|
+
"X-Request-ID": randomUUID(),
|
|
64
|
+
},
|
|
65
|
+
body: { name, slug },
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async uploadBundle(appId, artifact, idempotencyKey) {
|
|
71
|
+
return this.request(
|
|
72
|
+
`/api/developer/apps/${encodeURIComponent(appId)}/bundle-uploads`,
|
|
73
|
+
{
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Idempotency-Key": idempotencyKey,
|
|
77
|
+
"X-Zhike-Package-Format": artifact.packageFormat,
|
|
78
|
+
"X-Zhike-Tooling-Version": artifact.toolingVersion,
|
|
79
|
+
"X-Zhike-Artifact-Sha256": artifact.sha256,
|
|
80
|
+
"Content-Type":
|
|
81
|
+
artifact.contentType || "application/vnd.qiqizhike.mock-build+json",
|
|
82
|
+
},
|
|
83
|
+
rawBody: artifact.bytes,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getDevelopmentManifest(appId) {
|
|
89
|
+
return this.request(`/api/developer/apps/${encodeURIComponent(appId)}/development-manifest`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async updateDevelopmentManifest(appId, appBundleId, expectedScopeRevision) {
|
|
93
|
+
return this.request(
|
|
94
|
+
`/api/developer/apps/${encodeURIComponent(appId)}/development-manifest`,
|
|
95
|
+
{
|
|
96
|
+
method: "PUT",
|
|
97
|
+
headers: { "X-Request-ID": randomUUID() },
|
|
98
|
+
body: {
|
|
99
|
+
app_bundle_id: appBundleId,
|
|
100
|
+
...(expectedScopeRevision === undefined ? {} : {
|
|
101
|
+
expected_scope_configuration_revision: expectedScopeRevision,
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async listManageableWorkspaces() {
|
|
109
|
+
return this.request("/api/developer/workspaces");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async ensureWorkspaceVisibility(appId, workspaceId) {
|
|
113
|
+
return this.request(
|
|
114
|
+
`/api/developer/apps/${encodeURIComponent(appId)}`
|
|
115
|
+
+ `/workspace-visibilities/${encodeURIComponent(workspaceId)}`,
|
|
116
|
+
{ method: "PUT" },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async applyDevelopmentManifest(appId, workspaceId, snapshot) {
|
|
121
|
+
return this.request(
|
|
122
|
+
`/api/developer/apps/${encodeURIComponent(appId)}`
|
|
123
|
+
+ `/development-workspaces/${encodeURIComponent(workspaceId)}/apply`,
|
|
124
|
+
{ method: "POST", body: snapshot },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async request(
|
|
129
|
+
path,
|
|
130
|
+
{
|
|
131
|
+
method = "GET",
|
|
132
|
+
headers = {},
|
|
133
|
+
body,
|
|
134
|
+
rawBody,
|
|
135
|
+
authenticated = true,
|
|
136
|
+
allowNotFound = false,
|
|
137
|
+
expectEmpty = false,
|
|
138
|
+
retryAuthentication = true,
|
|
139
|
+
} = {},
|
|
140
|
+
) {
|
|
141
|
+
const response = await this.openResponse(path, {
|
|
142
|
+
method, headers, body, rawBody, authenticated, retryAuthentication,
|
|
143
|
+
});
|
|
144
|
+
if (allowNotFound && response.status === 404) return null;
|
|
145
|
+
if (!response.ok) throw await responseError(response);
|
|
146
|
+
if (expectEmpty || response.status === 204) return null;
|
|
147
|
+
return response.json();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async openLogStream(appId, workspaceId, { cursor, signal } = {}) {
|
|
151
|
+
const response = await this.openResponse(
|
|
152
|
+
`/api/developer/apps/${encodeURIComponent(appId)}`
|
|
153
|
+
+ `/development-workspaces/${encodeURIComponent(workspaceId)}/logs/stream`,
|
|
154
|
+
{ headers: { Accept: "text/event-stream", ...(cursor ? { "Last-Event-ID": cursor } : {}) },
|
|
155
|
+
signal, timeoutMs: null },
|
|
156
|
+
);
|
|
157
|
+
if (!response.ok) throw await responseError(response);
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// JSON 与 SSE 共用认证状态;连接取消和超时仍由每次请求独立管理。
|
|
162
|
+
async openResponse(path, {
|
|
163
|
+
method = "GET", headers = {}, body, rawBody, authenticated = true,
|
|
164
|
+
retryAuthentication = true, signal = this.signal, timeoutMs = 30_000,
|
|
165
|
+
} = {}) {
|
|
166
|
+
if (authenticated && !this.accessToken) {
|
|
167
|
+
throw new ZhikeCliError(
|
|
168
|
+
"zhike_cli_auth_required",
|
|
169
|
+
"需要先完成 Developer CLI 授权",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
let response;
|
|
173
|
+
const usedToken = this.accessToken;
|
|
174
|
+
const requestSignal = timeoutMs === null ? signal
|
|
175
|
+
: AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(timeoutMs)]);
|
|
176
|
+
try {
|
|
177
|
+
response = await this.fetchImpl(new URL(path, this.apiBaseUrl), {
|
|
178
|
+
method,
|
|
179
|
+
headers: {
|
|
180
|
+
Accept: "application/json",
|
|
181
|
+
...(body === undefined || rawBody !== undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { "Content-Type": "application/json" }),
|
|
184
|
+
...(authenticated
|
|
185
|
+
? { Authorization: `Bearer ${usedToken}` }
|
|
186
|
+
: {}),
|
|
187
|
+
...headers,
|
|
188
|
+
},
|
|
189
|
+
body: rawBody ?? (body === undefined ? undefined : JSON.stringify(body)),
|
|
190
|
+
signal: requestSignal,
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new ZhikeCliError(
|
|
194
|
+
"zhike_developer_server_unavailable",
|
|
195
|
+
"无法连接 Developer Server",
|
|
196
|
+
{ cause: error, retryable: true },
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (response.status === 401 && authenticated && retryAuthentication && this.refreshAccessToken) {
|
|
200
|
+
await response.body?.cancel();
|
|
201
|
+
// 迟到的旧 Token 401 不再触发第二次刷新。
|
|
202
|
+
if (this.accessToken === usedToken) {
|
|
203
|
+
this.refreshing ??= Promise.resolve().then(() => this.refreshAccessToken())
|
|
204
|
+
.then((token) => { this.accessToken = token; })
|
|
205
|
+
.finally(() => { this.refreshing = null; });
|
|
206
|
+
await waitForRefresh(this.refreshing, requestSignal);
|
|
207
|
+
}
|
|
208
|
+
requestSignal?.throwIfAborted();
|
|
209
|
+
return this.openResponse(path, {
|
|
210
|
+
method, headers, body, rawBody, authenticated, signal: requestSignal,
|
|
211
|
+
timeoutMs: null, retryAuthentication: false,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return response;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function waitForRefresh(refreshing, signal) {
|
|
219
|
+
if (!signal) return refreshing;
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
const aborted = () => reject(signal.reason);
|
|
222
|
+
if (signal.aborted) return aborted();
|
|
223
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
224
|
+
refreshing.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function responseError(response) {
|
|
229
|
+
const payload = await _safeJson(response);
|
|
230
|
+
const error = new ZhikeCliError(
|
|
231
|
+
payload?.code || payload?.detail?.code || "zhike_developer_api_failed",
|
|
232
|
+
payload?.detail?.message || (typeof payload?.detail === "string" ? payload.detail : null) || "Developer API 请求失败",
|
|
233
|
+
{ retryable: response.status >= 500 || response.status === 429 },
|
|
234
|
+
);
|
|
235
|
+
error.statusCode = response.status;
|
|
236
|
+
return error;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function _safeJson(response) {
|
|
240
|
+
try {
|
|
241
|
+
return await response.json();
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
import { CLI_CLIENT_ID } from "../config.js";
|
|
7
|
+
import { ZhikeCliError } from "../errors.js";
|
|
8
|
+
import { DeveloperApiClient } from "./api-client.js";
|
|
9
|
+
|
|
10
|
+
const execFile = promisify(execFileCallback);
|
|
11
|
+
|
|
12
|
+
export async function authorizeCli({
|
|
13
|
+
apiBaseUrl,
|
|
14
|
+
consoleBaseUrl,
|
|
15
|
+
credentialStore,
|
|
16
|
+
fetchImpl = globalThis.fetch,
|
|
17
|
+
openBrowser = openSystemBrowser,
|
|
18
|
+
onProgress = () => {},
|
|
19
|
+
timeoutMs = 120_000,
|
|
20
|
+
}) {
|
|
21
|
+
const state = randomBytes(32).toString("base64url");
|
|
22
|
+
const codeVerifier = randomBytes(48).toString("base64url");
|
|
23
|
+
const codeChallenge = createHash("sha256")
|
|
24
|
+
.update(codeVerifier, "ascii")
|
|
25
|
+
.digest("base64url");
|
|
26
|
+
const callback = await createLoopbackCallback({ state, timeoutMs });
|
|
27
|
+
const redirectUri = `http://127.0.0.1:${callback.port}/callback`;
|
|
28
|
+
const authorizationUrl = new URL("/cli/authorize", consoleBaseUrl);
|
|
29
|
+
authorizationUrl.searchParams.set("client_id", CLI_CLIENT_ID);
|
|
30
|
+
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
31
|
+
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
32
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
33
|
+
authorizationUrl.searchParams.set("state", state);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
callback.waitForCode.catch(() => {});
|
|
37
|
+
onProgress("正在浏览器中完成 Developer CLI 授权...");
|
|
38
|
+
await openBrowser(authorizationUrl.toString());
|
|
39
|
+
const code = await callback.waitForCode;
|
|
40
|
+
onProgress("已收到浏览器授权,正在完成 Developer CLI 登录...");
|
|
41
|
+
const api = new DeveloperApiClient({ apiBaseUrl, fetchImpl });
|
|
42
|
+
const token = await api.exchangeCliAuthorization({
|
|
43
|
+
code,
|
|
44
|
+
client_id: CLI_CLIENT_ID,
|
|
45
|
+
redirect_uri: redirectUri,
|
|
46
|
+
code_verifier: codeVerifier,
|
|
47
|
+
});
|
|
48
|
+
await credentialStore.set(apiBaseUrl, token.refresh_credential);
|
|
49
|
+
return renewableClient(api, token.access_token, credentialStore);
|
|
50
|
+
} finally {
|
|
51
|
+
await callback.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function authenticateCli({
|
|
56
|
+
apiBaseUrl,
|
|
57
|
+
consoleBaseUrl,
|
|
58
|
+
credentialStore,
|
|
59
|
+
fetchImpl = globalThis.fetch,
|
|
60
|
+
openBrowser = openSystemBrowser,
|
|
61
|
+
onProgress = () => {},
|
|
62
|
+
allowBrowserAuthorization = false,
|
|
63
|
+
}) {
|
|
64
|
+
const api = new DeveloperApiClient({ apiBaseUrl, fetchImpl });
|
|
65
|
+
const refreshCredential = await credentialStore.get(apiBaseUrl);
|
|
66
|
+
if (refreshCredential) {
|
|
67
|
+
onProgress("正在验证已保存的 Developer CLI 登录...");
|
|
68
|
+
let token;
|
|
69
|
+
try {
|
|
70
|
+
token = await api.refreshCliSession(refreshCredential);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!(error instanceof ZhikeCliError) || error.retryable) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
await credentialStore.delete(apiBaseUrl);
|
|
76
|
+
onProgress("已保存的 Developer CLI 登录已失效。");
|
|
77
|
+
}
|
|
78
|
+
if (token) {
|
|
79
|
+
if (token.refresh_credential !== refreshCredential) {
|
|
80
|
+
await credentialStore.set(apiBaseUrl, token.refresh_credential);
|
|
81
|
+
}
|
|
82
|
+
return renewableClient(api, token.access_token, credentialStore);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!allowBrowserAuthorization) {
|
|
86
|
+
throw new ZhikeCliError(
|
|
87
|
+
"zhike_cli_auth_required",
|
|
88
|
+
"Developer CLI 未登录,请先在本机终端显式运行 zhike login",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return authorizeCli({
|
|
92
|
+
apiBaseUrl,
|
|
93
|
+
consoleBaseUrl,
|
|
94
|
+
credentialStore,
|
|
95
|
+
fetchImpl,
|
|
96
|
+
openBrowser,
|
|
97
|
+
onProgress,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function openSystemBrowser(url, platform = process.platform) {
|
|
102
|
+
const command =
|
|
103
|
+
platform === "darwin"
|
|
104
|
+
? ["open", [url]]
|
|
105
|
+
: platform === "win32"
|
|
106
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
107
|
+
: ["xdg-open", [url]];
|
|
108
|
+
try {
|
|
109
|
+
await execFile(command[0], command[1], {
|
|
110
|
+
windowsHide: true,
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new ZhikeCliError(
|
|
114
|
+
"zhike_browser_open_failed",
|
|
115
|
+
`无法自动打开浏览器,请手动访问:${url}`,
|
|
116
|
+
{ cause: error },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renewableClient(api, accessToken, credentialStore) {
|
|
122
|
+
const client = api.withAccessToken(accessToken);
|
|
123
|
+
client.refreshAccessToken = async () => {
|
|
124
|
+
const refreshCredential = await credentialStore.get(api.apiBaseUrl);
|
|
125
|
+
if (!refreshCredential) {
|
|
126
|
+
throw new ZhikeCliError("zhike_cli_auth_required", "Developer CLI 登录已失效,请重新登录");
|
|
127
|
+
}
|
|
128
|
+
const token = await api.refreshCliSession(refreshCredential);
|
|
129
|
+
if (token.refresh_credential !== refreshCredential) {
|
|
130
|
+
await credentialStore.set(api.apiBaseUrl, token.refresh_credential);
|
|
131
|
+
}
|
|
132
|
+
return token.access_token;
|
|
133
|
+
};
|
|
134
|
+
return client;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function createLoopbackCallback({ state, timeoutMs }) {
|
|
138
|
+
let timer;
|
|
139
|
+
let resolveCode;
|
|
140
|
+
let rejectCode;
|
|
141
|
+
const waitForCode = new Promise((resolve, reject) => {
|
|
142
|
+
resolveCode = resolve;
|
|
143
|
+
rejectCode = reject;
|
|
144
|
+
});
|
|
145
|
+
const server = createServer((request, response) => {
|
|
146
|
+
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
147
|
+
const receivedState = url.searchParams.get("state") || "";
|
|
148
|
+
const code = url.searchParams.get("code");
|
|
149
|
+
if (
|
|
150
|
+
url.pathname !== "/callback" ||
|
|
151
|
+
!code ||
|
|
152
|
+
!_safeEqual(receivedState, state)
|
|
153
|
+
) {
|
|
154
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
155
|
+
response.end("CLI 授权回调无效,可以关闭此页面。");
|
|
156
|
+
rejectCode(
|
|
157
|
+
new ZhikeCliError(
|
|
158
|
+
"zhike_cli_callback_invalid",
|
|
159
|
+
"CLI 授权回调无效",
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
165
|
+
response.end("智客 Developer CLI 已授权,可以关闭此页面。");
|
|
166
|
+
resolveCode(code);
|
|
167
|
+
});
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
server.once("error", reject);
|
|
170
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
171
|
+
});
|
|
172
|
+
const address = server.address();
|
|
173
|
+
if (!address || typeof address === "string") {
|
|
174
|
+
server.close();
|
|
175
|
+
throw new ZhikeCliError(
|
|
176
|
+
"zhike_cli_callback_unavailable",
|
|
177
|
+
"无法启动本机 CLI 授权回调",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
timer = setTimeout(() => {
|
|
181
|
+
rejectCode(
|
|
182
|
+
new ZhikeCliError("zhike_cli_authorization_timeout", "CLI 授权已超时"),
|
|
183
|
+
);
|
|
184
|
+
server.close();
|
|
185
|
+
server.closeAllConnections();
|
|
186
|
+
}, timeoutMs);
|
|
187
|
+
timer.unref();
|
|
188
|
+
return {
|
|
189
|
+
port: address.port,
|
|
190
|
+
waitForCode,
|
|
191
|
+
close: async () => {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
if (!server.listening) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await new Promise((resolve) => {
|
|
197
|
+
server.close(resolve);
|
|
198
|
+
// Chrome 可能预建尚未发送 HTTP 的 TCP 连接;仅 close 会一直等待它。
|
|
199
|
+
server.closeAllConnections();
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _safeEqual(actual, expected) {
|
|
206
|
+
const actualBytes = Buffer.from(actual);
|
|
207
|
+
const expectedBytes = Buffer.from(expected);
|
|
208
|
+
return (
|
|
209
|
+
actualBytes.length === expectedBytes.length &&
|
|
210
|
+
timingSafeEqual(actualBytes, expectedBytes)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
import { ZhikeCliError } from "../errors.js";
|
|
5
|
+
|
|
6
|
+
const execFile = promisify(execFileCallback);
|
|
7
|
+
const KEYCHAIN_SERVICE = "com.77hub.zhike.developer-cli";
|
|
8
|
+
|
|
9
|
+
export class MemoryCredentialStore {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.values = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async get(environmentId) {
|
|
15
|
+
return this.values.get(environmentId) ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async set(environmentId, refreshCredential) {
|
|
19
|
+
this.values.set(environmentId, refreshCredential);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async delete(environmentId) {
|
|
23
|
+
this.values.delete(environmentId);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MacOSKeychainCredentialStore {
|
|
28
|
+
constructor({ runner = execFile } = {}) {
|
|
29
|
+
this.runner = runner;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async get(environmentId) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await this.runner("security", [
|
|
35
|
+
"find-generic-password",
|
|
36
|
+
"-a",
|
|
37
|
+
environmentId,
|
|
38
|
+
"-s",
|
|
39
|
+
KEYCHAIN_SERVICE,
|
|
40
|
+
"-w",
|
|
41
|
+
]);
|
|
42
|
+
return stdout.trim() || null;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error?.code === 44) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
throw _keychainError(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async set(environmentId, refreshCredential) {
|
|
52
|
+
try {
|
|
53
|
+
await this.runner("security", [
|
|
54
|
+
"add-generic-password",
|
|
55
|
+
"-U",
|
|
56
|
+
"-a",
|
|
57
|
+
environmentId,
|
|
58
|
+
"-s",
|
|
59
|
+
KEYCHAIN_SERVICE,
|
|
60
|
+
"-w",
|
|
61
|
+
refreshCredential,
|
|
62
|
+
]);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw _keychainError(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async delete(environmentId) {
|
|
69
|
+
try {
|
|
70
|
+
await this.runner("security", [
|
|
71
|
+
"delete-generic-password",
|
|
72
|
+
"-a",
|
|
73
|
+
environmentId,
|
|
74
|
+
"-s",
|
|
75
|
+
KEYCHAIN_SERVICE,
|
|
76
|
+
]);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error?.code !== 44) {
|
|
79
|
+
throw _keychainError(error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createSystemCredentialStore(platform = process.platform) {
|
|
86
|
+
if (platform === "darwin") {
|
|
87
|
+
return new MacOSKeychainCredentialStore();
|
|
88
|
+
}
|
|
89
|
+
throw new ZhikeCliError(
|
|
90
|
+
"zhike_secure_store_unavailable",
|
|
91
|
+
"当前操作系统尚未配置安全凭据存储适配器",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _keychainError(error) {
|
|
96
|
+
return new ZhikeCliError(
|
|
97
|
+
"zhike_secure_store_failed",
|
|
98
|
+
"无法访问操作系统安全凭据存储",
|
|
99
|
+
{ cause: error },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../errors.js";
|
|
2
|
+
import {
|
|
3
|
+
readDevelopmentWorkspace,
|
|
4
|
+
resolveDevelopmentWorkspace,
|
|
5
|
+
writeDevelopmentWorkspace,
|
|
6
|
+
} from "../development-workspace.js";
|
|
7
|
+
import { readProjectBinding } from "../project-binding.js";
|
|
8
|
+
import { uploadAppBundle } from "./upload-bundle.js";
|
|
9
|
+
|
|
10
|
+
export async function runDevelopmentApp({
|
|
11
|
+
projectDirectory,
|
|
12
|
+
apiBaseUrl,
|
|
13
|
+
workspaceBaseUrl,
|
|
14
|
+
authenticatedApi,
|
|
15
|
+
requestedWorkspaceId = null,
|
|
16
|
+
selectWorkspace,
|
|
17
|
+
upload = uploadAppBundle,
|
|
18
|
+
expectedScopeRevision,
|
|
19
|
+
onWorkspaceReady = async () => {},
|
|
20
|
+
}) {
|
|
21
|
+
const binding = await readProjectBinding(projectDirectory, apiBaseUrl);
|
|
22
|
+
const workspaces = await authenticatedApi.listManageableWorkspaces();
|
|
23
|
+
const state = await readDevelopmentWorkspace(projectDirectory);
|
|
24
|
+
const workspace = await resolveDevelopmentWorkspace({
|
|
25
|
+
workspaces,
|
|
26
|
+
currentWorkspaceId: state?.currentWorkspaceId ?? null,
|
|
27
|
+
requestedWorkspaceId,
|
|
28
|
+
...(selectWorkspace ? { selectWorkspace } : {}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await authenticatedApi.ensureWorkspaceVisibility(binding.appId, workspace.id);
|
|
32
|
+
await writeDevelopmentWorkspace(projectDirectory, workspace.id);
|
|
33
|
+
await onWorkspaceReady({ binding, workspace });
|
|
34
|
+
|
|
35
|
+
const uploaded = await upload({
|
|
36
|
+
projectDirectory,
|
|
37
|
+
apiBaseUrl,
|
|
38
|
+
authenticatedApi,
|
|
39
|
+
expectedScopeRevision,
|
|
40
|
+
});
|
|
41
|
+
const snapshot = {
|
|
42
|
+
development_revision: uploaded.developmentManifest.revision,
|
|
43
|
+
manifest_digest: uploaded.developmentManifest.manifest_digest,
|
|
44
|
+
...(expectedScopeRevision === undefined ? {} : {
|
|
45
|
+
expected_scope_configuration_revision: expectedScopeRevision,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
let application;
|
|
49
|
+
try {
|
|
50
|
+
application = await authenticatedApi.applyDevelopmentManifest(
|
|
51
|
+
binding.appId,
|
|
52
|
+
workspace.id,
|
|
53
|
+
snapshot,
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const partial = new ZhikeCliError(
|
|
57
|
+
"zhike_manifest_updated_workspace_apply_failed",
|
|
58
|
+
`Development Manifest revision ${snapshot.development_revision} 已更新,但Workspace「${workspace.name}」未应用:${error.message}`,
|
|
59
|
+
{ cause: error, retryable: error?.retryable ?? false },
|
|
60
|
+
);
|
|
61
|
+
partial.developmentRevision = snapshot.development_revision;
|
|
62
|
+
partial.manifestDigest = snapshot.manifest_digest;
|
|
63
|
+
partial.workspaceId = workspace.id;
|
|
64
|
+
throw partial;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
binding,
|
|
69
|
+
workspace,
|
|
70
|
+
uploaded,
|
|
71
|
+
application,
|
|
72
|
+
workspaceAppUrl: new URL(
|
|
73
|
+
`/web/settings/apps/${encodeURIComponent(binding.appId)}`,
|
|
74
|
+
workspaceBaseUrl,
|
|
75
|
+
).toString(),
|
|
76
|
+
};
|
|
77
|
+
}
|