@roubo/plugin-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -23,6 +23,59 @@ export interface NormalizedIssue {
23
23
  blockedBy: string[];
24
24
  updatedAt: string;
25
25
  raw: unknown;
26
+ facetValues?: Record<string, string | string[]>;
27
+ }
28
+ /**
29
+ * Self-reported connectivity for a plugin (host-API 1.1.0+). Plugins that omit
30
+ * `getConnectionStatus` are tolerated; the host falls back to `validateConfig`
31
+ * and infers `connected` vs `auth-problem` from the result.
32
+ */
33
+ export interface ConnectionStatus {
34
+ state: "connected" | "disconnected" | "auth-problem" | "errored";
35
+ detail?: string;
36
+ /** ISO-8601 timestamp; the plugin (or host fallback) sets this at observation. */
37
+ checkedAt: string;
38
+ /**
39
+ * Present on `connected` when the plugin can cheaply resolve the
40
+ * authenticated account (e.g. from the same `GET /user` probe). The host
41
+ * forwards it verbatim to the UI's "Connected as <login>" label; omit it
42
+ * otherwise. Kept in sync with `ConnectionStatus` in `@roubo/shared`.
43
+ */
44
+ account?: {
45
+ login: string;
46
+ };
47
+ }
48
+ /**
49
+ * One descriptor returned by `filterFacets`. Core renders generic filter UI
50
+ * from these; for `enum-async` the host requests options lazily on dropdown
51
+ * open via `getFacetOptions`. Plugins built against host-API 1.0.0 omit
52
+ * `filterFacets` and core falls back to a fixed common-facet set.
53
+ */
54
+ export interface FilterFacet {
55
+ id: string;
56
+ label: string;
57
+ type: "enum" | "enum-async" | "multi-enum";
58
+ options?: FilterFacetOption[];
59
+ }
60
+ /**
61
+ * One option for a `FilterFacet`. Used both inline (eager `enum`/`multi-enum`)
62
+ * and as the return shape of `getFacetOptions` (lazy `enum-async`).
63
+ */
64
+ export interface FilterFacetOption {
65
+ value: string;
66
+ label: string;
67
+ }
68
+ /**
69
+ * One sort field returned by `getSortFields` (host-API 1.2.0+, CLI-FR-009).
70
+ * Core renders a sort picker from these; `defaultDir` is the direction first
71
+ * applied when the user selects the field. Plugins built against host-API
72
+ * 1.0.0 / 1.1.0 omit `getSortFields` and core renders no picker (CLI-FR-011).
73
+ * Mirrored as `SortField` in `@roubo/shared`.
74
+ */
75
+ export interface SortField {
76
+ id: string;
77
+ label: string;
78
+ defaultDir: "asc" | "desc";
26
79
  }
