Skip to content

Commit aeb6370

Browse files
committed
feat: add plugin create command
chore: fixup
1 parent fdd28d8 commit aeb6370

File tree

14 files changed

+1227
-97
lines changed

14 files changed

+1227
-97
lines changed

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ pnpm check:fix # Auto-fix with Biome
9595
pnpm typecheck # TypeScript type checking across all packages
9696
```
9797

98+
### AppKit CLI
99+
When using the published SDK or running from the monorepo (after `pnpm build`), the `appkit` CLI is available:
100+
101+
```bash
102+
npx appkit plugin sync --write # Sync plugin manifests into appkit.plugins.json
103+
npx appkit plugin create # Scaffold a new plugin (interactive, uses @clack/prompts)
104+
npx appkit plugin validate # Validate manifest(s) against the JSON schema
105+
npx appkit plugin list # List plugins (from appkit.plugins.json or --dir)
106+
npx appkit plugin add-resource # Add a resource requirement to a plugin (interactive)
107+
```
108+
98109
### Deployment
99110
```bash
100111
pnpm pack:sdk # Package SDK for deployment

packages/shared/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@types/dependency-tree": "^8.1.4",
2626
"@types/express": "^4.17.21",
2727
"@types/json-schema": "^7.0.15",
28+
"@types/node": "^25.2.3",
2829
"@types/ws": "^8.18.1",
2930
"dependency-tree": "^11.2.0"
3031
},
@@ -42,6 +43,7 @@
4243
"@ast-grep/napi": "^0.37.0",
4344
"ajv": "^8.17.1",
4445
"ajv-formats": "^3.0.1",
46+
"@clack/prompts": "^1.0.1",
4547
"commander": "^12.1.0"
4648
}
4749
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import process from "node:process";
4+
import { cancel, intro, isCancel, outro, select, text } from "@clack/prompts";
5+
import { Command } from "commander";
6+
import {
7+
DEFAULT_PERMISSION_BY_TYPE,
8+
getDefaultFieldsForType,
9+
humanizeResourceType,
10+
RESOURCE_TYPE_OPTIONS,
11+
resourceKeyFromType,
12+
} from "../plugin-create/resource-defaults.js";
13+
import { validateManifest } from "../plugin-validate/validate-manifest.js";
14+
15+
interface ManifestWithResources {
16+
$schema?: string;
17+
name: string;
18+
displayName: string;
19+
description: string;
20+
resources: {
21+
required: unknown[];
22+
optional: unknown[];
23+
};
24+
[key: string]: unknown;
25+
}
26+
27+
async function runPluginAddResource(options: { path?: string }): Promise<void> {
28+
intro("Add resource to plugin manifest");
29+
30+
const cwd = process.cwd();
31+
const pluginDir = path.resolve(cwd, options.path ?? ".");
32+
const manifestPath = path.join(pluginDir, "manifest.json");
33+
34+
if (!fs.existsSync(manifestPath)) {
35+
console.error(`manifest.json not found at ${manifestPath}`);
36+
process.exit(1);
37+
}
38+
39+
let raw: string;
40+
let manifest: ManifestWithResources;
41+
try {
42+
raw = fs.readFileSync(manifestPath, "utf-8");
43+
const parsed = JSON.parse(raw) as unknown;
44+
const result = validateManifest(parsed, manifestPath);
45+
if (!result.valid || !result.manifest) {
46+
console.error(
47+
"Invalid manifest. Run `appkit plugin validate` for details.",
48+
);
49+
process.exit(1);
50+
}
51+
manifest = parsed as ManifestWithResources;
52+
} catch (err) {
53+
console.error(
54+
"Failed to read or parse manifest.json:",
55+
err instanceof Error ? err.message : err,
56+
);
57+
process.exit(1);
58+
}
59+
60+
const resourceType = await select({
61+
message: "Resource type",
62+
options: RESOURCE_TYPE_OPTIONS.map((o) => ({
63+
value: o.value,
64+
label: o.label,
65+
})),
66+
});
67+
if (isCancel(resourceType)) {
68+
cancel("Cancelled.");
69+
process.exit(0);
70+
}
71+
72+
const required = await select<boolean>({
73+
message: "Required or optional?",
74+
options: [
75+
{ value: true, label: "Required", hint: "plugin needs it to function" },
76+
{ value: false, label: "Optional", hint: "enhances functionality" },
77+
],
78+
});
79+
if (isCancel(required)) {
80+
cancel("Cancelled.");
81+
process.exit(0);
82+
}
83+
84+
const description = await text({
85+
message: "Short description for this resource",
86+
placeholder: required ? "Required for …" : "Optional for …",
87+
});
88+
if (isCancel(description)) {
89+
cancel("Cancelled.");
90+
process.exit(0);
91+
}
92+
93+
const type = resourceType as string;
94+
const permission = DEFAULT_PERMISSION_BY_TYPE[type] ?? "CAN_VIEW";
95+
const fields = getDefaultFieldsForType(type);
96+
const alias = humanizeResourceType(type);
97+
const resourceKey = resourceKeyFromType(type);
98+
const entry = {
99+
type,
100+
alias,
101+
resourceKey,
102+
description:
103+
(description as string)?.trim() || `Required for ${alias} functionality.`,
104+
permission,
105+
fields,
106+
};
107+
108+
if (required) {
109+
manifest.resources.required.push(entry);
110+
} else {
111+
manifest.resources.optional.push(entry);
112+
}
113+
114+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
115+
116+
outro("Resource added.");
117+
console.log(
118+
`\nAdded ${alias} as ${required ? "required" : "optional"} to ${path.relative(cwd, manifestPath)}`,
119+
);
120+
}
121+
122+
export const pluginAddResourceCommand = new Command("add-resource")
123+
.description(
124+
"Add a resource requirement to an existing plugin manifest (interactive)",
125+
)
126+
.option(
127+
"-p, --path <dir>",
128+
"Plugin directory containing manifest.json (default: .)",
129+
)
130+
.action((opts) =>
131+
runPluginAddResource(opts).catch((err) => {
132+
console.error(err);
133+
process.exit(1);
134+
}),
135+
);

0 commit comments

Comments
 (0)