Skip to content
10 changes: 10 additions & 0 deletions packages/shared/src/cli/commands/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,14 @@ export const docsCommand = new Command("docs")
"Section name (e.g. 'plugins') or path to a doc file (e.g. './docs.md')",
)
.option("--full", "Show complete index including all API reference entries")
.addHelpText(
"after",
`
Examples:
$ appkit docs
$ appkit docs plugins
$ appkit docs "appkit-ui API reference"
$ appkit docs ./docs/plugins/analytics.md
$ appkit docs --full`,
)
.action(runDocs);
21 changes: 20 additions & 1 deletion packages/shared/src/cli/commands/generate-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,24 @@ async function runGenerateTypes(
warehouseId: resolvedWarehouseId,
noCache,
});
console.log(`Generated query types: ${resolvedOutFile}`);
}
} else {
console.error(
"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.",
);
}

// Generate serving endpoint types (no warehouse required)
const servingOutFile = path.join(
process.cwd(),
"client/src/appKitServingTypes.d.ts",
);
await typeGen.generateServingTypes({
outFile: path.join(process.cwd(), "client/src/appKitServingTypes.d.ts"),
outFile: servingOutFile,
noCache,
});
console.log(`Generated serving types: ${servingOutFile}`);
} catch (error) {
if (
error instanceof Error &&
Expand All @@ -66,4 +76,13 @@ export const generateTypesCommand = new Command("generate-types")
)
.argument("[warehouseId]", "Databricks warehouse ID")
.option("--no-cache", "Disable caching for type generation")
.addHelpText(
"after",
`
Examples:
$ appkit generate-types
$ appkit generate-types . client/src/types.d.ts
$ appkit generate-types . client/src/types.d.ts my-warehouse-id
$ appkit generate-types --no-cache`,
)
.action(runGenerateTypes);
6 changes: 6 additions & 0 deletions packages/shared/src/cli/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,10 @@ function runLint() {

export const lintCommand = new Command("lint")
.description("Run AST-based linting on TypeScript files")
.addHelpText(
"after",
`
Examples:
$ appkit lint`,
)
.action(runLint);
150 changes: 139 additions & 11 deletions packages/shared/src/cli/commands/plugin/add-resource/add-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import process from "node:process";
import { cancel, intro, outro } from "@clack/prompts";
import { Command } from "commander";
import { promptOneResource } from "../create/prompt-resource";
import { humanizeResourceType } from "../create/resource-defaults";
import {
DEFAULT_PERMISSION_BY_TYPE,
getDefaultFieldsForType,
getValidResourceTypes,
humanizeResourceType,
resourceKeyFromType,
} from "../create/resource-defaults";
import { resolveManifestInDir } from "../manifest-resolve";
import type { PluginManifest, ResourceRequirement } from "../manifest-types";
import { validateManifest } from "../validate/validate-manifest";
Expand All @@ -14,17 +20,29 @@ interface ManifestWithExtras extends PluginManifest {
[key: string]: unknown;
}

async function runPluginAddResource(options: { path?: string }): Promise<void> {
intro("Add resource to plugin manifest");
interface AddResourceOptions {
path?: string;
type?: string;
required?: boolean;
resourceKey?: string;
description?: string;
permission?: string;
fieldsJson?: string;
dryRun?: boolean;
}

const cwd = process.cwd();
const pluginDir = path.resolve(cwd, options.path ?? ".");
function loadManifest(
pluginDir: string,
): { manifest: ManifestWithExtras; manifestPath: string } | null {
const resolved = resolveManifestInDir(pluginDir, { allowJsManifest: true });

if (!resolved) {
console.error(
`No manifest found in ${pluginDir}. This command requires manifest.json (manifest.js cannot be edited in place).`,
);
console.error(
" appkit plugin add-resource --path <dir-with-manifest.json>",
);
process.exit(1);
}

Expand All @@ -37,7 +55,6 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {

const manifestPath = resolved.path;

let manifest: ManifestWithExtras;
try {
const raw = fs.readFileSync(manifestPath, "utf-8");
const parsed = JSON.parse(raw) as unknown;
Expand All @@ -48,14 +65,96 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {
);
process.exit(1);
}
manifest = parsed as ManifestWithExtras;
return { manifest: parsed as ManifestWithExtras, manifestPath };
} catch (err) {
console.error(
"Failed to read or parse manifest.json:",
err instanceof Error ? err.message : err,
);
process.exit(1);
}
}

function buildEntry(
type: string,
opts: AddResourceOptions,
): { entry: ResourceRequirement; isRequired: boolean } {
const alias = humanizeResourceType(type);
const isRequired = opts.required !== false;

let fields = getDefaultFieldsForType(type);
if (opts.fieldsJson) {
try {
const parsed = JSON.parse(opts.fieldsJson) as Record<
string,
{ env: string; description?: string }
>;
fields = { ...fields, ...parsed };
} catch {
console.error("Error: --fields-json must be valid JSON.");
console.error(
' Example: --fields-json \'{"id":{"env":"MY_WAREHOUSE_ID"}}\'',
);
process.exit(1);
}
}

const entry: ResourceRequirement = {
type: type as ResourceRequirement["type"],
alias,
resourceKey: opts.resourceKey ?? resourceKeyFromType(type),
description:
opts.description ||
`${isRequired ? "Required" : "Optional"} for ${alias} functionality.`,
permission:
opts.permission ?? DEFAULT_PERMISSION_BY_TYPE[type] ?? "CAN_VIEW",
fields,
};

return { entry, isRequired };
}

function runNonInteractive(opts: AddResourceOptions): void {
const cwd = process.cwd();
const pluginDir = path.resolve(cwd, opts.path ?? ".");
const loaded = loadManifest(pluginDir);
if (!loaded) return;
const { manifest, manifestPath } = loaded;

const type = opts.type as string;
const validTypes = getValidResourceTypes();
if (!validTypes.includes(type)) {
console.error(`Error: Unknown resource type "${type}".`);
console.error(` Valid types: ${validTypes.join(", ")}`);
process.exit(1);
}
const { entry, isRequired } = buildEntry(type, opts);

if (isRequired) {
manifest.resources.required.push(entry);
} else {
manifest.resources.optional.push(entry);
}

if (opts.dryRun) {
console.log(JSON.stringify(manifest, null, 2));
return;
}

fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(
`Added ${entry.alias} as ${isRequired ? "required" : "optional"} to ${path.relative(cwd, manifestPath)}`,
);
}

async function runInteractive(opts: AddResourceOptions): Promise<void> {
intro("Add resource to plugin manifest");

const cwd = process.cwd();
const pluginDir = path.resolve(cwd, opts.path ?? ".");
const loaded = loadManifest(pluginDir);
if (!loaded) return;
const { manifest, manifestPath } = loaded;

const spec = await promptOneResource();
if (!spec) {
Expand All @@ -65,8 +164,6 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {

const alias = humanizeResourceType(spec.type);
const entry: ResourceRequirement = {
// Safe cast: spec.type comes from RESOURCE_TYPE_OPTIONS which reads values
// from the same JSON schema that generates the ResourceType union.
type: spec.type as ResourceRequirement["type"],
alias,
resourceKey: spec.resourceKey,
Expand All @@ -89,13 +186,44 @@ async function runPluginAddResource(options: { path?: string }): Promise<void> {
);
}

async function runPluginAddResource(opts: AddResourceOptions): Promise<void> {
if (opts.type) {
runNonInteractive(opts);
} else {
await runInteractive(opts);
}
}

export const pluginAddResourceCommand = new Command("add-resource")
.description(
"Add a resource requirement to an existing plugin manifest (interactive). Overwrites manifest.json in place.",
"Add a resource requirement to an existing plugin manifest. Overwrites manifest.json in place.",
)
.option(
"-p, --path <dir>",
"Plugin directory containing manifest.json, which will be edited in place (default: .)",
"Plugin directory containing manifest.json (default: .)",
)
.option(
"-t, --type <resource_type>",
"Resource type (e.g. sql_warehouse, volume). Enables non-interactive mode.",
)
.option("--required", "Mark resource as required (default: true)", true)
.option("--no-required", "Mark resource as optional")
.option("--resource-key <key>", "Resource key (default: derived from type)")
.option("--description <text>", "Description of the resource requirement")
.option("--permission <perm>", "Permission level (default: from schema)")
.option(
"--fields-json <json>",
'JSON object overriding field env vars (e.g. \'{"id":{"env":"MY_WAREHOUSE_ID"}}\')',
)
.option("--dry-run", "Preview the updated manifest without writing")
.addHelpText(
"after",
`
Examples:
$ appkit plugin add-resource
$ appkit plugin add-resource --path plugins/my-plugin --type sql_warehouse
$ appkit plugin add-resource --path plugins/my-plugin --type volume --no-required --dry-run
$ appkit plugin add-resource --type sql_warehouse --fields-json '{"id":{"env":"MY_WAREHOUSE_ID"}}'`,
)
.action((opts) =>
runPluginAddResource(opts).catch((err) => {
Expand Down
Loading
Loading