mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
feat(daemon, web): enhance plugin sharing workflows and UI components
- Updated the plugin sharing prompts to utilize local daemon endpoints for publishing to GitHub and contributing to Open Design, streamlining the user experience. - Refactored the `PluginsView` and `PluginShareMenu` components to support new sharing functionalities, including confirmation modals and improved link handling. - Enhanced the CSS styles for the plugin share confirmation modal and related UI elements for better visual consistency. - Added tests to verify the functionality of the new sharing workflows and ensure proper integration within the existing plugin management system. This update significantly improves the plugin sharing experience, making it easier for users to publish and contribute their plugins effectively.
This commit is contained in:
parent
c9cc3b88c0
commit
9e196d34af
49 changed files with 2850 additions and 137 deletions
|
|
@ -1221,7 +1221,15 @@ function renderPluginSharePrompt({ action, sourcePlugin, stagedPath }) {
|
|||
`Publish the local Open Design plugin "${title}" as a new public GitHub repository.`,
|
||||
'',
|
||||
`The plugin source files have been copied into this project at \`${stagedPath}\`.`,
|
||||
'Use the GitHub CLI (`gh`) for GitHub operations. Check `gh auth status` first, create a clean repository from the staged plugin folder, push the initial commit, and report the final repository URL.',
|
||||
'Use the local daemon share endpoint so the publish flow runs through Open Design\'s validated GitHub path:',
|
||||
'',
|
||||
'```bash',
|
||||
`curl -sS -X POST "$OD_DAEMON_URL/api/projects/$OD_PROJECT_ID/plugins/publish-github" \\`,
|
||||
` -H 'content-type: application/json' \\`,
|
||||
` -d '${JSON.stringify({ path: stagedPath })}'`,
|
||||
'```',
|
||||
'',
|
||||
'Read the JSON response. If `ok` is true, report the final repository URL and any validation/log summary. If it fails, report the `message`, `code`, and the useful log lines. The endpoint checks `gh` auth and performs the repository creation; do not hand-roll a second GitHub flow unless you are explaining a daemon endpoint failure.',
|
||||
'',
|
||||
'Do not rewrite the plugin unless publishing requires a small metadata fix. If you make any fix, explain it before publishing.',
|
||||
].join('\n');
|
||||
|
|
@ -1230,7 +1238,15 @@ function renderPluginSharePrompt({ action, sourcePlugin, stagedPath }) {
|
|||
`Open a pull request to add the local Open Design plugin "${title}" to the Open Design repository.`,
|
||||
'',
|
||||
`The plugin source files have been copied into this project at \`${stagedPath}\`.`,
|
||||
'Use the GitHub CLI (`gh`) for GitHub operations. Check `gh auth status` first, fork or reuse the fork of `nexu-io/open-design`, create a branch, copy the staged plugin into `plugins/community/`, push the branch, and open a PR against `nexu-io/open-design:main`.',
|
||||
'Use the local daemon share endpoint so the contribution flow runs through Open Design\'s validated GitHub path:',
|
||||
'',
|
||||
'```bash',
|
||||
`curl -sS -X POST "$OD_DAEMON_URL/api/projects/$OD_PROJECT_ID/plugins/contribute-open-design" \\`,
|
||||
` -H 'content-type: application/json' \\`,
|
||||
` -d '${JSON.stringify({ path: stagedPath })}'`,
|
||||
'```',
|
||||
'',
|
||||
'Read the JSON response. If `ok` is true, report the PR URL, branch, and any validation/log summary. If it fails, report the `message`, `code`, and the useful log lines. The endpoint checks `gh` auth, forks/clones, pushes, and opens the PR; do not hand-roll a second GitHub flow unless you are explaining a daemon endpoint failure.',
|
||||
'',
|
||||
'Keep the PR focused on this plugin. Report the PR URL and any validation you ran.',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ describe('Plan §8 e2e-3 (entry slice) — headless install → project → run'
|
|||
await fetch(`${baseUrl}/api/runs/${encodeURIComponent(runBody.runId)}/cancel`, { method: 'POST' });
|
||||
});
|
||||
|
||||
it('creates a share project for publishing a user plugin to GitHub', async () => {
|
||||
it('creates share projects for publishing and contributing a user plugin', async () => {
|
||||
const installResp = await fetch(`${baseUrl}/api/plugins/install`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
|
||||
|
|
@ -249,7 +249,8 @@ describe('Plan §8 e2e-3 (entry slice) — headless install → project → run'
|
|||
expect(shareBody.appliedPluginSnapshotId).toBeTruthy();
|
||||
expect(shareBody.stagedPath).toBe('plugin-source/sample-plugin');
|
||||
expect(shareBody.prompt).toContain('Publish the local Open Design plugin');
|
||||
expect(shareBody.prompt).toContain('gh');
|
||||
expect(shareBody.prompt).toContain('/api/projects/$OD_PROJECT_ID/plugins/publish-github');
|
||||
expect(shareBody.prompt).toContain('plugin-source/sample-plugin');
|
||||
expect(shareBody.project.pendingPrompt).toBe(shareBody.prompt);
|
||||
|
||||
const filesResp = await fetch(
|
||||
|
|
@ -274,6 +275,127 @@ describe('Plan §8 e2e-3 (entry slice) — headless install → project → run'
|
|||
source_plugin_id: 'sample-plugin',
|
||||
plugin_context_path: 'plugin-source/sample-plugin',
|
||||
});
|
||||
|
||||
const contributeResp = await fetch(`${baseUrl}/api/plugins/sample-plugin/share-project`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'contribute-open-design', locale: 'en' }),
|
||||
});
|
||||
expect(contributeResp.status).toBe(200);
|
||||
const contributeBody = (await contributeResp.json()) as {
|
||||
ok: boolean;
|
||||
project: { id: string };
|
||||
appliedPluginSnapshotId?: string;
|
||||
actionPluginId: string;
|
||||
sourcePluginId: string;
|
||||
stagedPath: string;
|
||||
prompt: string;
|
||||
};
|
||||
expect(contributeBody.ok).toBe(true);
|
||||
expect(contributeBody.actionPluginId).toBe('od-plugin-contribute-open-design');
|
||||
expect(contributeBody.sourcePluginId).toBe('sample-plugin');
|
||||
expect(contributeBody.appliedPluginSnapshotId).toBeTruthy();
|
||||
expect(contributeBody.stagedPath).toBe('plugin-source/sample-plugin');
|
||||
expect(contributeBody.prompt).toContain('/api/projects/$OD_PROJECT_ID/plugins/contribute-open-design');
|
||||
|
||||
const locator = process.platform === 'win32' ? 'where' : 'which';
|
||||
const realGit = ((await execFileP(locator, ['git'])).stdout as string)
|
||||
.split(/\r?\n/)
|
||||
.find(Boolean)
|
||||
?.trim();
|
||||
expect(realGit).toBeTruthy();
|
||||
const previousRealGit = process.env.OD_REAL_GIT;
|
||||
process.env.OD_REAL_GIT = realGit;
|
||||
try {
|
||||
await withFakeAgent(
|
||||
'gh',
|
||||
`
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const args = process.argv.slice(2);
|
||||
function ok(text) {
|
||||
if (text) process.stdout.write(text + '\\n');
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === '--version') ok('gh version 2.0.0');
|
||||
if (args[0] === 'auth' && args[1] === 'status') ok('Logged in to github.com as test-user');
|
||||
if (args[0] === 'api' && args[1] === 'user') ok('test-user');
|
||||
if (args[0] === 'repo' && args[1] === 'create') ok('https://github.com/test-user/' + args[2]);
|
||||
if (args[0] === 'repo' && args[1] === 'view') ok('https://github.com/test-user/' + path.basename(process.cwd()));
|
||||
if (args[0] === 'repo' && args[1] === 'fork') ok('forked nexu-io/open-design');
|
||||
if (args[0] === 'repo' && args[1] === 'clone') {
|
||||
const dest = args[3] || path.basename(args[2]);
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
const init = spawnSync(process.env.OD_REAL_GIT, ['init'], { cwd: dest, stdio: 'inherit' });
|
||||
process.exit(init.status ?? 0);
|
||||
}
|
||||
if (args[0] === 'pr' && args[1] === 'create') ok('https://github.com/nexu-io/open-design/pull/123');
|
||||
console.error('unexpected gh command: ' + args.join(' '));
|
||||
process.exit(1);
|
||||
`,
|
||||
async () => {
|
||||
await withFakeAgent(
|
||||
'git',
|
||||
`
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const args = process.argv.slice(2);
|
||||
if (args[0] === 'push') {
|
||||
console.log('pushed');
|
||||
process.exit(0);
|
||||
}
|
||||
const result = spawnSync(process.env.OD_REAL_GIT, args, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
if (result.stdout) process.stdout.write(result.stdout);
|
||||
if (result.stderr) process.stderr.write(result.stderr);
|
||||
process.exit(result.status ?? 0);
|
||||
`,
|
||||
async () => {
|
||||
const publishEndpointResp = await fetch(
|
||||
`${baseUrl}/api/projects/${encodeURIComponent(shareBody.project.id)}/plugins/publish-github`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ path: shareBody.stagedPath }),
|
||||
},
|
||||
);
|
||||
expect(publishEndpointResp.status).toBe(200);
|
||||
const publishEndpointBody = (await publishEndpointResp.json()) as {
|
||||
ok: boolean;
|
||||
url?: string;
|
||||
};
|
||||
expect(publishEndpointBody.ok).toBe(true);
|
||||
expect(publishEndpointBody.url).toBe('https://github.com/test-user/sample-plugin');
|
||||
|
||||
const contributeEndpointResp = await fetch(
|
||||
`${baseUrl}/api/projects/${encodeURIComponent(contributeBody.project.id)}/plugins/contribute-open-design`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ path: contributeBody.stagedPath }),
|
||||
},
|
||||
);
|
||||
expect(contributeEndpointResp.status).toBe(200);
|
||||
const contributeEndpointBody = (await contributeEndpointResp.json()) as {
|
||||
ok: boolean;
|
||||
url?: string;
|
||||
};
|
||||
expect(contributeEndpointBody.ok).toBe(true);
|
||||
expect(contributeEndpointBody.url).toBe('https://github.com/nexu-io/open-design/pull/123');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (previousRealGit === undefined) {
|
||||
delete process.env.OD_REAL_GIT;
|
||||
} else {
|
||||
process.env.OD_REAL_GIT = previousRealGit;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('runs the CLI install → project create → plugin run path with query and local SKILL.md in the agent prompt', async () => {
|
||||
|
|
|
|||
|
|
@ -621,13 +621,19 @@ export function App() {
|
|||
// If sessionStorage is unavailable, the project still opens with
|
||||
// the prepared prompt in the composer.
|
||||
}
|
||||
const project = outcome.appliedPluginSnapshotId
|
||||
? {
|
||||
...outcome.project,
|
||||
appliedPluginSnapshotId: outcome.appliedPluginSnapshotId,
|
||||
}
|
||||
: outcome.project;
|
||||
setProjects((curr) => [
|
||||
outcome.project,
|
||||
...curr.filter((p) => p.id !== outcome.project.id),
|
||||
project,
|
||||
...curr.filter((p) => p.id !== project.id),
|
||||
]);
|
||||
navigate({
|
||||
kind: 'project',
|
||||
projectId: outcome.project.id,
|
||||
projectId: project.id,
|
||||
fileName: null,
|
||||
});
|
||||
return outcome;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { ApplyResult, InstalledPluginRecord, PluginSourceKind } from '@open-design/contracts';
|
||||
import {
|
||||
PLUGIN_SHARE_ACTION_PLUGIN_IDS,
|
||||
type ApplyResult,
|
||||
type InstalledPluginRecord,
|
||||
type PluginSourceKind,
|
||||
} from '@open-design/contracts';
|
||||
import {
|
||||
applyPlugin,
|
||||
installPluginSource,
|
||||
|
|
@ -40,6 +45,39 @@ const PLUGINS_TABS: ReadonlyArray<{
|
|||
{ id: 'team', label: 'Team / Enterprise', hint: 'Coming soon' },
|
||||
];
|
||||
|
||||
const PLUGIN_SHARE_DETAILS: Record<PluginShareAction, {
|
||||
eyebrow: string;
|
||||
fallbackTitle: string;
|
||||
fallbackDescription: string;
|
||||
confirmLabel: string;
|
||||
steps: string[];
|
||||
}> = {
|
||||
'publish-github': {
|
||||
eyebrow: 'GitHub repository',
|
||||
fallbackTitle: 'Publish Plugin to GitHub',
|
||||
fallbackDescription:
|
||||
'Creates a public GitHub repository for this local Open Design plugin.',
|
||||
confirmLabel: 'Start publishing',
|
||||
steps: [
|
||||
'Create a new Open Design project for the publish workflow.',
|
||||
'Copy this plugin into that project as isolated source context.',
|
||||
'Run the official publish action plugin against the local daemon.',
|
||||
],
|
||||
},
|
||||
'contribute-open-design': {
|
||||
eyebrow: 'Open Design pull request',
|
||||
fallbackTitle: 'Contribute Plugin to Open Design',
|
||||
fallbackDescription:
|
||||
'Opens a pull request that adds this plugin to the Open Design community catalog.',
|
||||
confirmLabel: 'Start contribution',
|
||||
steps: [
|
||||
'Create a new Open Design project for the contribution workflow.',
|
||||
'Copy this plugin into that project as isolated source context.',
|
||||
'Run the official contribution action plugin against the local daemon.',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
interface PluginsViewProps {
|
||||
onCreatePlugin?: (goal?: string) => void;
|
||||
onCreatePluginShareProject?: (
|
||||
|
|
@ -69,6 +107,11 @@ export function PluginsView({
|
|||
result: ApplyResult;
|
||||
} | null>(null);
|
||||
const [detailsRecord, setDetailsRecord] = useState<InstalledPluginRecord | null>(null);
|
||||
const [shareConfirm, setShareConfirm] = useState<{
|
||||
sourceRecord: InstalledPluginRecord;
|
||||
action: PluginShareAction;
|
||||
actionRecord: InstalledPluginRecord | null;
|
||||
} | null>(null);
|
||||
const [notice, setNotice] = useState<PluginInstallOutcome | { ok: boolean; message: string } | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
|
|
@ -135,12 +178,14 @@ export function PluginsView({
|
|||
ok: false,
|
||||
message: 'Plugin sharing is not available in this shell.',
|
||||
});
|
||||
setShareConfirm(null);
|
||||
return;
|
||||
}
|
||||
setPendingShareAction({ pluginId: record.id, action });
|
||||
setNotice(null);
|
||||
const outcome = await onCreatePluginShareProject(record.id, action, locale);
|
||||
setPendingShareAction(null);
|
||||
setShareConfirm(null);
|
||||
if (!outcome.ok) {
|
||||
setNotice({
|
||||
ok: false,
|
||||
|
|
@ -149,6 +194,15 @@ export function PluginsView({
|
|||
}
|
||||
}
|
||||
|
||||
function requestPluginShareTask(
|
||||
record: InstalledPluginRecord,
|
||||
action: PluginShareAction,
|
||||
) {
|
||||
const actionRecord =
|
||||
plugins.find((plugin) => plugin.id === PLUGIN_SHARE_ACTION_PLUGIN_IDS[action]) ?? null;
|
||||
setShareConfirm({ sourceRecord: record, action, actionRecord });
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="plugins-view" aria-labelledby="plugins-title">
|
||||
<header className="plugins-view__hero">
|
||||
|
|
@ -256,7 +310,7 @@ export function PluginsView({
|
|||
onUse={(record) => void handleUsePlugin(record)}
|
||||
onOpenDetails={setDetailsRecord}
|
||||
onPluginShareAction={(record, action) =>
|
||||
void handleCreatePluginShareTask(record, action)
|
||||
requestPluginShareTask(record, action)
|
||||
}
|
||||
onCreatePlugin={onCreatePlugin}
|
||||
title="My plugins"
|
||||
|
|
@ -280,6 +334,26 @@ export function PluginsView({
|
|||
isApplying={pendingApplyId === detailsRecord.id}
|
||||
/>
|
||||
) : null}
|
||||
{shareConfirm ? (
|
||||
<PluginShareConfirmModal
|
||||
sourceRecord={shareConfirm.sourceRecord}
|
||||
action={shareConfirm.action}
|
||||
actionRecord={shareConfirm.actionRecord}
|
||||
pending={
|
||||
pendingShareAction?.pluginId === shareConfirm.sourceRecord.id &&
|
||||
pendingShareAction.action === shareConfirm.action
|
||||
}
|
||||
onClose={() => {
|
||||
if (!pendingShareAction) setShareConfirm(null);
|
||||
}}
|
||||
onConfirm={() =>
|
||||
void handleCreatePluginShareTask(
|
||||
shareConfirm.sourceRecord,
|
||||
shareConfirm.action,
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{importOpen ? (
|
||||
<PluginImportModal
|
||||
onClose={() => setImportOpen(false)}
|
||||
|
|
@ -292,6 +366,168 @@ export function PluginsView({
|
|||
);
|
||||
}
|
||||
|
||||
function PluginShareConfirmModal({
|
||||
sourceRecord,
|
||||
action,
|
||||
actionRecord,
|
||||
pending,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
sourceRecord: InstalledPluginRecord;
|
||||
action: PluginShareAction;
|
||||
actionRecord: InstalledPluginRecord | null;
|
||||
pending: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const details = PLUGIN_SHARE_DETAILS[action];
|
||||
const actionTitle = actionRecord?.title ?? details.fallbackTitle;
|
||||
const actionDescription =
|
||||
actionRecord?.manifest?.description ?? details.fallbackDescription;
|
||||
const actionQuery = readLocalizedUseCaseQuery(actionRecord);
|
||||
const stagedPath = `plugin-source/${pluginShareSlug(sourceRecord.id)}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="plugin-details-modal-backdrop plugin-share-confirm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`${actionTitle} for ${sourceRecord.title}`}
|
||||
onClick={(event) => {
|
||||
if (!pending && event.target === event.currentTarget) onClose();
|
||||
}}
|
||||
data-testid="plugin-share-confirm-modal"
|
||||
>
|
||||
<div className="plugin-details-modal plugin-share-confirm__panel">
|
||||
<header className="plugin-details-modal__head">
|
||||
<div className="plugin-details-modal__head-titles">
|
||||
<div className="plugin-details-modal__head-row">
|
||||
<h2 className="plugin-details-modal__title">{actionTitle}</h2>
|
||||
<span className="plugin-details-modal__trust trust-bundled">
|
||||
Action plugin
|
||||
</span>
|
||||
</div>
|
||||
<div className="plugin-details-modal__meta">
|
||||
<span>{details.eyebrow}</span>
|
||||
<span>· for {sourceRecord.title}</span>
|
||||
{actionRecord ? <span>· v{actionRecord.version}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="plugin-details-modal__close"
|
||||
onClick={onClose}
|
||||
disabled={pending}
|
||||
aria-label="Close share confirmation"
|
||||
title="Close"
|
||||
>
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="plugin-details-modal__body">
|
||||
<section className="plugin-details-modal__section">
|
||||
<div className="plugin-details-modal__section-head">
|
||||
<h3 className="plugin-details-modal__section-title">
|
||||
What this starts
|
||||
</h3>
|
||||
</div>
|
||||
<p className="plugin-details-modal__description">
|
||||
{actionDescription}
|
||||
</p>
|
||||
<ol className="plugin-share-confirm__steps">
|
||||
{details.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section className="plugin-details-modal__section">
|
||||
<div className="plugin-details-modal__section-head">
|
||||
<h3 className="plugin-details-modal__section-title">
|
||||
Source plugin
|
||||
</h3>
|
||||
</div>
|
||||
<dl className="plugin-share-confirm__facts">
|
||||
<div>
|
||||
<dt>Plugin</dt>
|
||||
<dd>{sourceRecord.title}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>ID</dt>
|
||||
<dd>
|
||||
<code>{sourceRecord.id}</code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Copied to</dt>
|
||||
<dd>
|
||||
<code>{stagedPath}</code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Trust</dt>
|
||||
<dd>{sourceRecord.trust}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{actionQuery ? (
|
||||
<section className="plugin-details-modal__section">
|
||||
<div className="plugin-details-modal__section-head">
|
||||
<h3 className="plugin-details-modal__section-title">
|
||||
Action prompt
|
||||
</h3>
|
||||
</div>
|
||||
<pre className="plugin-details-modal__query">{actionQuery}</pre>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<footer className="plugin-details-modal__foot">
|
||||
<button
|
||||
type="button"
|
||||
className="plugin-details-modal__secondary"
|
||||
onClick={onClose}
|
||||
disabled={pending}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="plugin-details-modal__primary"
|
||||
onClick={onConfirm}
|
||||
disabled={pending}
|
||||
aria-busy={pending ? 'true' : undefined}
|
||||
data-testid="plugin-share-confirm-start"
|
||||
>
|
||||
{pending ? 'Starting…' : details.confirmLabel}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function readLocalizedUseCaseQuery(record: InstalledPluginRecord | null): string | null {
|
||||
const query = record?.manifest?.od?.useCase?.query;
|
||||
if (typeof query === 'string' && query.trim()) return query.trim();
|
||||
if (!query || typeof query !== 'object') return null;
|
||||
const dict = query as Record<string, unknown>;
|
||||
const preferred = dict.en ?? Object.values(dict).find((value) => typeof value === 'string');
|
||||
return typeof preferred === 'string' && preferred.trim() ? preferred.trim() : null;
|
||||
}
|
||||
|
||||
function pluginShareSlug(name: string): string {
|
||||
return (
|
||||
name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, '-')
|
||||
.replace(/(^[-._]+|[-._]+$)/g, '') || 'open-design-plugin'
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="plugins-view__stat">
|
||||
|
|
|
|||
|
|
@ -74,15 +74,19 @@ export const HOME_HERO_CHIPS: ReadonlyArray<HomeHeroChip> = [
|
|||
label: 'Prototype',
|
||||
icon: 'palette',
|
||||
group: 'create',
|
||||
// Prototype now binds to the bundled `example-web-prototype` plugin,
|
||||
// which ships `assets/template.html` (single-file HTML prototype
|
||||
// seed), `references/layouts.md` (paste-ready section layouts), and
|
||||
// a P0 checklist. The previous routing to the generic
|
||||
// od-new-generation router left the agent to invent every section's
|
||||
// CSS, producing inconsistent type scales and density between turns.
|
||||
// Web-prototype's manifest has no `inputs` field, so the
|
||||
// chip-supplied inputs tuned for od-new-generation's
|
||||
// `{{artifactKind}} / {{audience}} / {{topic}}` template are dropped.
|
||||
action: {
|
||||
kind: 'apply-scenario',
|
||||
pluginId: 'od-new-generation',
|
||||
pluginId: 'example-web-prototype',
|
||||
projectKind: 'prototype',
|
||||
inputs: {
|
||||
artifactKind: 'interactive prototype',
|
||||
audience: 'product teams',
|
||||
topic: 'a new product experience',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -91,20 +95,15 @@ export const HOME_HERO_CHIPS: ReadonlyArray<HomeHeroChip> = [
|
|||
icon: 'pencil',
|
||||
group: 'create',
|
||||
hint: 'Build an interactive HTML/CSS/JS artifact you can preview live.',
|
||||
// No dedicated scenario plugin yet — the live-artifact authoring
|
||||
// flow shares od-new-generation's pipeline (file-write + live-
|
||||
// artifact atoms). We still surface it as a separate chip so the
|
||||
// user can pick their target surface up front instead of routing
|
||||
// through Prototype + a metadata flip.
|
||||
// Live artifact shares web-prototype's seed today — the difference
|
||||
// is intent (interactive HTML/CSS/JS) vs static prototype, not the
|
||||
// underlying template. The chip keeps a distinct id so active-state
|
||||
// tracking + analytics see "user picked live-artifact" rather than
|
||||
// "user picked prototype".
|
||||
action: {
|
||||
kind: 'apply-scenario',
|
||||
pluginId: 'od-new-generation',
|
||||
pluginId: 'example-web-prototype',
|
||||
projectKind: 'prototype',
|
||||
inputs: {
|
||||
artifactKind: 'live HTML artifact',
|
||||
audience: 'product teams',
|
||||
topic: 'an interactive product concept',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -112,15 +111,20 @@ export const HOME_HERO_CHIPS: ReadonlyArray<HomeHeroChip> = [
|
|||
label: 'Slide deck',
|
||||
icon: 'present',
|
||||
group: 'create',
|
||||
// Slide deck binds to `example-simple-deck`, which ships a 353-line
|
||||
// `assets/template.html` (the 1920×1080 + scale-to-fit + nav + print
|
||||
// framework paired with proven slide CSS), 8 paste-ready layouts in
|
||||
// `references/layouts.md` (cover, body, big-stat, three-point,
|
||||
// pipeline, dark quote, before/after, closing), and a P0/P1/P2
|
||||
// checklist that catches overflow at 1280×800 / 1440×900. The
|
||||
// previous routing to od-new-generation gave the agent only the
|
||||
// generic deck-framework directive — which fixed nav but not slide
|
||||
// layout — so density bugs (168px headline + absolute footer
|
||||
// collision) shipped on default decks.
|
||||
action: {
|
||||
kind: 'apply-scenario',
|
||||
pluginId: 'od-new-generation',
|
||||
pluginId: 'example-simple-deck',
|
||||
projectKind: 'deck',
|
||||
inputs: {
|
||||
artifactKind: 'slide deck',
|
||||
audience: 'stakeholders',
|
||||
topic: 'a product strategy',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ interface ShareItem {
|
|||
copies?: boolean;
|
||||
}
|
||||
|
||||
interface ShareLinkItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: 'github' | 'external-link' | 'eye';
|
||||
href: string;
|
||||
}
|
||||
|
||||
function buildInstallCommand(record: InstalledPluginRecord): string {
|
||||
// The daemon's install resolver accepts the raw `record.source`
|
||||
// shape for every kind (github:owner/repo[@ref][/sub], https URL,
|
||||
|
|
@ -145,17 +152,18 @@ export function PluginShareMenu({ record, variant = 'default' }: Props) {
|
|||
},
|
||||
];
|
||||
|
||||
// Open-in-tab actions only render when the underlying URL exists.
|
||||
const openItems: ShareItem[] = [];
|
||||
if (links.sourceUrl && record.sourceKind === 'github') {
|
||||
// Open-in-tab actions are real anchors so users can right-click,
|
||||
// copy the link address, or open in a new tab from browser chrome.
|
||||
const openItems: ShareLinkItem[] = [];
|
||||
if (links.sourceUrl) {
|
||||
openItems.push({
|
||||
key: 'github',
|
||||
label: 'Open source on GitHub',
|
||||
icon: 'github',
|
||||
onSelect: () => {
|
||||
window.open(links.sourceUrl!, '_blank', 'noopener,noreferrer');
|
||||
setOpen(false);
|
||||
},
|
||||
key: 'source',
|
||||
label:
|
||||
record.sourceKind === 'github' || links.sourceUrl.includes('github.com/')
|
||||
? 'Open source on GitHub'
|
||||
: 'Open source',
|
||||
icon: links.sourceUrl.includes('github.com/') ? 'github' : 'external-link',
|
||||
href: links.sourceUrl,
|
||||
});
|
||||
}
|
||||
if (links.homepageUrl) {
|
||||
|
|
@ -163,20 +171,14 @@ export function PluginShareMenu({ record, variant = 'default' }: Props) {
|
|||
key: 'homepage',
|
||||
label: 'Open homepage',
|
||||
icon: 'external-link',
|
||||
onSelect: () => {
|
||||
window.open(links.homepageUrl!, '_blank', 'noopener,noreferrer');
|
||||
setOpen(false);
|
||||
},
|
||||
href: links.homepageUrl,
|
||||
});
|
||||
}
|
||||
openItems.push({
|
||||
key: 'marketplace',
|
||||
label: 'Open in marketplace',
|
||||
icon: 'eye',
|
||||
onSelect: () => {
|
||||
window.open(buildShareUrl(record), '_blank', 'noopener,noreferrer');
|
||||
setOpen(false);
|
||||
},
|
||||
href: buildShareUrl(record),
|
||||
});
|
||||
|
||||
const triggerClass =
|
||||
|
|
@ -225,16 +227,18 @@ export function PluginShareMenu({ record, variant = 'default' }: Props) {
|
|||
<div className="plugin-share-popover__divider" />
|
||||
<div className="plugin-share-popover__group">
|
||||
{openItems.map((item) => (
|
||||
<button
|
||||
<a
|
||||
key={item.key}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="plugin-share-item"
|
||||
onClick={() => void item.onSelect()}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Icon name={item.icon} size={12} />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -153,14 +153,15 @@ export function PluginCard({
|
|||
onClick={() => onShareAction(record, 'publish-github')}
|
||||
disabled={pendingAny || shareBusy}
|
||||
aria-busy={sharePendingAction === 'publish-github' ? 'true' : undefined}
|
||||
title="Publish as a GitHub repository"
|
||||
aria-label={`Publish ${record.title} as a GitHub repository`}
|
||||
title="Publish plugin as a GitHub repository"
|
||||
data-testid={`plugins-home-publish-github-${record.id}`}
|
||||
>
|
||||
<Icon
|
||||
name={sharePendingAction === 'publish-github' ? 'spinner' : 'github'}
|
||||
size={12}
|
||||
/>
|
||||
<span>{sharePendingAction === 'publish-github' ? 'Starting…' : 'Repo'}</span>
|
||||
<span>{sharePendingAction === 'publish-github' ? 'Starting…' : 'Publish'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -168,14 +169,15 @@ export function PluginCard({
|
|||
onClick={() => onShareAction(record, 'contribute-open-design')}
|
||||
disabled={pendingAny || shareBusy}
|
||||
aria-busy={sharePendingAction === 'contribute-open-design' ? 'true' : undefined}
|
||||
title="Open an Open Design pull request"
|
||||
aria-label={`Contribute ${record.title} to Open Design`}
|
||||
title="Contribute plugin to Open Design with a pull request"
|
||||
data-testid={`plugins-home-contribute-open-design-${record.id}`}
|
||||
>
|
||||
<Icon
|
||||
name={sharePendingAction === 'contribute-open-design' ? 'spinner' : 'share'}
|
||||
size={12}
|
||||
/>
|
||||
<span>{sharePendingAction === 'contribute-open-design' ? 'Starting…' : 'PR'}</span>
|
||||
<span>{sharePendingAction === 'contribute-open-design' ? 'Starting…' : 'Contribute'}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export const GUIDE_SECTIONS: GuideSection[] = [
|
|||
'MCP server — wires Open Design as a Model Context Protocol server so any MCP-capable agent can list skills, run scenarios, and read artifacts.',
|
||||
'HTTP API — `http://127.0.0.1:7456/api/*` REST + SSE endpoints; the same surface the web UI uses.',
|
||||
'Skills — drop-in `SKILL.md` packs (Claude-compatible) that any agent already on your PATH can invoke without Open Design at all.',
|
||||
'Standard artifacts — seed real HTML projects from Skills, bundled default plugins, and community plugin examples before the daemon starts.',
|
||||
],
|
||||
snippets: [
|
||||
{
|
||||
|
|
@ -65,6 +66,15 @@ export const GUIDE_SECTIONS: GuideSection[] = [
|
|||
language: 'bash',
|
||||
body: 'curl -s http://127.0.0.1:7456/api/health | jq',
|
||||
},
|
||||
{
|
||||
label: 'Ingest standard artifacts before boot',
|
||||
language: 'bash',
|
||||
body:
|
||||
'pnpm seed:test-projects --offline --data-dir ./.od \\\n' +
|
||||
' --decks 2 --webs 2 --default-plugins 3 --community-plugins 3\n' +
|
||||
'# Then start Open Design in the shell you normally use for dev:\n' +
|
||||
'pnpm tools-dev',
|
||||
},
|
||||
],
|
||||
footer:
|
||||
'The daemon writes to `./.od/` (project-local) by default. Set ' +
|
||||
|
|
@ -113,6 +123,14 @@ export const GUIDE_SECTIONS: GuideSection[] = [
|
|||
language: 'bash',
|
||||
body: 'od skills list --json\nod design-systems list --json',
|
||||
},
|
||||
{
|
||||
label: 'Check seeded artifacts through the CLI',
|
||||
language: 'bash',
|
||||
body:
|
||||
'od project list --daemon-url http://127.0.0.1:7456\n' +
|
||||
'od files list <seed-project-id> --daemon-url http://127.0.0.1:7456\n' +
|
||||
'od files read <seed-project-id> index.html --daemon-url http://127.0.0.1:7456 | head',
|
||||
},
|
||||
{
|
||||
label: 'Verify environment + detected agents (Claude, Codex, Cursor, …)',
|
||||
language: 'bash',
|
||||
|
|
@ -238,6 +256,7 @@ export const GUIDE_SECTIONS: GuideSection[] = [
|
|||
'Symlink one skill into multiple projects to share it without copying.',
|
||||
'Each skill can declare connectors, atoms, design-system requirements, and a `preview` example output for the gallery.',
|
||||
'Headless: an agent with `od` on its PATH can call `od skills list` then run any skill; the daemon is optional for read-only flows.',
|
||||
'`pnpm seed:test-projects` exercises the same artifact shape with default plugin examples and community plugin examples, then stores the resulting `index.html` projects as reusable test data.',
|
||||
],
|
||||
snippets: [
|
||||
{
|
||||
|
|
@ -274,6 +293,18 @@ export const GUIDE_SECTIONS: GuideSection[] = [
|
|||
language: 'bash',
|
||||
body: 'od skills list --json | jq \'.skills[].name\'',
|
||||
},
|
||||
{
|
||||
label: 'Headless artifact fixture bundle',
|
||||
language: 'bash',
|
||||
body:
|
||||
'pnpm seed:test-projects --offline --data-dir ./.od \\\n' +
|
||||
' --decks 2 --webs 2 \\\n' +
|
||||
' --default-plugins 3 --community-plugins 3\n' +
|
||||
'# Shell 1: start Open Design after ingesting.\n' +
|
||||
'pnpm tools-dev\n' +
|
||||
'# Shell 2: inspect the produced projects.\n' +
|
||||
'od project list --json --daemon-url http://127.0.0.1:7456',
|
||||
},
|
||||
],
|
||||
footer:
|
||||
'Spec: `docs/skills-protocol.md` and `docs/agent-adapters.md` cover ' +
|
||||
|
|
|
|||
|
|
@ -16303,6 +16303,57 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
cursor: progress;
|
||||
}
|
||||
|
||||
.plugin-share-confirm__panel {
|
||||
max-width: 720px;
|
||||
}
|
||||
.plugin-share-confirm__steps {
|
||||
margin: 12px 0 0;
|
||||
padding-left: 20px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.plugin-share-confirm__steps li + li {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.plugin-share-confirm__facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.plugin-share-confirm__facts > div {
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
background: var(--bg);
|
||||
}
|
||||
.plugin-share-confirm__facts dt {
|
||||
margin: 0 0 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.plugin-share-confirm__facts dd {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.plugin-share-confirm__facts code {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.plugin-share-confirm__facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Plugin info pane — adapter for embedding the manifest-driven meta
|
||||
sections inside narrow contexts (PreviewModal sidebar, design-system
|
||||
sidebar). Tightens padding, shrinks chip type, and wraps long file
|
||||
|
|
@ -16609,6 +16660,7 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
color: var(--text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
}
|
||||
.plugin-share-item:hover,
|
||||
.plugin-share-item:focus-visible {
|
||||
|
|
|
|||
|
|
@ -117,9 +117,13 @@ describe('HomeHero intent rail', () => {
|
|||
expect(findChip('audio')?.action).toMatchObject({ pluginId: 'od-media-generation', projectKind: 'audio' });
|
||||
});
|
||||
|
||||
it('non-media scenario chips route to od-new-generation', () => {
|
||||
expect(findChip('prototype')?.action).toMatchObject({ pluginId: 'od-new-generation', projectKind: 'prototype' });
|
||||
expect(findChip('deck')?.action).toMatchObject({ pluginId: 'od-new-generation', projectKind: 'deck' });
|
||||
it('prototype and slide-deck chips route to their specialised bundled scenario plugin', () => {
|
||||
// Prototype now binds to web-prototype's seed template instead of
|
||||
// the generic od-new-generation router. Same for Slide deck →
|
||||
// simple-deck. See packages/contracts/src/plugins/scenario-defaults.ts
|
||||
// for the rationale (battle-tested seed + layouts + checklist).
|
||||
expect(findChip('prototype')?.action).toMatchObject({ pluginId: 'example-web-prototype', projectKind: 'prototype' });
|
||||
expect(findChip('deck')?.action).toMatchObject({ pluginId: 'example-simple-deck', projectKind: 'deck' });
|
||||
});
|
||||
|
||||
it('specialised category chips route to their bundled scenario plugin', () => {
|
||||
|
|
@ -131,12 +135,13 @@ describe('HomeHero intent rail', () => {
|
|||
pluginId: 'example-hyperframes',
|
||||
projectKind: 'video',
|
||||
});
|
||||
// Live artifact reuses od-new-generation's pipeline today but
|
||||
// keeps a distinct chip id + label so the rail's active state
|
||||
// tracks user intent independently from the Prototype chip.
|
||||
// Live artifact shares web-prototype's seed (interactive HTML/CSS/JS
|
||||
// is a flavour of prototype) but keeps a distinct chip id + label
|
||||
// so the rail's active state tracks user intent independently from
|
||||
// the Prototype chip.
|
||||
expect(findChip('live-artifact')?.action).toMatchObject({
|
||||
kind: 'apply-scenario',
|
||||
pluginId: 'od-new-generation',
|
||||
pluginId: 'example-web-prototype',
|
||||
projectKind: 'prototype',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,6 +63,25 @@ const HIDDEN_DEFAULT_PLUGIN = {
|
|||
},
|
||||
};
|
||||
|
||||
// The Prototype / Live-artifact chips now bind to the bundled
|
||||
// `example-web-prototype` plugin (which ships its own seed +
|
||||
// layouts + checklist) instead of the generic od-new-generation
|
||||
// router. Mirror that here so the chip-applies test can find a
|
||||
// matching plugin record and the apply call resolves to the new id.
|
||||
const WEB_PROTOTYPE_PLUGIN = {
|
||||
...DEFAULT_PLUGIN,
|
||||
id: 'example-web-prototype',
|
||||
title: 'Web Prototype',
|
||||
source: '/tmp/web-prototype',
|
||||
fsPath: '/tmp/web-prototype',
|
||||
manifest: {
|
||||
...DEFAULT_PLUGIN.manifest,
|
||||
name: 'example-web-prototype',
|
||||
title: 'Web Prototype',
|
||||
description: 'General-purpose desktop web prototype.',
|
||||
},
|
||||
};
|
||||
|
||||
const AUTHORING_APPLY_RESULT = {
|
||||
query: 'Create a plugin.',
|
||||
contextItems: [],
|
||||
|
|
@ -285,10 +304,10 @@ describe('HomeView prompt handoff', () => {
|
|||
}));
|
||||
});
|
||||
|
||||
it('applies Home rail scenario chips with required default inputs', async () => {
|
||||
it('applies Home rail Prototype chip against the bundled web-prototype scenario plugin', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
||||
if (typeof url === 'string' && url === '/api/plugins') {
|
||||
return new Response(JSON.stringify({ plugins: [DEFAULT_PLUGIN] }), {
|
||||
return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
|
@ -319,18 +338,18 @@ describe('HomeView prompt handoff', () => {
|
|||
fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/plugins/od-new-generation/apply',
|
||||
'/api/plugins/example-web-prototype/apply',
|
||||
expect.anything(),
|
||||
));
|
||||
// web-prototype's manifest has no `inputs` field, so the chip
|
||||
// doesn't carry artifactKind/audience/topic anymore. The apply
|
||||
// body's `inputs` map should be empty (chip passes no inputs and
|
||||
// the plugin defines none).
|
||||
const applyCall = fetchMock.mock.calls.find(([url]) => (
|
||||
typeof url === 'string' && url.includes('/api/plugins/od-new-generation/apply')
|
||||
typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
|
||||
));
|
||||
expect(JSON.parse(String((applyCall?.[1] as RequestInit).body))).toMatchObject({
|
||||
inputs: {
|
||||
artifactKind: 'interactive prototype',
|
||||
audience: 'product teams',
|
||||
topic: 'a new product experience',
|
||||
},
|
||||
inputs: {},
|
||||
});
|
||||
expect(screen.queryByRole('alert')).toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -174,25 +174,55 @@ describe('PluginShareMenu', () => {
|
|||
openPopover();
|
||||
const items = Array.from(
|
||||
container.querySelectorAll('.plugin-share-item'),
|
||||
) as HTMLButtonElement[];
|
||||
) as HTMLElement[];
|
||||
expect(items.some((b) => b.textContent?.includes('Open source on GitHub'))).toBe(
|
||||
true,
|
||||
);
|
||||
const sourceLink = container.querySelector<HTMLAnchorElement>(
|
||||
'a.plugin-share-item[href="https://github.com/owner/repo"]',
|
||||
);
|
||||
expect(sourceLink).toBeTruthy();
|
||||
});
|
||||
|
||||
it('surfaces the homepage link when manifest.homepage is set', () => {
|
||||
renderMenu(
|
||||
make({
|
||||
id: 'with-homepage',
|
||||
sourceKind: 'local',
|
||||
homepage: 'https://example.test/plugin-home',
|
||||
}),
|
||||
);
|
||||
openPopover();
|
||||
const items = Array.from(
|
||||
container.querySelectorAll('.plugin-share-item'),
|
||||
) as HTMLButtonElement[];
|
||||
) as HTMLElement[];
|
||||
expect(items.some((b) => b.textContent?.includes('Open homepage'))).toBe(
|
||||
true,
|
||||
);
|
||||
const homepageLink = Array.from(
|
||||
container.querySelectorAll<HTMLAnchorElement>('a.plugin-share-item'),
|
||||
).find((link) => link.textContent?.includes('Open homepage'));
|
||||
expect(homepageLink).toBeTruthy();
|
||||
expect(homepageLink?.getAttribute('href')).toBe('https://example.test/plugin-home');
|
||||
});
|
||||
|
||||
it('renders official bundled repo links as anchors', () => {
|
||||
renderMenu(
|
||||
make({
|
||||
id: 'official-plugin',
|
||||
sourceKind: 'bundled',
|
||||
source: 'plugins/_official/scenarios/official-plugin',
|
||||
}),
|
||||
);
|
||||
openPopover();
|
||||
const repoLinks = Array.from(
|
||||
container.querySelectorAll<HTMLAnchorElement>(
|
||||
'a.plugin-share-item[href="https://github.com/nexu-io/open-design"]',
|
||||
),
|
||||
);
|
||||
expect(repoLinks.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
repoLinks.some((link) => link.textContent?.includes('Open source on GitHub')),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { InstalledPluginRecord, PluginSourceKind, TrustTier } from '@open-design/contracts';
|
||||
import { PluginsView } from '../../src/components/PluginsView';
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
installPluginSource,
|
||||
listPluginMarketplaces,
|
||||
listPlugins,
|
||||
type PluginShareProjectOutcome,
|
||||
uploadPluginFolder,
|
||||
uploadPluginZip,
|
||||
} from '../../src/state/projects';
|
||||
|
|
@ -32,10 +33,12 @@ function makePlugin(
|
|||
id: string,
|
||||
sourceKind: PluginSourceKind,
|
||||
trust: TrustTier,
|
||||
title = id === 'official-plugin' ? 'Official Plugin' : 'User Plugin',
|
||||
description = `${id} description`,
|
||||
): InstalledPluginRecord {
|
||||
return {
|
||||
id,
|
||||
title: id === 'official-plugin' ? 'Official Plugin' : 'User Plugin',
|
||||
title,
|
||||
version: '1.0.0',
|
||||
sourceKind,
|
||||
source: '/tmp',
|
||||
|
|
@ -45,7 +48,7 @@ function makePlugin(
|
|||
name: id,
|
||||
version: '1.0.0',
|
||||
title: id,
|
||||
description: `${id} description`,
|
||||
description,
|
||||
od: {
|
||||
kind: 'scenario',
|
||||
mode: 'prototype',
|
||||
|
|
@ -207,4 +210,76 @@ describe('PluginsView', () => {
|
|||
await waitFor(() => expect(mockedUploadPluginFolder).toHaveBeenCalledWith([folderFile]));
|
||||
expect(await screen.findByText('Installed Folder Plugin.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('confirms a plugin share action before starting the GitHub repo task', async () => {
|
||||
mockedListPlugins.mockResolvedValue([
|
||||
makePlugin('official-plugin', 'bundled', 'bundled'),
|
||||
makePlugin('user-plugin', 'github', 'restricted'),
|
||||
makePlugin(
|
||||
'od-plugin-publish-github',
|
||||
'bundled',
|
||||
'bundled',
|
||||
'Publish Plugin to GitHub',
|
||||
'Creates a public GitHub repository for a local Open Design plugin using the GitHub CLI.',
|
||||
),
|
||||
makePlugin(
|
||||
'od-plugin-contribute-open-design',
|
||||
'bundled',
|
||||
'bundled',
|
||||
'Contribute Plugin to Open Design',
|
||||
'Opens a pull request that adds a local Open Design plugin to the Open Design community catalog.',
|
||||
),
|
||||
]);
|
||||
const onCreatePluginShareProject = vi.fn(async (): Promise<PluginShareProjectOutcome> => ({
|
||||
ok: true as const,
|
||||
project: {
|
||||
id: 'share-project',
|
||||
name: 'Publish to GitHub: User Plugin',
|
||||
skillId: null,
|
||||
designSystemId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
pendingPrompt: 'Publish it',
|
||||
metadata: { kind: 'prototype' },
|
||||
},
|
||||
conversationId: 'conversation-1',
|
||||
appliedPluginSnapshotId: 'snapshot-1',
|
||||
actionPluginId: 'od-plugin-publish-github',
|
||||
sourcePluginId: 'user-plugin',
|
||||
stagedPath: 'plugin-source/user-plugin',
|
||||
prompt: 'Publish it',
|
||||
message: 'Created a Publish to GitHub task.',
|
||||
}));
|
||||
|
||||
render(
|
||||
<PluginsView
|
||||
onCreatePluginShareProject={onCreatePluginShareProject}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('plugins-tab-mine'));
|
||||
const publish = await screen.findByTestId('plugins-home-publish-github-user-plugin');
|
||||
expect(publish.textContent).toContain('Publish');
|
||||
fireEvent.click(publish);
|
||||
|
||||
const dialog = await screen.findByRole('dialog', {
|
||||
name: /Publish Plugin to GitHub for User Plugin/i,
|
||||
});
|
||||
expect(dialog.textContent).toContain('Creates a public GitHub repository');
|
||||
expect(dialog.textContent).toContain('plugin-source/user-plugin');
|
||||
expect(onCreatePluginShareProject).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(within(dialog).getByTestId('plugin-share-confirm-start'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onCreatePluginShareProject).toHaveBeenCalledWith(
|
||||
'user-plugin',
|
||||
'publish-github',
|
||||
'en',
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByTestId('plugin-share-confirm-modal')).toBeNull(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,19 +7,35 @@
|
|||
// client and the server never disagree about what counts as the
|
||||
// "default" plugin for a given project kind / task kind.
|
||||
//
|
||||
// Today every kind defaults to `od-new-generation`. The plan
|
||||
// reserves slots for `od-media-generation` (Stage C) and the migration
|
||||
// scenarios (`od-figma-migration`, `od-code-migration`) once the Home
|
||||
// chip rail wires them in.
|
||||
// Kind → scenario plugin mapping. Surfaces that have a battle-tested
|
||||
// bundled skill+template (decks, web prototypes) point to the
|
||||
// specialised plugin so the agent gets a real seed (`assets/template.html`),
|
||||
// a layout vocabulary (`references/layouts.md`), and a P0 checklist —
|
||||
// instead of routing through the generic od-new-generation router and
|
||||
// re-inventing every slide/section's CSS from scratch. The latter is
|
||||
// the root cause of decks that overflow the 1080px canvas, mismatched
|
||||
// type scales, and "different aesthetic every turn" drift.
|
||||
//
|
||||
// Generic / catch-all kinds (template, other) keep od-new-generation,
|
||||
// which runs discovery → plan → generate → critique without a
|
||||
// surface-specific seed. Media kinds keep od-media-generation, which
|
||||
// dispatches through the media contract instead of emitting HTML.
|
||||
|
||||
import type { ProjectKind } from '../api/projects.js';
|
||||
import type { AppliedPluginSnapshot } from './apply.js';
|
||||
|
||||
export type TaskKind = AppliedPluginSnapshot['taskKind'];
|
||||
|
||||
// Plugin ids of the bundled `_official/scenarios/` rows. Kept as a
|
||||
// string-literal union so a typo here surfaces as a type error in both
|
||||
// the web shell and the daemon resolver.
|
||||
// Plugin ids the kind/task-kind defaults can resolve to. Two tiers:
|
||||
// 1. `od-*` scenarios (under `plugins/_official/scenarios/`) — generic
|
||||
// routers / pipelines without per-surface templates.
|
||||
// 2. `example-*` scenarios (under `plugins/_official/examples/`) —
|
||||
// specialised bundled skills that ship a seed template + layout
|
||||
// vocabulary + checklist. Promoted to first-class defaults here so
|
||||
// the chip rail / project create paths bind them without the user
|
||||
// having to manually pick the skill.
|
||||
// Kept as a string-literal union so a typo surfaces as a type error in
|
||||
// both the web shell and the daemon resolver.
|
||||
export type DefaultScenarioPluginId =
|
||||
| 'od-default'
|
||||
| 'od-new-generation'
|
||||
|
|
@ -27,14 +43,22 @@ export type DefaultScenarioPluginId =
|
|||
| 'od-plugin-authoring'
|
||||
| 'od-figma-migration'
|
||||
| 'od-code-migration'
|
||||
| 'od-tune-collab';
|
||||
| 'od-tune-collab'
|
||||
| 'example-simple-deck'
|
||||
| 'example-web-prototype';
|
||||
|
||||
export const DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID =
|
||||
'od-default' satisfies DefaultScenarioPluginId;
|
||||
|
||||
export const DEFAULT_SCENARIO_PLUGIN_BY_KIND: Record<ProjectKind, DefaultScenarioPluginId> = {
|
||||
prototype: 'od-new-generation',
|
||||
deck: 'od-new-generation',
|
||||
// Prototypes bind to web-prototype's seed template (single-file HTML,
|
||||
// 1280×800 frame, section layouts library, P0 checklist).
|
||||
prototype: 'example-web-prototype',
|
||||
// Decks bind to simple-deck's seed (1920×1080 canvas, 8-pattern
|
||||
// layout vocabulary including cover / body / big-stat / pipeline /
|
||||
// closing, plus an overflow checklist that catches the
|
||||
// "headline + subtitle + absolute footer" collision).
|
||||
deck: 'example-simple-deck',
|
||||
template: 'od-new-generation',
|
||||
image: 'od-media-generation',
|
||||
video: 'od-media-generation',
|
||||
|
|
|
|||
|
|
@ -15,8 +15,11 @@ import {
|
|||
describe('defaultScenarioPluginIdForKind', () => {
|
||||
it('maps every supported ProjectKind to a bundled scenario id', () => {
|
||||
const expected: Record<string, string> = {
|
||||
prototype: 'od-new-generation',
|
||||
deck: 'od-new-generation',
|
||||
// Surfaces with a battle-tested seed template + layouts +
|
||||
// checklist bind to the specialised example plugin, not the
|
||||
// generic od-new-generation router. See scenario-defaults.ts.
|
||||
prototype: 'example-web-prototype',
|
||||
deck: 'example-simple-deck',
|
||||
template: 'od-new-generation',
|
||||
image: 'od-media-generation',
|
||||
video: 'od-media-generation',
|
||||
|
|
|
|||
|
|
@ -19,11 +19,9 @@ Use this workflow when the active project contains a copied plugin folder and th
|
|||
|
||||
1. Read the active plugin inputs. `plugin_context_path` is the copied plugin folder relative to the project working directory.
|
||||
2. Inspect the copied plugin's manifest, skill instructions, examples, and compatibility metadata.
|
||||
3. Verify `gh auth status --hostname github.com`. If authentication is missing, stop with the exact command the user needs to run.
|
||||
4. Fork or reuse a fork of `nexu-io/open-design`, then clone a clean working copy.
|
||||
5. Create a branch named like `plugin/<source_plugin_id>`.
|
||||
6. Copy the staged plugin folder into `plugins/community/<source_plugin_id>` in that working copy. Create parent directories when needed.
|
||||
7. Commit only that plugin folder, push the branch to the user's fork, and run `gh pr create --repo nexu-io/open-design --base main`.
|
||||
8. Report the PR URL, branch name, and any validation performed.
|
||||
3. Call the local daemon endpoint instead of hand-rolling GitHub commands:
|
||||
`curl -sS -X POST "$OD_DAEMON_URL/api/projects/$OD_PROJECT_ID/plugins/contribute-open-design" -H 'content-type: application/json' -d '{"path":"<plugin_context_path>"}'`
|
||||
4. Read the JSON response. If `ok` is true, report the PR URL, branch name when present in the log, and any useful validation summary.
|
||||
5. If the endpoint fails, report its `message`, `code`, and useful log lines. When authentication is missing, tell the user to run `gh auth login --hostname github.com`.
|
||||
|
||||
Keep the pull request focused. Do not modify unrelated Open Design files unless a manifest validation issue requires a tiny supporting change.
|
||||
|
|
|
|||
|
|
@ -19,10 +19,9 @@ Use this workflow when the active project contains a copied plugin folder and th
|
|||
|
||||
1. Read the active plugin inputs. `plugin_context_path` is the copied plugin folder relative to the project working directory.
|
||||
2. Inspect `open-design.json`, `SKILL.md`, and any compatibility metadata in the copied folder.
|
||||
3. Verify `gh auth status --hostname github.com`. If authentication is missing, stop with the exact command the user needs to run.
|
||||
4. Create a clean temporary git repository from the copied plugin folder. Do not include project wrapper files or unrelated artifacts.
|
||||
5. Commit with a concise message such as `Publish <plugin title> plugin`.
|
||||
6. Create a public repository with `gh repo create <repo-name> --public --source <temp-repo> --push`.
|
||||
7. Report the final repository URL, commit hash, and any validation performed.
|
||||
3. Call the local daemon endpoint instead of hand-rolling GitHub commands:
|
||||
`curl -sS -X POST "$OD_DAEMON_URL/api/projects/$OD_PROJECT_ID/plugins/publish-github" -H 'content-type: application/json' -d '{"path":"<plugin_context_path>"}'`
|
||||
4. Read the JSON response. If `ok` is true, report the final repository URL and any useful log/validation summary.
|
||||
5. If the endpoint fails, report its `message`, `code`, and useful log lines. When authentication is missing, tell the user to run `gh auth login --hostname github.com`.
|
||||
|
||||
Prefer the manifest `name` as the repository slug. If that repository already exists, choose the next clear slug and mention the rename.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: create-hyperframes-launch
|
||||
description: Use this plugin when the user wants a HyperFrames-ready HTML motion composition, launch animation, kinetic typography clip, product reveal, or social video made from code.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
od:
|
||||
mode: hyperframes
|
||||
scenario: video
|
||||
---
|
||||
|
||||
# Create HyperFrames Launch
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Clarify duration, aspect ratio, product, text beats, and motion style.
|
||||
2. Plan the composition as scenes with exact timings.
|
||||
3. Create HyperFrames-ready HTML with named layers, restrained animation, and render notes.
|
||||
4. Keep composition source organized under a cache or source folder and return the rendered artifact or render instructions.
|
||||
5. Critique timing, legibility, and whether the composition can render deterministically.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce `hyperframes-plan.md` and a render-ready composition folder or rendered video.
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "create-hyperframes-launch",
|
||||
"title": "HyperFrames Launch",
|
||||
"version": "0.1.0",
|
||||
"description": "Create a HyperFrames-ready motion composition for product launches and social video.",
|
||||
"license": "MIT",
|
||||
"tags": ["create", "video", "hyperframes", "motion"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "video",
|
||||
"scenario": "video",
|
||||
"surface": "hyperframes",
|
||||
"useCase": {
|
||||
"query": "Create a {{duration}} HyperFrames launch composition for {{product}} with {{motionStyle}} motion."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["discovery-question-form", "todo-write", "file-write", "live-artifact", "media-video", "critique-theater"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "discovery", "atoms": ["discovery-question-form"] },
|
||||
{ "id": "plan", "atoms": ["todo-write"] },
|
||||
{ "id": "compose", "atoms": ["file-write", "live-artifact"] },
|
||||
{ "id": "render", "atoms": ["media-video"] },
|
||||
{
|
||||
"id": "critique",
|
||||
"atoms": ["critique-theater"],
|
||||
"repeat": true,
|
||||
"until": "critique.score>=4 || iterations>=2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "product", "type": "string", "required": true, "label": "Product" },
|
||||
{ "name": "duration", "type": "string", "required": false, "label": "Duration", "default": "5 seconds" },
|
||||
{ "name": "motionStyle", "type": "select", "required": false, "label": "Motion style", "options": ["minimal reveal", "kinetic typography", "data pulse"], "default": "minimal reveal" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:write", "subprocess"]
|
||||
}
|
||||
}
|
||||
|
||||
26
plugins/community/examples/create-image-campaign/SKILL.md
Normal file
26
plugins/community/examples/create-image-campaign/SKILL.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: create-image-campaign
|
||||
description: Use this plugin when the user wants image assets, posters, social visuals, ad concepts, or a small campaign image system from a creative brief.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
od:
|
||||
mode: image
|
||||
scenario: marketing
|
||||
---
|
||||
|
||||
# Create Image Campaign
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Clarify audience, product, visual subject, channel, and aspect ratio.
|
||||
2. Write a concise creative direction with composition, lighting, palette, and constraints.
|
||||
3. Generate or prepare image prompts and asset variants.
|
||||
4. Save prompt notes and produced assets with clear filenames.
|
||||
5. Critique for brand fit, legibility, and channel suitability.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce image assets or render-ready prompts plus `campaign-directions.md`.
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "create-image-campaign",
|
||||
"title": "Image Campaign",
|
||||
"version": "0.1.0",
|
||||
"description": "Generate image campaign assets and prompts from a structured creative brief.",
|
||||
"license": "MIT",
|
||||
"tags": ["create", "image", "campaign", "marketing"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "image",
|
||||
"scenario": "marketing",
|
||||
"useCase": {
|
||||
"query": "Create {{count}} image campaign concepts for {{product}} aimed at {{audience}} in {{aspectRatio}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["discovery-question-form", "direction-picker", "media-image", "file-write", "critique-theater"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "discovery", "atoms": ["discovery-question-form"] },
|
||||
{ "id": "direction", "atoms": ["direction-picker"] },
|
||||
{ "id": "generate", "atoms": ["media-image", "file-write"] },
|
||||
{
|
||||
"id": "critique",
|
||||
"atoms": ["critique-theater"],
|
||||
"repeat": true,
|
||||
"until": "critique.score>=4 || iterations>=2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "product", "type": "string", "required": true, "label": "Product" },
|
||||
{ "name": "audience", "type": "string", "required": true, "label": "Audience" },
|
||||
{ "name": "aspectRatio", "type": "select", "required": false, "label": "Aspect ratio", "options": ["1:1", "16:9", "9:16", "4:3"], "default": "1:1" },
|
||||
{ "name": "count", "type": "number", "required": false, "label": "Concept count", "default": 3 }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
||||
27
plugins/community/examples/create-live-artifact-ops/SKILL.md
Normal file
27
plugins/community/examples/create-live-artifact-ops/SKILL.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: create-live-artifact-ops
|
||||
description: Create a refreshable live operations artifact for customer success, support, or launch review workflows.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Open Design Community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Community Live Ops Artifact
|
||||
|
||||
Use this plugin when the user asks for a live artifact that summarizes changing operational data.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the source system or choose a mock source when no connector is available.
|
||||
2. Define the artifact schema: KPIs, freshness, feed items, and owner actions.
|
||||
3. Create a self-contained HTML artifact that renders a useful seeded state.
|
||||
4. Include stale and refresh affordances in the UI copy.
|
||||
5. Return `index.html` and note which connector can be wired later.
|
||||
|
||||
## Quality Checks
|
||||
|
||||
- The artifact still works with seeded mock data.
|
||||
- Freshness and source status are visible.
|
||||
- The user can tell what action to take next.
|
||||
- The layout remains useful when values change.
|
||||
173
plugins/community/examples/create-live-artifact-ops/example.html
Normal file
173
plugins/community/examples/create-live-artifact-ops/example.html
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Community Live Ops Artifact</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eef2f0;
|
||||
--ink: #18201d;
|
||||
--muted: #65736f;
|
||||
--line: #cdd9d4;
|
||||
--panel: #ffffff;
|
||||
--green: #0a7b5e;
|
||||
--blue: #2e5aac;
|
||||
--amber: #b86f00;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
padding: 28px;
|
||||
}
|
||||
.artifact {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 36px;
|
||||
line-height: 1.04;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.refresh {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fbfdfc;
|
||||
padding: 12px 14px;
|
||||
min-width: 230px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.refresh strong {
|
||||
display: block;
|
||||
color: var(--green);
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
}
|
||||
.label {
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
font-size: 12px;
|
||||
font-weight: 820;
|
||||
}
|
||||
.value {
|
||||
margin-top: 10px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
font-weight: 860;
|
||||
}
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(320px, .9fr);
|
||||
gap: 14px;
|
||||
}
|
||||
.source {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #edf1ef;
|
||||
gap: 12px;
|
||||
}
|
||||
.source:last-child { border-bottom: 0; }
|
||||
.source b { display: block; margin-bottom: 4px; }
|
||||
.source span { color: var(--muted); font-size: 14px; }
|
||||
.pill {
|
||||
border-radius: 999px;
|
||||
padding: 5px 9px;
|
||||
background: #e9f6f2;
|
||||
color: var(--green);
|
||||
font-size: 12px;
|
||||
font-weight: 820;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pill.warn {
|
||||
background: #fff2db;
|
||||
color: var(--amber);
|
||||
}
|
||||
.actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.action {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
background: #fbfdfc;
|
||||
}
|
||||
.action strong { font-size: 15px; }
|
||||
.action span { color: var(--muted); line-height: 1.4; }
|
||||
@media (max-width: 860px) {
|
||||
body { padding: 18px; }
|
||||
header { flex-direction: column; }
|
||||
.grid, .split { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="artifact">
|
||||
<header>
|
||||
<div>
|
||||
<h1>Customer success live artifact</h1>
|
||||
<div class="meta">Seeded mock source for connector-ready weekly account review. Designed to refresh from Notion, Linear, or Stripe later.</div>
|
||||
</div>
|
||||
<div class="refresh"><strong>Fresh</strong>Updated 4 minutes ago from mock data. Refresh target: every 60 seconds.</div>
|
||||
</header>
|
||||
<section class="grid" aria-label="Live KPIs">
|
||||
<div class="card"><div class="label">Accounts at risk</div><div class="value">9</div></div>
|
||||
<div class="card"><div class="label">Expansion signals</div><div class="value">17</div></div>
|
||||
<div class="card"><div class="label">Open blockers</div><div class="value">24</div></div>
|
||||
<div class="card"><div class="label">Health score</div><div class="value">82</div></div>
|
||||
</section>
|
||||
<section class="split">
|
||||
<div class="card">
|
||||
<div class="label">Source health</div>
|
||||
<div class="source"><div><b>Mock account table</b><span>12 rows mapped into KPI cards and action queue.</span></div><span class="pill">Ready</span></div>
|
||||
<div class="source"><div><b>Linear blockers</b><span>Connector not bound. Showing seeded issue counts.</span></div><span class="pill warn">Seeded</span></div>
|
||||
<div class="source"><div><b>Stripe expansion</b><span>ARR deltas use sample values until credentials are connected.</span></div><span class="pill warn">Seeded</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Next actions</div>
|
||||
<div class="actions">
|
||||
<div class="action"><strong>Escalate Northstar Labs</strong><span>Renewal date moved up. Owner: Priya. Send migration plan before Friday.</span></div>
|
||||
<div class="action"><strong>Unblock billing export</strong><span>Two enterprise teams are waiting on CSV field mapping.</span></div>
|
||||
<div class="action"><strong>Prepare expansion brief</strong><span>Three workspaces crossed 80% active-seat utilization this week.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "create-live-artifact-ops",
|
||||
"title": "Community Live Ops Artifact",
|
||||
"version": "0.1.0",
|
||||
"description": "Community example plugin for a refreshable live operations artifact with seeded data fallback.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Open Design Community",
|
||||
"url": "https://github.com/nexu-io/open-design"
|
||||
},
|
||||
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/community/examples/create-live-artifact-ops",
|
||||
"tags": ["community", "create", "live-artifact", "operations", "dashboard"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "prototype",
|
||||
"platform": "desktop",
|
||||
"scenario": "operations",
|
||||
"surface": "web",
|
||||
"preview": {
|
||||
"type": "html",
|
||||
"entry": "./example.html"
|
||||
},
|
||||
"useCase": {
|
||||
"query": {
|
||||
"en": "Create a live customer-success artifact that can refresh from a connector but ships with realistic seeded data.",
|
||||
"zh-CN": "使用社区插件创建一个客户成功实时产物,可接入连接器刷新,同时自带真实感种子数据。"
|
||||
},
|
||||
"exampleOutputs": [
|
||||
{
|
||||
"path": "./example.html",
|
||||
"title": "Community Live Ops Artifact"
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"craft": ["state-coverage", "accessibility-baseline", "motion-discipline"],
|
||||
"assets": ["./example.html"],
|
||||
"atoms": ["todo-write", "live-artifact", "handoff"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "plan", "atoms": ["todo-write"] },
|
||||
{ "id": "generate", "atoms": ["file-write", "live-artifact"] },
|
||||
{ "id": "handoff", "atoms": ["handoff"] }
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "source", "type": "string", "values": ["mock", "notion", "linear", "stripe"], "default": "mock" },
|
||||
{ "name": "refresh_seconds", "type": "number", "default": 60 }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Prototype Dashboard
|
||||
|
||||
This is the full reference example for a community create plugin. It includes a portable `SKILL.md`, OD sidecar, preview, seed asset, and eval file.
|
||||
|
||||
```bash
|
||||
od plugin validate .
|
||||
od plugin install .
|
||||
od plugin apply create-prototype-dashboard --input audience="support leads" --input topic="enterprise onboarding risk"
|
||||
```
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: create-prototype-dashboard
|
||||
description: Create a polished operations dashboard prototype with dense KPIs, status tables, and a focused command-center layout.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Open Design Community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Community Ops Dashboard
|
||||
|
||||
Use this plugin when the user wants a realistic product, operations, or customer-success dashboard prototype.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the operator role, the key decisions they need to make, and the time window.
|
||||
2. Build a single-file HTML artifact with a persistent navigation rail, KPI row, primary chart area, and action table.
|
||||
3. Use real-looking labels and numbers instead of placeholders.
|
||||
4. Check that the layout works at laptop width and that tables remain readable.
|
||||
5. Return `index.html` as the primary artifact.
|
||||
|
||||
## Quality Checks
|
||||
|
||||
- The dashboard is useful for scanning and repeated work.
|
||||
- The main action table has status, owner, priority, and next step columns.
|
||||
- Text stays inside compact controls and cards.
|
||||
- No external network assets are required.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Seed Brief
|
||||
|
||||
Use this as a fallback shape when the user gives a terse prompt.
|
||||
|
||||
- Audience: product or operations team.
|
||||
- Primary job: identify what needs attention now.
|
||||
- Core sections: metric strip, trend panel, prioritized queue, detail inspector, action footer.
|
||||
- States: empty data, loading, selected item, blocked item.
|
||||
- Tone: work-focused, dense, calm, clear.
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"skill_name": "create-prototype-dashboard",
|
||||
"evals": [
|
||||
{
|
||||
"id": "ops-dashboard",
|
||||
"prompt": "Create a dashboard prototype for customer success leads tracking enterprise onboarding risk.",
|
||||
"expected_output": "A runnable HTML dashboard with metrics, risk queue, detail panel, and realistic states.",
|
||||
"assertions": [
|
||||
"The output includes index.html",
|
||||
"The first screen is an application surface, not a landing page",
|
||||
"The dashboard contains realistic operational data",
|
||||
"At least three UI states are represented",
|
||||
"The final response names the artifact path"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Community Ops Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #15181d;
|
||||
--muted: #667085;
|
||||
--line: #d8dee8;
|
||||
--paper: #f7f4ee;
|
||||
--panel: #ffffff;
|
||||
--teal: #0b7a75;
|
||||
--blue: #3267d6;
|
||||
--red: #ba3a33;
|
||||
--gold: #b47a13;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
.shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
}
|
||||
aside {
|
||||
padding: 28px 22px;
|
||||
border-right: 1px solid var(--line);
|
||||
background: #fffaf1;
|
||||
}
|
||||
.brand {
|
||||
font-size: 21px;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.nav span {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.nav span:first-child {
|
||||
background: #10201f;
|
||||
color: #fff;
|
||||
}
|
||||
main {
|
||||
padding: 30px;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.subtitle {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
max-width: 720px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status-pill {
|
||||
border: 1px solid #a8d8d2;
|
||||
background: #e8faf6;
|
||||
color: #075e57;
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 760;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 10px 24px rgba(27, 34, 45, 0.05);
|
||||
}
|
||||
.label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
font-weight: 760;
|
||||
}
|
||||
.value {
|
||||
font-size: 30px;
|
||||
font-weight: 820;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.delta {
|
||||
color: var(--teal);
|
||||
font-size: 13px;
|
||||
font-weight: 720;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(320px, .8fr);
|
||||
gap: 14px;
|
||||
}
|
||||
.chart {
|
||||
height: 292px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
padding-top: 28px;
|
||||
}
|
||||
.bar {
|
||||
min-height: 42px;
|
||||
border-radius: 7px 7px 3px 3px;
|
||||
background: linear-gradient(180deg, var(--blue), #77a3ff);
|
||||
position: relative;
|
||||
}
|
||||
.bar::after {
|
||||
content: attr(data-day);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -24px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.table th {
|
||||
text-align: left;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.table td {
|
||||
padding: 13px 8px;
|
||||
border-bottom: 1px solid #edf0f4;
|
||||
vertical-align: top;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
background: #eef4ff;
|
||||
color: #244aa5;
|
||||
}
|
||||
.tag.warn { background: #fff5dc; color: var(--gold); }
|
||||
.tag.hot { background: #ffe9e7; color: var(--red); }
|
||||
.feed {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.feed div {
|
||||
border-left: 3px solid var(--teal);
|
||||
padding-left: 12px;
|
||||
color: #313946;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
aside { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.kpis, .grid { grid-template-columns: 1fr; }
|
||||
header { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<aside>
|
||||
<div class="brand">LaunchOps</div>
|
||||
<nav class="nav" aria-label="Sections">
|
||||
<span>Command room</span>
|
||||
<span>Incidents</span>
|
||||
<span>Owners</span>
|
||||
<span>Readiness</span>
|
||||
</nav>
|
||||
</aside>
|
||||
<main>
|
||||
<header>
|
||||
<div>
|
||||
<h1>Launch command dashboard</h1>
|
||||
<p class="subtitle">Seven-day view for release readiness, incident pressure, customer signals, and owner follow-through.</p>
|
||||
</div>
|
||||
<div class="status-pill">On track: 84%</div>
|
||||
</header>
|
||||
<section class="kpis" aria-label="Key metrics">
|
||||
<div class="card"><div class="label">Open risks</div><div class="value">12</div><div class="delta">4 need exec review</div></div>
|
||||
<div class="card"><div class="label">Resolved today</div><div class="value">28</div><div class="delta">+18% vs avg</div></div>
|
||||
<div class="card"><div class="label">SLA health</div><div class="value">96%</div><div class="delta">2 hot accounts</div></div>
|
||||
<div class="card"><div class="label">Owners active</div><div class="value">37</div><div class="delta">All functions staffed</div></div>
|
||||
</section>
|
||||
<section class="grid">
|
||||
<div class="card">
|
||||
<div class="label">Resolved work by day</div>
|
||||
<div class="chart" aria-label="Bar chart of resolved work">
|
||||
<div class="bar" style="height: 45%" data-day="Mon"></div>
|
||||
<div class="bar" style="height: 58%" data-day="Tue"></div>
|
||||
<div class="bar" style="height: 52%" data-day="Wed"></div>
|
||||
<div class="bar" style="height: 76%" data-day="Thu"></div>
|
||||
<div class="bar" style="height: 68%" data-day="Fri"></div>
|
||||
<div class="bar" style="height: 34%" data-day="Sat"></div>
|
||||
<div class="bar" style="height: 63%" data-day="Sun"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">Live activity</div>
|
||||
<div class="feed">
|
||||
<div>Payments rollback drill passed in staging. Finance owner signed off.</div>
|
||||
<div>Enterprise beta cohort moved to amber while support updates saved replies.</div>
|
||||
<div>Docs review queue cleared; final API examples are ready for publication.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card" style="margin-top:14px">
|
||||
<div class="label">Priority actions</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>Workstream</th><th>Status</th><th>Owner</th><th>Next step</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Billing migration</td><td><span class="tag hot">Hot</span></td><td>Mina</td><td>Confirm webhook replay plan</td></tr>
|
||||
<tr><td>Launch docs</td><td><span class="tag">Ready</span></td><td>Arun</td><td>Publish quickstart after QA pass</td></tr>
|
||||
<tr><td>Success outreach</td><td><span class="tag warn">Watch</span></td><td>Jules</td><td>Send saved replies to priority accounts</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "create-prototype-dashboard",
|
||||
"title": "Community Ops Dashboard",
|
||||
"version": "0.1.0",
|
||||
"description": "Community example plugin for creating a dense operations dashboard prototype with KPIs, charts, and an action table.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Open Design Community",
|
||||
"url": "https://github.com/nexu-io/open-design"
|
||||
},
|
||||
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/community/examples/create-prototype-dashboard",
|
||||
"tags": ["community", "create", "prototype", "dashboard", "operations"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "prototype",
|
||||
"platform": "desktop",
|
||||
"scenario": "operations",
|
||||
"surface": "web",
|
||||
"preview": {
|
||||
"type": "html",
|
||||
"entry": "./example.html"
|
||||
},
|
||||
"useCase": {
|
||||
"query": {
|
||||
"en": "Create an operations dashboard for a launch command room with KPIs, incident status, owners, and next actions.",
|
||||
"zh-CN": "使用社区插件创建一个发布指挥室运营仪表盘,包含 KPI、事件状态、负责人和下一步动作。"
|
||||
},
|
||||
"exampleOutputs": [
|
||||
{
|
||||
"path": "./example.html",
|
||||
"title": "Community Ops Dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"craft": ["state-coverage", "accessibility-baseline", "laws-of-ux"],
|
||||
"assets": ["./example.html"],
|
||||
"atoms": ["todo-write", "critique-theater", "handoff"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "plan", "atoms": ["todo-write"] },
|
||||
{ "id": "generate", "atoms": ["file-write", "live-artifact"] },
|
||||
{ "id": "critique", "atoms": ["critique-theater"] },
|
||||
{ "id": "handoff", "atoms": ["handoff"] }
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "team", "type": "string", "required": true, "label": "Team" },
|
||||
{ "name": "time_window", "type": "string", "default": "last 7 days", "label": "Time window" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Prototype Dashboard Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #f6f5f1;
|
||||
color: #18202a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; }
|
||||
main {
|
||||
width: min(960px, 100%);
|
||||
min-height: 520px;
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
border: 1px solid #dad6cb;
|
||||
background: #fffdf8;
|
||||
box-shadow: 0 20px 50px rgba(40, 36, 27, 0.12);
|
||||
}
|
||||
nav { border-right: 1px solid #e4dfd4; padding: 22px; }
|
||||
.brand { font-weight: 800; font-size: 18px; margin-bottom: 28px; }
|
||||
.nav-item { padding: 10px 0; color: #5f6670; font-size: 14px; }
|
||||
.nav-item.active { color: #111820; font-weight: 700; }
|
||||
section { padding: 24px; display: grid; gap: 18px; }
|
||||
header { display: flex; justify-content: space-between; gap: 16px; align-items: start; }
|
||||
h1 { font-size: 28px; margin: 0; letter-spacing: 0; }
|
||||
p { margin: 6px 0 0; color: #68707a; line-height: 1.45; }
|
||||
.pill { border: 1px solid #d9d4c8; padding: 8px 12px; font-size: 13px; color: #39414c; background: #fbfaf6; }
|
||||
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.metric, .panel, .queue { border: 1px solid #e1dccf; background: #ffffff; padding: 16px; }
|
||||
.label { color: #6d7480; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
.value { font-size: 30px; font-weight: 800; margin-top: 8px; }
|
||||
.grid { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 12px; }
|
||||
.bar { height: 12px; background: #ebe7dc; margin-top: 14px; overflow: hidden; }
|
||||
.bar span { display: block; height: 100%; background: #2f6f73; }
|
||||
.item { display: flex; justify-content: space-between; gap: 12px; padding: 12px 0; border-top: 1px solid #eee9df; }
|
||||
.item:first-child { border-top: 0; }
|
||||
.status { color: #a85036; font-weight: 700; }
|
||||
@media (max-width: 720px) {
|
||||
main { grid-template-columns: 1fr; }
|
||||
nav { border-right: 0; border-bottom: 1px solid #e4dfd4; }
|
||||
.metrics, .grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav>
|
||||
<div class="brand">Northstar Ops</div>
|
||||
<div class="nav-item active">Risk monitor</div>
|
||||
<div class="nav-item">Accounts</div>
|
||||
<div class="nav-item">Playbooks</div>
|
||||
<div class="nav-item">Reports</div>
|
||||
</nav>
|
||||
<section>
|
||||
<header>
|
||||
<div>
|
||||
<h1>Enterprise onboarding risk</h1>
|
||||
<p>Prioritized queue for accounts likely to miss activation targets this week.</p>
|
||||
</div>
|
||||
<div class="pill">Live artifact</div>
|
||||
</header>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="label">At risk</div><div class="value">18</div></div>
|
||||
<div class="metric"><div class="label">Blocked ARR</div><div class="value">$2.4M</div></div>
|
||||
<div class="metric"><div class="label">SLA health</div><div class="value">91%</div></div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<div class="label">Activation progress</div>
|
||||
<p>Workspace setup is healthy; security review is the current bottleneck.</p>
|
||||
<div class="bar"><span style="width: 72%"></span></div>
|
||||
</div>
|
||||
<div class="queue">
|
||||
<div class="item"><strong>Acme BioSystems</strong><span class="status">High</span></div>
|
||||
<div class="item"><strong>Vanta Labs</strong><span class="status">Med</span></div>
|
||||
<div class="item"><strong>Helio Grid</strong><span class="status">Med</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
27
plugins/community/examples/create-slides-pitch/SKILL.md
Normal file
27
plugins/community/examples/create-slides-pitch/SKILL.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: create-slides-pitch
|
||||
description: Create a concise HTML pitch deck for an early-stage product, with a strong narrative arc and finance-ready slide structure.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Open Design Community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Community Pitch Slides
|
||||
|
||||
Use this plugin when the user needs a seed or Series A pitch deck in HTML.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Ask only for missing company name, product promise, traction, and ask if they are absent.
|
||||
2. Create 8 to 10 slides covering problem, insight, product, market, traction, model, team, and ask.
|
||||
3. Keep every slide visually distinct but governed by one typography and color system.
|
||||
4. Include speaker-friendly headlines, not paragraph-heavy pages.
|
||||
5. Return `index.html` as the primary artifact.
|
||||
|
||||
## Quality Checks
|
||||
|
||||
- The deck can be read quickly in presentation mode.
|
||||
- Numbers are formatted consistently and have labels.
|
||||
- The ask slide names use of funds.
|
||||
- Print/export styling does not hide slide content.
|
||||
178
plugins/community/examples/create-slides-pitch/example.html
Normal file
178
plugins/community/examples/create-slides-pitch/example.html
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Community Pitch Slides</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #101219;
|
||||
--muted: #6d7280;
|
||||
--paper: #f6f2e9;
|
||||
--blue: #264fbb;
|
||||
--green: #08755f;
|
||||
--rose: #b8405a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #ded8cb;
|
||||
color: var(--ink);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
main {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 22px;
|
||||
}
|
||||
.slide {
|
||||
width: min(1120px, 100%);
|
||||
min-height: 630px;
|
||||
margin: 0 auto;
|
||||
background: var(--paper);
|
||||
border: 1px solid #cfc7b7;
|
||||
border-radius: 8px;
|
||||
padding: 52px;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
box-shadow: 0 18px 45px rgba(35, 31, 24, .12);
|
||||
break-after: page;
|
||||
}
|
||||
.eyebrow {
|
||||
color: var(--blue);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .12em;
|
||||
font-weight: 850;
|
||||
font-size: 13px;
|
||||
}
|
||||
h1, h2 {
|
||||
margin: 14px 0 0;
|
||||
letter-spacing: 0;
|
||||
line-height: .98;
|
||||
}
|
||||
h1 { font-size: clamp(48px, 8vw, 88px); max-width: 920px; }
|
||||
h2 { font-size: clamp(38px, 5vw, 62px); max-width: 880px; }
|
||||
p {
|
||||
color: var(--muted);
|
||||
font-size: 20px;
|
||||
line-height: 1.45;
|
||||
max-width: 780px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-self: center;
|
||||
margin-top: 34px;
|
||||
}
|
||||
.tile {
|
||||
border: 1px solid #d7cfbf;
|
||||
background: #fffaf0;
|
||||
border-radius: 8px;
|
||||
padding: 22px;
|
||||
min-height: 150px;
|
||||
}
|
||||
.num {
|
||||
font-size: 46px;
|
||||
font-weight: 880;
|
||||
color: var(--green);
|
||||
line-height: 1;
|
||||
}
|
||||
.tile span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
margin-top: 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.diagram {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18px;
|
||||
align-self: center;
|
||||
}
|
||||
.panel {
|
||||
border-radius: 8px;
|
||||
padding: 28px;
|
||||
min-height: 260px;
|
||||
border: 1px solid #d7cfbf;
|
||||
background: #fffaf0;
|
||||
}
|
||||
.panel strong {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 720;
|
||||
}
|
||||
@media print {
|
||||
body { background: white; }
|
||||
main { padding: 0; gap: 0; }
|
||||
.slide { width: 100vw; min-height: 100vh; box-shadow: none; border: 0; border-radius: 0; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.slide { min-height: auto; padding: 28px; }
|
||||
.grid, .diagram { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section class="slide">
|
||||
<div>
|
||||
<div class="eyebrow">Atlas Desk seed deck</div>
|
||||
<h1>The operating layer for design-led customer teams.</h1>
|
||||
<p>Atlas Desk turns messy launch work into one shared, measurable command surface.</p>
|
||||
</div>
|
||||
<footer><span>01</span><span>Confidential</span></footer>
|
||||
</section>
|
||||
<section class="slide">
|
||||
<div>
|
||||
<div class="eyebrow">Problem</div>
|
||||
<h2>Teams ship through scattered tabs, stale docs, and invisible owner drift.</h2>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="tile"><div class="num">37%</div><span>of launch work is duplicated across tools.</span></div>
|
||||
<div class="tile"><div class="num">4.6h</div><span>lost weekly to status reconstruction.</span></div>
|
||||
<div class="tile"><div class="num">2x</div><span>more escalations when owners are unclear.</span></div>
|
||||
</div>
|
||||
<footer><span>02</span><span>Problem</span></footer>
|
||||
</section>
|
||||
<section class="slide">
|
||||
<div>
|
||||
<div class="eyebrow">Solution</div>
|
||||
<h2>A live workspace that converts work signals into launch decisions.</h2>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<div class="panel"><strong>Before</strong><p>Manual status docs, fragmented charts, and late risk reviews.</p></div>
|
||||
<div class="panel"><strong>After</strong><p>One dashboard for owners, blockers, tasks, customer impact, and next actions.</p></div>
|
||||
</div>
|
||||
<footer><span>03</span><span>Product</span></footer>
|
||||
</section>
|
||||
<section class="slide">
|
||||
<div>
|
||||
<div class="eyebrow">Traction</div>
|
||||
<h2>Early teams use Atlas Desk as their daily launch room.</h2>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="tile"><div class="num">18</div><span>design-partner teams</span></div>
|
||||
<div class="tile"><div class="num">42k</div><span>workflow events processed</span></div>
|
||||
<div class="tile"><div class="num">91%</div><span>weekly active operators</span></div>
|
||||
</div>
|
||||
<footer><span>04</span><span>Traction</span></footer>
|
||||
</section>
|
||||
<section class="slide">
|
||||
<div>
|
||||
<div class="eyebrow">Ask</div>
|
||||
<h2>Raising $2.5M to turn design-partner pull into a repeatable sales motion.</h2>
|
||||
<p>Use of funds: product integrations, enterprise readiness, founder-led sales, and customer success playbooks.</p>
|
||||
</div>
|
||||
<footer><span>05</span><span>Ask</span></footer>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "create-slides-pitch",
|
||||
"title": "Community Pitch Slides",
|
||||
"version": "0.1.0",
|
||||
"description": "Community example plugin for producing an investor-ready HTML pitch deck artifact.",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Open Design Community",
|
||||
"url": "https://github.com/nexu-io/open-design"
|
||||
},
|
||||
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/community/examples/create-slides-pitch",
|
||||
"tags": ["community", "create", "deck", "slides", "pitch"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "deck",
|
||||
"scenario": "finance",
|
||||
"surface": "web",
|
||||
"preview": {
|
||||
"type": "html",
|
||||
"entry": "./example.html"
|
||||
},
|
||||
"useCase": {
|
||||
"query": {
|
||||
"en": "Create an 8-slide seed pitch deck for a product called Atlas Desk with traction, market, business model, and ask.",
|
||||
"zh-CN": "使用社区插件为 Atlas Desk 创建 8 页种子轮融资演示文稿,包含增长、市场、商业模式和融资诉求。"
|
||||
},
|
||||
"exampleOutputs": [
|
||||
{
|
||||
"path": "./example.html",
|
||||
"title": "Community Pitch Slides"
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"craft": ["typography", "color", "anti-ai-slop"],
|
||||
"assets": ["./example.html"],
|
||||
"atoms": ["todo-write", "critique-theater", "handoff"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "plan", "atoms": ["todo-write"] },
|
||||
{ "id": "generate", "atoms": ["file-write", "live-artifact"] },
|
||||
{ "id": "critique", "atoms": ["critique-theater"] },
|
||||
{ "id": "handoff", "atoms": ["handoff"] }
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "company", "type": "string", "required": true, "label": "Company" },
|
||||
{ "name": "ask", "type": "string", "default": "$2.5M seed", "label": "Ask" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:write"]
|
||||
}
|
||||
}
|
||||
26
plugins/community/examples/create-video-storyboard/SKILL.md
Normal file
26
plugins/community/examples/create-video-storyboard/SKILL.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: create-video-storyboard
|
||||
description: Use this plugin when the user wants a video concept, storyboard, shot list, prompt pack, or render-ready motion brief for a product, campaign, or explainer.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
od:
|
||||
mode: video
|
||||
scenario: marketing
|
||||
---
|
||||
|
||||
# Create Video Storyboard
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Clarify duration, audience, platform, visual style, and call to action.
|
||||
2. Create a scene-by-scene beat sheet with timings.
|
||||
3. Generate a storyboard prompt pack and optional video generation prompt.
|
||||
4. Save a shot list with motion, camera, typography, and audio notes.
|
||||
5. Critique for pacing, clarity, and whether each shot can be produced.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce `storyboard.md` and any generated video prompt or media asset.
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "create-video-storyboard",
|
||||
"title": "Video Storyboard",
|
||||
"version": "0.1.0",
|
||||
"description": "Create a video storyboard, shot list, and render-ready prompt pack.",
|
||||
"license": "MIT",
|
||||
"tags": ["create", "video", "storyboard", "marketing"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "video",
|
||||
"scenario": "marketing",
|
||||
"useCase": {
|
||||
"query": "Create a {{duration}} video storyboard for {{product}} aimed at {{audience}} on {{platform}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["discovery-question-form", "todo-write", "media-video", "file-write", "critique-theater"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "discovery", "atoms": ["discovery-question-form"] },
|
||||
{ "id": "plan", "atoms": ["todo-write"] },
|
||||
{ "id": "generate", "atoms": ["media-video", "file-write"] },
|
||||
{
|
||||
"id": "critique",
|
||||
"atoms": ["critique-theater"],
|
||||
"repeat": true,
|
||||
"until": "critique.score>=4 || iterations>=2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "product", "type": "string", "required": true, "label": "Product" },
|
||||
{ "name": "audience", "type": "string", "required": true, "label": "Audience" },
|
||||
{ "name": "platform", "type": "select", "required": false, "label": "Platform", "options": ["website", "YouTube", "TikTok", "LinkedIn"], "default": "website" },
|
||||
{ "name": "duration", "type": "string", "required": false, "label": "Duration", "default": "15 seconds" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
||||
23
plugins/community/examples/deploy-vercel-static/SKILL.md
Normal file
23
plugins/community/examples/deploy-vercel-static/SKILL.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: deploy-vercel-static
|
||||
description: Use this plugin when the user wants to deploy an accepted static web artifact to Vercel or prepare an equivalent deployment handoff with preview and production URLs.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Deploy Vercel Static
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the artifact path, project name, and whether this is preview-only or production.
|
||||
2. Validate that the artifact can run as a static web surface.
|
||||
3. Prepare deployment files if needed.
|
||||
4. Ask for confirmation before deployment.
|
||||
5. Deploy or produce exact deployment instructions and return links.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce `deploy-summary.md` and a preview URL, production URL, or prepared command list.
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "deploy-vercel-static",
|
||||
"title": "Deploy To Vercel",
|
||||
"version": "0.1.0",
|
||||
"description": "Deploy an accepted static artifact to Vercel or prepare a deployment handoff.",
|
||||
"license": "MIT",
|
||||
"tags": ["deploy", "vercel", "static", "deployment"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "code-migration",
|
||||
"mode": "deploy",
|
||||
"scenario": "deployment",
|
||||
"useCase": {
|
||||
"query": "Deploy the accepted static artifact to Vercel as {{projectName}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["file-read", "build-test", "handoff", "connector"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "inspect", "atoms": ["file-read"] },
|
||||
{ "id": "verify", "atoms": ["build-test"] },
|
||||
{ "id": "deploy", "atoms": ["connector"] },
|
||||
{ "id": "handoff", "atoms": ["handoff"] }
|
||||
]
|
||||
},
|
||||
"connectors": {
|
||||
"optional": [
|
||||
{ "id": "vercel", "tools": ["deploy-project"] }
|
||||
]
|
||||
},
|
||||
"genui": {
|
||||
"surfaces": [
|
||||
{
|
||||
"id": "confirm-deploy",
|
||||
"kind": "confirmation",
|
||||
"persist": "run",
|
||||
"trigger": { "stageId": "deploy", "atom": "connector" },
|
||||
"prompt": "Deploy this artifact to Vercel now?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "projectName", "type": "string", "required": true, "label": "Project name" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:read", "bash", "connector", "connector:vercel", "network"]
|
||||
}
|
||||
}
|
||||
|
||||
23
plugins/community/examples/export-nextjs-handoff/SKILL.md
Normal file
23
plugins/community/examples/export-nextjs-handoff/SKILL.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: export-nextjs-handoff
|
||||
description: Use this plugin when the user wants an accepted Open Design artifact converted into a Next.js App Router handoff with clean components, styles, assets, and implementation notes.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Export Next.js Handoff
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the accepted artifact and identify components, assets, layout tokens, and interactions.
|
||||
2. Generate a Next.js App Router folder with page, component, and style boundaries.
|
||||
3. Preserve visual fidelity while using maintainable component names and accessible markup.
|
||||
4. Run available typecheck or build commands when a package is present.
|
||||
5. Return a diff summary and handoff notes.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce `nextjs-handoff/` and `handoff-summary.md`.
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "export-nextjs-handoff",
|
||||
"title": "Next.js Handoff",
|
||||
"version": "0.1.0",
|
||||
"description": "Export an accepted artifact into a Next.js App Router handoff.",
|
||||
"license": "MIT",
|
||||
"tags": ["export", "nextjs", "react", "handoff", "downstream-export"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "code-migration",
|
||||
"mode": "export",
|
||||
"scenario": "handoff",
|
||||
"useCase": {
|
||||
"query": "Export the accepted artifact to a Next.js App Router handoff for {{destination}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["file-read", "rewrite-plan", "file-write", "build-test", "diff-review", "handoff"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "inspect", "atoms": ["file-read", "rewrite-plan"] },
|
||||
{ "id": "write", "atoms": ["file-write"] },
|
||||
{ "id": "verify", "atoms": ["build-test", "diff-review"] },
|
||||
{ "id": "handoff", "atoms": ["handoff"] }
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "destination", "type": "string", "required": false, "label": "Destination", "default": "nextjs-handoff" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:read", "fs:write", "bash"]
|
||||
}
|
||||
}
|
||||
|
||||
23
plugins/community/examples/extend-plugin-author/SKILL.md
Normal file
23
plugins/community/examples/extend-plugin-author/SKILL.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: extend-plugin-author
|
||||
description: Use this plugin when the user wants to create, improve, validate, publish, or submit an Open Design plugin using the community spec, examples, and PR workflow.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Extend Plugin Author
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read `plugins/community/SPEC.md` and the closest example plugin.
|
||||
2. Identify the target lane, mode, trigger description, required atoms, inputs, and capabilities.
|
||||
3. Scaffold a plugin folder with `SKILL.md`, `open-design.json`, `README.md`, and evals when useful.
|
||||
4. Validate JSON and run available repo checks.
|
||||
5. Prepare publish or PR instructions.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce a complete plugin folder and `authoring-summary.md`.
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "extend-plugin-author",
|
||||
"title": "Plugin Author",
|
||||
"version": "0.1.0",
|
||||
"description": "Guide an agent through creating, validating, and preparing an Open Design plugin contribution.",
|
||||
"license": "MIT",
|
||||
"tags": ["extend", "plugin-authoring", "create-plugin", "community-plugin", "marketplace"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "new-generation",
|
||||
"mode": "utility",
|
||||
"scenario": "plugin-authoring",
|
||||
"useCase": {
|
||||
"query": "Create an Open Design plugin for {{lane}} that helps users {{goal}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"assets": ["../../SPEC.md", "../../AGENT-DEVELOPMENT.md"],
|
||||
"atoms": ["discovery-question-form", "todo-write", "file-read", "file-write", "critique-theater"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "discovery", "atoms": ["discovery-question-form"] },
|
||||
{ "id": "plan", "atoms": ["todo-write", "file-read"] },
|
||||
{ "id": "write", "atoms": ["file-write"] },
|
||||
{
|
||||
"id": "critique",
|
||||
"atoms": ["critique-theater"],
|
||||
"repeat": true,
|
||||
"until": "critique.score>=4 || iterations>=2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "lane", "type": "select", "required": true, "label": "Lane", "options": ["import", "create", "export", "share", "deploy", "refine", "extend"] },
|
||||
{ "name": "goal", "type": "text", "required": true, "label": "Goal" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:read", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: import-screenshot-to-prototype
|
||||
description: Use this plugin when the user provides a screenshot or image reference and wants it reconstructed as an editable Open Design prototype with sensible components, layout, and responsive behavior.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Import Screenshot To Prototype
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the screenshot and identify layout regions, controls, typography scale, color roles, and content hierarchy.
|
||||
2. Ask for the target viewport only if it cannot be inferred.
|
||||
3. Rebuild the screenshot as a clean `index.html` artifact with semantic sections and reusable CSS tokens.
|
||||
4. Preserve the visual intent, but replace unreadable or unavailable text with realistic editable content.
|
||||
5. Add responsive behavior for at least one mobile width.
|
||||
6. Self-critique visual fidelity, text fit, and editability before final.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce `index.html` and a short `import-notes.md` that lists inferred decisions.
|
||||
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "import-screenshot-to-prototype",
|
||||
"title": "Screenshot To Prototype",
|
||||
"version": "0.1.0",
|
||||
"description": "Rebuild a screenshot as an editable Open Design prototype.",
|
||||
"license": "MIT",
|
||||
"tags": ["import", "screenshot-import", "image-import", "prototype"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "figma-migration",
|
||||
"mode": "prototype",
|
||||
"scenario": "import",
|
||||
"useCase": {
|
||||
"query": "Import the attached screenshot and rebuild it as an editable prototype for {{viewport}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["file-read", "token-map", "todo-write", "file-write", "live-artifact", "critique-theater"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "inspect", "atoms": ["file-read", "token-map"] },
|
||||
{ "id": "plan", "atoms": ["todo-write"] },
|
||||
{ "id": "generate", "atoms": ["file-write", "live-artifact"] },
|
||||
{
|
||||
"id": "critique",
|
||||
"atoms": ["critique-theater"],
|
||||
"repeat": true,
|
||||
"until": "critique.score>=4 || iterations>=3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "viewport", "type": "select", "required": false, "label": "Target viewport", "options": ["desktop", "mobile", "responsive"], "default": "responsive" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:read", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
||||
23
plugins/community/examples/refine-critique-loop/SKILL.md
Normal file
23
plugins/community/examples/refine-critique-loop/SKILL.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: refine-critique-loop
|
||||
description: Use this plugin when the user has an existing Open Design artifact and wants targeted critique, patching, brand tightening, responsive fixes, or quality improvement without starting over.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Refine Critique Loop
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the existing artifact and identify the user's refinement goal.
|
||||
2. Run a structured critique for hierarchy, fit, accessibility, responsiveness, and artifact-specific quality.
|
||||
3. Apply the smallest useful patch.
|
||||
4. Re-run critique and stop when quality converges or the iteration limit is reached.
|
||||
5. Return a diff summary and what changed.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Patch the existing artifact and produce `refine-summary.md`.
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "refine-critique-loop",
|
||||
"title": "Critique And Refine",
|
||||
"version": "0.1.0",
|
||||
"description": "Review, patch, and improve an existing Open Design artifact.",
|
||||
"license": "MIT",
|
||||
"tags": ["refine", "critique", "review", "patch-edit", "tune-collab"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "tune-collab",
|
||||
"mode": "refine",
|
||||
"scenario": "critique",
|
||||
"useCase": {
|
||||
"query": "Refine the current artifact with focus on {{focus}}."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["file-read", "critique-theater", "patch-edit", "diff-review"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "inspect", "atoms": ["file-read"] },
|
||||
{
|
||||
"id": "critique",
|
||||
"atoms": ["critique-theater"],
|
||||
"repeat": true,
|
||||
"until": "critique.score>=4 || iterations>=3"
|
||||
},
|
||||
{ "id": "patch", "atoms": ["patch-edit", "diff-review"] }
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "focus", "type": "string", "required": false, "label": "Focus", "default": "visual polish, responsive behavior, and text fit" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:read", "fs:write"]
|
||||
}
|
||||
}
|
||||
|
||||
23
plugins/community/examples/share-github-pr/SKILL.md
Normal file
23
plugins/community/examples/share-github-pr/SKILL.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: share-github-pr
|
||||
description: Use this plugin when the user wants to package an accepted plugin or artifact as a GitHub pull request for Open Design or another target repository.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: open-design-community
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Share GitHub PR
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm the target repository, branch, and PR intent before making externally visible changes.
|
||||
2. Read the changed artifact or plugin folder and create a concise PR summary.
|
||||
3. Run available validation commands.
|
||||
4. Stage only relevant files.
|
||||
5. Open or prepare the PR and report the URL or exact next command.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Produce `pr-summary.md` and a PR URL or prepared branch summary.
|
||||
|
||||
53
plugins/community/examples/share-github-pr/open-design.json
Normal file
53
plugins/community/examples/share-github-pr/open-design.json
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||
"name": "share-github-pr",
|
||||
"title": "Share as GitHub PR",
|
||||
"version": "0.1.0",
|
||||
"description": "Package an accepted artifact or plugin folder as a GitHub pull request.",
|
||||
"license": "MIT",
|
||||
"tags": ["share", "github-pr", "pull-request", "community-plugin"],
|
||||
"compat": {
|
||||
"agentSkills": [{ "path": "./SKILL.md" }]
|
||||
},
|
||||
"od": {
|
||||
"kind": "skill",
|
||||
"taskKind": "tune-collab",
|
||||
"mode": "share",
|
||||
"scenario": "github-pr",
|
||||
"useCase": {
|
||||
"query": "Prepare a GitHub PR for {{targetRepo}} from the accepted artifact or plugin folder."
|
||||
},
|
||||
"context": {
|
||||
"skills": [{ "path": "./SKILL.md" }],
|
||||
"atoms": ["file-read", "todo-write", "handoff", "connector"]
|
||||
},
|
||||
"pipeline": {
|
||||
"stages": [
|
||||
{ "id": "inspect", "atoms": ["file-read", "todo-write"] },
|
||||
{ "id": "confirm", "atoms": ["connector"] },
|
||||
{ "id": "handoff", "atoms": ["handoff"] }
|
||||
]
|
||||
},
|
||||
"connectors": {
|
||||
"required": [
|
||||
{ "id": "github", "tools": ["create-pull-request"] }
|
||||
]
|
||||
},
|
||||
"genui": {
|
||||
"surfaces": [
|
||||
{
|
||||
"id": "confirm-pr",
|
||||
"kind": "confirmation",
|
||||
"persist": "run",
|
||||
"trigger": { "stageId": "confirm", "atom": "connector" },
|
||||
"prompt": "Open a GitHub PR with the prepared branch and summary?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{ "name": "targetRepo", "type": "string", "required": true, "label": "Target repository", "placeholder": "nexu-io/open-design" }
|
||||
],
|
||||
"capabilities": ["prompt:inject", "fs:read", "connector", "connector:github", "network"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
// Seed the running daemon with pre-baked test projects so the UI has
|
||||
// real slide decks and web prototypes to work with without waiting for
|
||||
// an LLM run. Pulls each project's content straight from a skill's
|
||||
// `example.html`, drops it in as `index.html`, and adds a couple of
|
||||
// fake chat messages so the conversation panel isn't empty.
|
||||
// Seed Open Design with pre-baked test projects so the UI has real slide
|
||||
// decks and web prototypes to work with without waiting for an LLM run.
|
||||
// Pulls each project's content straight from a skill or plugin
|
||||
// `example.html`, drops it in as `index.html`, and adds a couple of fake
|
||||
// chat messages so the conversation panel isn't empty.
|
||||
//
|
||||
// Usage (daemon must be running — e.g. `pnpm tools-dev`):
|
||||
// Usage:
|
||||
// pnpm seed:test-projects # default bundle
|
||||
// pnpm seed:test-projects --decks 2 --webs 2 # cap counts
|
||||
// pnpm seed:test-projects --daemon http://127.0.0.1:17456
|
||||
// pnpm seed:test-projects --offline # ingest into ./.od before boot
|
||||
// pnpm seed:test-projects --clear # remove previously seeded projects
|
||||
//
|
||||
// The daemon URL is resolved in this order: --daemon flag > $OD_DAEMON_URL >
|
||||
|
|
@ -22,22 +23,30 @@
|
|||
// fixtures this script created.
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import os from 'node:os';
|
||||
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(HERE, '..');
|
||||
const SKILLS_DIR = path.join(REPO_ROOT, 'skills');
|
||||
const BUNDLED_PLUGIN_EXAMPLES_DIR = path.join(REPO_ROOT, 'plugins', '_official', 'examples');
|
||||
const COMMUNITY_PLUGIN_EXAMPLES_DIR = path.join(REPO_ROOT, 'plugins', 'community', 'examples');
|
||||
const SEED_PREFIX = 'seed-';
|
||||
|
||||
type SeedKind = 'deck' | 'prototype';
|
||||
type SeedSourceKind = 'skill' | 'default-plugin' | 'community-plugin';
|
||||
type SeedMode = 'auto' | 'online' | 'offline';
|
||||
|
||||
interface SeedFixture {
|
||||
skillId: string;
|
||||
sourceKind: SeedSourceKind;
|
||||
kind: SeedKind;
|
||||
name: string;
|
||||
pendingPrompt: string;
|
||||
pluginId?: string;
|
||||
// optional: path to the file inside skills/<skillId>/ to load as index.html
|
||||
// (defaults to example.html)
|
||||
source?: string;
|
||||
|
|
@ -66,13 +75,34 @@ interface SeedProjectSummary {
|
|||
metadata?: {
|
||||
seeded?: boolean;
|
||||
source?: string;
|
||||
sourceKind?: string;
|
||||
sourceId?: string;
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface OfflineDatabase {
|
||||
exec: (sql: string) => void;
|
||||
pragma: (sql: string) => void;
|
||||
prepare: (sql: string) => {
|
||||
all: (...args: unknown[]) => unknown[];
|
||||
get: (...args: unknown[]) => unknown;
|
||||
run: (...args: unknown[]) => unknown;
|
||||
};
|
||||
transaction: (fn: () => void) => () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
interface OfflineSeedContext {
|
||||
db: OfflineDatabase;
|
||||
dataDir: string;
|
||||
projectsDir: string;
|
||||
}
|
||||
|
||||
const DECKS: SeedFixture[] = [
|
||||
{
|
||||
skillId: 'html-ppt-pitch-deck',
|
||||
sourceKind: 'skill',
|
||||
kind: 'deck',
|
||||
name: 'Pitch deck — Series A',
|
||||
pendingPrompt:
|
||||
|
|
@ -80,6 +110,7 @@ const DECKS: SeedFixture[] = [
|
|||
},
|
||||
{
|
||||
skillId: 'kami-deck',
|
||||
sourceKind: 'skill',
|
||||
kind: 'deck',
|
||||
name: 'Kami deck — quarterly review',
|
||||
pendingPrompt:
|
||||
|
|
@ -87,6 +118,7 @@ const DECKS: SeedFixture[] = [
|
|||
},
|
||||
{
|
||||
skillId: 'html-ppt-weekly-report',
|
||||
sourceKind: 'skill',
|
||||
kind: 'deck',
|
||||
name: 'Weekly report — eng team',
|
||||
pendingPrompt:
|
||||
|
|
@ -94,6 +126,7 @@ const DECKS: SeedFixture[] = [
|
|||
},
|
||||
{
|
||||
skillId: 'html-ppt-product-launch',
|
||||
sourceKind: 'skill',
|
||||
kind: 'deck',
|
||||
name: 'Product launch — v2.0',
|
||||
pendingPrompt:
|
||||
|
|
@ -104,6 +137,7 @@ const DECKS: SeedFixture[] = [
|
|||
const WEBS: SeedFixture[] = [
|
||||
{
|
||||
skillId: 'open-design-landing',
|
||||
sourceKind: 'skill',
|
||||
kind: 'prototype',
|
||||
name: 'Editorial landing — Atelier Zero',
|
||||
pendingPrompt:
|
||||
|
|
@ -111,6 +145,7 @@ const WEBS: SeedFixture[] = [
|
|||
},
|
||||
{
|
||||
skillId: 'kami-landing',
|
||||
sourceKind: 'skill',
|
||||
kind: 'prototype',
|
||||
name: 'Kami landing — white paper',
|
||||
pendingPrompt:
|
||||
|
|
@ -118,6 +153,7 @@ const WEBS: SeedFixture[] = [
|
|||
},
|
||||
{
|
||||
skillId: 'dashboard',
|
||||
sourceKind: 'skill',
|
||||
kind: 'prototype',
|
||||
name: 'Admin dashboard — analytics',
|
||||
pendingPrompt:
|
||||
|
|
@ -125,18 +161,86 @@ const WEBS: SeedFixture[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const DEFAULT_PLUGINS: SeedFixture[] = [
|
||||
{
|
||||
skillId: 'html-ppt-pitch-deck',
|
||||
sourceKind: 'default-plugin',
|
||||
pluginId: 'example-html-ppt-pitch-deck',
|
||||
kind: 'deck',
|
||||
name: 'Default plugin — pitch deck',
|
||||
pendingPrompt:
|
||||
'Run the bundled pitch-deck plugin for a seed-stage AI design product and produce the HTML slide artifact.',
|
||||
},
|
||||
{
|
||||
skillId: 'dashboard',
|
||||
sourceKind: 'default-plugin',
|
||||
pluginId: 'example-dashboard',
|
||||
kind: 'prototype',
|
||||
name: 'Default plugin — analytics dashboard',
|
||||
pendingPrompt:
|
||||
'Run the bundled dashboard plugin for a product analytics control panel with KPIs and a weekly trend chart.',
|
||||
},
|
||||
{
|
||||
skillId: 'social-carousel',
|
||||
sourceKind: 'default-plugin',
|
||||
pluginId: 'example-social-carousel',
|
||||
kind: 'prototype',
|
||||
name: 'Default plugin — social carousel',
|
||||
pendingPrompt:
|
||||
'Run the bundled social carousel plugin for a three-card product launch announcement.',
|
||||
},
|
||||
];
|
||||
|
||||
const COMMUNITY_PLUGINS: SeedFixture[] = [
|
||||
{
|
||||
skillId: 'create-prototype-dashboard',
|
||||
sourceKind: 'community-plugin',
|
||||
pluginId: 'create-prototype-dashboard',
|
||||
kind: 'prototype',
|
||||
name: 'Community plugin — ops dashboard',
|
||||
pendingPrompt:
|
||||
'Run the community prototype dashboard plugin for a launch operations room.',
|
||||
},
|
||||
{
|
||||
skillId: 'create-slides-pitch',
|
||||
sourceKind: 'community-plugin',
|
||||
pluginId: 'create-slides-pitch',
|
||||
kind: 'deck',
|
||||
name: 'Community plugin — founder pitch',
|
||||
pendingPrompt:
|
||||
'Run the community pitch slides plugin for a founder fundraising narrative.',
|
||||
},
|
||||
{
|
||||
skillId: 'create-live-artifact-ops',
|
||||
sourceKind: 'community-plugin',
|
||||
pluginId: 'create-live-artifact-ops',
|
||||
kind: 'prototype',
|
||||
name: 'Community plugin — live ops artifact',
|
||||
pendingPrompt:
|
||||
'Run the community live artifact plugin for a refreshable customer success command center.',
|
||||
},
|
||||
];
|
||||
|
||||
interface Args {
|
||||
daemonUrl: string | null;
|
||||
dataDir: string | null;
|
||||
mode: SeedMode;
|
||||
decks: number;
|
||||
webs: number;
|
||||
defaultPlugins: number;
|
||||
communityPlugins: number;
|
||||
clear: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Args {
|
||||
const out: Args = {
|
||||
daemonUrl: null,
|
||||
dataDir: null,
|
||||
mode: 'auto',
|
||||
decks: DECKS.length,
|
||||
webs: WEBS.length,
|
||||
defaultPlugins: DEFAULT_PLUGINS.length,
|
||||
communityPlugins: COMMUNITY_PLUGINS.length,
|
||||
clear: false,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
|
|
@ -148,10 +252,32 @@ function parseArgs(argv: string[]): Args {
|
|||
process.exit(2);
|
||||
}
|
||||
out.daemonUrl = value;
|
||||
} else if (a === '--data-dir') {
|
||||
const value = argv[++i];
|
||||
if (!value) {
|
||||
console.error('--data-dir requires a directory argument');
|
||||
process.exit(2);
|
||||
}
|
||||
out.dataDir = value;
|
||||
} else if (a === '--mode') {
|
||||
const value = argv[++i];
|
||||
if (value !== 'auto' && value !== 'online' && value !== 'offline') {
|
||||
console.error('--mode must be one of: auto, online, offline');
|
||||
process.exit(2);
|
||||
}
|
||||
out.mode = value;
|
||||
} else if (a === '--online') {
|
||||
out.mode = 'online';
|
||||
} else if (a === '--offline') {
|
||||
out.mode = 'offline';
|
||||
} else if (a === '--decks') {
|
||||
out.decks = Math.max(0, Number(argv[++i]) || 0);
|
||||
} else if (a === '--webs' || a === '--prototypes') {
|
||||
out.webs = Math.max(0, Number(argv[++i]) || 0);
|
||||
} else if (a === '--default-plugins') {
|
||||
out.defaultPlugins = Math.max(0, Number(argv[++i]) || 0);
|
||||
} else if (a === '--community-plugins') {
|
||||
out.communityPlugins = Math.max(0, Number(argv[++i]) || 0);
|
||||
} else if (a === '--clear') {
|
||||
out.clear = true;
|
||||
} else if (a === '-h' || a === '--help') {
|
||||
|
|
@ -163,26 +289,46 @@ function parseArgs(argv: string[]): Args {
|
|||
process.exit(2);
|
||||
}
|
||||
}
|
||||
if (out.mode === 'offline' && out.daemonUrl) {
|
||||
console.error('--offline cannot be combined with --daemon/--daemon-url');
|
||||
process.exit(2);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: pnpm seed:test-projects [opts]
|
||||
|
||||
Seeds the running daemon with pre-baked slide decks and web prototypes
|
||||
loaded from each skill's example.html. Useful for working on the UI
|
||||
without waiting for an LLM run.
|
||||
Seeds Open Design with pre-baked, real HTML artifacts from:
|
||||
- Skills examples
|
||||
- Bundled default plugin examples
|
||||
- Community plugin examples
|
||||
|
||||
In online mode it writes through the running daemon API. In offline mode it
|
||||
writes <dataDir>/app.sqlite plus <dataDir>/projects/* directly, so you can
|
||||
ingest fixtures before starting pnpm tools-dev.
|
||||
|
||||
Options:
|
||||
--daemon <url> Daemon base URL. When omitted, the script reads
|
||||
\$OD_DAEMON_URL, then \$OD_PORT, and finally falls back
|
||||
to discovering the URL from \`pnpm tools-dev status --json\`.
|
||||
--mode <mode> auto | online | offline (default: auto). Auto uses the
|
||||
daemon when one is discoverable; otherwise offline ingest.
|
||||
--online Alias for --mode online.
|
||||
--offline Alias for --mode offline.
|
||||
--data-dir <dir> Offline target data dir (default: \$OD_DATA_DIR or ./.od).
|
||||
--decks <n> Number of slide decks to seed (default: ${DECKS.length}, max: ${DECKS.length})
|
||||
--webs <n> Number of web prototypes to seed (default: ${WEBS.length}, max: ${WEBS.length})
|
||||
--default-plugins <n>
|
||||
Number of bundled default plugin artifacts to seed
|
||||
(default: ${DEFAULT_PLUGINS.length}, max: ${DEFAULT_PLUGINS.length})
|
||||
--community-plugins <n>
|
||||
Number of community plugin artifacts to seed
|
||||
(default: ${COMMUNITY_PLUGINS.length}, max: ${COMMUNITY_PLUGINS.length})
|
||||
--clear Delete every previously seeded project (id prefix '${SEED_PREFIX}')
|
||||
-h, --help Show this help
|
||||
|
||||
Daemon URL resolution (first match wins):
|
||||
Online daemon URL resolution (first match wins):
|
||||
1. \`--daemon <url>\` on the command line.
|
||||
2. \`OD_DAEMON_URL\` env var.
|
||||
3. \`http://127.0.0.1:\$OD_PORT\` when \`OD_PORT\` is set to a real port.
|
||||
|
|
@ -191,6 +337,10 @@ Daemon URL resolution (first match wins):
|
|||
extra flags:
|
||||
pnpm tools-dev # in one shell
|
||||
pnpm seed:test-projects # in another — discovers the running daemon
|
||||
|
||||
Offline ingest before boot:
|
||||
pnpm seed:test-projects --offline --data-dir ./.od
|
||||
pnpm tools-dev
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +386,7 @@ async function discoverDaemonUrlFromToolsDev(): Promise<string | null> {
|
|||
});
|
||||
}
|
||||
|
||||
async function resolveDaemonUrl(args: Args): Promise<string> {
|
||||
async function resolveDaemonUrl(args: Args, { required }: { required: boolean }): Promise<string | null> {
|
||||
if (args.daemonUrl) return args.daemonUrl;
|
||||
if (process.env.OD_DAEMON_URL) return process.env.OD_DAEMON_URL;
|
||||
if (isDiscoverablePort(process.env.OD_PORT)) {
|
||||
|
|
@ -244,11 +394,13 @@ async function resolveDaemonUrl(args: Args): Promise<string> {
|
|||
}
|
||||
const discovered = await discoverDaemonUrlFromToolsDev();
|
||||
if (discovered) return discovered;
|
||||
if (!required) return null;
|
||||
throw new Error(
|
||||
'cannot determine daemon URL: no --daemon flag, no OD_DAEMON_URL, ' +
|
||||
'no usable OD_PORT, and `pnpm tools-dev status --json` did not report a ' +
|
||||
'running daemon. Start the daemon (e.g. `pnpm tools-dev`) or pass ' +
|
||||
'`--daemon http://127.0.0.1:<port>` explicitly.',
|
||||
'running daemon. Start the daemon (e.g. `pnpm tools-dev`), pass ' +
|
||||
'`--daemon http://127.0.0.1:<port>`, or use `--offline` to ingest ' +
|
||||
'fixtures before startup.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -281,24 +433,52 @@ async function api<T = unknown>(
|
|||
return (await resp.json()) as T;
|
||||
}
|
||||
|
||||
function makeSeedId(skillId: string): string {
|
||||
function makeSeedId(fix: SeedFixture): string {
|
||||
// unique-ish, sortable, easy to spot in the UI / db
|
||||
const ts = Date.now().toString(36);
|
||||
const rand = Math.random().toString(36).slice(2, 6);
|
||||
// Slug must match [A-Za-z0-9._-]{1,128}, see daemon validation.
|
||||
const slug = skillId.replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 60);
|
||||
const slug = `${fix.sourceKind}-${fix.skillId}`.replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 60);
|
||||
return `${SEED_PREFIX}${slug}-${ts}-${rand}`.slice(0, 128);
|
||||
}
|
||||
|
||||
function fixtureRoot(fix: SeedFixture): string {
|
||||
if (fix.sourceKind === 'default-plugin') {
|
||||
return path.join(BUNDLED_PLUGIN_EXAMPLES_DIR, fix.skillId);
|
||||
}
|
||||
if (fix.sourceKind === 'community-plugin') {
|
||||
return path.join(COMMUNITY_PLUGIN_EXAMPLES_DIR, fix.skillId);
|
||||
}
|
||||
return path.join(SKILLS_DIR, fix.skillId);
|
||||
}
|
||||
|
||||
function sourceLabel(fix: SeedFixture): string {
|
||||
if (fix.sourceKind === 'default-plugin') return `plugins/_official/examples/${fix.skillId}`;
|
||||
if (fix.sourceKind === 'community-plugin') return `plugins/community/examples/${fix.skillId}`;
|
||||
return `skills/${fix.skillId}`;
|
||||
}
|
||||
|
||||
function seedMetadata(fix: SeedFixture) {
|
||||
return {
|
||||
kind: fix.kind,
|
||||
seeded: true,
|
||||
source: 'seed-test-projects',
|
||||
sourceKind: fix.sourceKind,
|
||||
sourceId: fix.skillId,
|
||||
pluginId: fix.pluginId ?? null,
|
||||
entryFile: 'index.html',
|
||||
};
|
||||
}
|
||||
|
||||
async function loadExample(fix: SeedFixture): Promise<string> {
|
||||
const file = path.join(SKILLS_DIR, fix.skillId, fix.source ?? 'example.html');
|
||||
const file = path.join(fixtureRoot(fix), fix.source ?? 'example.html');
|
||||
return readFile(file, 'utf8');
|
||||
}
|
||||
|
||||
async function seedOne(daemonUrl: string, fix: SeedFixture): Promise<void> {
|
||||
async function seedOneOnline(daemonUrl: string, fix: SeedFixture): Promise<string> {
|
||||
const html = await loadExample(fix);
|
||||
const id = makeSeedId(fix.skillId);
|
||||
process.stdout.write(` - ${fix.kind.padEnd(9)} ${id} (${fix.skillId})\n`);
|
||||
const id = makeSeedId(fix);
|
||||
process.stdout.write(` - ${fix.kind.padEnd(9)} ${id} (${sourceLabel(fix)})\n`);
|
||||
|
||||
const created = await api<{
|
||||
project: { id: string };
|
||||
|
|
@ -306,9 +486,9 @@ async function seedOne(daemonUrl: string, fix: SeedFixture): Promise<void> {
|
|||
}>(daemonUrl, 'POST', '/api/projects', {
|
||||
id,
|
||||
name: fix.name,
|
||||
skillId: fix.skillId,
|
||||
skillId: fix.sourceKind === 'skill' ? fix.skillId : null,
|
||||
pendingPrompt: fix.pendingPrompt,
|
||||
metadata: { kind: fix.kind, seeded: true, source: 'seed-test-projects' },
|
||||
metadata: seedMetadata(fix),
|
||||
});
|
||||
|
||||
const uploaded = await api<ProjectFileResponse>(
|
||||
|
|
@ -351,7 +531,7 @@ async function seedOne(daemonUrl: string, fix: SeedFixture): Promise<void> {
|
|||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
`Seeded \`index.html\` from \`skills/${fix.skillId}/example.html\` ` +
|
||||
`Seeded \`index.html\` from \`${sourceLabel(fix)}/${fix.source ?? 'example.html'}\` ` +
|
||||
`as a starting point. Open the preview tab to see the rendered ${fix.kind}.`,
|
||||
agentId: 'seed-script',
|
||||
agentName: 'seed-test-projects',
|
||||
|
|
@ -362,9 +542,210 @@ async function seedOne(daemonUrl: string, fix: SeedFixture): Promise<void> {
|
|||
createdAt: now,
|
||||
},
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function clearSeeded(daemonUrl: string): Promise<void> {
|
||||
function expandHomePrefix(raw: string): string {
|
||||
if (raw === '~' || raw === '$HOME' || raw === '${HOME}') return os.homedir();
|
||||
const match = /^(~|\$\{HOME\}|\$HOME)[/\\](.*)$/.exec(raw);
|
||||
if (match) return path.join(os.homedir(), match[2] ?? '');
|
||||
return raw;
|
||||
}
|
||||
|
||||
function resolveDataDir(raw: string | null): string {
|
||||
const value = raw ?? process.env.OD_DATA_DIR ?? path.join(REPO_ROOT, '.od');
|
||||
const expanded = expandHomePrefix(value);
|
||||
return path.isAbsolute(expanded) ? expanded : path.resolve(REPO_ROOT, expanded);
|
||||
}
|
||||
|
||||
function loadBetterSqlite(): new (filename: string) => OfflineDatabase {
|
||||
const daemonRequire = createRequire(path.join(REPO_ROOT, 'apps', 'daemon', 'package.json'));
|
||||
return daemonRequire('better-sqlite3') as new (filename: string) => OfflineDatabase;
|
||||
}
|
||||
|
||||
function ensureColumn(db: OfflineDatabase, table: string, column: string, definition: string): void {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<Record<string, unknown>>;
|
||||
if (!cols.some((c) => c['name'] === column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateOffline(db: OfflineDatabase): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
skill_id TEXT,
|
||||
design_system_id TEXT,
|
||||
pending_prompt TEXT,
|
||||
metadata_json TEXT,
|
||||
applied_plugin_snapshot_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
title TEXT,
|
||||
applied_plugin_snapshot_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conv_project
|
||||
ON conversations(project_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
agent_id TEXT,
|
||||
agent_name TEXT,
|
||||
run_id TEXT,
|
||||
run_status TEXT,
|
||||
last_run_event_id TEXT,
|
||||
events_json TEXT,
|
||||
attachments_json TEXT,
|
||||
comment_attachments_json TEXT,
|
||||
produced_files_json TEXT,
|
||||
started_at INTEGER,
|
||||
ended_at INTEGER,
|
||||
position INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_conv
|
||||
ON messages(conversation_id, position);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tabs (
|
||||
project_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(project_id, name),
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tabs_project
|
||||
ON tabs(project_id, position);
|
||||
`);
|
||||
ensureColumn(db, 'projects', 'metadata_json', 'TEXT');
|
||||
ensureColumn(db, 'projects', 'applied_plugin_snapshot_id', 'TEXT');
|
||||
ensureColumn(db, 'conversations', 'applied_plugin_snapshot_id', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'agent_id', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'agent_name', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'run_id', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'run_status', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'last_run_event_id', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'comment_attachments_json', 'TEXT');
|
||||
ensureColumn(db, 'messages', 'produced_files_json', 'TEXT');
|
||||
}
|
||||
|
||||
async function openOfflineSeedContext(args: Args): Promise<OfflineSeedContext> {
|
||||
const dataDir = resolveDataDir(args.dataDir);
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
const Database = loadBetterSqlite();
|
||||
const db = new Database(path.join(dataDir, 'app.sqlite'));
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
migrateOffline(db);
|
||||
const projectsDir = path.join(dataDir, 'projects');
|
||||
await mkdir(projectsDir, { recursive: true });
|
||||
return { db, dataDir, projectsDir };
|
||||
}
|
||||
|
||||
function projectFileMeta(filePath: string, size: number, mtime: number): ProjectFile {
|
||||
return {
|
||||
name: filePath,
|
||||
path: filePath,
|
||||
type: 'file',
|
||||
size,
|
||||
mtime,
|
||||
kind: filePath.endsWith('.html') || filePath.endsWith('.htm') ? 'html' : 'code',
|
||||
mime: filePath.endsWith('.html') || filePath.endsWith('.htm')
|
||||
? 'text/html; charset=utf-8'
|
||||
: 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
|
||||
async function seedOneOffline(ctx: OfflineSeedContext, fix: SeedFixture): Promise<string> {
|
||||
const html = await loadExample(fix);
|
||||
const id = makeSeedId(fix);
|
||||
process.stdout.write(` - ${fix.kind.padEnd(9)} ${id} (${sourceLabel(fix)})\n`);
|
||||
|
||||
const projectDir = path.join(ctx.projectsDir, id);
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
const entryFile = 'index.html';
|
||||
const entryPath = path.join(projectDir, entryFile);
|
||||
await writeFile(entryPath, html, 'utf8');
|
||||
const written = await stat(entryPath);
|
||||
const file = projectFileMeta(entryFile, written.size, written.mtimeMs);
|
||||
const conversationId = `seed-conv-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
const userMid = `seed-msg-user-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
const asstMid = `seed-msg-asst-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
const now = Date.now();
|
||||
const metadata = seedMetadata(fix);
|
||||
|
||||
const tx = ctx.db.transaction(() => {
|
||||
ctx.db.prepare(
|
||||
`INSERT INTO projects
|
||||
(id, name, skill_id, design_system_id, pending_prompt,
|
||||
metadata_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
id,
|
||||
fix.name,
|
||||
fix.sourceKind === 'skill' ? fix.skillId : null,
|
||||
null,
|
||||
fix.pendingPrompt,
|
||||
JSON.stringify(metadata),
|
||||
now,
|
||||
now,
|
||||
);
|
||||
ctx.db.prepare(
|
||||
`INSERT INTO conversations
|
||||
(id, project_id, title, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
).run(conversationId, id, null, now, now);
|
||||
ctx.db.prepare(
|
||||
`INSERT INTO tabs (project_id, name, position, is_active)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
).run(id, entryFile, 0, 1);
|
||||
ctx.db.prepare(
|
||||
`INSERT INTO messages
|
||||
(id, conversation_id, role, content, position, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
).run(userMid, conversationId, 'user', fix.pendingPrompt, 0, now);
|
||||
ctx.db.prepare(
|
||||
`INSERT INTO messages
|
||||
(id, conversation_id, role, content, agent_id, agent_name,
|
||||
run_status, produced_files_json, started_at, ended_at, position,
|
||||
created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
asstMid,
|
||||
conversationId,
|
||||
'assistant',
|
||||
`Seeded \`index.html\` from \`${sourceLabel(fix)}/${fix.source ?? 'example.html'}\` as a starting point. Open the preview tab to see the rendered ${fix.kind}.`,
|
||||
'seed-script',
|
||||
'seed-test-projects',
|
||||
'succeeded',
|
||||
JSON.stringify([file]),
|
||||
now,
|
||||
now,
|
||||
1,
|
||||
now,
|
||||
);
|
||||
});
|
||||
tx();
|
||||
return id;
|
||||
}
|
||||
|
||||
async function clearSeededOnline(daemonUrl: string): Promise<void> {
|
||||
const { projects } = await api<{ projects: SeedProjectSummary[] }>(
|
||||
daemonUrl,
|
||||
'GET',
|
||||
|
|
@ -392,30 +773,101 @@ async function clearSeeded(daemonUrl: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function clearSeededOffline(ctx: OfflineSeedContext): Promise<void> {
|
||||
const rows = ctx.db.prepare(
|
||||
`SELECT id, metadata_json AS metadataJson FROM projects`,
|
||||
).all() as Array<{ id?: unknown; metadataJson?: unknown }>;
|
||||
const seeded = rows.filter((row) => {
|
||||
if (typeof row.id !== 'string' || !row.id.startsWith(SEED_PREFIX)) return false;
|
||||
if (typeof row.metadataJson !== 'string') return false;
|
||||
try {
|
||||
const metadata = JSON.parse(row.metadataJson) as SeedProjectSummary['metadata'];
|
||||
return metadata?.seeded === true && metadata.source === 'seed-test-projects';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (seeded.length === 0) {
|
||||
console.log('no seeded projects to remove.');
|
||||
return;
|
||||
}
|
||||
console.log(`removing ${seeded.length} seeded project(s):`);
|
||||
for (const row of seeded) {
|
||||
const id = String(row.id);
|
||||
process.stdout.write(` - ${id}\n`);
|
||||
ctx.db.prepare(`DELETE FROM projects WHERE id = ?`).run(id);
|
||||
await rm(path.join(ctx.projectsDir, id), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const daemonUrl = await resolveDaemonUrl(args);
|
||||
const daemonUrl = args.mode === 'offline'
|
||||
? null
|
||||
: await resolveDaemonUrl(args, { required: args.mode === 'online' });
|
||||
const mode: Exclude<SeedMode, 'auto'> = args.mode === 'offline' || !daemonUrl
|
||||
? 'offline'
|
||||
: 'online';
|
||||
const onlineDaemonUrl = mode === 'online' ? daemonUrl : null;
|
||||
|
||||
if (args.clear) {
|
||||
await clearSeeded(daemonUrl);
|
||||
if (mode === 'online') {
|
||||
if (!onlineDaemonUrl) throw new Error('online mode requires a daemon URL');
|
||||
await clearSeededOnline(onlineDaemonUrl);
|
||||
return;
|
||||
}
|
||||
const ctx = await openOfflineSeedContext(args);
|
||||
try {
|
||||
await clearSeededOffline(ctx);
|
||||
} finally {
|
||||
ctx.db.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const decks = DECKS.slice(0, args.decks);
|
||||
const webs = WEBS.slice(0, args.webs);
|
||||
if (decks.length === 0 && webs.length === 0) {
|
||||
console.error('--decks 0 and --webs 0 — nothing to do.');
|
||||
const defaultPlugins = DEFAULT_PLUGINS.slice(0, args.defaultPlugins);
|
||||
const communityPlugins = COMMUNITY_PLUGINS.slice(0, args.communityPlugins);
|
||||
const fixtures = [...decks, ...webs, ...defaultPlugins, ...communityPlugins];
|
||||
if (fixtures.length === 0) {
|
||||
console.error('--decks 0, --webs 0, --default-plugins 0, and --community-plugins 0 — nothing to do.');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
console.log(`seeding ${decks.length} deck(s) + ${webs.length} web prototype(s) → ${daemonUrl}`);
|
||||
const target = mode === 'online'
|
||||
? onlineDaemonUrl
|
||||
: resolveDataDir(args.dataDir);
|
||||
console.log(
|
||||
`seeding ${decks.length} skill deck(s) + ${webs.length} skill web prototype(s) + ` +
|
||||
`${defaultPlugins.length} default plugin artifact(s) + ` +
|
||||
`${communityPlugins.length} community plugin artifact(s) → ${mode}:${target}`,
|
||||
);
|
||||
const failures: string[] = [];
|
||||
for (const fix of [...decks, ...webs]) {
|
||||
const createdIds: string[] = [];
|
||||
if (mode === 'online') {
|
||||
if (!onlineDaemonUrl) throw new Error('online mode requires a daemon URL');
|
||||
for (const fix of fixtures) {
|
||||
try {
|
||||
createdIds.push(await seedOneOnline(onlineDaemonUrl, fix));
|
||||
} catch (err) {
|
||||
failures.push(fix.skillId);
|
||||
console.error(` ! ${sourceLabel(fix)} failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const ctx = await openOfflineSeedContext(args);
|
||||
try {
|
||||
await seedOne(daemonUrl, fix);
|
||||
} catch (err) {
|
||||
failures.push(fix.skillId);
|
||||
console.error(` ! ${fix.skillId} failed: ${(err as Error).message}`);
|
||||
for (const fix of fixtures) {
|
||||
try {
|
||||
createdIds.push(await seedOneOffline(ctx, fix));
|
||||
} catch (err) {
|
||||
failures.push(fix.skillId);
|
||||
console.error(` ! ${sourceLabel(fix)} failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ctx.db.close();
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
|
|
@ -424,7 +876,12 @@ async function main() {
|
|||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('done. Open the web UI — the seeded projects show up in the project list.');
|
||||
console.log(`done. Seeded ${createdIds.length} project(s).`);
|
||||
if (mode === 'offline') {
|
||||
console.log('Start the daemon/web UI next; the seeded projects will show up in the project list.');
|
||||
} else {
|
||||
console.log('Open the web UI — the seeded projects show up in the project list.');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue