@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,19 @@
1
+ // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
+ export const frontendPoints = Object.freeze([
3
+ {
4
+ "key": "app.settings",
5
+ "kind": "view",
6
+ "legacyType": "settings",
7
+ "definitionName": "SettingsExtensionDefinition",
8
+ "maxPerApp": 1
9
+ },
10
+ {
11
+ "key": "app.record-actions",
12
+ "kind": "action",
13
+ "legacyType": "record-action",
14
+ "definitionName": "RecordActionDefinition",
15
+ "callbackName": "onTrigger",
16
+ "maxPerApp": 100
17
+ }
18
+ ].map(Object.freeze));
19
+ export const frontendPoint = type => frontendPoints.find(point => point.legacyType === type);
package/src/config.js ADDED
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { ZhikeCliError } from "./errors.js";
4
+
5
+ export const CLI_CLIENT_ID = "zhike-cli";
6
+ const metadata = JSON.parse(
7
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
8
+ );
9
+ export const TOOLING_VERSION = metadata.version;
10
+
11
+ const CLI_ORIGIN_VARIABLES = Object.freeze([
12
+ "ZHIKE_DEVELOPER_API_URL",
13
+ "ZHIKE_DEVELOPER_CONSOLE_URL",
14
+ "ZHIKE_WORKSPACE_APP_URL",
15
+ ]);
16
+
17
+ export function normalizeOrigin(value, fieldName) {
18
+ try {
19
+ const url = new URL(value);
20
+ if (
21
+ !["http:", "https:"].includes(url.protocol) ||
22
+ url.username ||
23
+ url.password ||
24
+ url.pathname !== "/" ||
25
+ url.search ||
26
+ url.hash
27
+ ) {
28
+ throw new Error("invalid origin");
29
+ }
30
+ return url.origin;
31
+ } catch (error) {
32
+ throw new ZhikeCliError(
33
+ "zhike_environment_invalid",
34
+ `${fieldName} 必须是无凭据、无路径的 HTTP(S) origin`,
35
+ { cause: error },
36
+ );
37
+ }
38
+ }
39
+
40
+ export function loadCliConfig(environment = process.env) {
41
+ const missing = CLI_ORIGIN_VARIABLES.filter(
42
+ (name) => typeof environment[name] !== "string" || !environment[name].trim(),
43
+ );
44
+ if (missing.length) {
45
+ throw new ZhikeCliError(
46
+ "zhike_environment_required",
47
+ `缺少 CLI 环境变量:${missing.join(", ")}`,
48
+ );
49
+ }
50
+ return {
51
+ apiBaseUrl: normalizeOrigin(
52
+ environment.ZHIKE_DEVELOPER_API_URL,
53
+ "ZHIKE_DEVELOPER_API_URL",
54
+ ),
55
+ consoleBaseUrl: normalizeOrigin(
56
+ environment.ZHIKE_DEVELOPER_CONSOLE_URL,
57
+ "ZHIKE_DEVELOPER_CONSOLE_URL",
58
+ ),
59
+ workspaceBaseUrl: normalizeOrigin(
60
+ environment.ZHIKE_WORKSPACE_APP_URL,
61
+ "ZHIKE_WORKSPACE_APP_URL",
62
+ ),
63
+ };
64
+ }
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+
6
+ import { ZhikeCliError } from "./errors.js";
7
+
8
+ export const DEVELOPMENT_WORKSPACE_PATH = ".zhike/development.json";
9
+
10
+ export function parseDevelopmentWorkspaceState(value) {
11
+ if (
12
+ !value ||
13
+ typeof value !== "object" ||
14
+ Array.isArray(value) ||
15
+ Object.keys(value).sort().join(",") !== "currentWorkspaceId,schemaVersion" ||
16
+ value.schemaVersion !== 1 ||
17
+ !isStableId(value.currentWorkspaceId)
18
+ ) {
19
+ throw new ZhikeCliError(
20
+ "zhike_development_workspace_invalid",
21
+ `${DEVELOPMENT_WORKSPACE_PATH} 不是受支持的Development Workspace状态`,
22
+ );
23
+ }
24
+ return Object.freeze({ ...value });
25
+ }
26
+
27
+ export async function readDevelopmentWorkspace(projectDirectory) {
28
+ let content;
29
+ try {
30
+ content = await readFile(resolve(projectDirectory, DEVELOPMENT_WORKSPACE_PATH), "utf8");
31
+ } catch (error) {
32
+ if (error?.code === "ENOENT") return null;
33
+ throw new ZhikeCliError(
34
+ "zhike_development_workspace_read_failed",
35
+ "无法读取当前Development Workspace",
36
+ { cause: error },
37
+ );
38
+ }
39
+ try {
40
+ return parseDevelopmentWorkspaceState(JSON.parse(content));
41
+ } catch (error) {
42
+ if (error instanceof ZhikeCliError) throw error;
43
+ throw new ZhikeCliError(
44
+ "zhike_development_workspace_invalid",
45
+ `${DEVELOPMENT_WORKSPACE_PATH} 不是受支持的Development Workspace状态`,
46
+ { cause: error },
47
+ );
48
+ }
49
+ }
50
+
51
+ export async function writeDevelopmentWorkspace(projectDirectory, workspaceId) {
52
+ const state = parseDevelopmentWorkspaceState({
53
+ schemaVersion: 1,
54
+ currentWorkspaceId: workspaceId,
55
+ });
56
+ const directory = resolve(projectDirectory, ".zhike");
57
+ const target = resolve(projectDirectory, DEVELOPMENT_WORKSPACE_PATH);
58
+ const temporary = `${target}.${randomUUID()}.tmp`;
59
+ try {
60
+ await mkdir(directory, { recursive: true });
61
+ await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { flag: "wx" });
62
+ await rename(temporary, target);
63
+ } catch (error) {
64
+ throw new ZhikeCliError(
65
+ "zhike_development_workspace_write_failed",
66
+ "无法保存当前Development Workspace",
67
+ { cause: error },
68
+ );
69
+ }
70
+ return state;
71
+ }
72
+
73
+ export async function resolveDevelopmentWorkspace({
74
+ workspaces,
75
+ currentWorkspaceId = null,
76
+ requestedWorkspaceId = null,
77
+ selectWorkspace = promptForWorkspace,
78
+ }) {
79
+ const normalized = normalizeWorkspaces(workspaces);
80
+ if (normalized.length === 0) {
81
+ throw new ZhikeCliError(
82
+ "zhike_manageable_workspace_missing",
83
+ "当前账号没有可用于App开发的Workspace管理权限",
84
+ );
85
+ }
86
+ if (requestedWorkspaceId) {
87
+ const requested = normalized.find(({ id }) => id === requestedWorkspaceId);
88
+ if (!requested) throw workspaceNotManageable();
89
+ return requested;
90
+ }
91
+ const current = normalized.find(({ id }) => id === currentWorkspaceId);
92
+ const selectedId = await selectWorkspace(normalized, current?.id ?? null);
93
+ const selected = normalized.find(({ id }) => id === selectedId);
94
+ if (!selected) throw workspaceNotManageable();
95
+ return selected;
96
+ }
97
+
98
+ export async function promptForWorkspace(workspaces, defaultWorkspaceId = null) {
99
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
100
+ try {
101
+ const defaultIndex = workspaces.findIndex(({ id }) => id === defaultWorkspaceId);
102
+ process.stdout.write("请选择当前Development Workspace:\n");
103
+ workspaces.forEach((workspace, index) => {
104
+ process.stdout.write(
105
+ ` ${index + 1}. ${workspace.name} (${workspace.id})${index === defaultIndex ? " [上次选择,默认]" : ""}\n`,
106
+ );
107
+ });
108
+ const answer = (await prompt.question(defaultIndex >= 0
109
+ ? `输入序号(回车选择 ${defaultIndex + 1}):`
110
+ : "输入序号:")).trim();
111
+ if (!answer) return workspaces[defaultIndex]?.id ?? "";
112
+ const index = /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
113
+ return workspaces[index]?.id ?? "";
114
+ } finally {
115
+ prompt.close();
116
+ }
117
+ }
118
+
119
+ function normalizeWorkspaces(value) {
120
+ if (!Array.isArray(value)) throw workspaceResponseInvalid();
121
+ const ids = new Set();
122
+ return value.map((workspace) => {
123
+ if (
124
+ !workspace ||
125
+ typeof workspace !== "object" ||
126
+ !isStableId(workspace.id) ||
127
+ typeof workspace.name !== "string" ||
128
+ !workspace.name.trim() ||
129
+ ids.has(workspace.id)
130
+ ) {
131
+ throw workspaceResponseInvalid();
132
+ }
133
+ ids.add(workspace.id);
134
+ return Object.freeze({ id: workspace.id, name: workspace.name.trim() });
135
+ });
136
+ }
137
+
138
+ function isStableId(value) {
139
+ return typeof value === "string" && /^[0-9A-HJKMNP-TV-Z]{26}$/.test(value);
140
+ }
141
+
142
+ function workspaceNotManageable() {
143
+ return new ZhikeCliError(
144
+ "zhike_workspace_not_manageable",
145
+ "选择的Workspace不存在或当前账号已无管理权限",
146
+ );
147
+ }
148
+
149
+ function workspaceResponseInvalid() {
150
+ return new ZhikeCliError(
151
+ "zhike_manageable_workspaces_invalid",
152
+ "Developer Server返回了不受支持的Workspace列表",
153
+ );
154
+ }
package/src/errors.js ADDED
@@ -0,0 +1,20 @@
1
+ export class ZhikeCliError extends Error {
2
+ constructor(code, message, options = {}) {
3
+ super(message, options);
4
+ this.name = "ZhikeCliError";
5
+ this.code = code;
6
+ this.exitCode = options.exitCode ?? 1;
7
+ this.retryable = options.retryable ?? false;
8
+ }
9
+ }
10
+
11
+ export function asSafeCliError(error) {
12
+ if (error instanceof ZhikeCliError) {
13
+ return error;
14
+ }
15
+ return new ZhikeCliError(
16
+ "zhike_unexpected_error",
17
+ "操作失败,请检查 Developer Server 状态后重试",
18
+ { cause: error, retryable: true },
19
+ );
20
+ }
@@ -0,0 +1,4 @@
1
+ // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
+ export const fieldComponents = Object.freeze(["TextInput","Checkbox","Select","Combobox","MemberCombobox","TeamCombobox"]);
3
+ export const boundComponents = Object.freeze(["TextInput","Checkbox","Select","Combobox","MemberCombobox","TeamCombobox","SubmitButton"]);
4
+ export const formComponent = "Form";
@@ -0,0 +1,27 @@
1
+ import { boundComponents, formComponent } from './components.generated.js';
2
+ import { useNormalizedOptions } from './normalize-options.js';
3
+ export function createFormHook({ createElement, useId, useMemo, useRef }) {
4
+
5
+ return function useForm(options) {
6
+ const formId = useId();
7
+ const initial = useRef(options);
8
+ return useMemo(() => {
9
+ const { schema, initialValues } = initial.current;
10
+ const field = type => function Field({ children, ...props }) {
11
+ if (type === 'Combobox') {
12
+ const { options, ...rest } = props;
13
+ const normalized = useNormalizedOptions({ useRef, useMemo }, options);
14
+ return createElement(type, { ...rest, ...normalized, formId });
15
+ }
16
+ return createElement(type, { ...props, formId }, children);
17
+ };
18
+ return Object.freeze({
19
+ Form: function Form({ children, onSubmit }) {
20
+ return createElement(formComponent, { formId, schema, initialValues, onSubmit }, children);
21
+ },
22
+ ...Object.fromEntries(boundComponents.map(type => [type, field(type)])),
23
+ });
24
+ }, [formId]);
25
+ }
26
+
27
+ }
@@ -0,0 +1,54 @@
1
+ // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
+ import type { ComponentType, ReactNode } from "react";
3
+ import type { FormSchema, FormValues, FieldName, FormSubmitResult } from "./schema.js";
4
+ import type { Option, OptionsPage, OptionsProvider } from "./options.js";
5
+ export type { Option, OptionsPage, OptionsProvider } from "./options.js";
6
+ export type { FormField, FormSchema, FormValues, FieldName, FormSubmitResult } from "./schema.js";
7
+ export interface FormHandle<S extends FormSchema> {
8
+ Form: ComponentType<{
9
+ children?: ReactNode;
10
+ onSubmit: (values: FormValues<S>) => FormSubmitResult<S> | Promise<FormSubmitResult<S>>;
11
+ }>;
12
+ TextInput: ComponentType<{
13
+ name: FieldName<S, "string">;
14
+ description?: string;
15
+ placeholder?: string;
16
+ disabled?: boolean;
17
+ }>;
18
+ Checkbox: ComponentType<{
19
+ name: FieldName<S, "boolean">;
20
+ description?: string;
21
+ disabled?: boolean;
22
+ }>;
23
+ Select: ComponentType<{
24
+ name: FieldName<S, "enum">;
25
+ description?: string;
26
+ placeholder?: string;
27
+ disabled?: boolean;
28
+ }>;
29
+ Combobox: ComponentType<{
30
+ name: FieldName<S, "string">;
31
+ description?: string;
32
+ placeholder?: string;
33
+ disabled?: boolean;
34
+ options: readonly Option[] | OptionsProvider;
35
+ }>;
36
+ MemberCombobox: ComponentType<{
37
+ name: FieldName<S, "string">;
38
+ description?: string;
39
+ placeholder?: string;
40
+ disabled?: boolean;
41
+ }>;
42
+ TeamCombobox: ComponentType<{
43
+ name: FieldName<S, "string">;
44
+ description?: string;
45
+ placeholder?: string;
46
+ disabled?: boolean;
47
+ }>;
48
+ SubmitButton: ComponentType<{
49
+ children?: string;
50
+ disabled?: boolean;
51
+ }>;
52
+ }
53
+ /** schema 和 initialValues 仅在首次挂载使用;重新渲染不会重置输入。 */
54
+ export function useForm<const S extends FormSchema>(options: { schema: S; initialValues: FormValues<S> }): FormHandle<S>;
@@ -0,0 +1,3 @@
1
+ import * as React from "react";
2
+ import { createFormHook } from "./factory.js";
3
+ export const useForm = createFormHook(React);
@@ -0,0 +1,19 @@
1
+ import { assertOptions, assertOptionsSearch, assertOptionsPage, assertOptionValue, assertSelectedOption } from './options.js';
2
+ export function useNormalizedOptions({ useRef, useMemo }, options) {
3
+ const source = useRef({ options: null, search: null, getOption: null, revision: 0 });
4
+ const search = options?.search, getOption = options?.getOption;
5
+ if (source.current.options !== options || source.current.search !== search || source.current.getOption !== getOption) {
6
+ source.current = { options, search, getOption, revision: source.current.revision + 1 };
7
+ }
8
+ const revision = source.current.revision;
9
+ const normalized = useMemo(() => {
10
+ if (Array.isArray(options)) return { options: assertOptions(options) };
11
+ if (!options || typeof search !== 'function' || typeof getOption !== 'function') throw new Error('runtime_options_invalid');
12
+ return {
13
+ sourceRevision: String(revision),
14
+ onSearch: input => Promise.resolve(search.call(options, assertOptionsSearch(input))).then(assertOptionsPage),
15
+ onGetOption: value => Promise.resolve(getOption.call(options, assertOptionValue(value))).then(option => assertSelectedOption(option, value)),
16
+ };
17
+ }, [revision]);
18
+ return normalized;
19
+ }
@@ -0,0 +1,25 @@
1
+ /** value 是持久化标识,label 只负责显示。无法回显不能清除原 value。 */
2
+ export interface Option {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ disabled?: boolean;
7
+ disabledReason?: string;
8
+ }
9
+ export interface OptionsPage {
10
+ items: Option[];
11
+ /** null 表示末页;游标只能在当前搜索下使用。 */
12
+ nextCursor: string | null;
13
+ }
14
+ export interface OptionsProvider {
15
+ search(input: { query: string; cursor?: string }): Promise<OptionsPage>;
16
+ /** null 表示成功查询但无法解析;读取失败应拒绝 Promise。 */
17
+ getOption(value: string): Promise<Option | null>;
18
+ }
19
+ export const OPTIONS_PAGE_LIMIT: number;
20
+ export function assertOption(value: unknown): Option;
21
+ export function assertOptionValue(value: unknown): string;
22
+ export function assertOptions(value: unknown): Option[];
23
+ export function assertOptionsSearch(value: unknown): { query: string; cursor?: string };
24
+ export function assertOptionsPage(value: unknown): OptionsPage;
25
+ export function assertSelectedOption(option: unknown, value: string): Option | null;
@@ -0,0 +1,48 @@
1
+ // SDK 回调与 Host 共用有界 DTO 验证;函数留在 Guest,不传递 Provider 对象。
2
+ const fail = () => { throw new Error('runtime_options_invalid'); };
3
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ const only = (value, keys) => { if (!object(value) || Object.keys(value).some(key => !keys.includes(key))) fail(); };
5
+ export const OPTIONS_PAGE_LIMIT = 100;
6
+ export function assertOption(option) {
7
+ only(option, ['value', 'label', 'description', 'disabled', 'disabledReason']);
8
+ assertOptionValue(option.value);
9
+ if (typeof option.label !== 'string' || !option.label || option.label.length > 200) fail();
10
+ for (const key of ['description', 'disabledReason']) {
11
+ if (option[key] !== undefined && (typeof option[key] !== 'string' || option[key].length > 4096)) fail();
12
+ }
13
+ if (option.disabled !== undefined && typeof option.disabled !== 'boolean') fail();
14
+ return option;
15
+ }
16
+ export function assertOptionValue(value) {
17
+ if (typeof value !== 'string' || !value || value.length > 512) fail();
18
+ return value;
19
+ }
20
+ export function assertOptions(options) {
21
+ if (!Array.isArray(options) || options.length > OPTIONS_PAGE_LIMIT) fail();
22
+ const values = new Set();
23
+ for (const option of options) {
24
+ assertOption(option);
25
+ if (values.has(option.value)) fail();
26
+ values.add(option.value);
27
+ }
28
+ return options;
29
+ }
30
+ export function assertOptionsSearch(input) {
31
+ only(input, ['query', 'cursor']);
32
+ if (typeof input.query !== 'string' || input.query.length > 512) fail();
33
+ if (input.cursor !== undefined && (typeof input.cursor !== 'string' || !input.cursor || input.cursor.length > 4096)) fail();
34
+ return input;
35
+ }
36
+ export function assertOptionsPage(page) {
37
+ only(page, ['items', 'nextCursor']);
38
+ assertOptions(page.items);
39
+ if (page.nextCursor !== null && (typeof page.nextCursor !== 'string' || !page.nextCursor || page.nextCursor.length > 4096)) fail();
40
+ return page;
41
+ }
42
+ export function assertSelectedOption(option, value) {
43
+ assertOptionValue(value);
44
+ if (option === null) return null;
45
+ assertOption(option);
46
+ if (option.value !== value) fail();
47
+ return option;
48
+ }
@@ -0,0 +1,18 @@
1
+ export type FormField =
2
+ | { type: "string"; label: string; required?: boolean; maxLength?: number }
3
+ | { type: "boolean"; label: string; required?: boolean }
4
+ | { type: "enum"; label: string; required?: boolean; options: readonly { value: string; label: string }[] };
5
+ export interface FormSchema { fields: Readonly<Record<string, FormField>>; }
6
+ export type FormValues<S extends FormSchema> = {
7
+ [K in keyof S["fields"]]: S["fields"][K] extends { type: "boolean" } ? boolean : string;
8
+ };
9
+ export type FieldName<S extends FormSchema, T extends FormField['type']> = {
10
+ [K in keyof S['fields']]: S['fields'][K]['type'] extends T ? K : never;
11
+ }[keyof S['fields']] & string;
12
+ /** void 兼容既有提交;失败保留输入,字段名必须属于当前表单。 */
13
+ export type FormSubmitResult<S extends FormSchema> = void | { ok: true } | {
14
+ ok: false;
15
+ code: string;
16
+ message: string;
17
+ fieldErrors?: Partial<Record<Extract<keyof S['fields'], string>, string>>;
18
+ };
package/src/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /** App SDK 支持的严格 JSON 值。 */
2
+ export type JsonValue =
3
+ | null
4
+ | boolean
5
+ | number
6
+ | string
7
+ | JsonValue[]
8
+ | { [key: string]: JsonValue };
9
+
10
+ /** 当前 App Bundle 代码 Manifest schema。 */
11
+ export const APP_MANIFEST_SCHEMA_VERSION: "zhike.app-manifest.v1";
12
+ /** 当前 Host SDK ABI 版本。 */
13
+ export const SDK_ABI_VERSION: "2";
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export const APP_MANIFEST_SCHEMA_VERSION = "zhike.app-manifest.v1";
2
+ export const SDK_ABI_VERSION = "2";
@@ -0,0 +1,84 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+
4
+ import { normalizeOrigin } from "./config.js";
5
+ import { ZhikeCliError } from "./errors.js";
6
+
7
+ export const PROJECT_BINDING_PATH = ".zhike/project.json";
8
+ const BINDING_KEYS = [
9
+ "appId",
10
+ "appSlug",
11
+ "organizationId",
12
+ "platformEnvironment",
13
+ "schemaVersion",
14
+ ];
15
+
16
+ export function parseProjectBinding(value, expectedEnvironment = null) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18
+ throw _bindingError();
19
+ }
20
+ const keys = Object.keys(value).sort();
21
+ if (
22
+ keys.length !== BINDING_KEYS.length ||
23
+ keys.some((key, index) => key !== BINDING_KEYS[index])
24
+ ) {
25
+ throw _bindingError();
26
+ }
27
+ const environment = normalizeOrigin(
28
+ value.platformEnvironment,
29
+ "platformEnvironment",
30
+ );
31
+ if (
32
+ value.schemaVersion !== 1 ||
33
+ !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(value.appId) ||
34
+ !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(value.organizationId) ||
35
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.appSlug) ||
36
+ environment !== value.platformEnvironment
37
+ ) {
38
+ throw _bindingError();
39
+ }
40
+ if (
41
+ expectedEnvironment &&
42
+ environment !== normalizeOrigin(expectedEnvironment, "Developer API URL")
43
+ ) {
44
+ throw new ZhikeCliError(
45
+ "zhike_project_environment_mismatch",
46
+ "当前项目绑定的 Developer 环境与 CLI 目标环境不一致",
47
+ );
48
+ }
49
+ return Object.freeze({ ...value });
50
+ }
51
+
52
+ export async function readProjectBinding(projectDirectory, expectedEnvironment) {
53
+ let content;
54
+ try {
55
+ content = await readFile(resolve(projectDirectory, PROJECT_BINDING_PATH), "utf8");
56
+ } catch (error) {
57
+ throw new ZhikeCliError(
58
+ "zhike_project_binding_missing",
59
+ `当前目录缺少 ${PROJECT_BINDING_PATH}`,
60
+ { cause: error },
61
+ );
62
+ }
63
+ try {
64
+ return parseProjectBinding(JSON.parse(content), expectedEnvironment);
65
+ } catch (error) {
66
+ if (error instanceof ZhikeCliError) {
67
+ throw error;
68
+ }
69
+ throw _bindingError(error);
70
+ }
71
+ }
72
+
73
+ export function serializeProjectBinding(binding) {
74
+ const validated = parseProjectBinding(binding);
75
+ return `${JSON.stringify(validated, null, 2)}\n`;
76
+ }
77
+
78
+ function _bindingError(cause) {
79
+ return new ZhikeCliError(
80
+ "zhike_project_binding_invalid",
81
+ `${PROJECT_BINDING_PATH} 不是受支持的严格 Project Binding`,
82
+ { cause },
83
+ );
84
+ }