diff --git a/apps/web/public/schema.json b/apps/web/public/schema.json index a25172e8..74cfc9d8 100644 --- a/apps/web/public/schema.json +++ b/apps/web/public/schema.json @@ -35,6 +35,11 @@ "format": "uri", "description": "Optional URL to the source code of the project (eg, GitHub repo)." }, + "websiteUrl": { + "type": "string", + "format": "uri", + "description": "Optional URL to the product's website." + }, "origin": { "anyOf": [ { diff --git a/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-project-index.tsx b/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-project-index.tsx index 844de0b1..4cf076aa 100644 --- a/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-project-index.tsx +++ b/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-project-index.tsx @@ -1,6 +1,8 @@ 'use client' import { assert, omit, sanitizeSearchParams } from '@agentic/platform-core' +import { ExternalLinkIcon } from 'lucide-react' +import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' @@ -8,6 +10,7 @@ import { useAgentic } from '@/components/agentic-provider' import { LoadingIndicator } from '@/components/loading-indicator' import { PageContainer } from '@/components/page-container' import { ProjectPricingPlans } from '@/components/project-pricing-plans' +import { GitHubIcon } from '@/icons/github' import { toast, toastError } from '@/lib/notifications' import { useQuery } from '@/lib/query-client' @@ -148,13 +151,52 @@ export function MarketplaceProjectIndex({ ) : !project ? (

Project "{projectIdentifier}" not found

) : ( - <> -

- {project.name} -

+
+
+
+ {project.name} + +

+ {project.name} +

+
+ +
+
+ {project.identifier} +
+ + {project.lastPublishedDeployment?.websiteUrl && ( + + + + Homepage + + )} + + {project.lastPublishedDeployment?.sourceUrl && ( + + + + GitHub + + )} +
+
@@ -172,7 +214,7 @@ export function MarketplaceProjectIndex({
               isLoadingStripeCheckoutForPlan={isLoadingStripeCheckoutForPlan}
               onSubscribe={onSubscribe}
             />
-          
+          
)} diff --git a/examples/mcp-servers/search/agentic.config.ts b/examples/mcp-servers/search/agentic.config.ts index 6e902df2..2a604dde 100644 --- a/examples/mcp-servers/search/agentic.config.ts +++ b/examples/mcp-servers/search/agentic.config.ts @@ -11,6 +11,9 @@ export default defineConfig({ type: 'mcp', url: process.env.MCP_ORIGIN_URL! }, + sourceUrl: + 'https://github.com/transitive-bullshit/agentic/tree/main/examples/mcp-servers/search', + websiteUrl: 'https://agentic.so', toolConfigs: [ { name: 'search', diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 128ba1ec..2a98db44 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -19,7 +19,8 @@ export function registerDeployCommand({ 'The directory to load the Agentic project config from (defaults to cwd). This directory must contain an "agentic.config.{ts,js,json}" project file.' ) .option('-d, --debug', 'Print out the parsed agentic config and return.') - .option('-p, --publish', 'Publishes the deployment after creating it.') + // TODO + //.option('-p, --publish', 'Publishes the deployment after creating it.') .action(async (opts) => { AuthStore.requireAuth() @@ -47,12 +48,17 @@ export function registerDeployCommand({ return } - // Create the deployment on the backend, validate it, and optionally - // publish it. + // Create the deployment on the backend, validating it in the process. + // Note that the backend performs more validation than the client does + // and is the ultimate source of truth. const deployment = await oraPromise( - client.createDeployment(config, { - publish: opts.publish ? 'true' : 'false' - }), + client.createDeployment( + config + // TODO: need to prompt to get or confirm version before publishing + // { + // publish: opts.publish ? 'true' : 'false' + // } + ), { text: `Creating deployment for project "${config.slug}"`, successText: `Deployment created successfully`, diff --git a/packages/cli/src/commands/publish.ts b/packages/cli/src/commands/publish.ts index 15753b77..e1af6726 100644 --- a/packages/cli/src/commands/publish.ts +++ b/packages/cli/src/commands/publish.ts @@ -1,11 +1,10 @@ -import { select } from '@clack/prompts' import { Command } from 'commander' import { gracefulExit } from 'exit-hook' import { oraPromise } from 'ora' -import semver from 'semver' import type { Context } from '../types' import { AuthStore } from '../lib/auth-store' +import { promptForDeploymentVersion } from '../lib/prompt-for-deployment-version' import { resolveDeployment } from '../lib/resolve-deployment' export function registerPublishCommand({ @@ -65,39 +64,10 @@ export function registerPublishCommand({ return gracefulExit(1) } - const initialVersion = deployment.version - const baseVersion = - initialVersion || project.lastPublishedDeploymentVersion || '0.0.0' - - const options = [ - initialVersion - ? { value: initialVersion, label: initialVersion } - : null, - { - value: semver.inc(baseVersion, 'patch'), - label: `${semver.inc(baseVersion, 'patch')} (patch)` - }, - { - value: semver.inc(baseVersion, 'minor'), - label: `${semver.inc(baseVersion, 'minor')} (minor)` - }, - { - value: semver.inc(baseVersion, 'major'), - label: `${semver.inc(baseVersion, 'major')} (major)` - } - ].filter(Boolean) - - if (project.lastPublishedDeploymentVersion) { - logger.info( - `Project "${project.identifier}" latest published version is "${project.lastPublishedDeploymentVersion}".\n` - ) - } else { - logger.info(`Project "${project.identifier}" is not published yet.\n`) - } - - const version = await select({ - message: `Select version of deployment "${deployment.identifier}" to publish:`, - options + const version = await promptForDeploymentVersion({ + deployment, + project, + logger }) if (!version || typeof version !== 'string') { diff --git a/packages/cli/src/lib/prompt-for-deployment-version.ts b/packages/cli/src/lib/prompt-for-deployment-version.ts new file mode 100644 index 00000000..fc5b9f30 --- /dev/null +++ b/packages/cli/src/lib/prompt-for-deployment-version.ts @@ -0,0 +1,57 @@ +import type { Deployment, Project } from '@agentic/platform-types' +import { select } from '@clack/prompts' +import { gracefulExit } from 'exit-hook' +import semver from 'semver' + +import type { Context } from '../types' + +export async function promptForDeploymentVersion({ + deployment, + project, + logger +}: { + deployment: Deployment + project: Project + logger: Context['logger'] +}): Promise { + const initialVersion = deployment.version + const baseVersion = + initialVersion || project.lastPublishedDeploymentVersion || '0.0.0' + + const options = [ + initialVersion ? { value: initialVersion, label: initialVersion } : null, + { + value: semver.inc(baseVersion, 'patch'), + label: `${semver.inc(baseVersion, 'patch')} (patch)` + }, + { + value: semver.inc(baseVersion, 'minor'), + label: `${semver.inc(baseVersion, 'minor')} (minor)` + }, + { + value: semver.inc(baseVersion, 'major'), + label: `${semver.inc(baseVersion, 'major')} (major)` + } + ].filter(Boolean) + + if (project.lastPublishedDeploymentVersion) { + logger.info( + `Project "${project.identifier}" latest published version is "${project.lastPublishedDeploymentVersion}".\n` + ) + } else { + logger.info(`Project "${project.identifier}" is not published yet.\n`) + } + + const version = await select({ + message: `Select version of deployment "${deployment.identifier}" to publish:`, + options + }) + + if (!version || typeof version !== 'string') { + logger.error('No version selected') + gracefulExit(1) + return + } + + return version +} diff --git a/packages/types/src/openapi.d.ts b/packages/types/src/openapi.d.ts index e56ccd4c..a142c857 100644 --- a/packages/types/src/openapi.d.ts +++ b/packages/types/src/openapi.d.ts @@ -768,6 +768,11 @@ export interface components { * @description Optional URL to the source code of the project (eg, GitHub repo). */ sourceUrl?: string; + /** + * Format: uri + * @description Optional URL to the product's website. + */ + websiteUrl?: string; /** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */ userId: string; /** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */ @@ -1040,6 +1045,11 @@ export interface components { * @description Optional URL to the source code of the project (eg, GitHub repo). */ sourceUrl?: string; + /** + * Format: uri + * @description Optional URL to the product's website. + */ + websiteUrl?: string; /** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */ userId: string; /** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */ @@ -2409,6 +2419,11 @@ export interface operations { * @description Optional URL to the source code of the project (eg, GitHub repo). */ sourceUrl?: string; + /** + * Format: uri + * @description Optional URL to the product's website. + */ + websiteUrl?: string; origin: components["schemas"]["OriginAdapterConfig"]; /** * @description List of PricingPlans configuring which Stripe subscriptions should be available for the project. Defaults to a single free plan which is useful for developing and testing your project. diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index bdfeeebd..fc6149b6 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -38,11 +38,14 @@ export type Consumer = Simplify< } > export type Project = Simplify< - components['schemas']['Project'] & { + Omit< + components['schemas']['Project'], + 'lastPublishedDeployment' | 'lastDeployment' + > & { user?: User team?: Team - lastPublishedDeployment?: Deployment - lastDeployment?: Deployment + lastPublishedDeployment?: Simplify> + lastDeployment?: Simplify> /** * The public base HTTP URL for the project supporting HTTP POST requests for @@ -78,12 +81,14 @@ export type Project = Simplify< export type Deployment = Simplify< Omit< components['schemas']['Deployment'], - 'pricingPlans' | 'toolConfigs' | 'defaultRateLimit' + 'pricingPlans' | 'toolConfigs' | 'defaultRateLimit' | 'project' > & { pricingPlans: PricingPlan[] toolConfigs: ToolConfig[] defaultRateLimit: RateLimit - project?: components['schemas']['Project'] + project?: Simplify< + Omit + > /** * The public base HTTP URL for the deployment supporting HTTP POST requests @@ -123,16 +128,19 @@ export type Deployment = Simplify< export type AdminDeployment = Simplify< Omit< components['schemas']['AdminDeployment'], - 'pricingPlans' | 'toolConfigs' | 'defaultRateLimit' | 'origin' + 'pricingPlans' | 'toolConfigs' | 'defaultRateLimit' | 'origin' | 'project' > & { pricingPlans: PricingPlan[] toolConfigs: ToolConfig[] defaultRateLimit: RateLimit origin: OriginAdapter - project?: components['schemas']['Project'] } & Pick< Deployment, - 'gatewayBaseUrl' | 'gatewayMcpUrl' | 'marketplaceUrl' | 'adminUrl' + | 'gatewayBaseUrl' + | 'gatewayMcpUrl' + | 'marketplaceUrl' + | 'adminUrl' + | 'project' > > diff --git a/todo.md b/todo.md index 40e76438..bb5cf60f 100644 --- a/todo.md +++ b/todo.md @@ -53,7 +53,7 @@ - stripe - re-add coupons - declarative json-based pricing - - like https://github.com/tierrun/tier and Saasify + - like Saasify and https://github.com/tierrun/tier - https://github.com/tierrun/tier/blob/main/pricing/schema.json - https://blog.tier.run/tier-hello-world-demo - stripe connect