@openchamber/sdk 2.4.0 → 2.5.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/parse.d.ts CHANGED
@@ -114,6 +114,7 @@ export declare const openChamberManifestSchema: z.ZodObject<{
114
114
  }>>>;
115
115
  provides: z.ZodOptional<z.ZodArray<z.ZodEnum<{
116
116
  browser: "browser";
117
+ "source-control": "source-control";
117
118
  }>>>;
118
119
  surface: z.ZodOptional<z.ZodLiteral<true>>;
119
120
  }, z.core.$strip>>;
@@ -170,6 +171,115 @@ export declare const openChamberManifestSchema: z.ZodObject<{
170
171
  text: "text";
171
172
  }>>;
172
173
  }, z.core.$strip>>>;
174
+ sourceControl: z.ZodOptional<z.ZodObject<{
175
+ wire: z.ZodLiteral<1>;
176
+ label: z.ZodString;
177
+ terminology: z.ZodObject<{
178
+ changeRequest: z.ZodEnum<{
179
+ change: "change";
180
+ mergeRequest: "mergeRequest";
181
+ pullRequest: "pullRequest";
182
+ }>;
183
+ ci: z.ZodEnum<{
184
+ checks: "checks";
185
+ pipeline: "pipeline";
186
+ }>;
187
+ }, z.core.$strict>;
188
+ referencePrefix: z.ZodObject<{
189
+ changeRequest: z.ZodString;
190
+ issue: z.ZodString;
191
+ }, z.core.$strict>;
192
+ repositoryPath: z.ZodEnum<{
193
+ "namespace/name": "namespace/name";
194
+ "owner/name": "owner/name";
195
+ }>;
196
+ instances: z.ZodObject<{
197
+ defaultInstances: z.ZodArray<z.ZodString>;
198
+ custom: z.ZodBoolean;
199
+ allowLoopbackHttp: z.ZodBoolean;
200
+ allowPort: z.ZodBoolean;
201
+ placeholder: z.ZodString;
202
+ }, z.core.$strict>;
203
+ capabilities: z.ZodObject<{
204
+ projects: z.ZodDefault<z.ZodBoolean>;
205
+ issues: z.ZodDefault<z.ZodBoolean>;
206
+ changeRequests: z.ZodDefault<z.ZodBoolean>;
207
+ draftChangeRequests: z.ZodDefault<z.ZodBoolean>;
208
+ merge: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
209
+ methods: z.ZodArray<z.ZodEnum<{
210
+ merge: "merge";
211
+ rebase: "rebase";
212
+ squash: "squash";
213
+ submit: "submit";
214
+ }>>;
215
+ }, z.core.$strict>]>>;
216
+ ci: z.ZodDefault<z.ZodBoolean>;
217
+ comments: z.ZodDefault<z.ZodBoolean>;
218
+ reviews: z.ZodDefault<z.ZodBoolean>;
219
+ state: z.ZodDefault<z.ZodBoolean>;
220
+ labels: z.ZodDefault<z.ZodBoolean>;
221
+ reviewers: z.ZodDefault<z.ZodBoolean>;
222
+ changeRequestPatch: z.ZodDefault<z.ZodBoolean>;
223
+ liveSummaries: z.ZodDefault<z.ZodBoolean>;
224
+ reviewModel: z.ZodOptional<z.ZodEnum<{
225
+ verdict: "verdict";
226
+ votes: "votes";
227
+ }>>;
228
+ revisions: z.ZodOptional<z.ZodBoolean>;
229
+ }, z.core.$strict>;
230
+ token: z.ZodObject<{
231
+ header: z.ZodEnum<{
232
+ authorization: "authorization";
233
+ "private-token": "private-token";
234
+ }>;
235
+ scheme: z.ZodEnum<{
236
+ basic: "basic";
237
+ bearer: "bearer";
238
+ none: "none";
239
+ token: "token";
240
+ }>;
241
+ username: z.ZodOptional<z.ZodBoolean>;
242
+ scopes: z.ZodArray<z.ZodString>;
243
+ createPath: z.ZodOptional<z.ZodString>;
244
+ }, z.core.$strict>;
245
+ transport: z.ZodObject<{
246
+ httpsUsername: z.ZodUnion<readonly [z.ZodObject<{
247
+ kind: z.ZodLiteral<"login">;
248
+ }, z.core.$strict>, z.ZodObject<{
249
+ kind: z.ZodLiteral<"fixed">;
250
+ value: z.ZodString;
251
+ }, z.core.$strict>]>;
252
+ appendDotGitToHttpsClone: z.ZodBoolean;
253
+ }, z.core.$strict>;
254
+ limits: z.ZodObject<{
255
+ commentMaxLength: z.ZodNumber;
256
+ reviewerId: z.ZodEnum<{
257
+ login: "login";
258
+ numeric: "numeric";
259
+ }>;
260
+ labelsMax: z.ZodNumber;
261
+ reviewersMax: z.ZodNumber;
262
+ }, z.core.$strict>;
263
+ trackedItems: z.ZodObject<{
264
+ batchLimit: z.ZodNumber;
265
+ }, z.core.$strict>;
266
+ presentation: z.ZodObject<{
267
+ abbreviation: z.ZodString;
268
+ promptNoun: z.ZodString;
269
+ sharedNumberSpace: z.ZodBoolean;
270
+ iconPath: z.ZodOptional<z.ZodString>;
271
+ urls: z.ZodObject<{
272
+ changeRequest: z.ZodArray<z.ZodString>;
273
+ issue: z.ZodArray<z.ZodString>;
274
+ }, z.core.$strict>;
275
+ features: z.ZodObject<{
276
+ authorCanReview: z.ZodDefault<z.ZodBoolean>;
277
+ issueCloseReason: z.ZodDefault<z.ZodBoolean>;
278
+ submitRequirements: z.ZodOptional<z.ZodBoolean>;
279
+ }, z.core.$strict>;
280
+ changeRequestTemplates: z.ZodArray<z.ZodString>;
281
+ }, z.core.$strict>;
282
+ }, z.core.$strict>>;
173
283
  }, z.core.$strip>;
