Showing the changes to packages/plugins/apps/src/plugin (2 of 19 files). Show all
Unified · Split
| @@ -4,10 +4,12 @@ import { · 3 unmodified lines | ||
| 4 | 4 | definePlugin, |
| 5 | 5 | tool, |
| 6 | 6 | ToolName, |
| 7 | − ConnectionName, | |
| 8 | 7 | IntegrationSlug, |
| 9 | 8 | AuthTemplateSlug, |
| 10 | 9 | connectionIdentifier, |
| 10 | + type ConnectionRef, | |
| 11 | + type Owner, | |
| 12 | + type PluginCtx, | |
| 11 | 13 | type ResolveToolsInput, |
| 12 | 14 | type ResolveToolsResult, |
| 13 | 15 | type InvokeToolInput, |
| @@ -18,6 +20,7 @@ import type { AppsRuntime } from "./runtime"; · 4 unmodified lines | ||
| 18 | 20 | import { makeAppsStore } from "./store"; |
| 19 | 21 | import type { ClientResolver, ConnectionCandidate } from "./bindings"; |
| 20 | 22 | import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor"; |
| 23 | +import { syncGitHubSource, type GitHubSyncResult } from "../source/github-source"; | |
| 21 | 24 | |
| 22 | 25 | export const APPS_INTEGRATION_SLUG = "apps"; |
| 23 | 26 | export const APPS_PLUGIN_ID = "apps"; |
| @@ -42,6 +45,40 @@ const isRecord = (value: unknown): value is Record<string, unknown> => · 18 unmodified lines | ||
| 42 | 45 | |
| 43 | 46 | const unique = (values: readonly string[]): readonly string[] => [...new Set(values)]; |
| 44 | 47 | |
| 48 | +const syncFailure = (message: string, path?: string): GitHubSyncResult => ({ | |
| 49 | + status: "failed", | |
| 50 | + tools: [], | |
| 51 | + skipped: [], | |
| 52 | + errors: [ | |
| 53 | + { | |
| 54 | + stage: "source", | |
| 55 | + message, | |
| 56 | + ...(path ? { diagnostics: [{ path, message }] } : {}), | |
| 57 | + }, | |
| 58 | + ], | |
| 59 | +}); | |
| 60 | + | |
| 61 | +const sourceScopeFor = (input: { repo: string; connection: string }): string => | |
| 62 | + String(connectionIdentifier(`github ${input.connection} ${input.repo}`, "githubSource")); | |
| 63 | + | |
| 64 | +const parseConnectionAddress = (address: string): ConnectionRef | null => { | |
| 65 | + const parts = address.split("."); | |
| 66 | + if (parts.length !== 4 || parts[0] !== "tools") return null; | |
| 67 | + const [, integration, owner, name] = parts; | |
| 68 | + if (!integration || !name) return null; | |
| 69 | + if (owner !== "org" && owner !== "user") return null; | |
| 70 | + return { | |
| 71 | + owner: owner as Owner, | |
| 72 | + integration: IntegrationSlug.make(integration), | |
| 73 | + name: connectionIdentifier(name), | |
| 74 | + }; | |
| 75 | +}; | |
| 76 | + | |
| 77 | +const configBaseUrl = (config: unknown): string | undefined => | |
| 78 | + isRecord(config) && typeof config.baseUrl === "string" && config.baseUrl.length > 0 | |
| 79 | + ? config.baseUrl | |
| 80 | + : undefined; | |
| 81 | + | |
| 45 | 82 | const projectInputSchema = ( |
| 46 | 83 | schema: unknown, |
| 47 | 84 | integrations: Readonly<Record<string, IntegrationDecl>>, |
| @@ -94,6 +131,49 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 46 unmodified lines | ||
| 94 | 131 | Effect.map((mapped) => mapped ?? scopeFromConnection(connectionName)), |
| 95 | 132 | ); |
| 96 | 133 | |
| 134 | + const ensureCatalogConnection = (scope: string, ctx: PluginCtx<AppsStoreShape>) => | |
| 135 | + Effect.gen(function* () { | |
| 136 | + const slug = IntegrationSlug.make(APPS_INTEGRATION_SLUG); | |
| 137 | + const existing = yield* ctx.core.integrations | |
| 138 | + .get(slug) | |
| 139 | + .pipe(Effect.orElseSucceed(() => null)); | |
| 140 | + if (!existing) { | |
| 141 | + yield* ctx.core.integrations.register({ | |
| 142 | + slug, | |
| 143 | + name: "Apps", | |
| 144 | + description: "User-authored, published custom tools.", | |
| 145 | + config: {}, | |
| 146 | + canRemove: false, | |
| 147 | + canRefresh: true, | |
| 148 | + }); | |
| 149 | + } | |
| 150 | + const rawName = connectionNameForScope(scope); | |
| 151 | + const connName = connectionIdentifier(rawName); | |
| 152 | + const conns = yield* ctx.connections | |
| 153 | + .list({ integration: slug }) | |
| 154 | + .pipe(Effect.orElseSucceed(() => [])); | |
| 155 | + if (!conns.some((c) => String(c.name) === String(connName))) { | |
| 156 | + yield* ctx.connections.create({ | |
| 157 | + owner: "user", | |
| 158 | + name: connName, | |
| 159 | + integration: slug, | |
| 160 | + template: AuthTemplateSlug.make("none"), | |
| 161 | + value: "", | |
| 162 | + }); | |
| 163 | + } | |
| 164 | + yield* runtime.deps.store | |
| 165 | + .putScopeForConnection(String(rawName), scope) | |
| 166 | + .pipe(Effect.orElseSucceed(() => undefined)); | |
| 167 | + yield* runtime.deps.store | |
| 168 | + .putScopeForConnection(String(connName), scope) | |
| 169 | + .pipe(Effect.orElseSucceed(() => undefined)); | |
| 170 | + return { | |
| 171 | + scope, | |
| 172 | + connection: String(connName), | |
| 173 | + ref: { owner: "user" as const, integration: slug, name: connName }, | |
| 174 | + }; | |
| 175 | + }); | |
| 176 | + | |
| 97 | 177 | return { |
| 98 | 178 | id: APPS_PLUGIN_ID as "apps", |
| 99 | 179 | packageName: "@executor-js/plugin-apps", |
| @@ -135,59 +215,78 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 35 unmodified lines | ||
| 135 | 215 | Effect.gen(function* () { |
| 136 | 216 | const scope = |
| 137 | 217 | (args as { scope?: string } | undefined)?.scope ?? DEFAULT_CATALOG_SCOPE; |
| 138 | − const slug = IntegrationSlug.make(APPS_INTEGRATION_SLUG); | |
| 139 | − const existing = yield* ctx.core.integrations | |
| 140 | − .get(slug) | |
| 141 | − .pipe(Effect.orElseSucceed(() => null)); | |
| 142 | − if (!existing) { | |
| 143 | − yield* ctx.core.integrations.register({ | |
| 144 | − slug, | |
| 145 | − name: "Apps", | |
| 146 | − description: "User-authored, published custom tools.", | |
| 147 | − config: {}, | |
| 148 | − canRemove: false, | |
| 149 | − canRefresh: true, | |
| 150 | − }); | |
| 218 | + const connected = yield* ensureCatalogConnection(scope, ctx); | |
| 219 | + return { scope, connection: connected.connection }; | |
| 220 | + }), | |
| 221 | + }), | |
| 222 | + tool<AppsStoreShape>({ | |
| 223 | + name: "sync_github_source", | |
| 224 | + description: "Sync a GitHub repository containing custom tool source files.", | |
| 225 | + execute: (args, { ctx }) => | |
| 226 | + Effect.gen(function* () { | |
| 227 | + const payload = isRecord(args) ? args : {}; | |
| 228 | + const repo = typeof payload.repo === "string" ? payload.repo : ""; | |
| 229 | + const connection = typeof payload.connection === "string" ? payload.connection : ""; | |
| 230 | + const ref = typeof payload.ref === "string" ? payload.ref : undefined; | |
| 231 | + const explicitScope = typeof payload.scope === "string" ? payload.scope : undefined; | |
| 232 | + if (!repo) return syncFailure('sync_github_source requires "repo"'); | |
| 233 | + if (!connection) return syncFailure('sync_github_source requires "connection"'); | |
| 234 | + | |
| 235 | + const sourceConnection = parseConnectionAddress(connection); | |
| 236 | + if (!sourceConnection) { | |
| 237 | + return syncFailure( | |
| 238 | + `GitHub connection must be a connection address like tools.github.user.main; got "${connection}"`, | |
| 239 | + connection, | |
| 240 | + ); | |
| 151 | 241 | } |
| 152 | − const connName = ConnectionName.make(connectionNameForScope(scope)); | |
| 153 | − const conns = yield* ctx.connections | |
| 154 | − .list({ integration: slug }) | |
| 155 | − .pipe(Effect.orElseSucceed(() => [])); | |
| 156 | − if (!conns.some((c) => String(c.name) === String(connName))) { | |
| 157 | − yield* ctx.connections.create({ | |
| 158 | − owner: "user", | |
| 159 | − name: connName, | |
| 160 | − integration: slug, | |
| 161 | − template: AuthTemplateSlug.make("none"), | |
| 162 | − value: "", | |
| 163 | − }); | |
| 242 | + if (String(sourceConnection.integration) !== "github") { | |
| 243 | + return syncFailure( | |
| 244 | + `GitHub source sync requires a github connection; got "${sourceConnection.integration}"`, | |
| 245 | + connection, | |
| 246 | + ); | |
| 164 | 247 | } |
| 165 | − yield* runtime.deps.store | |
| 166 | − .putScopeForConnection(String(connName), scope) | |
| 167 | − .pipe(Effect.orElseSucceed(() => undefined)); | |
| 168 | − const normalized = connectionIdentifier(String(connName)); | |
| 169 | − if (normalized !== String(connName)) { | |
| 170 | − yield* runtime.deps.store | |
| 171 | − .putScopeForConnection(normalized, scope) | |
| 172 | − .pipe(Effect.orElseSucceed(() => undefined)); | |
| 248 | + const existing = yield* ctx.connections | |
| 249 | + .get(sourceConnection) | |
| 250 | + .pipe(Effect.orElseSucceed(() => null)); | |
| 251 | + if (!existing) return syncFailure(`GitHub connection not found: ${connection}`); | |
| 252 | + const token = yield* ctx.connections | |
| 253 | + .resolveValue(sourceConnection) | |
| 254 | + .pipe(Effect.orElseSucceed(() => null)); | |
| 255 | + const integration = yield* ctx.core.integrations | |
| 256 | + .get(sourceConnection.integration) | |
| 257 | + .pipe(Effect.orElseSucceed(() => null)); | |
| 258 | + const scope = explicitScope ?? sourceScopeFor({ repo, connection }); | |
| 259 | + const result = yield* syncGitHubSource({ | |
| 260 | + runtime, | |
| 261 | + scope, | |
| 262 | + repo, | |
| 263 | + ref, | |
| 264 | + token, | |
| 265 | + baseUrl: configBaseUrl(integration?.config), | |
| 266 | + }); | |
| 267 | + if (result.status === "published") { | |
| 268 | + const catalog = yield* ensureCatalogConnection(scope, ctx); | |
| 269 | + yield* ctx.connections.refresh(catalog.ref).pipe(Effect.orElseSucceed(() => [])); | |
| 173 | 270 | } |
| 174 | − return { scope, connection: String(connName) }; | |
| 271 | + return result; | |
| 175 | 272 | }), |
| 176 | 273 | }), |
| 177 | 274 | ], |
| 178 | 275 | }, |
| 179 | 276 | ], |
| 180 | 277 | |
| 181 | − resolveTools: ({ connection }: ResolveToolsInput<AppsStoreShape>) => | |
| 278 | + resolveTools: ({ ctx, connection }: ResolveToolsInput<AppsStoreShape>) => | |
| 182 | 279 | Effect.gen(function* () { |
| 183 | 280 | const scope = yield* scopeFor(String(connection.name)); |
| 184 | 281 | const descriptor = yield* runtime.getDescriptor(scope); |
| 185 | 282 | if (!descriptor) return { tools: [] } satisfies ResolveToolsResult; |
| 283 | + const resolver = | |
| 284 | + makeResolver && ctx ? makeResolver({ ctx, scope, tool: "*" }) : runtime.deps.resolver; | |
| 186 | 285 | const tools: ToolDef[] = []; |
| 187 | 286 | for (const t of descriptor.tools) { |
| 188 | 287 | const byRole: Record<string, readonly ConnectionCandidate[]> = {}; |
| 189 | 288 | for (const [role, decl] of Object.entries(t.integrations)) { |
| 190 | − byRole[role] = yield* runtime.deps.resolver | |
| 289 | + byRole[role] = yield* resolver | |
| 191 | 290 | .listConnections({ integration: decl.integration }) |
| 192 | 291 | .pipe(Effect.orElseSucceed(() => [])); |
| 193 | 292 | } |
| @@ -13,7 +13,7 @@ import { · 12 unmodified lines | ||
| 13 | 13 | type PublishOutput, |
| 14 | 14 | } from "../pipeline/publish"; |
| 15 | 15 | import { PublishError } from "../pipeline/discover"; |
| 16 | −import type { AppDescriptor, ToolDescriptor } from "../pipeline/descriptor"; | |
| 16 | +import type { AppDescriptor, AppSourceRef, ToolDescriptor } from "../pipeline/descriptor"; | |
| 17 | 17 | import { bundleEntry } from "../pipeline/bundle"; |
| 18 | 18 | import { |
| 19 | 19 | buildBridge, |
| @@ -42,6 +42,8 @@ export interface AppsRuntime { · 22 unmodified lines | ||
| 42 | 42 | readonly scope: string; |
| 43 | 43 | readonly files: ReadonlyMap<string, string>; |
| 44 | 44 | readonly message?: string; |
| 45 | + readonly description?: string; | |
| 46 | + readonly source?: AppSourceRef; | |
| 45 | 47 | }) => Effect.Effect<PublishOutput, PublishError>; |
| 46 | 48 | readonly getDescriptor: (scope: string) => Effect.Effect<AppDescriptor | null>; |
| 47 | 49 | /** Re-derive the published-descriptor pointer from the latest committed |
| @@ -264,6 +266,8 @@ export const makeAppsRuntime = (deps: AppsRuntimeDeps): AppsRuntime => { · 216 unmodified lines | ||
| 264 | 266 | scope: input.scope, |
| 265 | 267 | files: input.files, |
| 266 | 268 | commitMessage: input.message, |
| 269 | + description: input.description, | |
| 270 | + source: input.source, | |
| 267 | 271 | }, |
| 268 | 272 | ); |
| 269 | 273 | yield* putDescriptorPointer(out.descriptor); |