@databricks/appkit 0.59.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +11 -10
- package/dist/agents/databricks.js +1 -2
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.js +1 -2
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.js +2 -3
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js +3 -11
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/databricks-client.js +18 -22
- package/dist/cli/commands/doctor/databricks-client.js.map +1 -1
- package/dist/cli/commands/registry/add.js +410 -0
- package/dist/cli/commands/registry/add.js.map +1 -0
- package/dist/cli/commands/registry/client.js +94 -0
- package/dist/cli/commands/registry/client.js.map +1 -0
- package/dist/cli/commands/registry/config-plan.js +110 -0
- package/dist/cli/commands/registry/config-plan.js.map +1 -0
- package/dist/cli/commands/registry/config-writer.js +176 -0
- package/dist/cli/commands/registry/config-writer.js.map +1 -0
- package/dist/cli/commands/registry/constants.js +62 -0
- package/dist/cli/commands/registry/constants.js.map +1 -0
- package/dist/cli/commands/registry/env-reconcile.js +116 -0
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -0
- package/dist/cli/commands/registry/env-writer.js +198 -0
- package/dist/cli/commands/registry/env-writer.js.map +1 -0
- package/dist/cli/commands/registry/index.js +25 -0
- package/dist/cli/commands/registry/index.js.map +1 -0
- package/dist/cli/commands/registry/info.js +45 -0
- package/dist/cli/commands/registry/info.js.map +1 -0
- package/dist/cli/commands/registry/list.js +130 -0
- package/dist/cli/commands/registry/list.js.map +1 -0
- package/dist/cli/commands/registry/requirements.js +92 -0
- package/dist/cli/commands/registry/requirements.js.map +1 -0
- package/dist/cli/commands/registry/server-register.js +124 -0
- package/dist/cli/commands/registry/server-register.js.map +1 -0
- package/dist/cli/commands/registry/workspace-picker.js +353 -0
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -0
- package/dist/cli/deploy-config.js +36 -0
- package/dist/cli/deploy-config.js.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/context.js +1 -2
- package/dist/connectors/context.js.map +1 -1
- package/dist/connectors/files/client.js +1 -2
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/genie/client.js +1 -2
- package/dist/connectors/genie/client.js.map +1 -1
- package/dist/connectors/jobs/client.js +1 -2
- package/dist/connectors/jobs/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +1 -2
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/execution-context.d.ts +0 -1
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/service-context.d.ts +1 -2
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +2 -3
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/appkit.d.ts +1 -2
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -3
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +117 -80
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/manifest.js +10 -0
- package/dist/plugins/agents/mlflow.js +123 -0
- package/dist/plugins/agents/mlflow.js.map +1 -0
- package/dist/plugins/agents/schemas.js +4 -2
- package/dist/plugins/agents/schemas.js.map +1 -1
- package/dist/plugins/files/plugin.js +1 -2
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts +1 -2
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/jobs/types.d.ts +1 -2
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +1 -2
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/schemas/manifest.js +4 -4
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/index.d.ts +4 -1
- package/dist/shared/src/schemas/manifest.d.ts +6 -6
- package/dist/shared/src/workspace-client/client.js +57 -0
- package/dist/shared/src/workspace-client/client.js.map +1 -0
- package/dist/shared/src/workspace-client/errors.d.ts +2 -0
- package/dist/shared/src/workspace-client/errors.js +3 -0
- package/dist/shared/src/workspace-client/factory.d.ts +19 -0
- package/dist/shared/src/workspace-client/factory.d.ts.map +1 -0
- package/dist/shared/src/workspace-client/factory.js +24 -0
- package/dist/shared/src/workspace-client/factory.js.map +1 -0
- package/dist/shared/src/workspace-client/index.d.ts +4 -0
- package/dist/shared/src/workspace-client/legacy.d.ts +31 -0
- package/dist/shared/src/workspace-client/legacy.d.ts.map +1 -0
- package/dist/shared/src/workspace-client/legacy.js +23 -0
- package/dist/shared/src/workspace-client/legacy.js.map +1 -0
- package/dist/shared/src/workspace-client/types.d.ts +49 -0
- package/dist/shared/src/workspace-client/types.d.ts.map +1 -0
- package/dist/type-generator/index.js +1 -2
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/query-registry.js +1 -2
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/fetcher.js +1 -2
- package/dist/type-generator/serving/fetcher.js.map +1 -1
- package/dist/type-generator/serving/generator.js +1 -2
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/vite-plugin.js +1 -2
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/workspace-client/index.d.ts +4 -3
- package/dist/workspace-client/index.js +2 -2
- package/dist/workspace-client/legacy.d.ts +9 -4
- package/dist/workspace-client/legacy.d.ts.map +1 -1
- package/dist/workspace-client/legacy.js +3 -3
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts +2 -2
- package/docs/api/appkit/Function.createWorkspaceClient.md +4 -4
- package/docs/api/appkit/Interface.WorkspaceClientOptions.md +11 -0
- package/package.json +6 -4
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { collectEnvNeeds, parseEnv, reconcileEnv, serializeEnvAppend } from "./env-reconcile.js";
|
|
2
|
+
import { composeResourceId, isFlatListable, isParentContext, listParentContextStep, listWorkspaceResources, parentContextDepth } from "./workspace-picker.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { autocomplete, isCancel, select, text } from "@clack/prompts";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/commands/registry/env-writer.ts
|
|
9
|
+
/** Sentinel select value meaning "let me type the id myself". */
|
|
10
|
+
const MANUAL = "__manual__";
|
|
11
|
+
/**
|
|
12
|
+
* Max resources shown in a picker select. Real workspaces can have thousands
|
|
13
|
+
* (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we
|
|
14
|
+
* show the first N and log how many were hidden — never silently drop — and the
|
|
15
|
+
* "Enter manually" option always lets the user type an id the list omits.
|
|
16
|
+
*/
|
|
17
|
+
const PICKER_LIMIT = 25;
|
|
18
|
+
/** Reads a `.env`-style file into a map; empty when the file is absent. */
|
|
19
|
+
function readEnvFile(file) {
|
|
20
|
+
if (!fs.existsSync(file)) return {};
|
|
21
|
+
return parseEnv(fs.readFileSync(file, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
/** Appends text to a file, creating it (with a trailing newline) if needed. */
|
|
24
|
+
function appendToFile(file, text) {
|
|
25
|
+
if (text === "") return;
|
|
26
|
+
if (fs.existsSync(file)) {
|
|
27
|
+
const existing = fs.readFileSync(file, "utf-8");
|
|
28
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
29
|
+
fs.writeFileSync(file, existing + sep + text);
|
|
30
|
+
} else fs.writeFileSync(file, text);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were
|
|
34
|
+
* hidden so the truncation is never silent. The caller always appends an
|
|
35
|
+
* "Enter manually" option, so an omitted resource is still reachable.
|
|
36
|
+
*/
|
|
37
|
+
function capChoices(choices, resourceType, limit = PICKER_LIMIT) {
|
|
38
|
+
if (choices.length <= limit) return choices;
|
|
39
|
+
console.log(pc.dim(` ${choices.length} ${resourceType}s found; showing first ${limit}. Use "Enter manually" if yours is not listed.`));
|
|
40
|
+
return choices.slice(0, limit);
|
|
41
|
+
}
|
|
42
|
+
/** Free-text prompt for one env need; undefined to skip. */
|
|
43
|
+
async function promptText(need) {
|
|
44
|
+
const tag = need.required ? "required" : "optional";
|
|
45
|
+
const answer = await text({
|
|
46
|
+
message: `${need.env} (${need.resourceType}, ${tag})`,
|
|
47
|
+
placeholder: need.description ?? "leave blank to skip"
|
|
48
|
+
});
|
|
49
|
+
if (isCancel(answer)) return void 0;
|
|
50
|
+
const value = (answer ?? "").trim();
|
|
51
|
+
return value === "" ? void 0 : value;
|
|
52
|
+
}
|
|
53
|
+
/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */
|
|
54
|
+
async function selectFrom(message, choices) {
|
|
55
|
+
const picked = await select({
|
|
56
|
+
message,
|
|
57
|
+
options: [...choices, {
|
|
58
|
+
value: MANUAL,
|
|
59
|
+
label: "Enter manually / skip"
|
|
60
|
+
}]
|
|
61
|
+
});
|
|
62
|
+
if (isCancel(picked)) return null;
|
|
63
|
+
return String(picked);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Type-to-filter picker over the full choice list (no cap): the user searches
|
|
67
|
+
* by name/id as they type. Appends "Enter manually" so an omitted value is
|
|
68
|
+
* still reachable. Returns MANUAL to fall through to free-text, or null on
|
|
69
|
+
* cancel.
|
|
70
|
+
*/
|
|
71
|
+
async function autocompleteFrom(message, choices) {
|
|
72
|
+
const picked = await autocomplete({
|
|
73
|
+
message,
|
|
74
|
+
options: [...choices, {
|
|
75
|
+
value: MANUAL,
|
|
76
|
+
label: "Enter manually / skip"
|
|
77
|
+
}],
|
|
78
|
+
placeholder: "type to search…"
|
|
79
|
+
});
|
|
80
|
+
if (isCancel(picked)) return null;
|
|
81
|
+
return String(picked);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Drill-down picker for parent-context types (volume→catalog/schema,
|
|
85
|
+
* secret→scope, vector_search_index→endpoint). Walks each step, listing the
|
|
86
|
+
* next level from the prior pick. Returns the final resource id, or undefined
|
|
87
|
+
* to fall back to free-text (on cancel, empty level, or MANUAL at any step).
|
|
88
|
+
*/
|
|
89
|
+
async function pickParentContext(need, profile) {
|
|
90
|
+
const depth = parentContextDepth(need.resourceType);
|
|
91
|
+
const picks = [];
|
|
92
|
+
for (let i = 0; i < depth; i++) {
|
|
93
|
+
const step = listParentContextStep(need.resourceType, i, picks, profile);
|
|
94
|
+
if (!step || step.choices.length === 0) {
|
|
95
|
+
console.log(pc.dim(` No ${step?.key ?? need.resourceType} found — enter the id manually.`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const picked = await selectFrom(`${need.env} — pick a ${step.key}`, capChoices(step.choices, step.key));
|
|
99
|
+
if (picked === null || picked === MANUAL) return void 0;
|
|
100
|
+
picks.push(picked);
|
|
101
|
+
}
|
|
102
|
+
return composeResourceId(need.resourceType, picks);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Builds the value provider. Precedence: --env flag, then (interactive only) a
|
|
106
|
+
* workspace picker — flat select for flat-listable types, drill-down for
|
|
107
|
+
* parent-context types — else a free-text prompt. The picker degrades to
|
|
108
|
+
* free-text whenever the workspace can't be listed (no profile, offline, auth
|
|
109
|
+
* error, empty) so it never hard-fails.
|
|
110
|
+
*/
|
|
111
|
+
function makeProvider(opts) {
|
|
112
|
+
return async (need) => {
|
|
113
|
+
const fromFlag = opts.values?.[need.env];
|
|
114
|
+
if (fromFlag !== void 0) return fromFlag;
|
|
115
|
+
if (opts.nonInteractive) return void 0;
|
|
116
|
+
if (isFlatListable(need.resourceType)) {
|
|
117
|
+
const { choices, truncated, error } = await listWorkspaceResources(need.resourceType, opts.profile);
|
|
118
|
+
if (choices.length > 0) {
|
|
119
|
+
if (truncated) console.log(pc.dim(` Showing the first ${choices.length} ${need.resourceType}s; use "Enter manually" if yours isn't listed.`));
|
|
120
|
+
const picked = await autocompleteFrom(`${need.env} — search ${need.resourceType}s`, choices);
|
|
121
|
+
if (picked === null) return void 0;
|
|
122
|
+
if (picked !== MANUAL) return picked;
|
|
123
|
+
} else if (error) {
|
|
124
|
+
console.log(pc.yellow(` Couldn't list ${need.resourceType}s from the workspace (${error}).`));
|
|
125
|
+
console.log(pc.dim(" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace."));
|
|
126
|
+
} else console.log(pc.dim(` No ${need.resourceType} found in the workspace — enter an id manually.`));
|
|
127
|
+
} else if (isParentContext(need.resourceType)) {
|
|
128
|
+
const picked = await pickParentContext(need, opts.profile);
|
|
129
|
+
if (picked !== void 0) return picked;
|
|
130
|
+
}
|
|
131
|
+
return promptText(need);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Reconciles a plugin's declared resource env vars into the app's local `.env`
|
|
136
|
+
* (and mirrors variable names into `.env.example`). Never overwrites keys the
|
|
137
|
+
* user already set; skips platform-injected fields. Returns the per-var
|
|
138
|
+
* resolutions so callers can report what happened.
|
|
139
|
+
*/
|
|
140
|
+
async function syncEnv(rows, opts) {
|
|
141
|
+
const needs = collectEnvNeeds(rows);
|
|
142
|
+
if (needs.length === 0) return [];
|
|
143
|
+
const envPath = path.join(opts.cwd, ".env");
|
|
144
|
+
const examplePath = path.join(opts.cwd, ".env.example");
|
|
145
|
+
const resolutions = await reconcileEnv(needs, {
|
|
146
|
+
existing: readEnvFile(envPath),
|
|
147
|
+
provide: makeProvider(opts)
|
|
148
|
+
});
|
|
149
|
+
appendToFile(envPath, serializeEnvAppend(resolutions.filter((r) => r.status === "written" && r.value !== void 0).map((r) => ({
|
|
150
|
+
env: r.env,
|
|
151
|
+
value: r.value
|
|
152
|
+
}))));
|
|
153
|
+
const exampleExisting = readEnvFile(examplePath);
|
|
154
|
+
appendToFile(examplePath, serializeEnvAppend(needs.filter((n) => !(n.env in exampleExisting)).map((n) => ({
|
|
155
|
+
env: n.env,
|
|
156
|
+
value: ""
|
|
157
|
+
}))));
|
|
158
|
+
return resolutions;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't
|
|
162
|
+
* collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are
|
|
163
|
+
* not written to `.env` (these fields have no env var). Non-interactive mode
|
|
164
|
+
* uses `values[fieldKey]` if provided, else leaves the field unset.
|
|
165
|
+
*/
|
|
166
|
+
async function collectBindingValues(needs, opts) {
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const need of needs) {
|
|
169
|
+
const fromFlag = opts.values?.[need.fieldKey];
|
|
170
|
+
if (fromFlag !== void 0) {
|
|
171
|
+
out[need.fieldKey] = fromFlag;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (opts.nonInteractive) continue;
|
|
175
|
+
const answer = await text({
|
|
176
|
+
message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,
|
|
177
|
+
placeholder: need.description ?? "leave blank to set before deploy"
|
|
178
|
+
});
|
|
179
|
+
if (isCancel(answer)) continue;
|
|
180
|
+
const value = (answer ?? "").trim();
|
|
181
|
+
if (value !== "") out[need.fieldKey] = value;
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** Prints a concise summary of what env reconciliation did. */
|
|
186
|
+
function reportEnvResolutions(resolutions) {
|
|
187
|
+
if (resolutions.length === 0) return;
|
|
188
|
+
const written = resolutions.filter((r) => r.status === "written");
|
|
189
|
+
const already = resolutions.filter((r) => r.status === "already-set");
|
|
190
|
+
const skipped = resolutions.filter((r) => r.status === "skipped");
|
|
191
|
+
if (written.length > 0) console.log(`${pc.green("Wrote to .env:")} ${written.map((r) => r.env).join(", ")}`);
|
|
192
|
+
if (already.length > 0) console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(", ")}`));
|
|
193
|
+
if (skipped.length > 0) console.log(`${pc.yellow("Left unset (set before deploy):")} ${skipped.map((r) => r.env).join(", ")}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
export { collectBindingValues, reportEnvResolutions, syncEnv };
|
|
198
|
+
//# sourceMappingURL=env-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-writer.js","names":[],"sources":["../../../../src/cli/commands/registry/env-writer.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { autocomplete, isCancel, select, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport type { BindingValueNeed } from \"./config-plan\";\nimport {\n collectEnvNeeds,\n type EnvNeed,\n type EnvResolution,\n parseEnv,\n reconcileEnv,\n serializeEnvAppend,\n type ValueProvider,\n} from \"./env-reconcile\";\nimport type { ResourceRequirementRow } from \"./requirements\";\nimport {\n composeResourceId,\n isFlatListable,\n isParentContext,\n listParentContextStep,\n listWorkspaceResources,\n parentContextDepth,\n} from \"./workspace-picker\";\n\nexport interface EnvSyncOptions {\n /** Directory holding `.env` / `.env.example` (the app root). */\n cwd: string;\n /** true = never prompt (agent/CI). Uses flag values or leaves unset. */\n nonInteractive: boolean;\n /** Pre-supplied env values from flags, e.g. { DATABRICKS_WAREHOUSE_ID: \"abc\" }. */\n values?: Record<string, string>;\n /** Databricks profile for the workspace picker (else the CLI default). */\n profile?: string;\n}\n\n/** Sentinel select value meaning \"let me type the id myself\". */\nconst MANUAL = \"__manual__\";\n\n/**\n * Max resources shown in a picker select. Real workspaces can have thousands\n * (e.g. 5000+ SQL warehouses); an unbounded select is unusable. Beyond this we\n * show the first N and log how many were hidden — never silently drop — and the\n * \"Enter manually\" option always lets the user type an id the list omits.\n */\nconst PICKER_LIMIT = 25;\n\n/** Reads a `.env`-style file into a map; empty when the file is absent. */\nfunction readEnvFile(file: string): Record<string, string> {\n if (!fs.existsSync(file)) return {};\n return parseEnv(fs.readFileSync(file, \"utf-8\"));\n}\n\n/** Appends text to a file, creating it (with a trailing newline) if needed. */\nfunction appendToFile(file: string, text: string): void {\n if (text === \"\") return;\n if (fs.existsSync(file)) {\n const existing = fs.readFileSync(file, \"utf-8\");\n const sep = existing.length > 0 && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\";\n fs.writeFileSync(file, existing + sep + text);\n } else {\n fs.writeFileSync(file, text);\n }\n}\n\n/**\n * Caps a choice list to {@link PICKER_LIMIT} for display, logging how many were\n * hidden so the truncation is never silent. The caller always appends an\n * \"Enter manually\" option, so an omitted resource is still reachable.\n */\nexport function capChoices<T>(\n choices: T[],\n resourceType: string,\n limit = PICKER_LIMIT,\n): T[] {\n if (choices.length <= limit) return choices;\n console.log(\n pc.dim(\n ` ${choices.length} ${resourceType}s found; showing first ${limit}. ` +\n 'Use \"Enter manually\" if yours is not listed.',\n ),\n );\n return choices.slice(0, limit);\n}\n\n/** Free-text prompt for one env need; undefined to skip. */\nasync function promptText(need: EnvNeed): Promise<string | undefined> {\n const tag = need.required ? \"required\" : \"optional\";\n const answer = await text({\n message: `${need.env} (${need.resourceType}, ${tag})`,\n placeholder: need.description ?? \"leave blank to skip\",\n });\n if (isCancel(answer)) return undefined;\n const value = (answer ?? \"\").trim();\n return value === \"\" ? undefined : value;\n}\n\n/** Presents one workspace list as a select; MANUAL/cancel handled by caller. */\nasync function selectFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await select({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Type-to-filter picker over the full choice list (no cap): the user searches\n * by name/id as they type. Appends \"Enter manually\" so an omitted value is\n * still reachable. Returns MANUAL to fall through to free-text, or null on\n * cancel.\n */\nasync function autocompleteFrom(\n message: string,\n choices: { value: string; label: string }[],\n): Promise<string | typeof MANUAL | null> {\n const picked = await autocomplete({\n message,\n options: [...choices, { value: MANUAL, label: \"Enter manually / skip\" }],\n placeholder: \"type to search…\",\n });\n if (isCancel(picked)) return null;\n return String(picked) as string | typeof MANUAL;\n}\n\n/**\n * Drill-down picker for parent-context types (volume→catalog/schema,\n * secret→scope, vector_search_index→endpoint). Walks each step, listing the\n * next level from the prior pick. Returns the final resource id, or undefined\n * to fall back to free-text (on cancel, empty level, or MANUAL at any step).\n */\nasync function pickParentContext(\n need: EnvNeed,\n profile: string | undefined,\n): Promise<string | undefined> {\n const depth = parentContextDepth(need.resourceType);\n const picks: string[] = [];\n for (let i = 0; i < depth; i++) {\n const step = listParentContextStep(need.resourceType, i, picks, profile);\n if (!step || step.choices.length === 0) {\n console.log(\n pc.dim(\n ` No ${step?.key ?? need.resourceType} found — enter the id manually.`,\n ),\n );\n return undefined;\n }\n const picked = await selectFrom(\n `${need.env} — pick a ${step.key}`,\n capChoices(step.choices, step.key),\n );\n if (picked === null || picked === MANUAL) return undefined;\n picks.push(picked);\n }\n // Compose the id from the picks: most types end on a self-qualified id, but a\n // secret needs both scope and key (scope/key).\n return composeResourceId(need.resourceType, picks);\n}\n\n/**\n * Builds the value provider. Precedence: --env flag, then (interactive only) a\n * workspace picker — flat select for flat-listable types, drill-down for\n * parent-context types — else a free-text prompt. The picker degrades to\n * free-text whenever the workspace can't be listed (no profile, offline, auth\n * error, empty) so it never hard-fails.\n */\nfunction makeProvider(opts: EnvSyncOptions): ValueProvider {\n return async (need: EnvNeed) => {\n const fromFlag = opts.values?.[need.env];\n if (fromFlag !== undefined) return fromFlag;\n if (opts.nonInteractive) return undefined;\n\n if (isFlatListable(need.resourceType)) {\n const { choices, truncated, error } = await listWorkspaceResources(\n need.resourceType,\n opts.profile,\n );\n if (choices.length > 0) {\n if (truncated) {\n console.log(\n pc.dim(\n ` Showing the first ${choices.length} ${need.resourceType}s; use \"Enter manually\" if yours isn't listed.`,\n ),\n );\n }\n const picked = await autocompleteFrom(\n `${need.env} — search ${need.resourceType}s`,\n choices,\n );\n if (picked === null) return undefined;\n if (picked !== MANUAL) return picked;\n // fall through to free-text\n } else if (error) {\n // Listing failed (usually auth/profile) — say so, don't pretend the\n // workspace is empty, and point at the fix.\n console.log(\n pc.yellow(\n ` Couldn't list ${need.resourceType}s from the workspace (${error}).`,\n ),\n );\n console.log(\n pc.dim(\n \" Enter an id manually, or re-run with --profile <name> (or set DATABRICKS_CONFIG_PROFILE) so the picker can reach the workspace.\",\n ),\n );\n } else {\n console.log(\n pc.dim(\n ` No ${need.resourceType} found in the workspace — enter an id manually.`,\n ),\n );\n }\n } else if (isParentContext(need.resourceType)) {\n const picked = await pickParentContext(need, opts.profile);\n if (picked !== undefined) return picked;\n // fall through to free-text\n }\n\n return promptText(need);\n };\n}\n\n/**\n * Reconciles a plugin's declared resource env vars into the app's local `.env`\n * (and mirrors variable names into `.env.example`). Never overwrites keys the\n * user already set; skips platform-injected fields. Returns the per-var\n * resolutions so callers can report what happened.\n */\nexport async function syncEnv(\n rows: ResourceRequirementRow[],\n opts: EnvSyncOptions,\n): Promise<EnvResolution[]> {\n const needs = collectEnvNeeds(rows);\n if (needs.length === 0) return [];\n\n const envPath = path.join(opts.cwd, \".env\");\n const examplePath = path.join(opts.cwd, \".env.example\");\n const existing = readEnvFile(envPath);\n\n const resolutions = await reconcileEnv(needs, {\n existing,\n provide: makeProvider(opts),\n });\n\n const written = resolutions.filter(\n (r): r is EnvResolution & { value: string } =>\n r.status === \"written\" && r.value !== undefined,\n );\n appendToFile(\n envPath,\n serializeEnvAppend(written.map((r) => ({ env: r.env, value: r.value }))),\n );\n\n // .env.example carries the variable names (no secret values), and only for\n // vars not already documented there.\n const exampleExisting = readEnvFile(examplePath);\n const newExampleKeys = needs.filter((n) => !(n.env in exampleExisting));\n appendToFile(\n examplePath,\n serializeEnvAppend(newExampleKeys.map((n) => ({ env: n.env, value: \"\" }))),\n );\n\n return resolutions;\n}\n\n/**\n * Prompts for the {@link BindingValueNeed}s that `.env` reconciliation can't\n * collect, returning a `fieldKey -> value` map for buildConfigPlan. Values are\n * not written to `.env` (these fields have no env var). Non-interactive mode\n * uses `values[fieldKey]` if provided, else leaves the field unset.\n */\nexport async function collectBindingValues(\n needs: BindingValueNeed[],\n opts: EnvSyncOptions,\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const need of needs) {\n const fromFlag = opts.values?.[need.fieldKey];\n if (fromFlag !== undefined) {\n out[need.fieldKey] = fromFlag;\n continue;\n }\n if (opts.nonInteractive) continue;\n const answer = await text({\n message: `${need.fieldKey} (${need.resourceType}) — required for databricks.yml`,\n placeholder: need.description ?? \"leave blank to set before deploy\",\n });\n if (isCancel(answer)) continue;\n const value = (answer ?? \"\").trim();\n if (value !== \"\") out[need.fieldKey] = value;\n }\n return out;\n}\n\n/** Prints a concise summary of what env reconciliation did. */\nexport function reportEnvResolutions(resolutions: EnvResolution[]): void {\n if (resolutions.length === 0) return;\n const written = resolutions.filter((r) => r.status === \"written\");\n const already = resolutions.filter((r) => r.status === \"already-set\");\n const skipped = resolutions.filter((r) => r.status === \"skipped\");\n\n if (written.length > 0) {\n console.log(\n `${pc.green(\"Wrote to .env:\")} ${written.map((r) => r.env).join(\", \")}`,\n );\n }\n if (already.length > 0) {\n console.log(pc.dim(`Already set: ${already.map((r) => r.env).join(\", \")}`));\n }\n if (skipped.length > 0) {\n console.log(\n `${pc.yellow(\"Left unset (set before deploy):\")} ${skipped\n .map((r) => r.env)\n .join(\", \")}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { registryInfoCommand } from "./info.js";
|
|
2
|
+
import { registryListCommand, registrySearchCommand } from "./list.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/commands/registry/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* Parent command for AppKit component registry operations.
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* - list: Enumerate items available in the registry
|
|
10
|
+
* - search: Find items by name, description, type, or keyword
|
|
11
|
+
* - info: Show an item's resource requirements and dependencies
|
|
12
|
+
*
|
|
13
|
+
* Note: `appkit add <item>` is exposed as a top-level command (see add.ts)
|
|
14
|
+
* since it is the primary entry point for consumers.
|
|
15
|
+
*/
|
|
16
|
+
const registryCommand = new Command("registry").description("AppKit component registry commands").addCommand(registryListCommand).addCommand(registrySearchCommand).addCommand(registryInfoCommand).addHelpText("after", `
|
|
17
|
+
Examples:
|
|
18
|
+
$ appkit registry list
|
|
19
|
+
$ appkit registry search kpi dashboard
|
|
20
|
+
$ appkit registry info analytics
|
|
21
|
+
$ appkit add metric-card`);
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { registryCommand };
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/registry/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { registryInfoCommand } from \"./info\";\nimport { registryListCommand, registrySearchCommand } from \"./list\";\n\n/**\n * Parent command for AppKit component registry operations.\n * Subcommands:\n * - list: Enumerate items available in the registry\n * - search: Find items by name, description, type, or keyword\n * - info: Show an item's resource requirements and dependencies\n *\n * Note: `appkit add <item>` is exposed as a top-level command (see add.ts)\n * since it is the primary entry point for consumers.\n */\nexport const registryCommand = new Command(\"registry\")\n .description(\"AppKit component registry commands\")\n .addCommand(registryListCommand)\n .addCommand(registrySearchCommand)\n .addCommand(registryInfoCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry list\n $ appkit registry search kpi dashboard\n $ appkit registry info analytics\n $ appkit add metric-card`,\n );\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,kBAAkB,IAAI,QAAQ,WAAW,CACnD,YAAY,qCAAqC,CACjD,WAAW,oBAAoB,CAC/B,WAAW,sBAAsB,CACjC,WAAW,oBAAoB,CAC/B,YACC,SACA;;;;;4BAMD"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { resolveToken } from "./constants.js";
|
|
2
|
+
import { fetchRegistryItem, isValidItemName, stripNamespace } from "./client.js";
|
|
3
|
+
import { extractRequirements, renderRequirements } from "./requirements.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/commands/registry/info.ts
|
|
9
|
+
async function runInfo(ref, opts) {
|
|
10
|
+
const token = resolveToken();
|
|
11
|
+
const name = stripNamespace(ref);
|
|
12
|
+
if (!isValidItemName(name)) {
|
|
13
|
+
console.error(`Invalid registry item name: ${JSON.stringify(ref)}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const item = await fetchRegistryItem(name, token);
|
|
17
|
+
const rows = extractRequirements(item);
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify({
|
|
20
|
+
name: item.name,
|
|
21
|
+
type: item.type,
|
|
22
|
+
dependencies: item.dependencies ?? [],
|
|
23
|
+
registryDependencies: item.registryDependencies ?? [],
|
|
24
|
+
resources: rows
|
|
25
|
+
}, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(pc.bold(item.name));
|
|
29
|
+
const deps = item.dependencies ?? [];
|
|
30
|
+
const registryDeps = item.registryDependencies ?? [];
|
|
31
|
+
if (deps.length > 0) console.log(pc.dim(` npm dependencies: ${deps.join(", ")}`));
|
|
32
|
+
if (registryDeps.length > 0) console.log(pc.dim(` registry dependencies: ${registryDeps.join(", ")}`));
|
|
33
|
+
console.log(`\n${renderRequirements(item, rows)}`);
|
|
34
|
+
}
|
|
35
|
+
const registryInfoCommand = new Command("info").description("Show an item's resource requirements and dependencies").argument("<item>", "Registry item name, e.g. analytics").option("--json", "Output as JSON").addHelpText("after", `
|
|
36
|
+
Examples:
|
|
37
|
+
$ appkit registry info analytics
|
|
38
|
+
$ appkit registry info @databricks-appkit/analytics --json`).action((ref, opts) => runInfo(ref, opts).catch((err) => {
|
|
39
|
+
console.error(err);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { registryInfoCommand };
|
|
45
|
+
//# sourceMappingURL=info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"info.js","names":[],"sources":["../../../../src/cli/commands/registry/info.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { fetchRegistryItem, isValidItemName, stripNamespace } from \"./client\";\nimport { resolveToken } from \"./constants\";\nimport { extractRequirements, renderRequirements } from \"./requirements\";\n\nasync function runInfo(ref: string, opts: { json?: boolean }): Promise<void> {\n const token = resolveToken();\n // Validate before fetching: the name is interpolated into the fetch path, so\n // reject non-slug refs (matches the `add` guard) rather than let `/` or `..`\n // redirect the request to another path in the repo.\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n console.error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n process.exit(1);\n }\n const item = await fetchRegistryItem(name, token);\n const rows = extractRequirements(item);\n\n if (opts.json) {\n console.log(\n JSON.stringify(\n {\n name: item.name,\n type: item.type,\n dependencies: item.dependencies ?? [],\n registryDependencies: item.registryDependencies ?? [],\n resources: rows,\n },\n null,\n 2,\n ),\n );\n return;\n }\n\n console.log(pc.bold(item.name));\n const deps = item.dependencies ?? [];\n const registryDeps = item.registryDependencies ?? [];\n if (deps.length > 0) {\n console.log(pc.dim(` npm dependencies: ${deps.join(\", \")}`));\n }\n if (registryDeps.length > 0) {\n console.log(pc.dim(` registry dependencies: ${registryDeps.join(\", \")}`));\n }\n console.log(`\\n${renderRequirements(item, rows)}`);\n}\n\nexport const registryInfoCommand = new Command(\"info\")\n .description(\"Show an item's resource requirements and dependencies\")\n .argument(\"<item>\", \"Registry item name, e.g. analytics\")\n .option(\"--json\", \"Output as JSON\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry info analytics\n $ appkit registry info @databricks-appkit/analytics --json`,\n )\n .action((ref: string, opts: { json?: boolean }) =>\n runInfo(ref, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":";;;;;;;;AAOA,eAAe,QAAQ,KAAa,MAAyC;CAC3E,MAAM,QAAQ,cAAc;CAI5B,MAAM,OAAO,eAAe,IAAI;AAChC,KAAI,CAAC,gBAAgB,KAAK,EAAE;AAC1B,UAAQ,MAAM,+BAA+B,KAAK,UAAU,IAAI,GAAG;AACnE,UAAQ,KAAK,EAAE;;CAEjB,MAAM,OAAO,MAAM,kBAAkB,MAAM,MAAM;CACjD,MAAM,OAAO,oBAAoB,KAAK;AAEtC,KAAI,KAAK,MAAM;AACb,UAAQ,IACN,KAAK,UACH;GACE,MAAM,KAAK;GACX,MAAM,KAAK;GACX,cAAc,KAAK,gBAAgB,EAAE;GACrC,sBAAsB,KAAK,wBAAwB,EAAE;GACrD,WAAW;GACZ,EACD,MACA,EACD,CACF;AACD;;AAGF,SAAQ,IAAI,GAAG,KAAK,KAAK,KAAK,CAAC;CAC/B,MAAM,OAAO,KAAK,gBAAgB,EAAE;CACpC,MAAM,eAAe,KAAK,wBAAwB,EAAE;AACpD,KAAI,KAAK,SAAS,EAChB,SAAQ,IAAI,GAAG,IAAI,uBAAuB,KAAK,KAAK,KAAK,GAAG,CAAC;AAE/D,KAAI,aAAa,SAAS,EACxB,SAAQ,IAAI,GAAG,IAAI,4BAA4B,aAAa,KAAK,KAAK,GAAG,CAAC;AAE5E,SAAQ,IAAI,KAAK,mBAAmB,MAAM,KAAK,GAAG;;AAGpD,MAAa,sBAAsB,IAAI,QAAQ,OAAO,CACnD,YAAY,wDAAwD,CACpE,SAAS,UAAU,qCAAqC,CACxD,OAAO,UAAU,iBAAiB,CAClC,YACC,SACA;;;8DAID,CACA,QAAQ,KAAa,SACpB,QAAQ,KAAK,KAAK,CAAC,OAAO,QAAQ;AAChC,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf,CACH"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { REGISTRY_INDEX_API_URL, REGISTRY_INDEX_URL, REGISTRY_REPO, resolveToken } from "./constants.js";
|
|
2
|
+
import { registryAuthHeaders } from "./client.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/commands/registry/list.ts
|
|
8
|
+
function isVerified(item) {
|
|
9
|
+
return item.meta?.verified === true;
|
|
10
|
+
}
|
|
11
|
+
/** Free-text haystack for search matching. */
|
|
12
|
+
function searchHaystack(item) {
|
|
13
|
+
return [
|
|
14
|
+
item.name,
|
|
15
|
+
item.title ?? "",
|
|
16
|
+
item.description ?? "",
|
|
17
|
+
itemKind(item),
|
|
18
|
+
...item.categories ?? []
|
|
19
|
+
].join(" ").toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
/** True if every whitespace-separated term in `query` appears in the item. */
|
|
22
|
+
function matchesQuery(item, query) {
|
|
23
|
+
const haystack = searchHaystack(item);
|
|
24
|
+
return query.toLowerCase().split(/\s+/).filter(Boolean).every((term) => haystack.includes(term));
|
|
25
|
+
}
|
|
26
|
+
/** A friendly kind for the TYPE column: plugin, component, hook, theme, … */
|
|
27
|
+
function itemKind(item) {
|
|
28
|
+
if ((item.files ?? []).some((f) => f.target?.endsWith("manifest.json") || f.path?.endsWith("manifest.json"))) return "plugin";
|
|
29
|
+
switch (item.type) {
|
|
30
|
+
case "registry:component":
|
|
31
|
+
case "registry:block": return "component";
|
|
32
|
+
case "registry:hook": return "hook";
|
|
33
|
+
case "registry:lib": return "lib";
|
|
34
|
+
case "registry:theme": return "theme";
|
|
35
|
+
case "registry:ui": return "ui";
|
|
36
|
+
case "registry:page": return "page";
|
|
37
|
+
default: return item.type?.replace(/^registry:/, "") || "item";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const KIND_COLOR = {
|
|
41
|
+
plugin: pc.magenta,
|
|
42
|
+
component: pc.blue,
|
|
43
|
+
hook: pc.cyan,
|
|
44
|
+
theme: pc.yellow,
|
|
45
|
+
lib: pc.green
|
|
46
|
+
};
|
|
47
|
+
function printTable(items) {
|
|
48
|
+
if (items.length === 0) {
|
|
49
|
+
console.log(pc.dim("No items found in the registry."));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const kinds = items.map(itemKind);
|
|
53
|
+
const maxName = Math.max(4, ...items.map((i) => i.name.length));
|
|
54
|
+
const maxKind = Math.max(4, ...kinds.map((k) => k.length));
|
|
55
|
+
const header = `${"NAME".padEnd(maxName)} ${"TYPE".padEnd(maxKind)} VERIFIED DESCRIPTION`;
|
|
56
|
+
console.log(pc.bold(header));
|
|
57
|
+
console.log(pc.dim("─".repeat(header.length)));
|
|
58
|
+
for (const [i, item] of items.entries()) {
|
|
59
|
+
const verified = isVerified(item);
|
|
60
|
+
const kind = kinds[i];
|
|
61
|
+
const colorKind = KIND_COLOR[kind] ?? pc.white;
|
|
62
|
+
const name = pc.cyan(item.name.padEnd(maxName));
|
|
63
|
+
const kindCell = colorKind(kind.padEnd(maxKind));
|
|
64
|
+
const mark = verified ? pc.green("✓".padEnd(8)) : " ".repeat(8);
|
|
65
|
+
const desc = item.description ?? item.title ?? "";
|
|
66
|
+
console.log(`${name} ${kindCell} ${mark} ${verified ? desc : pc.dim(desc)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Fetches the registry index (token-aware), or exits with a helpful message. */
|
|
70
|
+
async function fetchIndex() {
|
|
71
|
+
const token = resolveToken();
|
|
72
|
+
const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;
|
|
73
|
+
let res;
|
|
74
|
+
try {
|
|
75
|
+
res = await fetch(url, { headers: registryAuthHeaders(token) });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(pc.red(`Failed to reach the registry at ${url}`));
|
|
78
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
if (res.status === 404 || res.status === 401 || res.status === 403) {
|
|
82
|
+
console.error(pc.red(`Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`));
|
|
83
|
+
if (!token) console.error(" If the repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
return (await res.json()).items ?? [];
|
|
91
|
+
}
|
|
92
|
+
function output(items, opts) {
|
|
93
|
+
if (opts.json) console.log(JSON.stringify(items.map((i) => ({
|
|
94
|
+
...i,
|
|
95
|
+
kind: itemKind(i),
|
|
96
|
+
verified: isVerified(i)
|
|
97
|
+
})), null, 2));
|
|
98
|
+
else printTable(items);
|
|
99
|
+
}
|
|
100
|
+
async function runList(opts) {
|
|
101
|
+
let items = await fetchIndex();
|
|
102
|
+
if (opts.verified) items = items.filter(isVerified);
|
|
103
|
+
output(items, opts);
|
|
104
|
+
}
|
|
105
|
+
async function runSearch(query, opts) {
|
|
106
|
+
let items = await fetchIndex();
|
|
107
|
+
items = items.filter((i) => matchesQuery(i, query));
|
|
108
|
+
if (opts.verified) items = items.filter(isVerified);
|
|
109
|
+
if (items.length === 0 && !opts.json) {
|
|
110
|
+
console.log(pc.dim(`No items match "${query}".`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
output(items, opts);
|
|
114
|
+
}
|
|
115
|
+
const registryListCommand = new Command("list").description("List items available in the AppKit registry").option("--json", "Output as JSON").option("--verified", "Show only verified items").action((opts) => runList(opts).catch((err) => {
|
|
116
|
+
console.error(err);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}));
|
|
119
|
+
const registrySearchCommand = new Command("search").description("Search registry items by name, description, type, or keyword").argument("<query...>", "Search terms (all must match)").option("--json", "Output as JSON").option("--verified", "Show only verified items").addHelpText("after", `
|
|
120
|
+
Examples:
|
|
121
|
+
$ appkit registry search chart
|
|
122
|
+
$ appkit registry search kpi dashboard
|
|
123
|
+
$ appkit registry search plugin --json`).action((query, opts) => runSearch(query.join(" "), opts).catch((err) => {
|
|
124
|
+
console.error(err);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
export { registryListCommand, registrySearchCommand };
|
|
130
|
+
//# sourceMappingURL=list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.js","names":[],"sources":["../../../../src/cli/commands/registry/list.ts"],"sourcesContent":["import process from \"node:process\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { registryAuthHeaders } from \"./client\";\nimport {\n REGISTRY_INDEX_API_URL,\n REGISTRY_INDEX_URL,\n REGISTRY_REPO,\n resolveToken,\n} from \"./constants\";\n\ninterface RegistryIndexItem {\n name: string;\n type?: string;\n title?: string;\n description?: string;\n categories?: string[];\n meta?: { verified?: boolean };\n files?: Array<{ path?: string; target?: string }>;\n}\n\nfunction isVerified(item: RegistryIndexItem): boolean {\n return item.meta?.verified === true;\n}\n\n/** Free-text haystack for search matching. */\nfunction searchHaystack(item: RegistryIndexItem): string {\n return [\n item.name,\n item.title ?? \"\",\n item.description ?? \"\",\n itemKind(item),\n ...(item.categories ?? []),\n ]\n .join(\" \")\n .toLowerCase();\n}\n\n/** True if every whitespace-separated term in `query` appears in the item. */\nfunction matchesQuery(item: RegistryIndexItem, query: string): boolean {\n const haystack = searchHaystack(item);\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((term) => haystack.includes(term));\n}\n\n/** A friendly kind for the TYPE column: plugin, component, hook, theme, … */\nfunction itemKind(item: RegistryIndexItem): string {\n const hasManifest = (item.files ?? []).some(\n (f) =>\n f.target?.endsWith(\"manifest.json\") || f.path?.endsWith(\"manifest.json\"),\n );\n if (hasManifest) return \"plugin\";\n switch (item.type) {\n case \"registry:component\":\n case \"registry:block\":\n return \"component\";\n case \"registry:hook\":\n return \"hook\";\n case \"registry:lib\":\n return \"lib\";\n case \"registry:theme\":\n return \"theme\";\n case \"registry:ui\":\n return \"ui\";\n case \"registry:page\":\n return \"page\";\n default:\n return item.type?.replace(/^registry:/, \"\") || \"item\";\n }\n}\n\nconst KIND_COLOR: Record<string, (s: string) => string> = {\n plugin: pc.magenta,\n component: pc.blue,\n hook: pc.cyan,\n theme: pc.yellow,\n lib: pc.green,\n};\n\nfunction printTable(items: RegistryIndexItem[]): void {\n if (items.length === 0) {\n console.log(pc.dim(\"No items found in the registry.\"));\n return;\n }\n const kinds = items.map(itemKind);\n const maxName = Math.max(4, ...items.map((i) => i.name.length));\n const maxKind = Math.max(4, ...kinds.map((k) => k.length));\n const verifiedCol = \"VERIFIED\";\n // Pad plain text before coloring so ANSI codes don't break alignment.\n const header = `${\"NAME\".padEnd(maxName)} ${\"TYPE\".padEnd(maxKind)} ${verifiedCol} DESCRIPTION`;\n console.log(pc.bold(header));\n console.log(pc.dim(\"─\".repeat(header.length)));\n for (const [i, item] of items.entries()) {\n const verified = isVerified(item);\n const kind = kinds[i];\n const colorKind = KIND_COLOR[kind] ?? pc.white;\n const name = pc.cyan(item.name.padEnd(maxName));\n const kindCell = colorKind(kind.padEnd(maxKind));\n const mark = verified\n ? pc.green(\"✓\".padEnd(verifiedCol.length))\n : \" \".repeat(verifiedCol.length);\n const desc = item.description ?? item.title ?? \"\";\n console.log(\n `${name} ${kindCell} ${mark} ${verified ? desc : pc.dim(desc)}`,\n );\n }\n}\n\n/** Fetches the registry index (token-aware), or exits with a helpful message. */\nasync function fetchIndex(): Promise<RegistryIndexItem[]> {\n const token = resolveToken();\n const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL;\n\n let res: Awaited<ReturnType<typeof fetch>>;\n try {\n res = await fetch(url, { headers: registryAuthHeaders(token) });\n } catch (err) {\n console.error(pc.red(`Failed to reach the registry at ${url}`));\n console.error(` ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n if (res.status === 404 || res.status === 401 || res.status === 403) {\n console.error(\n pc.red(\n `Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`,\n ),\n );\n if (!token) {\n console.error(\n \" If the repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.\",\n );\n }\n process.exit(1);\n }\n if (!res.ok) {\n console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`));\n process.exit(1);\n }\n\n const data = (await res.json()) as { items?: RegistryIndexItem[] };\n return data.items ?? [];\n}\n\nfunction output(items: RegistryIndexItem[], opts: { json?: boolean }): void {\n if (opts.json) {\n // Surface `kind` + `verified` as top-level fields for easy scripting.\n console.log(\n JSON.stringify(\n items.map((i) => ({\n ...i,\n kind: itemKind(i),\n verified: isVerified(i),\n })),\n null,\n 2,\n ),\n );\n } else {\n printTable(items);\n }\n}\n\nasync function runList(opts: {\n json?: boolean;\n verified?: boolean;\n}): Promise<void> {\n let items = await fetchIndex();\n if (opts.verified) items = items.filter(isVerified);\n output(items, opts);\n}\n\nasync function runSearch(\n query: string,\n opts: { json?: boolean; verified?: boolean },\n): Promise<void> {\n let items = await fetchIndex();\n items = items.filter((i) => matchesQuery(i, query));\n if (opts.verified) items = items.filter(isVerified);\n if (items.length === 0 && !opts.json) {\n console.log(pc.dim(`No items match \"${query}\".`));\n return;\n }\n output(items, opts);\n}\n\nexport const registryListCommand = new Command(\"list\")\n .description(\"List items available in the AppKit registry\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .action((opts: { json?: boolean; verified?: boolean }) =>\n runList(opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n\nexport const registrySearchCommand = new Command(\"search\")\n .description(\"Search registry items by name, description, type, or keyword\")\n .argument(\"<query...>\", \"Search terms (all must match)\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verified\", \"Show only verified items\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit registry search chart\n $ appkit registry search kpi dashboard\n $ appkit registry search plugin --json`,\n )\n .action((query: string[], opts: { json?: boolean; verified?: boolean }) =>\n runSearch(query.join(\" \"), opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { computeOriginFromField } from "../../../schemas/manifest.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/commands/registry/requirements.ts
|
|
6
|
+
function toFields(fields) {
|
|
7
|
+
return Object.entries(fields ?? {}).map(([key, f]) => ({
|
|
8
|
+
key,
|
|
9
|
+
env: f.env,
|
|
10
|
+
origin: f.origin,
|
|
11
|
+
description: f.description,
|
|
12
|
+
value: f.value,
|
|
13
|
+
resolve: f.resolve,
|
|
14
|
+
localOnly: f.localOnly
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function toRows(resources, required) {
|
|
18
|
+
return (resources ?? []).map((r) => ({
|
|
19
|
+
type: r.type ?? "unknown",
|
|
20
|
+
resourceKey: r.resourceKey,
|
|
21
|
+
permission: r.permission,
|
|
22
|
+
required,
|
|
23
|
+
description: r.description,
|
|
24
|
+
fields: toFields(r.fields)
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
/** The manifest.json file shipped by a plugin item, or null for UI items. */
|
|
28
|
+
function findManifest(item) {
|
|
29
|
+
const file = (item.files ?? []).find((f) => path.basename(f.target ?? f.path) === "manifest.json");
|
|
30
|
+
if (!file) return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(file.content);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extracts a plugin item's declared resource requirements (required first,
|
|
39
|
+
* then optional). Returns an empty array for UI items or plugins that declare
|
|
40
|
+
* no resources.
|
|
41
|
+
*/
|
|
42
|
+
function extractRequirements(item) {
|
|
43
|
+
const manifest = findManifest(item);
|
|
44
|
+
if (!manifest) return [];
|
|
45
|
+
return [...toRows(manifest.resources?.required, true), ...toRows(manifest.resources?.optional, false)];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Renders the resource requirements for an item as human-readable lines.
|
|
49
|
+
* Returns a single "no resources" line when there are none, so callers can
|
|
50
|
+
* print unconditionally.
|
|
51
|
+
*/
|
|
52
|
+
function renderRequirements(item, rows = extractRequirements(item)) {
|
|
53
|
+
if (rows.length === 0) return pc.dim(`${item.name}: no resource requirements.`);
|
|
54
|
+
const lines = [pc.bold(`Resources required by ${item.name}:`)];
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const tag = row.required ? pc.yellow("required") : pc.dim("optional");
|
|
57
|
+
const perm = row.permission ? pc.dim(` [${row.permission}]`) : "";
|
|
58
|
+
lines.push(` ${pc.cyan(row.type)} (${tag})${perm}`);
|
|
59
|
+
if (row.description) lines.push(` ${pc.dim(row.description)}`);
|
|
60
|
+
for (const field of row.fields) {
|
|
61
|
+
if (!field.env) continue;
|
|
62
|
+
const origin = field.origin ? pc.dim(` (${field.origin})`) : "";
|
|
63
|
+
lines.push(` - ${field.env}${origin}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A syntactically valid environment variable name. Field `env` names come from
|
|
70
|
+
* an untrusted manifest and are written into `.env` as `NAME=value` (a newline
|
|
71
|
+
* in the name would inject a second line — e.g. `PORT=x\nDATABRICKS_HOST=…` →
|
|
72
|
+
* credential exfil) and emitted into `app.yaml`. Anything that isn't a plain
|
|
73
|
+
* env identifier is dropped before it reaches those sinks.
|
|
74
|
+
*/
|
|
75
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
76
|
+
/** True when `name` is a safe, well-formed environment variable name. */
|
|
77
|
+
function isValidEnvName(name) {
|
|
78
|
+
return ENV_NAME.test(name);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Effective origin of a field: trust a synced manifest's stamped `origin`, else
|
|
82
|
+
* derive it from the field shape via the same rule `plugin sync` uses, so an
|
|
83
|
+
* authored manifest (no `origin`) still classifies correctly.
|
|
84
|
+
*/
|
|
85
|
+
function fieldOrigin(field) {
|
|
86
|
+
if (field.origin) return field.origin;
|
|
87
|
+
return computeOriginFromField(field);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { extractRequirements, fieldOrigin, isValidEnvName, renderRequirements };
|
|
92
|
+
//# sourceMappingURL=requirements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requirements.js","names":[],"sources":["../../../../src/cli/commands/registry/requirements.ts"],"sourcesContent":["import path from \"node:path\";\nimport pc from \"picocolors\";\nimport { computeOriginFromField } from \"../../../schemas/manifest\";\nimport type { RegistryItem } from \"./client\";\n\n/**\n * A single resource field as declared in a plugin manifest. `origin` is the\n * computed classifier written by `plugin sync` (platform/static/cli/user) that\n * says how the value reaches the running app.\n */\nexport interface RequirementField {\n key: string;\n env?: string;\n /**\n * Computed classifier written by `plugin sync` — only present in a synced\n * manifest. Authored manifests omit it, so consumers must derive the\n * effective origin via {@link fieldOrigin} rather than reading this directly.\n */\n origin?: string;\n description?: string;\n /** Default literal value (static origin); pre-filled without prompting. */\n value?: string;\n /** Resolver key (cli origin), e.g. \"postgres:host\". */\n resolve?: string;\n /** Local-dev-only field; platform-injected at deploy time. */\n localOnly?: boolean;\n}\n\n/** A resource requirement flattened for display. */\nexport interface ResourceRequirementRow {\n type: string;\n resourceKey?: string;\n permission?: string;\n required: boolean;\n description?: string;\n fields: RequirementField[];\n}\n\ninterface ManifestFieldShape {\n env?: string;\n origin?: string;\n description?: string;\n value?: string;\n resolve?: string;\n localOnly?: boolean;\n}\ninterface ManifestResourceShape {\n type?: string;\n resourceKey?: string;\n permission?: string;\n description?: string;\n fields?: Record<string, ManifestFieldShape>;\n}\ninterface ManifestShape {\n resources?: {\n required?: ManifestResourceShape[];\n optional?: ManifestResourceShape[];\n };\n}\n\nfunction toFields(\n fields: Record<string, ManifestFieldShape> | undefined,\n): RequirementField[] {\n return Object.entries(fields ?? {}).map(([key, f]) => ({\n key,\n env: f.env,\n origin: f.origin,\n description: f.description,\n value: f.value,\n resolve: f.resolve,\n localOnly: f.localOnly,\n }));\n}\n\nfunction toRows(\n resources: ManifestResourceShape[] | undefined,\n required: boolean,\n): ResourceRequirementRow[] {\n return (resources ?? []).map((r) => ({\n type: r.type ?? \"unknown\",\n resourceKey: r.resourceKey,\n permission: r.permission,\n required,\n description: r.description,\n fields: toFields(r.fields),\n }));\n}\n\n/** The manifest.json file shipped by a plugin item, or null for UI items. */\nfunction findManifest(item: RegistryItem): ManifestShape | null {\n const file = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n if (!file) return null;\n try {\n return JSON.parse(file.content) as ManifestShape;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts a plugin item's declared resource requirements (required first,\n * then optional). Returns an empty array for UI items or plugins that declare\n * no resources.\n */\nexport function extractRequirements(\n item: RegistryItem,\n): ResourceRequirementRow[] {\n const manifest = findManifest(item);\n if (!manifest) return [];\n return [\n ...toRows(manifest.resources?.required, true),\n ...toRows(manifest.resources?.optional, false),\n ];\n}\n\n/**\n * Renders the resource requirements for an item as human-readable lines.\n * Returns a single \"no resources\" line when there are none, so callers can\n * print unconditionally.\n */\nexport function renderRequirements(\n item: RegistryItem,\n rows: ResourceRequirementRow[] = extractRequirements(item),\n): string {\n if (rows.length === 0) {\n return pc.dim(`${item.name}: no resource requirements.`);\n }\n\n const lines: string[] = [pc.bold(`Resources required by ${item.name}:`)];\n for (const row of rows) {\n const tag = row.required ? pc.yellow(\"required\") : pc.dim(\"optional\");\n const perm = row.permission ? pc.dim(` [${row.permission}]`) : \"\";\n lines.push(` ${pc.cyan(row.type)} (${tag})${perm}`);\n if (row.description) lines.push(` ${pc.dim(row.description)}`);\n for (const field of row.fields) {\n if (!field.env) continue;\n const origin = field.origin ? pc.dim(` (${field.origin})`) : \"\";\n lines.push(` - ${field.env}${origin}`);\n }\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * A syntactically valid environment variable name. Field `env` names come from\n * an untrusted manifest and are written into `.env` as `NAME=value` (a newline\n * in the name would inject a second line — e.g. `PORT=x\\nDATABRICKS_HOST=…` →\n * credential exfil) and emitted into `app.yaml`. Anything that isn't a plain\n * env identifier is dropped before it reaches those sinks.\n */\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** True when `name` is a safe, well-formed environment variable name. */\nexport function isValidEnvName(name: string): boolean {\n return ENV_NAME.test(name);\n}\n\n/**\n * Effective origin of a field: trust a synced manifest's stamped `origin`, else\n * derive it from the field shape via the same rule `plugin sync` uses, so an\n * authored manifest (no `origin`) still classifies correctly.\n */\nexport function fieldOrigin(\n field: RequirementField,\n): \"platform\" | \"static\" | \"cli\" | \"user\" {\n if (field.origin)\n return field.origin as \"platform\" | \"static\" | \"cli\" | \"user\";\n return computeOriginFromField(field);\n}\n"],"mappings":";;;;;AA4DA,SAAS,SACP,QACoB;AACpB,QAAO,OAAO,QAAQ,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ;EACrD;EACA,KAAK,EAAE;EACP,QAAQ,EAAE;EACV,aAAa,EAAE;EACf,OAAO,EAAE;EACT,SAAS,EAAE;EACX,WAAW,EAAE;EACd,EAAE;;AAGL,SAAS,OACP,WACA,UAC0B;AAC1B,SAAQ,aAAa,EAAE,EAAE,KAAK,OAAO;EACnC,MAAM,EAAE,QAAQ;EAChB,aAAa,EAAE;EACf,YAAY,EAAE;EACd;EACA,aAAa,EAAE;EACf,QAAQ,SAAS,EAAE,OAAO;EAC3B,EAAE;;;AAIL,SAAS,aAAa,MAA0C;CAC9D,MAAM,QAAQ,KAAK,SAAS,EAAE,EAAE,MAC7B,MAAM,KAAK,SAAS,EAAE,UAAU,EAAE,KAAK,KAAK,gBAC9C;AACD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI;AACF,SAAO,KAAK,MAAM,KAAK,QAAQ;SACzB;AACN,SAAO;;;;;;;;AASX,SAAgB,oBACd,MAC0B;CAC1B,MAAM,WAAW,aAAa,KAAK;AACnC,KAAI,CAAC,SAAU,QAAO,EAAE;AACxB,QAAO,CACL,GAAG,OAAO,SAAS,WAAW,UAAU,KAAK,EAC7C,GAAG,OAAO,SAAS,WAAW,UAAU,MAAM,CAC/C;;;;;;;AAQH,SAAgB,mBACd,MACA,OAAiC,oBAAoB,KAAK,EAClD;AACR,KAAI,KAAK,WAAW,EAClB,QAAO,GAAG,IAAI,GAAG,KAAK,KAAK,6BAA6B;CAG1D,MAAM,QAAkB,CAAC,GAAG,KAAK,yBAAyB,KAAK,KAAK,GAAG,CAAC;AACxE,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,MAAM,IAAI,WAAW,GAAG,OAAO,WAAW,GAAG,GAAG,IAAI,WAAW;EACrE,MAAM,OAAO,IAAI,aAAa,GAAG,IAAI,KAAK,IAAI,WAAW,GAAG,GAAG;AAC/D,QAAM,KAAK,KAAK,GAAG,KAAK,IAAI,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO;AACpD,MAAI,IAAI,YAAa,OAAM,KAAK,OAAO,GAAG,IAAI,IAAI,YAAY,GAAG;AACjE,OAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,OAAI,CAAC,MAAM,IAAK;GAChB,MAAM,SAAS,MAAM,SAAS,GAAG,IAAI,KAAK,MAAM,OAAO,GAAG,GAAG;AAC7D,SAAM,KAAK,SAAS,MAAM,MAAM,SAAS;;;AAG7C,QAAO,MAAM,KAAK,KAAK;;;;;;;;;AAUzB,MAAM,WAAW;;AAGjB,SAAgB,eAAe,MAAuB;AACpD,QAAO,SAAS,KAAK,KAAK;;;;;;;AAQ5B,SAAgB,YACd,OACwC;AACxC,KAAI,MAAM,OACR,QAAO,MAAM;AACf,QAAO,uBAAuB,MAAM"}
|