mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(plugins): wire Open Design "Open Design PR" button end-to-end (#2182)
Some checks failed
ci / Detect CI change scopes (push) Failing after 2s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 1s
ci / Preflight (push) Has been skipped
ci / Core package tests (push) Has been skipped
ci / Tools workspace tests (push) Has been skipped
ci / Daemon workspace tests (push) Has been skipped
ci / Web workspace tests (push) Has been skipped
ci / E2E vitest (push) Has been skipped
ci / Playwright critical (push) Has been skipped
ci / Build workspaces (push) Has been skipped
ci / App workspace tests (push) Has been skipped
ci / Validate workspace (push) Failing after 0s
Some checks failed
ci / Detect CI change scopes (push) Failing after 2s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 1s
ci / Preflight (push) Has been skipped
ci / Core package tests (push) Has been skipped
ci / Tools workspace tests (push) Has been skipped
ci / Daemon workspace tests (push) Has been skipped
ci / Web workspace tests (push) Has been skipped
ci / E2E vitest (push) Has been skipped
ci / Playwright critical (push) Has been skipped
ci / Build workspaces (push) Has been skipped
ci / App workspace tests (push) Has been skipped
ci / Validate workspace (push) Failing after 0s
Two-part fix for the Plugin folder card's "Open Design PR" button. The
flow was broken end-to-end: the CLI emitted a 404 URL, and the agent
prompt under-specified the contribution steps so the agent stalled
mid-turn or fell back to the legacy issue-URL path.
Catalog target — `apps/daemon/src/plugins/publish.ts`:
`buildPublishLink({ catalog: 'open-design' })` hardcoded a submission URL
at `github.com/open-design/plugin-registry`, the dedicated registry repo
proposed in docs/plans/plugin-registry.md §1.2. That repo doesn't exist
yet (P3.1 notes "creating the external GitHub repo is an operational
launch step, not a code blocker"), so every generated URL 404'd. Retarget
the catalog at the live `nexu-io/open-design` monorepo and update the
target-path hint in the PR body to `plugins/community/<plugin-name>/`
(the actual layout under main). Plan §1.2 stays the long-term goal — see
the code comment.
Also updates the matching `od plugin yank` issue URL in cli.ts.
Agent prompt — `apps/web/src/components/design-files/pluginFolderActions.ts`:
The `contribute` action prompt only said "use the supported `od plugin
publish` Open Design registry flow", which produces an issue URL (the
legacy path) and left the agent to invent the remaining steps. The agent
ended up calling `AskUserQuestion` mid-turn waiting for input that the
DesignFilesPanel buttons couldn't satisfy, then stalled for 600s.
Rewrite the `contribute` prompt to drive the full PR flow via raw `gh`
commands:
1. preflight `gh --version` / `gh auth status` (detect-and-instruct on
missing CLI, never auto-install anything)
2. read manifest, resolve author login
3. `gh repo fork nexu-io/open-design --remote=false`
4. clone fork, branch, cp plugin into `plugins/community/<name>/`,
commit/push using author's git identity (not a hardcoded bot)
5. `gh pr create ... --web` so the author reviews and clicks Create
Hard bans on `AskUserQuestion`, retry-on-failure, and the legacy
`od plugin publish --to open-design` CLI keep the turn fire-and-forget.
The `install` / `publish` action prompts are unchanged.
Tests:
* `apps/daemon/tests/plugins-publish.test.ts` — assert URL host +
catalogLabel match `nexu-io/open-design`, body contains the new path
hint.
* `apps/web/tests/components/pluginFolderActions.test.ts` (new) — lock
the contribute prompt's command surface, the `--web` review window,
the `AskUserQuestion` ban, the install-tool ban, and folder-path
interpolation. Nine assertions covering the prompt contract without
coupling to exact wording.
Validation:
* `pnpm --filter @open-design/daemon exec vitest run tests/plugins-publish.test.ts`
→ 11/11 passed
* `pnpm --filter @open-design/web exec vitest run tests/components/pluginFolderActions.test.ts`
→ 9/9 passed
* `pnpm --filter @open-design/web typecheck` clean
* E2E: button click in DesignFilesPanel under namespace=e2e-pr-test ran
`gh --version` → fork → clone → branch → commit/push → ended at
`gh pr create --web` opening the GitHub PR draft in browser. No
AskUserQuestion stall this time.
Doesn't touch: `5f71968f` server-side endpoints (`/contribute-open-design`
+ `/publish-github` still in main as dead code — separate cleanup),
plan/spec docs (per maintainer call to keep the dedicated-repo as the
long-term goal), or `registry-backends.test.ts` (plan terminal-state
fixture; no production caller).
This commit is contained in:
parent
9596a0ccd5
commit
387dc83b27
5 changed files with 169 additions and 14 deletions
|
|
@ -3137,7 +3137,7 @@ marks a version unresolvable for new installs while preserving lockfile replay.`
|
|||
name: parsed.name,
|
||||
version: parsed.range,
|
||||
reason,
|
||||
url: `https://github.com/open-design/plugin-registry/issues/new?${params.toString()}`,
|
||||
url: `https://github.com/nexu-io/open-design/issues/new?${params.toString()}`,
|
||||
body,
|
||||
};
|
||||
if (flags.json) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@
|
|||
// - awesome-agent-skills → VoltAgent/awesome-agent-skills
|
||||
// - clawhub → openclaw/clawhub
|
||||
// - skills-sh → skills.sh discovery hint
|
||||
// - open-design → open-design/plugin-registry
|
||||
// - open-design → nexu-io/open-design (plugins/community/<plugin-name>/).
|
||||
// The dedicated `open-design/plugin-registry` repo per
|
||||
// docs/plans/plugin-registry.md §1.2 stays the long-term
|
||||
// target, but submissions land in the monorepo until
|
||||
// that operational launch step happens — keeping the
|
||||
// contribution surface where stars / PRs already are.
|
||||
//
|
||||
// The function is pure: it accepts the plugin's metadata and returns
|
||||
// the catalog target description. The CLI is the side-effect-bearing
|
||||
|
|
@ -143,14 +148,14 @@ export function buildPublishLink(args: {
|
|||
'',
|
||||
'## Open Design registry entry',
|
||||
'',
|
||||
'- Target path: `community/<vendor>/<plugin-name>/open-design.json`',
|
||||
'- Generated index: `open-design-marketplace.json`',
|
||||
'- Target path: `plugins/community/<plugin-name>/open-design.json`',
|
||||
'- Generated index: `plugins/registry/community/open-design-marketplace.json`',
|
||||
'- Required checks: `od plugin validate`, `od plugin pack`, integrity digest, preview smoke.',
|
||||
].join('\n');
|
||||
const url = newIssueUrl('open-design/plugin-registry', title, bodyWithRegistry);
|
||||
const url = newIssueUrl('nexu-io/open-design', title, bodyWithRegistry);
|
||||
return {
|
||||
catalog: args.catalog,
|
||||
catalogLabel: 'open-design/plugin-registry',
|
||||
catalogLabel: 'nexu-io/open-design',
|
||||
url,
|
||||
prBody: bodyWithRegistry,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,10 +60,16 @@ describe('buildPublishLink', () => {
|
|||
});
|
||||
|
||||
it('builds an Open Design registry submission URL', () => {
|
||||
// The dedicated `open-design/plugin-registry` repo per
|
||||
// docs/plans/plugin-registry.md §1.2 is the long-term target; until that
|
||||
// operational launch step happens, submissions land in `nexu-io/open-design`
|
||||
// (plugins/community/<plugin-name>/), keeping contribution where stars and
|
||||
// PR traffic already are.
|
||||
const link = buildPublishLink({ catalog: 'open-design', meta: META });
|
||||
expect(link.catalogLabel).toBe('open-design/plugin-registry');
|
||||
expect(link.url).toMatch(/^https:\/\/github\.com\/open-design\/plugin-registry\/issues\/new\?/);
|
||||
expect(link.prBody).toContain('open-design-marketplace.json');
|
||||
expect(link.catalogLabel).toBe('nexu-io/open-design');
|
||||
expect(link.url).toMatch(/^https:\/\/github\.com\/nexu-io\/open-design\/issues\/new\?/);
|
||||
expect(link.prBody).toContain('plugins/community/<plugin-name>/open-design.json');
|
||||
expect(link.prBody).toContain('plugins/registry/community/open-design-marketplace.json');
|
||||
});
|
||||
|
||||
it('falls back to owner/repo placeholder when repoUrl is missing for skills-sh', () => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
export type PluginFolderAgentAction = 'install' | 'publish' | 'contribute';
|
||||
|
||||
const ACTION_TITLES: Record<PluginFolderAgentAction, string> = {
|
||||
const ACTION_TITLES: Record<Exclude<PluginFolderAgentAction, 'contribute'>, string> = {
|
||||
install: 'Install this generated plugin into My plugins.',
|
||||
publish: 'Publish this generated plugin to a public repository.',
|
||||
contribute: 'Prepare an Open Design registry PR for this generated plugin.',
|
||||
};
|
||||
|
||||
const ACTION_NOTES: Record<PluginFolderAgentAction, string> = {
|
||||
const ACTION_NOTES: Record<Exclude<PluginFolderAgentAction, 'contribute'>, string> = {
|
||||
install:
|
||||
'Prefer the supported `od plugin install --source` flow after confirming the manifest.',
|
||||
publish:
|
||||
'Use the supported `od plugin publish` or repository-publish flow after confirming the manifest.',
|
||||
contribute:
|
||||
'Use the supported `od plugin publish` Open Design registry flow after confirming the manifest.',
|
||||
};
|
||||
|
||||
export function buildPluginFolderAgentActionPrompt(
|
||||
|
|
@ -20,6 +17,9 @@ export function buildPluginFolderAgentActionPrompt(
|
|||
action: PluginFolderAgentAction,
|
||||
): string {
|
||||
const folderPath = normalizePluginFolderPath(relativePath);
|
||||
if (action === 'contribute') {
|
||||
return buildContributePrompt(folderPath);
|
||||
}
|
||||
return [
|
||||
ACTION_TITLES[action],
|
||||
'',
|
||||
|
|
@ -33,6 +33,67 @@ export function buildPluginFolderAgentActionPrompt(
|
|||
].join('\n');
|
||||
}
|
||||
|
||||
// `contribute` opens a draft PR against the `nexu-io/open-design` community
|
||||
// catalog. The agent drives the whole git/gh sequence — fork, branch, copy
|
||||
// the plugin into `plugins/community/<name>/`, commit, push, then hand the
|
||||
// `gh pr create --web` URL back so the author reviews and clicks Create in
|
||||
// their browser. Two design constraints encoded in the prompt:
|
||||
// - `--web` flag preserves the author's final review window (see
|
||||
// `apps/daemon/src/plugins/publish.ts` "We never POST anywhere" — the
|
||||
// author always sees the PR form before it lands).
|
||||
// - Hard ban on `AskUserQuestion`: a previous run stalled for 600s when
|
||||
// the agent paused mid-turn waiting for a host answer card that the
|
||||
// user expected the plugin-folder buttons to satisfy.
|
||||
function buildContributePrompt(folderPath: string): string {
|
||||
return [
|
||||
'Open a draft Pull Request that adds this generated plugin to the Open Design community catalog at `nexu-io/open-design`.',
|
||||
'The goal is to end this turn with a single PR URL the user can click in their browser to review the pre-filled form and press Create.',
|
||||
'',
|
||||
`Plugin folder: \`${folderPath}\``,
|
||||
`Manifest: \`${folderPath}/open-design.json\``,
|
||||
'',
|
||||
'Run the steps below in order. Report each command and its result. Stop on the first hard failure — do not retry blindly.',
|
||||
'',
|
||||
'1. **Pre-flight.** Check `gh --version` and `gh auth status`. If `gh` is missing or not logged in, print the exact install/login command for the user\'s platform and STOP — do not try to install anything yourself.',
|
||||
'',
|
||||
`2. **Read manifest.** Load \`${folderPath}/open-design.json\` and capture \`name\`, \`title\`, \`description\`, and \`version\`. These drive the PR title, body, and target path.`,
|
||||
'',
|
||||
'3. **Resolve author identity.** Run `gh api user --jq .login` to get the author\'s GitHub login.',
|
||||
'',
|
||||
'4. **Fork the registry repo.** Run `gh repo fork nexu-io/open-design --remote=false`. Tolerate "already exists" / "existing fork" — it is idempotent.',
|
||||
'',
|
||||
'5. **Prepare contribution branch.** In a fresh temp directory:',
|
||||
' - `gh repo clone <login>/open-design <tmp>` (clone the author\'s fork)',
|
||||
' - `cd <tmp>` and `git checkout -b plugin/<name>-<unix-timestamp>`',
|
||||
' - `mkdir -p plugins/community/<name>/`',
|
||||
` - Copy the plugin folder contents from \`${folderPath}\` into \`plugins/community/<name>/\` (use \`cp -R\` or equivalent; preserve the directory layout).`,
|
||||
' - `git add plugins/community/<name>`',
|
||||
' - `git commit -m "Add <title> plugin"` (use the author\'s configured git identity from `gh auth setup-git`; do not override `user.name`/`user.email`).',
|
||||
' - `git push -u origin plugin/<name>-<unix-timestamp>`',
|
||||
'',
|
||||
'6. **Open the PR in the browser.** Run:',
|
||||
' ```',
|
||||
' gh pr create \\',
|
||||
' --repo nexu-io/open-design \\',
|
||||
' --head <login>:plugin/<name>-<unix-timestamp> \\',
|
||||
' --base main \\',
|
||||
' --title "Add <title> plugin" \\',
|
||||
' --body "<short summary citing manifest name, version, and description>" \\',
|
||||
' --web',
|
||||
' ```',
|
||||
' The `--web` flag opens GitHub\'s PR-create form in the user\'s browser with the title and body pre-filled. **Do not omit `--web`. Do not auto-submit. Do not call `gh issue create`.** The author reviews the diff and clicks Create themselves.',
|
||||
'',
|
||||
'7. **Hand off.** Capture the URL `gh pr create --web` opened (the `https://github.com/<login>/open-design/pull/new/plugin/<name>-...` URL printed to stdout) and paste it into chat with a one-line instruction: "Open this URL and click Create to file the PR." Then end the turn.',
|
||||
'',
|
||||
'**Hard constraints.** Treat these as inviolable:',
|
||||
'- Do NOT call the `AskUserQuestion` tool at any point in this turn. This flow is fire-and-forget; no mid-turn questions.',
|
||||
'- Do NOT try to install `gh`, `git`, or any other binary. Detect-and-instruct only.',
|
||||
'- Do NOT auto-submit the PR. The final Create click is the author\'s.',
|
||||
'- Do NOT retry a failed step. Report the error and stop.',
|
||||
'- Do NOT call the legacy `od plugin publish --to open-design` CLI — that flow produces an issue URL, which is the old path we are replacing.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function normalizePluginFolderPath(relativePath: string): string {
|
||||
return relativePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
||||
}
|
||||
|
|
|
|||
83
apps/web/tests/components/pluginFolderActions.test.ts
Normal file
83
apps/web/tests/components/pluginFolderActions.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Contract test for the prompts the plugin-folder card buttons send to the
|
||||
// agent. `install` / `publish` use the simple shared template; `contribute`
|
||||
// is the PR-flow prompt that drives `gh repo fork → branch → commit →
|
||||
// gh pr create --web` end-to-end. The tests below lock the *shape* of each
|
||||
// prompt (keywords + folder interpolation) without coupling to exact wording,
|
||||
// so prose tweaks don't break the suite but accidental removal of a critical
|
||||
// step would.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPluginFolderAgentActionPrompt } from '../../src/components/design-files/pluginFolderActions';
|
||||
|
||||
const FOLDER = 'generated-plugin';
|
||||
|
||||
describe('buildPluginFolderAgentActionPrompt', () => {
|
||||
describe('install', () => {
|
||||
it('mentions the folder path and the supported install CLI', () => {
|
||||
const prompt = buildPluginFolderAgentActionPrompt(FOLDER, 'install');
|
||||
expect(prompt).toContain(`Plugin folder: \`${FOLDER}\``);
|
||||
expect(prompt).toContain('od plugin install --source');
|
||||
});
|
||||
});
|
||||
|
||||
describe('publish', () => {
|
||||
it('mentions the folder path and the supported publish CLI', () => {
|
||||
const prompt = buildPluginFolderAgentActionPrompt(FOLDER, 'publish');
|
||||
expect(prompt).toContain(`Plugin folder: \`${FOLDER}\``);
|
||||
expect(prompt).toContain('od plugin publish');
|
||||
});
|
||||
});
|
||||
|
||||
describe('contribute (PR-based flow)', () => {
|
||||
const prompt = buildPluginFolderAgentActionPrompt(FOLDER, 'contribute');
|
||||
|
||||
it('targets the nexu-io/open-design community catalog', () => {
|
||||
expect(prompt).toContain('nexu-io/open-design');
|
||||
expect(prompt).toContain('plugins/community/<name>/');
|
||||
});
|
||||
|
||||
it('drives the full PR flow via gh, not via the issue-URL CLI', () => {
|
||||
// The agent must drive raw gh commands rather than fall back to the
|
||||
// legacy `od plugin publish --to open-design` issue-URL launcher.
|
||||
expect(prompt).toContain('gh repo fork nexu-io/open-design');
|
||||
expect(prompt).toContain('gh repo clone');
|
||||
expect(prompt).toContain('git checkout -b plugin/');
|
||||
expect(prompt).toContain('gh pr create');
|
||||
// The legacy CLI is named in the prompt only as part of an explicit
|
||||
// ban ("Do NOT call the legacy `od plugin publish --to open-design`")
|
||||
// — verify the ban is in place, not the bare command.
|
||||
expect(prompt).toMatch(/do not call the legacy `od plugin publish --to open-design`/i);
|
||||
});
|
||||
|
||||
it('uses --web so the author confirms the PR in browser', () => {
|
||||
// The "author keeps the final review click" invariant — preserved from
|
||||
// 45f52d71's "We never POST anywhere" principle.
|
||||
expect(prompt).toContain('--web');
|
||||
expect(prompt).toMatch(/do not auto-submit/i);
|
||||
});
|
||||
|
||||
it('hard-bans AskUserQuestion to avoid 600s mid-turn stalls', () => {
|
||||
// Regression guard for the stall we observed during e2e: agent paused
|
||||
// mid-turn on an AskUserQuestion tool waiting for a host answer the
|
||||
// user never sent (they clicked the plugin-folder card instead).
|
||||
expect(prompt).toContain('AskUserQuestion');
|
||||
expect(prompt).toMatch(/do not call the `AskUserQuestion` tool|fire-and-forget/i);
|
||||
});
|
||||
|
||||
it('forbids the agent from installing tools or retrying failures', () => {
|
||||
expect(prompt).toMatch(/do not try to install/i);
|
||||
expect(prompt).toMatch(/do not retry/i);
|
||||
});
|
||||
|
||||
it('interpolates the actual folder path into manifest and copy steps', () => {
|
||||
// Sanity check that template-string interpolation didn't regress into
|
||||
// literal `${folderPath}` substrings (we already shipped that bug once).
|
||||
expect(prompt).toContain(`${FOLDER}/open-design.json`);
|
||||
expect(prompt).not.toContain('${folderPath}');
|
||||
});
|
||||
|
||||
it('ends by handing the PR URL back to chat', () => {
|
||||
expect(prompt).toMatch(/PR URL|pull\/new|paste it into chat/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue