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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/ci-dsh-design-studio.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: DeepSeek Harness Design Studio plugin

on:
push:
branches: [main, dev]
paths:
- "plugins/deepseek-harness/design-studio/**"
- "packages/design-studio/**"
- "apps/app/src/react-app/domains/session/design/**"
- ".github/workflows/ci-dsh-design-studio.yml"
tags:
- "deepseek-idesign-v*"
pull_request:
branches: [main, dev]
paths:
- "plugins/deepseek-harness/design-studio/**"
- "packages/design-studio/**"
- "apps/app/src/react-app/domains/session/design/**"
- ".github/workflows/ci-dsh-design-studio.yml"

permissions:
contents: read

defaults:
run:
working-directory: plugins/deepseek-harness/design-studio

jobs:
check:
name: Build and inspect package
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 11.4.0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: pnpm

- name: Install iPolloWork workspace
run: pnpm --dir ../../.. install --frozen-lockfile

- name: Install plugin workspace
run: pnpm install --frozen-lockfile

- name: Typecheck and build
run: pnpm run check && pnpm run build

- name: Dry-run publish
run: npm publish --dry-run --access public

publish:
name: Publish to npm
needs: check
if: startsWith(github.ref, 'refs/tags/deepseek-idesign-v')
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 11.4.0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: pnpm

- name: Install iPolloWork workspace
run: pnpm --dir ../../.. install --frozen-lockfile

- name: Install plugin workspace
run: pnpm install --frozen-lockfile

- name: Publish
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/ibm-plex-sans": "^5.2.8",
"@heroicons/react": "^2.2.0",
"@ipollowork/design-studio": "workspace:*",
"@ipollowork/types": "workspace:*",
"@ipollowork/ui": "workspace:*",
"@lexical/react": "^0.35.0",
Expand Down
29 changes: 29 additions & 0 deletions apps/app/src/app/lib/den-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,35 @@ export type DenOrgMarketplace = {
updatedAt: string | null;
};

export type DenMarketplacePlugin = {
pluginId: string;
name: string;
description: string;
category: string;
publisher: string;
icon: Record<string, unknown> | null;
version: string;
manifest: iPolloWorkExtensionManifest;
pointsCost: number;
acquired: boolean;
featured: boolean;
digest: string;
size: number;
updatedAt: string;
};

export type DenMarketplaceAcquireResult = {
acquired: true;
spentPoints: number;
balance: number | null;
};

export type DenMarketplacePluginDownload = {
bytes: Uint8Array;
fileName: string;
digest: string | null;
};