174
284
  }, z.core.$strip>;
175
285
  export declare const PACKAGE_VERSION_PATTERN: RegExp;
@@ -289,6 +399,7 @@ export declare const packageManifestSchema: z.ZodObject<{
289
399
  }>>>;
290
400
  provides: z.ZodOptional<z.ZodArray<z.ZodEnum<{
291
401
  browser: "browser";
402
+ "source-control": "source-control";
292
403
  }>>>;
293
404
  surface: z.ZodOptional<z.ZodLiteral<true>>;
294
405
  }, z.core.$strip>>;
@@ -345,6 +456,115 @@ export declare const packageManifestSchema: z.ZodObject<{
345
456
  text: "text";
346
457
  }>>;
347
458
  }, z.core.$strip>>>;
459
+ sourceControl: z.ZodOptional<z.ZodObject<{
460
+ wire: z.ZodLiteral<1>;
461
+ label: z.ZodString;
462
+ terminology: z.ZodObject<{
463
+ changeRequest: z.ZodEnum<{
464
+ change: "change";
465
+ mergeRequest: "mergeRequest";
466
+ pullRequest: "pullRequest";
467
+ }>;
468
+ ci: z.ZodEnum<{
469
+ checks: "checks";
470
+ pipeline: "pipeline";
471
+ }>;
472
+ }, z.core.$strict>;
473
+ referencePrefix: z.ZodObject<{
474
+ changeRequest: z.ZodString;
475
+ issue: z.ZodString;
476
+ }, z.core.$strict>;
477
+ repositoryPath: z.ZodEnum<{
478
+ "namespace/name": "namespace/name";
479
+ "owner/name": "owner/name";
480
+ }>;
481
+ instances: z.ZodObject<{
482
+ defaultInstances: z.ZodArray<z.ZodString>;
483
+ custom: z.ZodBoolean;
484
+ allowLoopbackHttp: z.ZodBoolean;
485
+ allowPort: z.ZodBoolean;
486
+ placeholder: z.ZodString;
487
+ }, z.core.$strict>;
488
+ capabilities: z.ZodObject<{
489
+ projects: z.ZodDefault<z.ZodBoolean>;
490
+ issues: z.ZodDefault<z.ZodBoolean>;
491
+ changeRequests: z.ZodDefault<z.ZodBoolean>;
492
+ draftChangeRequests: z.ZodDefault<z.ZodBoolean>;
493
+ merge: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
494
+ methods: z.ZodArray<z.ZodEnum<{
495
+ merge: "merge";
496
+ rebase: "rebase";
497
+ squash: "squash";
498
+ submit: "submit";
499
+ }>>;
500
+ }, z.core.$strict>]>>;
501
+ ci: z.ZodDefault<z.ZodBoolean>;
502
+ comments: z.ZodDefault<z.ZodBoolean>;
503
+ reviews: z.ZodDefault<z.ZodBoolean>;
504
+ state: z.ZodDefault<z.ZodBoolean>;
505
+ labels: z.ZodDefault<z.ZodBoolean>;
506
+ reviewers: z.ZodDefault<z.ZodBoolean>;
507
+ changeRequestPatch: z.ZodDefault<z.ZodBoolean>;
508
+ liveSummaries: z.ZodDefault<z.ZodBoolean>;
509
+ reviewModel: z.ZodOptional<z.ZodEnum<{
510
+ verdict: "verdict";
511
+ votes: "votes";
512
+ }>>;
513
+ revisions: z.ZodOptional<z.ZodBoolean>;
514
+ }, z.core.$strict>;
515
+ token: z.ZodObject<{
516
+ header: z.ZodEnum<{
517
+ authorization: "authorization";
518
+ "private-token": "private-token";
519
+ }>;
520
+ scheme: z.ZodEnum<{
521
+ basic: "basic";
522
+ bearer: "bearer";
523
+ none: "none";
524
+ token: "token";
525
+ }>;
526
+ username: z.ZodOptional<z.ZodBoolean>;
527
+ scopes: z.ZodArray<z.ZodString>;
528
+ createPath: z.ZodOptional<z.ZodString>;
529
+ }, z.core.$strict>;
530
+ transport: z.ZodObject<{
531
+ httpsUsername: z.ZodUnion<readonly [z.ZodObject<{
532
+ kind: z.ZodLiteral<"login">;
533
+ }, z.core.$strict>, z.ZodObject<{
534
+ kind: z.ZodLiteral<"fixed">;
535
+ value: z.ZodString;
536
+ }, z.core.$strict>]>;
537
+ appendDotGitToHttpsClone: z.ZodBoolean;
538
+ }, z.core.$strict>;
539
+ limits: z.ZodObject<{
540
+ commentMaxLength: z.ZodNumber;
541
+ reviewerId: z.ZodEnum<{
542
+ login: "login";
543
+ numeric: "numeric";
544
+ }>;
545
+ labelsMax: z.ZodNumber;
546
+ reviewersMax: z.ZodNumber;
547
+ }, z.core.$strict>;
548
+ trackedItems: z.ZodObject<{
549
+ batchLimit: z.ZodNumber;
550
+ }, z.core.$strict>;
551
+ presentation: z.ZodObject<{
552
+ abbreviation: z.ZodString;
553
+ promptNoun: z.ZodString;
554
+ sharedNumberSpace: z.ZodBoolean;
555
+ iconPath: z.ZodOptional<z.ZodString>;
556
+ urls: z.ZodObject<{
557
+ changeRequest: z.ZodArray<z.ZodString>;
558
+ issue: z.ZodArray<z.ZodString>;
559
+ }, z.core.$strict>;
560
+ features: z.ZodObject<{
561
+ authorCanReview: z.ZodDefault<z.ZodBoolean>;
562
+ issueCloseReason: z.ZodDefault<z.ZodBoolean>;
563
+ submitRequirements: z.ZodOptional<z.ZodBoolean>;
564
+ }, z.core.$strict>;
565
+ changeRequestTemplates: z.ZodArray<z.ZodString>;
566
+ }, z.core.$strict>;
567
+ }, z.core.$strict>>;
348
568
  }, z.core.$strip>;