27
80
  export interface NormalizedComment {
28
81
  externalId: string;
@@ -34,23 +87,194 @@ export interface NormalizedComment {
34
87
  createdAt: string;
35
88
  updatedAt: string;
36
89
  }
90
+ /**
91
+ * One entry of the source list a host passes into source-bound contract
92
+ * methods. `kind` is plugin-defined (e.g. `"repo"`, `"project"` for the
93
+ * GitHub plugins); `externalId` is the plugin-native id for that source
94
+ * (e.g. `"owner/repo"`, `"owner/#42"`). The host derives this list per
95
+ * request from the project's `roubo.yaml` integration block, so plugins
96
+ * never share source state across projects.
97
+ */
98
+ export interface ConfiguredSource {
99
+ kind: string;
100
+ externalId: string;
101
+ /**
102
+ * Jira self-hosted only: the project key this source is scoped to under the
103
+ * project-first selection model. Plugins outside the Jira family ignore it.
104
+ */
105
+ project?: string;
106
+ /**
107
+ * Jira self-hosted only: for a `board` source, whether to resolve to the
108
+ * board's active sprint (default) or the whole board's backing filter.
109
+ */
110
+ boardMode?: "active-sprint" | "whole-board";
111
+ /**
112
+ * Jira self-hosted only: for the synthetic `mine` ("assigned to me") source,
113
+ * whether it is scoped to the in-scope projects or matches anywhere.
114
+ */
115
+ mineScope?: "in-project" | "anywhere";
116
+ /**
117
+ * github.com / GHE only: per-source toggles for the GitHub Advanced Security
118
+ * alert categories surfaced as security-* issue types. Plugins outside the
119
+ * GitHub family ignore these fields. Default false on each.
120
+ */
121
+ includeCodeQLAlerts?: boolean;
122
+ includeSecretScanningAlerts?: boolean;
123
+ includeDependabotAlerts?: boolean;
124
+ }
125
+ /**
126
+ * Discriminator for `ListIssuesWarning.code`. The client maps these to chip
127
+ * variants in the cut-list source picker. `missing-scope` and
128
+ * `scope-unverifiable` drive the GitHub family's PAT/OAuth remediation
129
+ * affordances (WU-032); other codes share the generic "Unavailable" chip.
130
+ */
131
+ export type ListIssuesWarningCode = "missing-scope" | "scope-unverifiable" | "feature-disabled" | "insufficient-permission" | "not-found" | "rate-limited" | "unknown";
132
+ /**
133
+ * Non-fatal warning emitted alongside a `listIssues` result. Used by the
134
+ * GitHub plugins to surface per-source per-category fetch failures without
135
+ * failing the entire pull. Categories are stable string identifiers; the
136
+ * host treats unknown values as opaque and surfaces `cause` verbatim to UI.
137
+ *
138
+ * `code` is an optional discriminator the client uses to pick a chip variant
139
+ * (e.g. `missing-scope` → link chip pointing at PAT settings / OAuth re-auth).
140
+ * Absent means the client renders the generic chip with `cause` as the tooltip.
141
+ *
142
+ * A warning with a given `(sourceExternalId, category)` is cleared on the
143
+ * next successful pull for that pair: a subsequent `listIssues` page-1
144
+ * result that omits it constitutes a clear.
145
+ */
146
+ export interface ListIssuesWarning {
147
+ category: "code-scanning" | "secret-scanning" | "dependabot" | string;
148
+ sourceExternalId: string;
149
+ cause: string;
150
+ code?: ListIssuesWarningCode;
151
+ detail?: {
152
+ status?: number;
153
+ code?: string;
154
+ missingScope?: string;
155
+ };
156
+ }
37
157
  export interface ListIssuesParams {
158
+ sources: ConfiguredSource[];
38
159
  cursor: string | null;
39
160
  pageSize: number;
40
161
  filters?: {
41
162
  labels?: string[];
42
163
  search?: string;
43
164
  };
165
+ /**
166
+ * Status exclusion resolved by the host from the three-layer merge (FR-009,
167
+ * FR-010), applied in the query so excluded issues never occupy a result
168
+ * page. `excludedStatusCategories` is the category-first default (e.g.
169
+ * `["Done"]`); `excludedStatuses` is the status-name list a plugin uses as
170
+ * the fallback when the instance does not support `statusCategory` in its
171
+ * query language. A plugin that does not do server-side exclusion ignores both.
172
+ */
173
+ excludedStatusCategories?: string[];
174
+ excludedStatuses?: string[];
175
+ /**
176
+ * Plugin-declared sort selection (CLI-FR-009/CLI-FR-010). `sortBy` is one of
177
+ * the field ids the plugin returned from `getSortFields`; `sortDir` is the
178
+ * direction. Plugins MUST apply the sort source-side so the order is stable
179
+ * across pages. Absent means the plugin's natural order; a plugin that does
180
+ * not declare any sort fields (and so never receives these) is unaffected.
181
+ */
182
+ sortBy?: string;
183
+ sortDir?: "asc" | "desc";
184
+ }
185
+ export interface ListIssueTypesParams {
186
+ sources: ConfiguredSource[];
187
+ }
188
+ export interface ListLabelsParams {
189
+ sources: ConfiguredSource[];
190
+ }
191
+ /**
192
+ * Params for the lazy facet-option loader. `facetId` matches a `FilterFacet.id`
193
+ * the plugin previously returned from `filterFacets()`. `sources` follows the
194
+ * existing source-bound pattern so plugins remain stateless across projects.
195
+ * `search` is the optional user-typed prefix/substring; plugins MAY ignore it
196
+ * and return the full set.
197
+ */
198
+ export interface GetFacetOptionsParams {
199
+ facetId: string;
200
+ sources: ConfiguredSource[];
201
+ search?: string;
44
202
  }
45
203
  export interface ListIssuesResult {
46
204
  items: NormalizedIssue[];
47
205
  nextCursor: string | null;
206
+ /** Absent or empty means "no per-category problems on this page." */
207
+ warnings?: ListIssuesWarning[];
208
+ /**
209
+ * Count of issues the plugin dropped in-query (e.g. the status-category
210
+ * exclusion of FR-009/FR-010), surfaced so the cut list can show "N filtered
211
+ * out by status". Additive and optional: the host sums it across pages and
212
+ * treats absence as "unknown". Plugins that filter in memory report it
213
+ * per page; the jira-self-hosted plugin excludes server-side in JQL, so it
214
+ * reports the whole-result-set count once on the first page via a count-only
215
+ * companion query (and omits it when the companion count is unavailable).
216
+ */
217
+ excludedCount?: number;
48
218
  }
49
- export interface SourceCandidate {
50
- category: string;
219
+ export type SourceCandidateIcon = "repo" | "project" | "board" | "epic" | "filter";
220
+ export interface SourceCandidateItem {
51
221
  externalId: string;
52
- displayName: string;
53
- description?: string;
222
+ label: string;
223
+ sublabel?: string;
224
+ icon?: SourceCandidateIcon;
225
+ }
226
+ export interface SourceCandidateCategory {
227
+ id: string;
228
+ label: string;
229
+ items: SourceCandidateItem[];
230
+ }
231
+ export type SourceCandidatesShape = "multi-list" | "categorized-multi-list" | "searchable-categorized";
232
+ export interface SourceCategoryOption {
233
+ id: string;
234
+ label: string;
235
+ }
236
+ export interface SearchableSourceCategory {
237
+ id: "project" | "board" | "filter" | "epic" | "mine";
238
+ label: string;
239
+ icon?: SourceCandidateIcon;
240
+ scopedBy?: "project";
241
+ options?: SourceCategoryOption[];
242
+ }
243
+ /**
244
+ * Declarative source-picker payload returned by `listSourceCandidates`. Roubo's
245
+ * host renders the UI from this envelope; plugins ship no React. See
246
+ * `.specifications/integration-plugins/architecture.md`.
247
+ */
248
+ export interface SourceCandidatesResponse {
249
+ shape: SourceCandidatesShape;
250
+ items?: SourceCandidateItem[];
251
+ categories?: SourceCandidateCategory[];
252
+ searchableCategories?: SearchableSourceCategory[];
253
+ nextCursor?: string | null;
254
+ }
255
+ /**
256
+ * Params for the scoped, paginated source-option search (`getSourceOptions`).
257
+ * Generalizes `getFacetOptions` with a parent `scope` (e.g. the Jira project
258
+ * keys a board/filter/epic search is confined to) and an opaque `cursor`.
259
+ * `search` is the optional user-typed term (debounced client-side); plugins
260
+ * MAY ignore it. Scoped categories with no `scope.project` return an empty page.
261
+ */
262
+ export interface GetSourceOptionsParams {
263
+ category: "project" | "board" | "filter" | "epic";
264
+ scope?: {
265
+ project?: string[];
266
+ };
267
+ search?: string;
268
+ cursor?: string | null;
269
+ }
270
+ /**
271
+ * One page of source options. `nextCursor` is an opaque token the host passes
272
+ * back verbatim to fetch the following page; `null` means the result set is
273
+ * exhausted (NFR-004: every item reachable, no page dropped or duplicated).
274
+ */
275
+ export interface SourceOptionsResult {
276
+ items: SourceCandidateItem[];
277
+ nextCursor: string | null;
54
278
  }
55
279
  export interface CurrentUser {
56
280
  externalId: string;
@@ -64,16 +288,102 @@ export interface ValidateConfigResult {
64
288
  code?: string;
65
289
  }>;
66
290
  }
291
+ /**
292
+ * Result of a lightweight activation call (`setActiveConfig`). Plugins that
293
+ * hold plugin-wide configuration (e.g. an API instance URL, TLS toggles)
294
+ * implement this to receive that configuration before source-bound RPCs run.
295
+ *
296
+ * `setActiveConfig` is no longer used to convey per-project state: source
297
+ * selections flow through `sources` on each source-bound call so the plugin
298
+ * process holds no per-project state. Plugins with no plugin-wide config
299
+ * (e.g. github.com, which has a fixed API host) can skip implementing this
300
+ * method entirely.
301
+ */
302
+ export interface SetActiveConfigResult {
303
+ ok: boolean;
304
+ errors?: Array<{
305
+ field?: string;
306
+ message: string;
307
+ code?: string;
308
+ }>;
309
+ }
67
310
  export interface IssueTypeOption {
68
311
  id: string;
69
312
  name: string;
70
313
  }
314
+ /**
315
+ * Result of the privileged `createIssue` op (verify-gate FR-011, spike #704).
316
+ * `ref` is the created issue's external id in the plugin's own form
317
+ * (`owner/repo#number` for GitHub) so the host can immediately use it (e.g. as
318
+ * `blockerRef` in `addBlockedBy`). `nodeId` is the provider's GraphQL node id
319
+ * when the tracker exposes one, omitted otherwise.
320
+ */
321
+ export interface CreateIssueResult {
322
+ ref: string;
323
+ url: string;
324
+ nodeId?: string;
325
+ }
326
+ /**
327
+ * Stable identifier for an alert category probed by `probeAlertCategories`.
328
+ * The host's Test Connection result strip surfaces one row per probe result.
329
+ */
330
+ export type ProbeAlertCategory = "code-scanning" | "secret-scanning" | "dependabot";
331
+ /**
332
+ * Per-probe status returned by `probeAlertCategories`. The host maps these
333
+ * directly into result-strip rows; semantics match the host's
334
+ * `IntegrationCategoryStatus`:
335
+ *
336
+ * - `ok`: probe succeeded (HTTP 2xx)
337
+ * - `scope-missing`: token lacks the required scope (HTTP 401/403)
338
+ * - `not-enabled`: feature is not enabled for the probed repo (HTTP 404/410/451)
339
+ * - `timed-out`: probe exceeded the per-probe cap. Rendered as an amber
340
+ * warning; does not fail the overall Test Connection result.
341
+ * - `error`: probe returned an unexpected status or threw a non-timeout error
342
+ */
343
+ export type ProbeAlertCategoryStatus = "ok" | "scope-missing" | "not-enabled" | "timed-out" | "error";
344
+ export interface ProbeAlertCategoriesParams {
345
+ /**
346
+ * The same source list a host would pass to `listIssues`. The plugin picks
347
+ * its sample target from this list (typically the first repo source) and
348
+ * may return an `error` row for every requested category if none of the
349
+ * sources are probeable.
350
+ */
351
+ sources: ConfiguredSource[];
352
+ /** Subset of categories the host wants probed; never empty. */
353
+ enabledCategories: ProbeAlertCategory[];
354
+ /**
355
+ * Host-supplied hint for the per-probe timeout. Plugins SHOULD honour this;
356
+ * the host defaults to 5000ms when omitted (FR-047: 5s per-probe cap).
357
+ */
358
+ timeoutMsPerProbe?: number;
359
+ }
360
+ export interface ProbeAlertCategoryReport {
361
+ category: ProbeAlertCategory;
362
+ status: ProbeAlertCategoryStatus;
363
+ detail?: string;
364
+ httpStatus?: number;
365
+ }
366
+ export interface ProbeAlertCategoriesResult {
367
+ reports: ProbeAlertCategoryReport[];
368
+ }
369
+ /**
370
+ * Result of directly probing access to a single source (e.g. a GitHub repo).
371
+ * Lets the host distinguish "no such source" from "access blocked by policy"
372
+ * when a source is missing from `listSourceCandidates`: `status` and `message`
373
+ * carry the underlying HTTP error verbatim so the host can classify it into an
374
+ * actionable code rather than a generic miss.
375
+ */
376
+ export interface ProbeRepoAccessResult {
377
+ accessible: boolean;
378
+ status?: number;
379
+ message?: string;
380
+ }
71
381
  /**
72
382
  * The contract methods a plugin may implement. All methods are optional;
73
383
  * a host call to an unimplemented method receives JSON-RPC MethodNotFound.
74
384
  */
75
385
  export interface PluginContract {
76
- listSourceCandidates?: () => Promise<SourceCandidate[]> | SourceCandidate[];
386
+ listSourceCandidates?: () => Promise<SourceCandidatesResponse> | SourceCandidatesResponse;
77
387
  listIssues?: (params: ListIssuesParams) => Promise<ListIssuesResult> | ListIssuesResult;
78
388
  getIssue?: (params: {
79
389
  externalId: string;
@@ -85,10 +395,40 @@ export interface PluginContract {
85
395
  validateConfig?: (params: {
86
396
  config: Record<string, unknown>;
87
397
  }) => Promise<ValidateConfigResult> | ValidateConfigResult;
398
+ setActiveConfig?: (params: {
399
+ config: Record<string, unknown>;
400
+ }) => Promise<SetActiveConfigResult> | SetActiveConfigResult;
88
401
  applyTransition?: (params: {
89
402
  externalId: string;
90
403
  transition: string;
91
404
  }) => Promise<void> | void;
405
+ /**
406
+ * Create a tracker issue (verify-gate FR-011, spike #704). Privileged write
407
+ * routed only through the host's TrackerActionGateway, which gates it on the
408
+ * `supportsCreateIssue` manifest capability and the plugin's consent. Returns
409
+ * the created issue's external ref (the same `owner/repo#number` form the
410
+ * other issue-scoped methods accept), its URL, and the provider node id when
411
+ * the tracker exposes one (GitHub's GraphQL node id, used to wire blocking
412
+ * links without a second lookup).
413
+ */
414
+ createIssue?: (params: {
415
+ repoFullName: string;
416
+ title: string;
417
+ body?: string;
418
+ labels?: string[];
419
+ }) => Promise<CreateIssueResult> | CreateIssueResult;
420
+ /**
421
+ * Register an "is blocked by" relationship: `blockedRef` is blocked by
422
+ * `blockerRef` (verify-gate FR-010/FR-011, spike #704). Privileged write
423
+ * routed only through the host's TrackerActionGateway, which gates it on the
424
+ * `supportsBlockingLinks` manifest capability and the plugin's consent. Both
425
+ * refs are external ids in the plugin's own form (`owner/repo#number` for
426
+ * GitHub).
427
+ */
428
+ addBlockedBy?: (params: {
429
+ blockedRef: string;
430
+ blockerRef: string;
431
+ }) => Promise<void> | void;
92
432
  assignIssue?: (params: {
93
433
  externalId: string;
94
434
  assigneeExternalId: string;
@@ -100,8 +440,52 @@ export interface PluginContract {
100
440
  getAvailableTransitions?: (params: {
101
441
  externalId: string;
102
442
  }) => Promise<string[]> | string[];
103
- listIssueTypes?: () => Promise<IssueTypeOption[]> | IssueTypeOption[];
104
- listLabels?: () => Promise<string[]> | string[];
443
+ listIssueTypes?: (params: ListIssueTypesParams) => Promise<IssueTypeOption[]> | IssueTypeOption[];
444
+ /**
445
+ * Enumerate the connected instance's available status categories (issue #453).
446
+ * The host exposes these as the option list for the Configure dialog's
447
+ * status-category exclusion toggle, falling back to a canonical set when a
448
+ * plugin does not implement this method (`MethodNotFound`) or discovery fails.
449
+ * Returned names must be valid wherever the plugin consumes excluded
450
+ * categories (e.g. Jira returns `statusCategory` names usable in JQL).
451
+ */
452
+ listStatusCategories?: () => Promise<string[]> | string[];
453
+ listLabels?: (params: ListLabelsParams) => Promise<string[]> | string[];
454
+ getConnectionStatus?: () => Promise<ConnectionStatus> | ConnectionStatus;
455
+ /**
456
+ * Probe each requested alert-category endpoint for a sample source and
457
+ * return one report per category. Invoked by the host as part of Test
458
+ * Connection (FR-047, WU-041). A throw or `MethodNotFound` is treated by
459
+ * the host as "no per-category data"; it never fails the overall test.
460
+ */
461
+ probeAlertCategories?: (params: ProbeAlertCategoriesParams) => Promise<ProbeAlertCategoriesResult> | ProbeAlertCategoriesResult;
462
+ /**
463
+ * Directly probe access to a single repo (`GET /repos/{owner}/{repo}`) so the
464
+ * host can explain why a configured repo is missing from
465
+ * `listSourceCandidates` (e.g. org OAuth App access restrictions), rather than
466
+ * silently reporting "not found".
467
+ */
468
+ probeRepoAccess?: (params: {
469
+ repoFullName: string;
470
+ }) => Promise<ProbeRepoAccessResult> | ProbeRepoAccessResult;
471
+ filterFacets?: () => Promise<FilterFacet[]> | FilterFacet[];
472
+ getFacetOptions?: (params: GetFacetOptionsParams) => Promise<FilterFacetOption[]> | FilterFacetOption[];
473
+ /**
474
+ * Declare the sort fields the cut-list picker offers (host-API 1.2.0+,
475
+ * CLI-FR-009). Each field carries a stable `id` (forwarded back as
476
+ * `ListIssuesParams.sortBy`), a human `label`, and a `defaultDir`. Plugins
477
+ * omitting this method resolve to `MethodNotFound`, which core maps to an
478
+ * empty list so no picker renders (CLI-FR-011). A plugin that declares fields
479
+ * MUST honour `sortBy`/`sortDir` source-side in `listIssues` (CLI-FR-010).
480
+ */
481
+ getSortFields?: () => Promise<SortField[]> | SortField[];
482
+ /**
483
+ * Scoped, paginated, type-ahead search over a plugin's selectable source
484
+ * categories (project / board / filter / epic). The host calls this from the
485
+ * searchable source picker as the user types and pages; the plugin stays
486
+ * stateless across calls (the parent `scope` is supplied each time).
487
+ */
488
+ getSourceOptions?: (params: GetSourceOptionsParams) => Promise<SourceOptionsResult> | SourceOptionsResult;
105
489
  }
106
490
  export type ContractMethodName = keyof PluginContract;
107
491
  export interface FetchInit {
@@ -153,4 +537,272 @@ export interface PluginHandle {
153
537
  /** Tear down the RPC connection. Tests use this; production plugins do not. */
154
538
  dispose(): void;
155
539
  }
540
+ /**
541
+ * The SDK-level contract version a component plugin declares. The host gates
542
+ * compatibility at validation time (a mismatch is rejected before any
543
+ * lifecycle method is called, never at call time). A single integer mirrors
544
+ * the `schemaVersion: 1` precedent on `ProvisionDescriptor`.
545
+ */
546
+ export declare const SUPPORTED_CONTRACT_VERSION: 1;
547
+ /**
548
+ * Minimal structural copy of `@roubo/shared`'s `ProvisionDescriptor` union.
549
+ *
550
+ * `@roubo/plugin-sdk` is a published, dependency-light package (`private:
551
+ * false`, only `vscode-jsonrpc`) whereas `@roubo/shared` is a `private: true`
552
+ * workspace package that ships raw TypeScript. Taking a workspace dependency on
553
+ * it would break both `npm publish` (an unpublished dependency) and the SDK's
554
+ * own `tsc` build (`rootDir: ./src` cannot import a `.ts` file outside `src`).
555
+ * So the descriptor shape is restated here. It MUST stay structurally in sync
556
+ * with `shared/provision-descriptor-schema.ts` (the Zod schema is the
557
+ * authority; the host validates every descriptor against it).
558
+ */
559
+ export interface DockerProvisionDescriptor {
560
+ schemaVersion: 1;
561
+ kind: "docker";
562
+ composeFile: string;
563
+ service: string;
564
+ initService?: string;
565
+ portEnvVar?: string;
566
+ migration?: {
567
+ command: string;
568
+ args?: string[];
569
+ };
570
+ connection?: {
571
+ template: string;
572
+ };
573
+ assignedContainerId?: string;
574
+ env?: Record<string, string>;
575
+ healthcheck?: boolean;
576
+ }
577
+ export interface ProcessProvisionDescriptor {
578
+ schemaVersion: 1;
579
+ kind: "process";
580
+ command: string;
581
+ env?: Record<string, string>;
582
+ envFile?: string;
583
+ cwd?: string;
584
+ setup?: string;
585
+ dependsOn?: string[];
586
+ }
587
+ export interface OneshotProvisionDescriptor {
588
+ schemaVersion: 1;
589
+ kind: "oneshot";
590
+ command: string;
591
+ env?: Record<string, string>;
592
+ envFile?: string;
593
+ cwd?: string;
594
+ dependsOn?: string[];
595
+ timeoutMs?: number;
596
+ }
597
+ /**
598
+ * Discriminated (on `kind`) union a declarative component plugin returns from
599
+ * `translate`. The host's `LifecycleEngine` validates it against the supported
600
+ * `schemaVersion` and then executes it.
601
+ */
602
+ export type ProvisionDescriptor = DockerProvisionDescriptor | ProcessProvisionDescriptor | OneshotProvisionDescriptor;
603
+ /**
604
+ * Per-bench context the host resolves (ports allocated, env merged) before any
605
+ * lifecycle method runs. A component plugin is spawned once per plugin and
606
+ * multiplexes benches; `benchId` distinguishes the active bench so a single
607
+ * process serves several concurrently. Mirrors `BenchContext` in
608
+ * architecture.md ('Data model').
609
+ */
610
+ export interface BenchContext {
611
+ projectId: string;
612
+ benchId: number;
613
+ componentName: string;
614
+ workspacePath: string;
615
+ ports: Record<string, number>;
616
+ env: Record<string, string>;
617
+ }
618
+ /**
619
+ * Lifecycle status a component plugin reports (imperative `health`, or pushed
620
+ * via `host.component.reportStatus`). `completed` is the terminal state for a
621
+ * successful one-shot lifecycle (FR-014 / FR-022 delta), distinct from
622
+ * `stopped` (idle) and `error`. This SDK-facing shape intentionally diverges
623
+ * from `@roubo/shared`'s `ComponentStatus`: it adds the `completed` status,
624
+ * treats `name` / `setupComplete` as optional (or absent) rather than required,
625
+ * and models `phases` as a `Record<string, string>` rather than a
626
+ * `ComponentPhase[]`. The shared type stays authoritative host-side; keep the
627
+ * two reconciled deliberately, not field-for-field identical.
628
+ */
629
+ export interface ComponentStatus {
630
+ status: "stopped" | "starting" | "running" | "error" | "stopping" | "completed";
631
+ pid?: number;
632
+ containerId?: string;
633
+ phases?: Record<string, string>;
634
+ setupComplete?: boolean;
635
+ error?: string;
636
+ statusDetail?: string;
637
+ startedAt?: string;
638
+ }
639
+ /**
640
+ * Declarative (preferred) component contract: a pure function mapping the
641
+ * plugin's `config` plus the `BenchContext` to a `ProvisionDescriptor` the host
642
+ * executes. A plugin implements EITHER `translate` OR the imperative hooks
643
+ * below, never both (`defineComponentPlugin` rejects both at validation time).
644
+ */
645
+ export interface DeclarativeComponentContract {
646
+ translate: (params: {
647
+ config: Record<string, unknown>;
648
+ context: BenchContext;
649
+ }) => Promise<ProvisionDescriptor> | ProvisionDescriptor;
650
+ start?: never;
651
+ stop?: never;
652
+ health?: never;
653
+ cleanup?: never;
654
+ }
655
+ /**
656
+ * Imperative (escape-hatch) component contract: lifecycle hooks the plugin
657
+ * drives through the broker (`host.process.*`, `host.docker.*`, `host.ports.*`)
658
+ * for a novel lifecycle a `ProvisionDescriptor` cannot express. All four hooks
659
+ * are required so the host never reaches a half-implemented lifecycle (a plugin
660
+ * missing `stop` is rejected at validation, not at stop-time).
661
+ */
662
+ export interface ImperativeComponentContract {
663
+ start: (context: BenchContext) => Promise<void> | void;
664
+ stop: (context: BenchContext) => Promise<void> | void;
665
+ health: (context: BenchContext) => Promise<ComponentStatus> | ComponentStatus;
666
+ cleanup: (context: BenchContext) => Promise<void> | void;
667
+ translate?: never;
668
+ }
669
+ /**
670
+ * The contract a component plugin implements: the declarative `translate` path
671
+ * XOR the imperative lifecycle hooks. The TypeScript `never` guards make the
672
+ * two variants mutually exclusive at compile time; `defineComponentPlugin` also
673
+ * enforces the rule at runtime/validation time.
674
+ */
675
+ export type ComponentContract = DeclarativeComponentContract | ImperativeComponentContract;
676
+ export type ComponentContractMethodName = "translate" | "start" | "stop" | "health" | "cleanup";
677
+ /** Options for `defineComponentPlugin`. */
678
+ export interface DefineComponentPluginOptions {
679
+ /**
680
+ * The contract version the plugin declares. Must equal
681
+ * `SUPPORTED_CONTRACT_VERSION`; a mismatch is rejected synchronously at
682
+ * definition (validation) time, never deferred to a lifecycle call.
683
+ * Defaults to `SUPPORTED_CONTRACT_VERSION` when omitted.
684
+ */
685
+ contractVersion?: number;
686
+ /**
687
+ * Replace the default stdio streams. Test harnesses inject paired streams;
688
+ * production plugin code never sets this.
689
+ */
690
+ streams?: {
691
+ input: NodeJS.ReadableStream;
692
+ output: NodeJS.WritableStream;
693
+ };
694
+ }
695
+ /** Result of `host.process.run` (a blocking run-to-completion). */
696
+ export interface ProcessRunResult {
697
+ exitCode: number;
698
+ }
699
+ /** Result of `host.process.status`. */
700
+ export interface ProcessStatusResult {
701
+ alive: boolean;
702
+ exitCode?: number;
703
+ }
704
+ /** Result of `host.capability.query` (the FR-017 graceful version gate). */
705
+ export interface CapabilityQueryResult {
706
+ available: boolean;
707
+ introducedIn?: string;
708
+ }
709
+ /**
710
+ * The host client surface available to a component plugin inside its contract
711
+ * methods. Each call is an RPC request (or notification) over the bound
712
+ * connection. The host owns every process and container handle; the plugin
713
+ * never spawns anything itself. Mirrors the broker surface in architecture.md.
714
+ */
715
+ export interface ComponentHostClient {
716
+ process: {
717
+ start(params: {
718
+ id: string;
719
+ command: string;
720
+ args?: string[];
721
+ env: Record<string, string>;
722
+ cwd: string;
723
+ }): Promise<{
724
+ pid: number;
725
+ }>;
726
+ run(params: {
727
+ id: string;
728
+ command: string;
729
+ args?: string[];
730
+ env: Record<string, string>;
731
+ cwd: string;
732
+ timeoutMs: number;
733
+ }): Promise<ProcessRunResult>;
734
+ stop(params: {
735
+ id: string;
736
+ }): Promise<void>;
737
+ status(params: {
738
+ id: string;
739
+ }): Promise<ProcessStatusResult>;
740
+ logs(params: {
741
+ id: string;
742
+ }): Promise<string[]>;
743
+ };
744
+ docker: {
745
+ composeUp(params: {
746
+ projectName: string;
747
+ composeFile: string;
748
+ cwd: string;
749
+ service: string;
750
+ env: Record<string, string>;
751
+ }): Promise<{
752
+ containerId: string;
753
+ }>;
754
+ waitForHealthy(params: {
755
+ projectName: string;
756
+ service: string;
757
+ timeoutMs: number;
758
+ }): Promise<{
759
+ healthy: boolean;
760
+ }>;
761
+ composeRunInit(params: {
762
+ projectName: string;
763
+ composeFile: string;
764
+ cwd: string;
765
+ initService: string;
766
+ }): Promise<void>;
767
+ composeStop(params: {
768
+ projectName: string;
769
+ composeFile: string;
770
+ cwd: string;
771
+ service?: string;
772
+ }): Promise<void>;
773
+ composeDown(params: {
774
+ projectName: string;
775
+ composeFile: string;
776
+ cwd: string;
777
+ }): Promise<void>;
778
+ assignContainer(params: {
779
+ componentName: string;
780
+ containerId: string;
781
+ }): Promise<void>;
782
+ };
783
+ ports: {
784
+ get(params: {
785
+ componentName: string;
786
+ }): Promise<number>;
787
+ };
788
+ component: {
789
+ reportStatus(status: ComponentStatus): void;
790
+ reportLog(params: {
791
+ source: "stdout" | "stderr";
792
+ text: string;
793
+ ts: number;
794
+ }): void;
795
+ };
796
+ capability: {
797
+ query(params: {
798
+ method: string;
799
+ }): Promise<CapabilityQueryResult>;
800
+ };
801
+ }
802
+ export interface ComponentPluginHandle {
803
+ /** The connected component host client. Available before any hook is called. */
804
+ host: ComponentHostClient;
805
+ /** Tear down the RPC connection. Tests use this; production plugins do not. */
806
+ dispose(): void;
807
+ }
156
808
  //# sourceMappingURL=types.d.ts.map