Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
14 changes: 14 additions & 0 deletions packages/shared/src/cli/commands/generate-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ async function runGenerateTypes(
warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;

if (!resolvedWarehouseId) {
console.error(
"Skipping type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.",
);
process.exit(0);
}

Expand All @@ -42,6 +45,8 @@ async function runGenerateTypes(
warehouseId: resolvedWarehouseId,
noCache: options?.noCache || false,
});

console.log(`Generated types: ${resolvedOutFile}`);
} catch (error) {
if (
error instanceof Error &&
Expand All @@ -67,4 +72,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