349
569
  }, z.core.$strip>;
350
570
  }, z.core.$strip>;
package/dist/parse.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { OPENCHAMBER_SDK_MANIFEST_API_VERSIONS } from './api-version.js';
3
3
  import { OPENCHAMBER_ENGINE_PATTERN } from './host-version.js';
4
- import { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, PANEL_ID, hasGuestPage, isGuestFilesystemPattern, isGuestPackageSvgIcon, isSafeAssetPath, resolveIntegrationAuth, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, } from './manifest.js';
4
+ import { DECLARED_GUEST_CAPABILITIES, GUEST_ACTIONS_MAX, GUEST_ACTION_LABEL_MAX, GUEST_COMMANDS_MAX, GUEST_COMMAND_DESCRIPTION_MAX, GUEST_COMMAND_NAME, GUEST_FILESYSTEM_PATTERNS_MAX, GUEST_FILESYSTEM_PATTERN_MAX, GUEST_ORIGINS_MAX, GUEST_SERVICE_PROVIDES, GUEST_TOOLS_MAX, GUEST_TOOL_COLUMNS_MAX, GUEST_TOOL_COLUMN_MAX, GUEST_TOOL_LANGUAGE_MAX, GUEST_TOOL_MATCH, GUEST_TOOL_MATCH_MAX, GUEST_TOOL_NAME_MAX, GUEST_TOOL_OUTPUTS, GUEST_TOOL_TEMPLATE_MAX, PANEL_ID, hasGuestPage, isGuestFilesystemPattern, isGuestPackageSvgIcon, isSafeAssetPath, resolveIntegrationAuth, GUEST_SURFACE_DOCKS, GUEST_SURFACE_DOCK_SIZE_MAX, GUEST_SURFACE_DOCK_SIZE_MIN, GUEST_STATUS_SECTION_HEIGHT_MAX, GUEST_STATUS_SECTION_HEIGHT_MIN, GUEST_STATUS_SECTION_TITLE_MAX, SOURCE_CONTROL_DEFAULT_INSTANCES_MAX, SOURCE_CONTROL_GUEST_ID_MAX, SOURCE_CONTROL_URL_TEMPLATES_MAX, SOURCE_CONTROL_URL_TEMPLATE_MAX, SOURCE_CONTROL_URL_TOKENS, } from './manifest.js';
5
5
  import { GUEST_FILE_EDITORS_MAX, GUEST_FILE_EDITOR_PATTERNS_MAX, GUEST_FILE_EDITOR_TITLE_MAX, isFileEditorPattern, } from './file-editor.js';
6
6
  const isPanelIcon = (value) => (PANEL_ID.test(value) || isGuestPackageSvgIcon(value));