export type DenOrgPluginResolved = {
plugin: DenOrgPlugin;
memberships: DenPluginMembership[];
Expand Down
132 changes: 132 additions & 0 deletions apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "./den-session-events";
import {
desktopFetch,
desktopFetchBinaryViaMain,
desktopFetchViaMain,
getDesktopBootstrapConfig as getDesktopBootstrapConfigFromShell,
setDesktopBootstrapConfig as setDesktopBootstrapConfigInShell,
Expand Down Expand Up @@ -59,6 +60,9 @@ export const DEN_INFERENCE_PATH = "/dashboard/inference";
export type * from "./den-types";
import type {
DenOrgExtensionProjection,
DenMarketplaceAcquireResult,
DenMarketplacePlugin,
DenMarketplacePluginDownload,
DenOrgMarketplace,
DenOrgPlugin,
DenOrgPluginResolved,
Expand Down Expand Up @@ -1279,6 +1283,48 @@ function parseiPolloWorkExtensionManifest(value: unknown): iPolloWorkExtensionMa
return result.success ? result.manifest : null;
}

function parseMarketplacePlugin(value: unknown): DenMarketplacePlugin | null {
if (!isRecord(value)) return null;
const manifest = parseiPolloWorkExtensionManifest(value.manifest);
if (
!manifest
|| typeof value.pluginId !== "string"
|| typeof value.name !== "string"
|| typeof value.description !== "string"
|| typeof value.category !== "string"
|| typeof value.publisher !== "string"
|| typeof value.version !== "string"
|| typeof value.pointsCost !== "number"
|| typeof value.acquired !== "boolean"
|| typeof value.featured !== "boolean"
|| typeof value.digest !== "string"
|| typeof value.size !== "number"
|| typeof value.updatedAt !== "string"
) return null;
return {
pluginId: value.pluginId,
name: value.name,
description: value.description,
category: value.category,
publisher: value.publisher,
icon: isRecord(value.icon) ? value.icon : null,
version: value.version,
manifest,
pointsCost: value.pointsCost,
acquired: value.acquired,
featured: value.featured,
digest: value.digest,
size: value.size,
updatedAt: value.updatedAt,
};
}

function parseMarketplaceAcquireResult(value: unknown): DenMarketplaceAcquireResult | null {
if (!isRecord(value) || value.acquired !== true || typeof value.spentPoints !== "number") return null;
if (value.balance !== null && typeof value.balance !== "number") return null;
return { acquired: true, spentPoints: value.spentPoints, balance: value.balance };
}

function parseDenExtensionProjection(value: unknown): DenOrgExtensionProjection | null {
if (!isRecord(value) || typeof value.id !== "string" || typeof value.name !== "string") return null;
const sourceFormat = parseExtensionSourceFormat(value.sourceFormat);
Expand Down Expand Up @@ -1611,6 +1657,51 @@ async function requestJson<T>(
return raw.json as T;
}

async function requestBinary(
input: string | DenBaseUrls,
path: string,
options: DenRequestOptions = {},
): Promise<Response> {
const baseUrls = typeof input === "string" ? resolveDenBaseUrls(input) : input;
const url = `${resolveRequestBaseUrl(baseUrls, path)}${path}`;
const headers: Record<string, string> = { Accept: "application/zip" };
const token = options.token?.trim() ?? "";
if (token) headers.Authorization = `Bearer ${token}`;

const fetchImpl: FetchLike = isDesktopRuntime()
? (requestInput, init) => desktopFetchBinaryViaMain(requestInput, init, options.timeoutMs ?? DEFAULT_DEN_TIMEOUT_MS)
: globalThis.fetch;
const response = await fetchWithTimeout(fetchImpl, url, {
method: options.method ?? "GET",
headers,
credentials: "include",
}, options.timeoutMs ?? DEFAULT_DEN_TIMEOUT_MS);
if (response.ok) return response;

const text = await response.text();
let payload: unknown = null;
try {
payload = text ? JSON.parse(text) : null;
} catch {
payload = null;
}
const code = isRecord(payload) && typeof payload.error === "string" ? payload.error : "request_failed";
throw new DenApiError(response.status, code, getErrorMessage(payload, `Request failed with ${response.status}.`));
}

function responseFileName(response: Response, fallback: string): string {
const disposition = response.headers.get("Content-Disposition") ?? "";
const encoded = disposition.match(/filename\*=UTF-8''([^;]+)/iu)?.[1];
if (encoded) {
try {
return decodeURIComponent(encoded);
} catch {
return fallback;
}
}
return disposition.match(/filename="([^"]+)"/u)?.[1] ?? fallback;
}

async function ensureActiveOrganization(
baseUrls: DenBaseUrls,
token: string | null,
Expand Down Expand Up @@ -1899,6 +1990,47 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
);
},

async listMarketplacePlugins(): Promise<DenMarketplacePlugin[]> {
const payload = await requestJson<unknown>(baseUrls, "/api/v1/marketplace/plugins", { token });
if (!isRecord(payload) || !Array.isArray(payload.items)) {
throw new DenApiError(500, "invalid_marketplace_payload", "Marketplace response was invalid.");
}
const items = payload.items.map(parseMarketplacePlugin);
if (items.some((item) => item === null)) {
throw new DenApiError(500, "invalid_marketplace_payload", "Marketplace response contained an invalid plugin package.");
}
return items as DenMarketplacePlugin[];
},

async getMarketplacePlugin(pluginId: string): Promise<DenMarketplacePlugin> {
const payload = await requestJson<unknown>(baseUrls, `/api/v1/marketplace/plugins/${encodeURIComponent(pluginId)}`, { token });
const item = isRecord(payload) ? parseMarketplacePlugin(payload.item) : null;
if (!item) throw new DenApiError(500, "invalid_marketplace_payload", "Marketplace plugin response was invalid.");
return item;
},

async acquireMarketplacePlugin(pluginId: string): Promise<DenMarketplaceAcquireResult> {
const payload = await requestJson<unknown>(baseUrls, `/api/v1/marketplace/plugins/${encodeURIComponent(pluginId)}/acquire`, {
method: "POST",
token,
});
const result = parseMarketplaceAcquireResult(payload);
if (!result) throw new DenApiError(500, "invalid_marketplace_payload", "Marketplace purchase response was invalid.");
return result;
},

async downloadMarketplacePlugin(pluginId: string): Promise<DenMarketplacePluginDownload> {
const response = await requestBinary(baseUrls, `/api/v1/marketplace/plugins/${encodeURIComponent(pluginId)}/download`, {
token,
timeoutMs: 30_000,
});
return {
bytes: new Uint8Array(await response.arrayBuffer()),
fileName: responseFileName(response, `${pluginId.replaceAll("/", "-")}.ipollowork-plugin`),
digest: response.headers.get("X-iPollo-Artifact-SHA256"),
};
},

async listOrgMarketplaces(orgId: string): Promise<DenOrgMarketplace[]> {
const payload = await requestJson<unknown>(
baseUrls,
Expand Down
49 changes: 43 additions & 6 deletions apps/app/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ export default {
"plugin_platform.default_category": "Service plugin",
"plugin_platform.category_design_development": "Design and development",
"plugin_platform.capability_summary": "{apps} apps · {skills} skills · {more} additional capabilities",
"plugin_library.navigation_label": "Extension type",
"plugin_library.plugins_tab": "Plugins",
"plugin_library.skills_tab": "Skills",
"plugin_library.title": "Plugins",
"plugin_library.description": "Add tools, skills, and services to agents through complete capability packages.",
"plugin_library.add": "Add",
"plugin_library.search": "Search plugins",
"plugin_library.installed": "Installed",
"plugin_library.open_plugin": "Open {name}",
"plugin_library.source_label": "Plugin source",
"plugin_library.marketplace": "Marketplace",
"plugin_library.personal": "Personal",
"plugin_library.personal_title": "Personal plugins",
"plugin_library.personal_description": "Manage complete plugin packages installed or imported into this workspace.",
"plugin_library.personal_empty": "No personal plugins yet.",
"plugin_library.featured": "Featured",
"plugin_library.category.ai-agents": "AI Agents & Automation",
"plugin_library.category.development-operations": "Development & Operations",
"plugin_library.category.design-creative": "Design & Creative",
"plugin_library.category.productivity-collaboration": "Productivity & Collaboration",
"plugin_library.category.business-operations": "Business & Operations",
"plugin_library.category.finance": "Finance",
"plugin_library.category.other": "Other",
"design.export.download": "Download",
"design.export.download_pdf": "Download PDF",
"design.export.download_pptx": "Download PPTX",
Expand Down Expand Up @@ -1306,7 +1329,7 @@ export default {
"extensions.filter_apps": "Apps",
"extensions.filter_plugins": "Plugins",
"extensions.marketplace_active_cloud_label": "Active · runs in cloud",
"extensions.marketplace_description": "Browse built-in iPolloWork extensions and organization marketplace extensions. Claude-compatible plugins are normalized into iPolloWork extensions with installable resources such as skills, MCPs, commands, or tools.",
"extensions.marketplace_description": "Browse complete plugin capability packages from iPolloWork Cloud, then purchase or install them into My Extensions.",
"extensions.marketplace_local_description": "Desktop-only and unsynced marketplace items stay installable here. Cloud-runnable apps are listed in Connect.",
"extensions.marketplace_local_title": "From your marketplace — installs on this machine",
"extensions.marketplace_runs_in_cloud": "Runs in cloud",
Expand Down Expand Up @@ -2195,10 +2218,10 @@ export default {
"settings.tab_description_appearance": "Adjust how iPolloWork looks across desktop, system theme, and app frame.",
"settings.tab_description_cloud_account": "Sign in and configure your personal cloud workspace.",
"settings.tab_description_connect": "Use cloud-managed MCP connections shared by your organization.",
"settings.tab_description_cloud_marketplaces": "Browse and import plugins from your organization's marketplaces.",
"settings.tab_description_cloud_marketplaces": "Browse complete plugin capability packages from iPolloWork Cloud.",
"settings.tab_description_cloud_providers": "Import and manage LLM provider keys from your organization.",
"settings.tab_description_debug": "Review runtime diagnostics, logs, and low-level debugging utilities.",
"settings.tab_description_extensions": "Manage MCP apps and OpenCode plugins for this workspace.",
"settings.tab_description_extensions": "Manage complete plugin capability packages and skills for this workspace.",
"settings.tab_description_general": "Connect providers, choose the default model, authorize folders, and control the selected iPolloWork workspace plus its runtime connection.",
"settings.environment.add_button": "Add variable",
"settings.environment.add_title": "Add environment variable",
Expand Down Expand Up @@ -2861,8 +2884,22 @@ export default {
"settings.ai.not_connected": "Not connected",
"settings.ai.models_connect_description": "Hosted frontier models without managing API keys.",
"settings.extensions.opencode_plugins": "OpenCode Plugins",
"settings.marketplace.signin_hint": "You can use iPolloWork without an account. Sign in to iPolloWork Cloud to load Marketplace content, including built-in extensions and organization marketplaces.",
"settings.marketplace.loading": "Loading marketplace extensions…",
"settings.marketplace.signin_title": "Sign in to browse the Plugin Marketplace",
"settings.marketplace.signin_hint": "Marketplace content comes from your iPolloWork Cloud account. Sign in to browse, purchase, and install complete plugin capability packages.",
"settings.marketplace.loading": "Loading the Plugin Marketplace…",
"settings.marketplace.load_failed": "Unable to load the Plugin Marketplace.",
"settings.marketplace.install_failed": "Unable to install this plugin package.",
"settings.marketplace.digest_mismatch": "Plugin package integrity verification failed. Download it again.",
"settings.marketplace.buy_install": "Buy & install · {points} iPoints",
"settings.marketplace.installed": "Installed",
"settings.marketplace.featured": "Featured",
"settings.marketplace.free": "Free",
"settings.marketplace.back": "Back to Marketplace",
"settings.marketplace.capabilities": "Package capabilities",
"settings.marketplace.package_info": "Package information",
"settings.marketplace.size": "Size",
"settings.marketplace.resources": "Capabilities",
"settings.marketplace.permissions": "Permissions",
"settings.marketplace.working": "Working…",
"settings.marketplace.search": "Search marketplace extensions…",
"settings.marketplace.filter_all": "All",
Expand All @@ -2872,7 +2909,7 @@ export default {
"settings.marketplace.marketplace_label": "Marketplace",
"settings.marketplace.all_marketplaces": "All marketplaces",
"settings.marketplace.signin_empty": "Sign in to view marketplace extensions.",
"settings.marketplace.empty": "No marketplace extensions are available yet.",
"settings.marketplace.empty": "No plugin capability packages are available yet.",
"settings.marketplace.choose_org": "Choose an organization to view marketplace extensions.",
"settings.marketplace.no_match": "No marketplace extensions match your search or filters.",
"settings.marketplace.composition": "Composition",
Expand Down
Loading
Loading