AINATIVEM-47 AI plugin layer — CLI subcommands and Claude Code plugin#9
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an "AI plugin layer" on top of the existing scaffolder: two new CLI subcommands (add-oauth, add-app-extension) that re-run individual generators against an existing project, plus a Claude Code plugin (plugin/) bundling five slash-command skills that drive the CLI and provide guidance. The npm files whitelist is widened so plugin/ and dist/ ship with the package.
Changes:
- New
dispatchSubcommandincli.tsplussrc/subcommands/{addOAuth,addAppExtension}.ts(and tests) that wrapNodeProjectBuilderwith apackage.jsonprecondition check. - New Claude Code plugin manifest and five SKILL.md files under
plugin/skills/. package.jsongains afileswhitelist includingdistandplugin.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/cli.ts | Adds dispatchSubcommand argv parser and top-level try/catch around it. |
| src/cli.test.ts | Tests for dispatchSubcommand happy paths. |
| src/subcommands/addOAuth.ts | Wraps NodeProjectBuilder.addOAuth().build() with a package.json check. |
| src/subcommands/addOAuth.test.ts | Tests target dir output and missing-package.json error. |
| src/subcommands/addAppExtension.ts | Same pattern for app extensions, with optional interactive prompt fallback. |
| src/subcommands/addAppExtension.test.ts | Verifies panel/modal generation and precondition. |
| plugin/.claude-plugin/plugin.json | Plugin manifest (name, description, version, author). |
| plugin/skills/pipedrive-new-app/SKILL.md | Skill that just runs npx create-pipedrive-app. |
| plugin/skills/pipedrive-add-oauth/SKILL.md | Skill that runs add-oauth and edits app.ts / .env.example. |
| plugin/skills/pipedrive-add-app-extension/SKILL.md | Skill that runs add-app-extension and wires routers/Vite. |
| plugin/skills/pipedrive-review-marketplace-readiness/SKILL.md | Skill that audits a project against marketplace-checklist.md. |
| plugin/skills/pipedrive-api/SKILL.md | Reference doc on Pipedrive API usage from generated client. |
| package.json | Adds files: ["dist", "plugin"] so the plugin ships with npm. |
Comments suppressed due to low confidence (2)
src/subcommands/addOAuth.ts:12
- Hard-coding
database: 'postgres'andprojectName: ''here is brittle. It happens to work today becausegenerateOauthignores those fields, but if the OAuth template ever starts to depend on the chosen database (for example, to import a database-specific token repository) this subcommand will silently produce wrong code in projects scaffolded withmysqlorsqlite. Consider either reading the existing project's options (e.g. from a marker file) or making the builder API explicit about which options are required foraddOAuth().
await new NodeProjectBuilder(resolved, { projectName: '', database: 'postgres', appExtensions: [] })
.addOAuth()
.build();
plugin/skills/pipedrive-api/SKILL.md:58
- Step 1 also tells the user to "Read
src/pipedrive-client/to understand the existing wrapper pattern", but the generated path issrc/pipedrive/client.ts. This will lead the agent to attempt to read a directory that does not exist.
1. Read `src/pipedrive-client/` to understand the existing wrapper pattern
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (subcommand === 'add-app-extension') { | ||
| const appExtIdx = argv.indexOf('--app-extensions'); | ||
| const appExtensions = | ||
| appExtIdx !== -1 ? ([argv[appExtIdx + 1]] as AppExtensionType[]) : undefined; | ||
| await addAppExtension(outputDir, appExtensions); | ||
| return true; | ||
| } |
| const appExtIdx = argv.indexOf('--app-extensions'); | ||
| const appExtensions = | ||
| appExtIdx !== -1 ? ([argv[appExtIdx + 1]] as AppExtensionType[]) : undefined; | ||
| await addAppExtension(outputDir, appExtensions); |
| const outputDirIdx = argv.indexOf('--output-dir'); | ||
| const outputDir = outputDirIdx !== -1 ? argv[outputDirIdx + 1] : undefined; | ||
|
|
||
| if (subcommand === 'add-oauth') { | ||
| await addOAuth(outputDir); | ||
| return true; | ||
| } | ||
|
|
||
| if (subcommand === 'add-app-extension') { | ||
| const appExtIdx = argv.indexOf('--app-extensions'); | ||
| const appExtensions = | ||
| appExtIdx !== -1 ? ([argv[appExtIdx + 1]] as AppExtensionType[]) : undefined; | ||
| await addAppExtension(outputDir, appExtensions); | ||
| return true; | ||
| } |
| await new NodeProjectBuilder(resolved, { projectName: '', database: 'postgres', appExtensions: extensions }) | ||
| .addAppExtensions() | ||
| .build(); |
| export async function addOAuth(outputDir: string = process.cwd()): Promise<void> { | ||
| const resolved = resolve(outputDir); | ||
| if (!existsSync(join(resolved, 'package.json'))) { | ||
| throw new Error(`No package.json found in ${resolved}. Run this command from your project root.`); | ||
| } | ||
| await new NodeProjectBuilder(resolved, { projectName: '', database: 'postgres', appExtensions: [] }) | ||
| .addOAuth() | ||
| .build(); | ||
| } |
| import { afterEach, describe, expect, it } from 'vitest'; | ||
| import { access, mkdir, rm, writeFile } from 'node:fs/promises'; | ||
| import { join } from 'node:path'; | ||
| import { tmpdir } from 'node:os'; | ||
|
|
||
| const tmpDir = join(tmpdir(), 'cpa-add-oauth-test'); | ||
| const exists = (p: string) => access(p).then(() => true, () => false); | ||
|
|
||
| afterEach(async () => { | ||
| await rm(tmpDir, { recursive: true, force: true }); |
| const tmpDir = join(tmpdir(), 'cpa-add-appext-test'); | ||
| const exists = (p: string) => access(p).then(() => true, () => false); | ||
|
|
||
| afterEach(async () => { | ||
| await rm(tmpDir, { recursive: true, force: true }); | ||
| }); |
| const dispatchTmpDir = join(tmpdir(), 'cpa-dispatch-test'); | ||
| const dispatchExists = (p: string) => access(p).then(() => true, () => false); | ||
|
|
||
| afterEach(async () => { | ||
| await rm(dispatchTmpDir, { recursive: true, force: true }); | ||
| }); |
| export async function dispatchSubcommand(argv: string[]): Promise<boolean> { | ||
| const subcommand = argv[2]; | ||
| const outputDirIdx = argv.indexOf('--output-dir'); | ||
| const outputDir = outputDirIdx !== -1 ? argv[outputDirIdx + 1] : undefined; | ||
|
|
||
| if (subcommand === 'add-oauth') { | ||
| await addOAuth(outputDir); | ||
| return true; | ||
| } | ||
|
|
||
| if (subcommand === 'add-app-extension') { | ||
| const appExtIdx = argv.indexOf('--app-extensions'); | ||
| const appExtensions = | ||
| appExtIdx !== -1 ? ([argv[appExtIdx + 1]] as AppExtensionType[]) : undefined; | ||
| await addAppExtension(outputDir, appExtensions); | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } |
| if (!existsSync(join(resolved, 'package.json'))) { | ||
| throw new Error(`No package.json found in ${resolved}. Run this command from your project root.`); | ||
| } | ||
| const extensions = appExtensions ?? (await promptAppExtensions()); |
| { | ||
| "name": "create-pipedrive-app-local", | ||
| "interface": { | ||
| "displayName": "Create Pipedrive App Local" |
There was a problem hiding this comment.
| "displayName": "Create Pipedrive App Local" | |
| "displayName": "Create Pipedrive App" |
maybe we can name it Create Pipedrive App , without local ?
There was a problem hiding this comment.
will fix it. codex still needs that for the installation
Summary
add-app-extensionsubcommandNodeProjectBuilderto acceptPartial<GeneratorOptions>, removing hardcoded dummy values from subcommandsadd-oauthsubcommand andpipedrive-add-oauthskill — OAuth is always included in every scaffold, making the addon redundantadd-app-extensionskill to match actual generated outputsrc/pipedrive-client/path references inpipedrive-apiskill (nowsrc/pipedrive/)rgwithgrepin Codex marketplace readiness referenceTest plan
Automated
npm run typecheck npm testCLI subcommands
Install and test the Claude Code plugin locally
Once the repo is public, install via GitHub:
Before the repo is public, install from a local path:
Then test the skills from any project directory:
Install and test the Codex plugin locally