7
7
  const isHttpsUrl = (value) => {
@@ -156,6 +156,148 @@ const serviceSchema = z.object({
156
156
  .optional(),
157
157
  surface: z.literal(true).optional(),
158
158
  });
159
+ // SVG path data only, the same rule the UI applies to a provider's icon path:
160
+ // commands, numbers and separators, nothing that could close the attribute.
161
+ const SVG_PATH_DATA = /^[MmLlHhVvCcSsQqTtAaZz0-9eE.,\s+-]+$/;
162
+ // A literal URL template segment: no placeholder braces, no regex syntax.
163
+ const URL_TEMPLATE_LITERAL = /^[A-Za-z0-9._~-]+$/;
164
+ const URL_TEMPLATE_TOKENS = new Set(SOURCE_CONTROL_URL_TOKENS);
165
+ /**
166
+ * A web path template of a change request or issue: `/`, then segments that
167
+ * are each a literal or one of the known placeholders. `{repo}` and
168
+ * `{number}` appear exactly once, and the repository's owner exactly once as
169
+ * `{owner}` (`owner/name` paths) or `{namespace+}` (`namespace/name` paths).
170
+ */
171
+ const isUrlTemplate = (value, repositoryPath) => {
172
+ if (!value.startsWith('/') || value.length > SOURCE_CONTROL_URL_TEMPLATE_MAX)
173
+ return false;
174
+ const segments = value.slice(1).split('/');
175
+ // A normalized URL never has a `.` or `..` segment, so a template with one matches nothing.
176
+ if (segments.some((segment) => segment === '.' || segment === '..'))
177
+ return false;
178
+ if (segments.some((segment) => !URL_TEMPLATE_TOKENS.has(segment) && !URL_TEMPLATE_LITERAL.test(segment)))
179
+ return false;
180
+ const count = (token) => segments.filter((segment) => segment === token).length;
181
+ const owner = repositoryPath === 'owner/name' ? '{owner}' : '{namespace+}';
182
+ const other = repositoryPath === 'owner/name' ? '{namespace+}' : '{owner}';
183
+ return count('{repo}') === 1 && count('{number}') === 1 && count(owner) === 1 && count(other) === 0;
184
+ };
185
+ // A file inside the repository, as a change request template names it:
186
+ // relative, no `.` or `..` segment, dot-folders like `.gitea/` allowed.
187
+ const REPOSITORY_FILE_SEGMENT = /^[A-Za-z0-9._-]+$/;
188
+ const isRepositoryFilePath = (value) => value.length > 0 && value.split('/')
189
+ .every((segment) => REPOSITORY_FILE_SEGMENT.test(segment) && segment !== '.' && segment !== '..');
190
+ const prefixSchema = z.string().min(1).max(4).regex(/^\S+$/);
191
+ const literalSchema = z.string().trim().min(1).max(64);
192
+ const sourceControlCapabilitiesSchema = z.object({
193
+ projects: z.boolean().default(false),
194
+ issues: z.boolean().default(false),
195
+ changeRequests: z.boolean().default(false),
196
+ draftChangeRequests: z.boolean().default(false),
197
+ merge: z.union([
198
+ z.literal(false),
199
+ z.object({
200
+ methods: z.array(z.enum(['merge', 'squash', 'rebase', 'submit'])).min(1).max(3)
201
+ .refine((methods) => new Set(methods).size === methods.length, { message: 'merge methods must be unique' })
202
+ // A host that submits a change merges it the one way its project is set up.
203
+ .refine((methods) => !methods.includes('submit') || methods.length === 1, { message: '"submit" is the only merge method when present' }),
204
+ }).strict(),
205
+ ]).default(false),
206
+ ci: z.boolean().default(false),
207
+ comments: z.boolean().default(false),
208
+ reviews: z.boolean().default(false),
209
+ state: z.boolean().default(false),
210
+ labels: z.boolean().default(false),
211
+ reviewers: z.boolean().default(false),
212
+ changeRequestPatch: z.boolean().default(false),
213
+ liveSummaries: z.boolean().default(false),
214
+ // Left out rather than defaulted, so a manifest that does not use them parses as before.
215
+ reviewModel: z.enum(['verdict', 'votes']).optional(),
216
+ revisions: z.boolean().optional(),
217
+ }).strict();
218
+ // A Git username OpenChamber puts before the token in an HTTPS credential.
219
+ const FIXED_USERNAME = /^[A-Za-z0-9._-]{1,64}$/;
220
+ const urlTemplatesSchema = z.array(z.string().trim().min(1)).max(SOURCE_CONTROL_URL_TEMPLATES_MAX);
221
+ const sourceControlSchema = z.object({
222
+ wire: z.literal(1),
223
+ label: literalSchema,
224
+ terminology: z.object({
225
+ changeRequest: z.enum(['pullRequest', 'mergeRequest', 'change']),
226
+ ci: z.enum(['checks', 'pipeline']),
227
+ }).strict(),
228
+ referencePrefix: z.object({ changeRequest: prefixSchema, issue: prefixSchema }).strict(),
229
+ repositoryPath: z.enum(['owner/name', 'namespace/name']),
230
+ instances: z.object({
231
+ defaultInstances: z.array(z.string().trim().refine(isHttpsOrigin, { message: 'must be an https origin with no path, like https://git.example.com' }))
232
+ .max(SOURCE_CONTROL_DEFAULT_INSTANCES_MAX)
233
+ .refine((origins) => new Set(origins).size === origins.length, { message: 'default instances must be unique' }),
234
+ custom: z.boolean(),
235
+ allowLoopbackHttp: z.boolean(),
236
+ allowPort: z.boolean(),
237
+ placeholder: z.string().trim().max(256),
238
+ }).strict().refine((instances) => instances.custom || instances.defaultInstances.length > 0, { message: 'without custom instances, defaultInstances names at least one' }),
239
+ capabilities: sourceControlCapabilitiesSchema,
240
+ token: z.object({
241
+ header: z.enum(['authorization', 'private-token']),
242
+ scheme: z.enum(['bearer', 'token', 'basic', 'none']),
243
+ username: z.boolean().optional(),
244
+ scopes: z.array(z.string().trim().min(1).max(64)).max(32),
245
+ createPath: z.string().trim().max(256).refine(isSafeApiPath, { message: 'must be a path on the instance starting with "/", with no "." or ".." segment' }).optional(),
246
+ }).strict().refine((token) => (token.header === 'private-token') === (token.scheme === 'none'), { message: 'private-token takes the bare token; authorization takes a bearer, token or basic scheme' }).refine(
247
+ // Basic sends `login:token`, so Settings must ask for the login; no other scheme uses one.
248
+ (token) => (token.scheme === 'basic') === (token.username === true), { message: 'the basic scheme needs username: true, and username is only for the basic scheme' }),
249
+ transport: z.object({
250
+ httpsUsername: z.union([
251
+ z.object({ kind: z.literal('login') }).strict(),
252
+ z.object({ kind: z.literal('fixed'), value: z.string().regex(FIXED_USERNAME) }).strict(),
253
+ ]),
254
+ appendDotGitToHttpsClone: z.boolean(),
255
+ }).strict(),
256
+ limits: z.object({
257
+ commentMaxLength: z.number().int().min(1).max(1_000_000),
258
+ reviewerId: z.enum(['numeric', 'login']),
259
+ labelsMax: z.number().int().min(0).max(100),
260
+ reviewersMax: z.number().int().min(0).max(50),
261
+ }).strict(),
262
+ trackedItems: z.object({ batchLimit: z.number().int().min(1).max(50) }).strict(),
263
+ presentation: z.object({
264
+ abbreviation: z.string().trim().min(1).max(8),
265
+ promptNoun: literalSchema,
266
+ sharedNumberSpace: z.boolean(),
267
+ iconPath: z.string().max(8192).regex(SVG_PATH_DATA).optional(),
268
+ urls: z.object({ changeRequest: urlTemplatesSchema, issue: urlTemplatesSchema }).strict(),
269
+ features: z.object({
270
+ authorCanReview: z.boolean().default(false),
271
+ issueCloseReason: z.boolean().default(false),
272
+ submitRequirements: z.boolean().optional(),
273
+ }).strict(),
274
+ changeRequestTemplates: z.array(z.string().trim().max(256).refine(isRepositoryFilePath, {
275
+ message: 'must be a file path inside the repository, like .gitea/pull_request_template.md, with no "." or ".." segment',
276
+ })).max(8),
277
+ }).strict(),
278
+ }).strict().superRefine((value, ctx) => {
279
+ for (const kind of ['changeRequest', 'issue']) {
280
+ value.presentation.urls[kind].forEach((template, index) => {
281
+ if (isUrlTemplate(template, value.repositoryPath))
282
+ return;
283
+ ctx.addIssue({
284
+ code: 'custom', path: ['presentation', 'urls', kind, index],
285
+ message: `templates start with "/" and use literal segments and ${SOURCE_CONTROL_URL_TOKENS.join(', ')}: {repo} and {number} once, and ${value.repositoryPath === 'owner/name' ? '{owner}' : '{namespace+}'} once for ${value.repositoryPath} paths.`,
286
+ });
287
+ });
288
+ }
289
+ // Change request patterns are matched first, so an issue template that is
290
+ // also a change request template would read every issue link as one.
291
+ const changeRequestTemplates = new Set(value.presentation.urls.changeRequest);
292
+ value.presentation.urls.issue.forEach((template, index) => {
293
+ if (!changeRequestTemplates.has(template))
294
+ return;
295
+ ctx.addIssue({
296
+ code: 'custom', path: ['presentation', 'urls', 'issue', index],
297
+ message: 'is also a changeRequest template; a link names either a change request or an issue.',
298
+ });
299
+ });
300
+ });
159
301
  const uniqueBy = (items, key) => (new Set(items.map(key)).size === items.length);
160
302
  // `roles` only means something on a message action and `payload` only on a
161
303
  // session action; a dead field is a misconfiguration and fails closed.
@@ -229,6 +371,7 @@ const contributesSchema = z.object({
229
371
  commands: commandsSchema.optional(),
230
372
  tools: toolsSchema.optional(),
231
373
  fileEditors: fileEditorsSchema.optional(),
374
+ sourceControl: sourceControlSchema.optional(),
232
375
  });
233
376
  /**
234
377
  * Contributions that need either the panel or background execution frame.
@@ -264,6 +407,23 @@ export const openChamberManifestSchema = z.object({
264
407
  openchamber: z.string().trim().regex(OPENCHAMBER_ENGINE_PATTERN),
265
408
  }).strict().optional(),
266
409
  contributes: contributesSchema.superRefine((contributes, ctx) => {
410
+ // The Git host and the role that serves it come together, and the host
411
+ // names it `ext-<panel.id>` within the 32-character provider id.
412
+ const providesSourceControl = contributes.service?.provides?.includes('source-control') === true;
413
+ if (providesSourceControl !== (contributes.sourceControl !== undefined)) {
414
+ ctx.addIssue({
415
+ code: 'custom', path: ['service', 'provides'],
416
+ message: 'service.provides "source-control" and contributes.sourceControl come together: declare both or neither.',
417
+ });
418
+ return;
419
+ }
420
+ if (providesSourceControl && contributes.panel.id.length > SOURCE_CONTROL_GUEST_ID_MAX) {
421
+ ctx.addIssue({
422
+ code: 'custom', path: ['service', 'provides'],
423
+ message: `A source-control extension's panel.id is at most ${SOURCE_CONTROL_GUEST_ID_MAX} characters: OpenChamber names its Git host "ext-<panel.id>".`,
424
+ });
425
+ return;
426
+ }
267
427
  // Docking only means something for a page beside a shared surface.
268
428
  const docked = contributes.panel.dock !== undefined || contributes.panel.size !== undefined;
269
429
  if (docked && !(contributes.service?.surface && hasGuestPage(contributes))) {
@@ -343,6 +503,9 @@ const fail = (code, message) => ({
343
503
  code,
344
504
  message,
345
505
  });
506
+ /** `contributes.sourceControl.instances.defaultInstances[0]`, relative to the `openchamber` block. */
507
+ const fieldPath = (issuePath) => (issuePath[0] === 'openchamber' ? issuePath.slice(1) : issuePath)
508
+ .reduce((field, segment) => (Number.isInteger(segment) ? `${field}[${String(segment)}]` : `${field ? `${field}.` : ''}${String(segment)}`), '');
346
509
  const failureFromIssue = (issue) => {
347
510
  // Paths are reported relative to the `openchamber` block whether the
348
511
  // document was a bare manifest or a package.json envelope.
@@ -427,8 +590,17 @@ const failureFromIssue = (issue) => {
427
590
  if (path.startsWith('contributes.integration')) {
428
591
  return fail('invalid-integration', 'contributes.integration needs a name, description, and oauth, token, or host.');
429
592
  }
593
+ if (path.startsWith('contributes.sourceControl')) {
594
+ // The field is named either way: a refine's message alone does not say which entry failed.
595
+ const field = fieldPath(issue.path);
596
+ return fail('invalid-service', issue.code === 'custom'
597
+ ? `${field}: ${issue.message}`
598
+ : `${field} is invalid: see "contributes.sourceControl" in GUEST_SERVICES.md for every field.`);
599
+ }
430
600
  if (path.startsWith('contributes.service')) {
431
- return fail('invalid-service', 'contributes.service needs entry, runtime "host", optional permissions, optional provides ("browser"), and optional surface (true).');
601
+ return fail('invalid-service', issue.code === 'custom'
602
+ ? issue.message
603
+ : 'contributes.service needs entry, runtime "host", optional permissions, optional provides ("browser", "source-control"), and optional surface (true).');
432
604
  }
433
605
  return fail('missing-panel', 'contributes.panel is required.');
434
606
  };
@@ -258,6 +258,7 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
258
258
  deviceStorage: z.ZodOptional<z.ZodLiteral<true>>;
259
259
  statusControls: z.ZodOptional<z.ZodLiteral<true>>;
260
260
  popovers: z.ZodOptional<z.ZodLiteral<true>>;
261
+ lateGenerateResults: z.ZodOptional<z.ZodLiteral<true>>;
261
262
  }, z.core.$strip>>;
262
263
  }, z.core.$strip>;
