@databricks/appkit-ui 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/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/schemas/manifest.js +4 -4
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/index.d.ts +2 -1
- package/dist/shared/src/workspace-client/errors.d.ts +1 -0
- package/dist/shared/src/workspace-client/factory.d.ts +2 -0
- package/dist/shared/src/workspace-client/index.d.ts +4 -0
- package/dist/shared/src/workspace-client/legacy.d.ts +3 -0
- package/dist/shared/src/workspace-client/types.d.ts +2 -0
- package/dist/workspace-client/client.js +57 -0
- package/dist/workspace-client/client.js.map +1 -0
- package/dist/workspace-client/errors.d.ts +2 -0
- package/dist/workspace-client/errors.js +3 -0
- package/dist/workspace-client/factory.d.ts +19 -0
- package/dist/workspace-client/factory.d.ts.map +1 -0
- package/dist/workspace-client/factory.js +24 -0
- package/dist/workspace-client/factory.js.map +1 -0
- package/dist/workspace-client/index.d.ts +5 -0
- package/dist/workspace-client/index.js +5 -0
- package/dist/workspace-client/legacy.d.ts +34 -0
- package/dist/workspace-client/legacy.d.ts.map +1 -0
- package/dist/workspace-client/legacy.js +23 -0
- package/dist/workspace-client/legacy.js.map +1 -0
- package/dist/workspace-client/types.d.ts +49 -0
- package/dist/workspace-client/types.d.ts.map +1 -0
- package/docs/api/appkit/Function.createWorkspaceClient.md +4 -4
- package/docs/api/appkit/Interface.WorkspaceClientOptions.md +11 -0
- package/package.json +4 -2
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { fieldOrigin, isValidEnvName } from "./requirements.js";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/commands/registry/env-reconcile.ts
|
|
5
|
+
/**
|
|
6
|
+
* A `.env` value is a single line: `KEY=VALUE`. A value carrying a CR/LF would
|
|
7
|
+
* write extra lines when serialized, so a malicious static default like
|
|
8
|
+
* `value: "y\nDATABRICKS_HOST=attacker"` could inject an unrelated key (host
|
|
9
|
+
* override → credential exfil). Registry manifests are untrusted, so any value
|
|
10
|
+
* with a line break is rejected rather than written.
|
|
11
|
+
*/
|
|
12
|
+
function isSafeEnvValue(value) {
|
|
13
|
+
return !/[\r\n]/.test(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Flattens requirement rows into the env vars that belong in local `.env`.
|
|
17
|
+
* Excludes fields with no `env` name and `platform`-origin fields (deploy-time
|
|
18
|
+
* platform injection). Order: required resources first (as given), then optional.
|
|
19
|
+
*/
|
|
20
|
+
function collectEnvNeeds(rows) {
|
|
21
|
+
const needs = [];
|
|
22
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23
|
+
const ordered = [...rows.filter((r) => r.required), ...rows.filter((r) => !r.required)];
|
|
24
|
+
for (const row of ordered) for (const field of row.fields) {
|
|
25
|
+
if (!includeInEnv(field)) continue;
|
|
26
|
+
const env = field.env;
|
|
27
|
+
if (seen.has(env)) continue;
|
|
28
|
+
seen.add(env);
|
|
29
|
+
needs.push({
|
|
30
|
+
env,
|
|
31
|
+
resourceType: row.type,
|
|
32
|
+
required: row.required,
|
|
33
|
+
defaultValue: field.value,
|
|
34
|
+
origin: fieldOrigin(field),
|
|
35
|
+
description: field.description
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return needs;
|
|
39
|
+
}
|
|
40
|
+
/** A field belongs in `.env` iff it names a valid env var and isn't platform-injected. */
|
|
41
|
+
function includeInEnv(field) {
|
|
42
|
+
if (!field.env) return false;
|
|
43
|
+
if (!isValidEnvName(field.env)) return false;
|
|
44
|
+
return fieldOrigin(field) !== "platform";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parses a `.env` file body into a KEY -> value map, using the same `dotenv`
|
|
48
|
+
* parser the app loads `.env` with at runtime so the CLI reads it identically.
|
|
49
|
+
*/
|
|
50
|
+
function parseEnv(content) {
|
|
51
|
+
return dotenv.parse(content);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Serializes new env entries for appending to a `.env` file. Only keys not
|
|
55
|
+
* already present are emitted; existing keys are never rewritten (we don't
|
|
56
|
+
* clobber user edits). Returns the text to append (empty if nothing new).
|
|
57
|
+
*/
|
|
58
|
+
function serializeEnvAppend(entries) {
|
|
59
|
+
if (entries.length === 0) return "";
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const e of entries) {
|
|
62
|
+
if (e.comment) lines.push(`# ${e.comment}`);
|
|
63
|
+
lines.push(`${e.env}=${e.value}`);
|
|
64
|
+
}
|
|
65
|
+
return `${lines.join("\n")}\n`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reconciles the needed env vars against what's already in `.env`.
|
|
69
|
+
* - Already-set keys are reported as "already-set" and never overwritten.
|
|
70
|
+
* - static-origin defaults are used without invoking `provide`.
|
|
71
|
+
* - Everything else defers to `provide`; a returned undefined means skip.
|
|
72
|
+
*/
|
|
73
|
+
async function reconcileEnv(needs, opts) {
|
|
74
|
+
const resolutions = [];
|
|
75
|
+
for (const need of needs) {
|
|
76
|
+
const current = opts.existing[need.env];
|
|
77
|
+
if (current !== void 0 && current !== "") {
|
|
78
|
+
resolutions.push({
|
|
79
|
+
env: need.env,
|
|
80
|
+
value: current,
|
|
81
|
+
status: "already-set"
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (need.defaultValue !== void 0) {
|
|
86
|
+
if (!isSafeEnvValue(need.defaultValue)) {
|
|
87
|
+
resolutions.push({
|
|
88
|
+
env: need.env,
|
|
89
|
+
status: "skipped"
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
resolutions.push({
|
|
94
|
+
env: need.env,
|
|
95
|
+
value: need.defaultValue,
|
|
96
|
+
status: "written"
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const value = await opts.provide(need);
|
|
101
|
+
if (value === void 0 || value === "" || !isSafeEnvValue(value)) resolutions.push({
|
|
102
|
+
env: need.env,
|
|
103
|
+
status: "skipped"
|
|
104
|
+
});
|
|
105
|
+
else resolutions.push({
|
|
106
|
+
env: need.env,
|
|
107
|
+
value,
|
|
108
|
+
status: "written"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return resolutions;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { collectEnvNeeds, parseEnv, reconcileEnv, serializeEnvAppend };
|
|
116
|
+
//# sourceMappingURL=env-reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-reconcile.js","names":[],"sources":["../../../../src/cli/commands/registry/env-reconcile.ts"],"sourcesContent":["import dotenv from \"dotenv\";\nimport {\n fieldOrigin,\n isValidEnvName,\n type RequirementField,\n type ResourceRequirementRow,\n} from \"./requirements\";\n\n/**\n * A single env var that an installed plugin needs in the local `.env`.\n * `platform`-origin fields are excluded upstream — they are injected by\n * Databricks Apps at deploy time and never belong in a hand-managed `.env`.\n */\nexport interface EnvNeed {\n env: string;\n resourceType: string;\n required: boolean;\n /** static-origin default value, pre-filled without prompting. */\n defaultValue?: string;\n origin?: string;\n description?: string;\n}\n\n/** The resolved decision for one env var after reconciliation. */\nexport interface EnvResolution {\n env: string;\n /** The value to write, or undefined when skipped / left unset. */\n value?: string;\n status: \"written\" | \"already-set\" | \"skipped\";\n}\n\n/**\n * A `.env` value is a single line: `KEY=VALUE`. A value carrying a CR/LF would\n * write extra lines when serialized, so a malicious static default like\n * `value: \"y\\nDATABRICKS_HOST=attacker\"` could inject an unrelated key (host\n * override → credential exfil). Registry manifests are untrusted, so any value\n * with a line break is rejected rather than written.\n */\nexport function isSafeEnvValue(value: string): boolean {\n return !/[\\r\\n]/.test(value);\n}\n\n/**\n * Flattens requirement rows into the env vars that belong in local `.env`.\n * Excludes fields with no `env` name and `platform`-origin fields (deploy-time\n * platform injection). Order: required resources first (as given), then optional.\n */\nexport function collectEnvNeeds(rows: ResourceRequirementRow[]): EnvNeed[] {\n const needs: EnvNeed[] = [];\n const seen = new Set<string>();\n const ordered = [\n ...rows.filter((r) => r.required),\n ...rows.filter((r) => !r.required),\n ];\n for (const row of ordered) {\n for (const field of row.fields) {\n if (!includeInEnv(field)) continue;\n const env = field.env as string;\n if (seen.has(env)) continue;\n seen.add(env);\n needs.push({\n env,\n resourceType: row.type,\n required: row.required,\n defaultValue: field.value,\n origin: fieldOrigin(field),\n description: field.description,\n });\n }\n }\n return needs;\n}\n\n/** A field belongs in `.env` iff it names a valid env var and isn't platform-injected. */\nfunction includeInEnv(field: RequirementField): boolean {\n if (!field.env) return false;\n // The env name comes from an untrusted manifest and is written as `NAME=value`;\n // a malformed name (e.g. one containing a newline) could inject an extra .env\n // line, so drop anything that isn't a plain env identifier.\n if (!isValidEnvName(field.env)) return false;\n // Origin is derived from the authored contract (localOnly/value/resolve) so\n // registry-fetched manifests without a computed origin classify correctly.\n return fieldOrigin(field) !== \"platform\";\n}\n\n/**\n * Parses a `.env` file body into a KEY -> value map, using the same `dotenv`\n * parser the app loads `.env` with at runtime so the CLI reads it identically.\n */\nexport function parseEnv(content: string): Record<string, string> {\n return dotenv.parse(content);\n}\n\n/**\n * Serializes new env entries for appending to a `.env` file. Only keys not\n * already present are emitted; existing keys are never rewritten (we don't\n * clobber user edits). Returns the text to append (empty if nothing new).\n */\nexport function serializeEnvAppend(\n entries: Array<{ env: string; value: string; comment?: string }>,\n): string {\n if (entries.length === 0) return \"\";\n const lines: string[] = [];\n for (const e of entries) {\n if (e.comment) lines.push(`# ${e.comment}`);\n lines.push(`${e.env}=${e.value}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Provides a value for an env need, or undefined to skip it. */\nexport type ValueProvider = (need: EnvNeed) => Promise<string | undefined>;\n\nexport interface ReconcileOptions {\n /** Existing parsed `.env` values (keys already present are left untouched). */\n existing: Record<string, string>;\n /** Resolves a value for each unset need (prompt in interactive, flag in CI). */\n provide: ValueProvider;\n}\n\n/**\n * Reconciles the needed env vars against what's already in `.env`.\n * - Already-set keys are reported as \"already-set\" and never overwritten.\n * - static-origin defaults are used without invoking `provide`.\n * - Everything else defers to `provide`; a returned undefined means skip.\n */\nexport async function reconcileEnv(\n needs: EnvNeed[],\n opts: ReconcileOptions,\n): Promise<EnvResolution[]> {\n const resolutions: EnvResolution[] = [];\n for (const need of needs) {\n const current = opts.existing[need.env];\n if (current !== undefined && current !== \"\") {\n // Carry the existing value so callers can still feed it into deploy\n // config (databricks.yml target variables) — the var is set in .env but\n // its bundle binding still needs the value assigned.\n resolutions.push({\n env: need.env,\n value: current,\n status: \"already-set\",\n });\n continue;\n }\n if (need.defaultValue !== undefined) {\n // Static default from an untrusted manifest — refuse a value that would\n // inject extra `.env` lines rather than silently writing it.\n if (!isSafeEnvValue(need.defaultValue)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n continue;\n }\n resolutions.push({\n env: need.env,\n value: need.defaultValue,\n status: \"written\",\n });\n continue;\n }\n const value = await opts.provide(need);\n if (value === undefined || value === \"\" || !isSafeEnvValue(value)) {\n resolutions.push({ env: need.env, status: \"skipped\" });\n } else {\n resolutions.push({ env: need.env, value, status: \"written\" });\n }\n }\n return resolutions;\n}\n"],"mappings":";;;;;;;;;;;AAsCA,SAAgB,eAAe,OAAwB;AACrD,QAAO,CAAC,SAAS,KAAK,MAAM;;;;;;;AAQ9B,SAAgB,gBAAgB,MAA2C;CACzE,MAAM,QAAmB,EAAE;CAC3B,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,UAAU,CACd,GAAG,KAAK,QAAQ,MAAM,EAAE,SAAS,EACjC,GAAG,KAAK,QAAQ,MAAM,CAAC,EAAE,SAAS,CACnC;AACD,MAAK,MAAM,OAAO,QAChB,MAAK,MAAM,SAAS,IAAI,QAAQ;AAC9B,MAAI,CAAC,aAAa,MAAM,CAAE;EAC1B,MAAM,MAAM,MAAM;AAClB,MAAI,KAAK,IAAI,IAAI,CAAE;AACnB,OAAK,IAAI,IAAI;AACb,QAAM,KAAK;GACT;GACA,cAAc,IAAI;GAClB,UAAU,IAAI;GACd,cAAc,MAAM;GACpB,QAAQ,YAAY,MAAM;GAC1B,aAAa,MAAM;GACpB,CAAC;;AAGN,QAAO;;;AAIT,SAAS,aAAa,OAAkC;AACtD,KAAI,CAAC,MAAM,IAAK,QAAO;AAIvB,KAAI,CAAC,eAAe,MAAM,IAAI,CAAE,QAAO;AAGvC,QAAO,YAAY,MAAM,KAAK;;;;;;AAOhC,SAAgB,SAAS,SAAyC;AAChE,QAAO,OAAO,MAAM,QAAQ;;;;;;;AAQ9B,SAAgB,mBACd,SACQ;AACR,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,KAAK,SAAS;AACvB,MAAI,EAAE,QAAS,OAAM,KAAK,KAAK,EAAE,UAAU;AAC3C,QAAM,KAAK,GAAG,EAAE,IAAI,GAAG,EAAE,QAAQ;;AAEnC,QAAO,GAAG,MAAM,KAAK,KAAK,CAAC;;;;;;;;AAmB7B,eAAsB,aACpB,OACA,MAC0B;CAC1B,MAAM,cAA+B,EAAE;AACvC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,SAAS,KAAK;AACnC,MAAI,YAAY,UAAa,YAAY,IAAI;AAI3C,eAAY,KAAK;IACf,KAAK,KAAK;IACV,OAAO;IACP,QAAQ;IACT,CAAC;AACF;;AAEF,MAAI,KAAK,iBAAiB,QAAW;AAGnC,OAAI,CAAC,eAAe,KAAK,aAAa,EAAE;AACtC,gBAAY,KAAK;KAAE,KAAK,KAAK;KAAK,QAAQ;KAAW,CAAC;AACtD;;AAEF,eAAY,KAAK;IACf,KAAK,KAAK;IACV,OAAO,KAAK;IACZ,QAAQ;IACT,CAAC;AACF;;EAEF,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK;AACtC,MAAI,UAAU,UAAa,UAAU,MAAM,CAAC,eAAe,MAAM,CAC/D,aAAY,KAAK;GAAE,KAAK,KAAK;GAAK,QAAQ;GAAW,CAAC;MAEtD,aAAY,KAAK;GAAE,KAAK,KAAK;GAAK;GAAO,QAAQ;GAAW,CAAC;;AAGjE,QAAO"}
|
|
@@ -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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"}
|