263
264
  }, z.core.$strip>, z.ZodObject<{
@@ -924,6 +925,52 @@ export declare const hostMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
924
925
  payload: z.ZodObject<{
925
926
  version: z.ZodString;
926
927
  }, z.core.$strip>;
928
+ }, z.core.$strip>, z.ZodObject<{
929
+ channel: z.ZodLiteral<"openchamber.sdk">;
930
+ v: z.ZodLiteral<1>;
931
+ type: z.ZodLiteral<"generate-late">;
932
+ payload: z.ZodDiscriminatedUnion<[z.ZodObject<{
933
+ request: z.ZodObject<{
934
+ prompt: z.ZodString;
935
+ system: z.ZodOptional<z.ZodString>;
936
+ maxOutputTokens: z.ZodOptional<z.ZodNumber>;
937
+ }, z.core.$strip>;
938
+ ok: z.ZodLiteral<true>;
939
+ result: z.ZodObject<{
940
+ text: z.ZodString;
941
+ }, z.core.$strip>;
942
+ }, z.core.$strip>, z.ZodObject<{
943
+ request: z.ZodObject<{
944
+ prompt: z.ZodString;
945
+ system: z.ZodOptional<z.ZodString>;
946
+ maxOutputTokens: z.ZodOptional<z.ZodNumber>;
947
+ }, z.core.$strip>;
948
+ ok: z.ZodLiteral<false>;
949
+ code: z.ZodEnum<{
950
+ BAD_PATH: "BAD_PATH";
951
+ DENIED: "DENIED";
952
+ DISABLED: "DISABLED";
953
+ DISCONNECTED: "DISCONNECTED";
954
+ FILE_TOO_LARGE: "FILE_TOO_LARGE";
955
+ HOST_REJECTED: "HOST_REJECTED";
956
+ HOST_TIMEOUT: "HOST_TIMEOUT";
957
+ HOST_UNAVAILABLE: "HOST_UNAVAILABLE";
958
+ MODEL_FAILED: "MODEL_FAILED";
959
+ NOT_FOUND: "NOT_FOUND";
960
+ NOT_GRANTED: "NOT_GRANTED";
961
+ NO_DIRECTORY: "NO_DIRECTORY";
962
+ NO_INTEGRATION: "NO_INTEGRATION";
963
+ NO_MODEL: "NO_MODEL";
964
+ NO_SERVICE: "NO_SERVICE";
965
+ NO_SESSION: "NO_SESSION";
966
+ REQUEST_FAILED: "REQUEST_FAILED";
967
+ RESPONSE_TOO_LARGE: "RESPONSE_TOO_LARGE";
968
+ SERVICE_FAILED: "SERVICE_FAILED";
969
+ SESSION_BUSY: "SESSION_BUSY";
970
+ UNSUPPORTED: "UNSUPPORTED";
971
+ }>;
972
+ error: z.ZodString;
973
+ }, z.core.$strip>], "ok">;
927
974
  }, z.core.$strip>]>;
928
975
  export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
929
976
  channel: z.ZodLiteral<"openchamber.sdk">;
@@ -1453,6 +1500,10 @@ export declare const guestMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1453
1500
  channel: z.ZodLiteral<"openchamber.sdk">;
1454
1501
  v: z.ZodLiteral<1>;
1455
1502
  type: z.ZodLiteral<"file-unsupported">;
1503
+ }, z.core.$strip>, z.ZodObject<{
1504
+ channel: z.ZodLiteral<"openchamber.sdk">;
1505
+ v: z.ZodLiteral<1>;
1506
+ type: z.ZodLiteral<"generate-late-listen">;
1456
1507
  }, z.core.$strip>], "type">;
1457
1508
  type HostWire = z.input<typeof hostMessageSchema>;
1458
1509
  type GuestWire = z.input<typeof guestMessageSchema>;
package/dist/protocol.js CHANGED
@@ -4,7 +4,7 @@ import { guestStatusControlEventSchema, guestStatusControlsSchema } from './stat
4
4
  import { guestPopoverClosedEventSchema, guestPopoverDataSchema, guestPopoverRequestSchema } from './popover-schemas.js';
5
5
  import { GUEST_SHELL_END_STATUSES, GUEST_SHELL_ID_MAX, GUEST_SHELLS_MAX, GUEST_SHELL_OUTPUT_TAIL_MAX } from './shells.js';
6
6
  import { OPENCHAMBER_SDK_API_VERSION, OPENCHAMBER_SDK_CHANNEL } from './api-version.js';
7
- import { SERVICE_STATUS_VALUES, ATTACH_PROVIDER_ID, GUEST_ACCOUNT_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_COMMIT_SHA, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_FILE_PATH_MAX, GUEST_FILE_STAT_KINDS, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_SETTING_VALUE_MAX, GUEST_TOAST_MAX, SESSION_LIFECYCLE_PHASES, SETTING_KEY, START_SESSION_SENT, isGuestFilePath, isGuestRequestPath, resolveHostRequestErrorCode, } from './contract.js';
7
+ import { SERVICE_STATUS_VALUES, ATTACH_PROVIDER_ID, GUEST_ACCOUNT_MAX, GUEST_ATTACH_AUTHOR_MAX, GUEST_BADGE_MAX, GUEST_FRAME_HEIGHT_MAX, GUEST_COMMIT_SHA, GUEST_ITEM_MESSAGE_TEXT_MAX, GUEST_ITEM_SESSION_MAX, GUEST_RESOLVE_ERROR_MAX, GUEST_ATTACH_BRANCH_MAX, GUEST_ATTACH_DATA_MAX, GUEST_ATTACH_ID_MAX, GUEST_ATTACH_TEXT_MAX, GUEST_ATTACH_TITLE_MAX, GUEST_ATTACH_URL_MAX, GUEST_CLIPBOARD_TEXT_MAX, GUEST_COMPOSE_TEXT_MAX, GUEST_FILE_CONTENT_MAX, GUEST_FILE_ENTRY_KINDS, GUEST_FILE_LIST_MAX, GUEST_GENERATE_OUTPUT_TOKENS_MAX, GUEST_GENERATE_PROMPT_MAX, GUEST_GENERATE_SYSTEM_MAX, GUEST_GENERATE_TEXT_MAX, GUEST_FILE_PATH_MAX, GUEST_FILE_STAT_KINDS, GUEST_REQUEST_BODY_MAX, GUEST_REQUEST_PATH_MAX, GUEST_REQUEST_RESPONSE_MAX, GUEST_SESSION_AGENT_MAX, GUEST_SESSION_MODEL_MAX, GUEST_SETTING_VALUE_MAX, GUEST_TOAST_MAX, SESSION_LIFECYCLE_PHASES, SETTING_KEY, START_SESSION_SENT, isGuestFilePath, isGuestRequestPath, resolveHostRequestErrorCode, HOST_REQUEST_ERROR_CODES, } from './contract.js';
8
8
  import { GUEST_FILE_EDITOR_CONTENT_MAX, GUEST_FILE_EDITOR_VERSION_MAX } from './file-editor.js';
9
9
  // Structured clone recreates a frame's Uint8Array in the receiving realm, so
10
10
  // `instanceof` holds for bytes that crossed postMessage.
@@ -99,6 +99,15 @@ const fileStatResultPayloadSchema = z.object({
99
99
  const generateResultPayloadSchema = z.object({
100
100
  text: z.string().max(GUEST_GENERATE_TEXT_MAX),
101
101
  });
102
+ const generateRequestSchema = z.object({
103
+ prompt: z.string().trim().min(1).max(GUEST_GENERATE_PROMPT_MAX),
104
+ system: z.string().trim().min(1).max(GUEST_GENERATE_SYSTEM_MAX).optional(),
105
+ maxOutputTokens: z.number().int().min(1).max(GUEST_GENERATE_OUTPUT_TOKENS_MAX).optional(),
106
+ });
107
+ const lateGenerateResultSchema = z.discriminatedUnion('ok', [
108
+ z.object({ request: generateRequestSchema, ok: z.literal(true), result: generateResultPayloadSchema }),
109
+ z.object({ request: generateRequestSchema, ok: z.literal(false), code: z.enum(HOST_REQUEST_ERROR_CODES), error: z.string().min(1) }),
110
+ ]);
102
111
  const shellOutputResultPayloadSchema = z.object({
103
112
  output: z.string(),
104
113
  cursor: z.number(),
@@ -181,6 +190,7 @@ const readyPayloadSchema = z.object({
181
190
  deviceStorage: z.literal(true).optional(),
182
191
  statusControls: z.literal(true).optional(),
183
192
  popovers: z.literal(true).optional(),
193
+ lateGenerateResults: z.literal(true).optional(),
184
194
  }).optional(),
185
195
  });
186
196
  const hostResultSchema = z.object({
@@ -356,6 +366,7 @@ export const hostMessageSchema = z.union([
356
366
  type: z.literal('file-saved'),
357
367
  payload: z.object({ version: z.string().max(GUEST_FILE_EDITOR_VERSION_MAX) }),
358
368
  }),
369
+ z.object({ ...envelope, type: z.literal('generate-late'), payload: lateGenerateResultSchema }),
359
370
  ]);
360
371
  const filePathSchema = z.string().min(1).max(GUEST_FILE_PATH_MAX).refine(isGuestFilePath);
361
372
  export const guestMessageSchema = z.discriminatedUnion('type', [
@@ -539,11 +550,7 @@ export const guestMessageSchema = z.discriminatedUnion('type', [
539
550
  ...envelope,
540
551
  type: z.literal('generate'),
541
552
  id: z.string().min(1),
542
- payload: z.object({
543
- prompt: z.string().trim().min(1).max(GUEST_GENERATE_PROMPT_MAX),
544
- system: z.string().trim().min(1).max(GUEST_GENERATE_SYSTEM_MAX).optional(),
545
- maxOutputTokens: z.number().int().min(1).max(GUEST_GENERATE_OUTPUT_TOKENS_MAX).optional(),
546
- }),
553
+ payload: generateRequestSchema,
547
554
  }),
548
555
  z.object({
549
556
  ...envelope,
@@ -599,6 +606,7 @@ export const guestMessageSchema = z.discriminatedUnion('type', [
599
606
  }),
600
607
  z.object({ ...envelope, type: z.literal('file-save') }),
601
608
  z.object({ ...envelope, type: z.literal('file-unsupported') }),
609
+ z.object({ ...envelope, type: z.literal('generate-late-listen') }),
602
610
  ]);
603
611
  // The hand-written contract types and the schemas must describe the same wire
604
612
  // shapes; either drifting from the other fails this file's type-check.
package/dist/schemas.d.ts CHANGED
@@ -4,6 +4,6 @@ export { guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceSnap
4
4
  export { guestStatusControlsSchema, guestStatusControlEventSchema } from './status-control-schemas.ts';
5
5
  export { guestPopoverClosedEventSchema, guestPopoverRequestSchema } from './popover-schemas.ts';
6
6
  export { guestMessageSchema, hostMessageSchema, parseGuestMessage, parseHostMessage, } from './protocol.ts';
7
- export { browserControlActionSchema, browserProviderResultSchema } from './service-provider-schemas.ts';
7
+ export { browserControlActionSchema, browserProviderResultSchema, sourceControlProviderMethodSchema, sourceControlProviderSchemasV1, } from './service-provider-schemas.ts';
8
8
  export { surfaceClipboardAnswerSchema, surfaceControllerSchema, surfaceHostMessageSchema, surfaceInputEventSchema, surfaceResizeAnswerSchema, surfaceViewerMessageSchema, } from './service-surface-schemas.ts';
9
9
  export type { SurfaceHostMessage, SurfaceViewerMessage } from './service-surface-schemas.ts';
package/dist/schemas.js CHANGED
@@ -6,5 +6,5 @@ export { guestStorageRequestSchema, guestStorageResultSchema, guestWorkspaceSnap
6
6
  export { guestStatusControlsSchema, guestStatusControlEventSchema } from './status-control-schemas.js';
7
7
  export { guestPopoverClosedEventSchema, guestPopoverRequestSchema } from './popover-schemas.js';
8
8
  export { guestMessageSchema, hostMessageSchema, parseGuestMessage, parseHostMessage, } from './protocol.js';
9
- export { browserControlActionSchema, browserProviderResultSchema } from './service-provider-schemas.js';
9
+ export { browserControlActionSchema, browserProviderResultSchema, sourceControlProviderMethodSchema, sourceControlProviderSchemasV1, } from './service-provider-schemas.js';
10
10
  export { surfaceClipboardAnswerSchema, surfaceControllerSchema, surfaceHostMessageSchema, surfaceInputEventSchema, surfaceResizeAnswerSchema, surfaceViewerMessageSchema, } from './service-surface-schemas.js';