@openforge-app/plugin-sdk 0.1.0 → 0.2.3

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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/browserSurfaces.d.ts +78 -0
  4. package/dist/browserSurfaces.js +17 -0
  5. package/dist/browserSurfacesTesting.d.ts +60 -0
  6. package/dist/browserSurfacesTesting.js +240 -0
  7. package/dist/domain.d.ts +74 -15
  8. package/dist/domain.js +5 -1
  9. package/dist/fileIcons.d.ts +6 -0
  10. package/dist/fileIcons.js +84 -0
  11. package/dist/frontend.d.ts +5 -2
  12. package/dist/frontend.js +1 -0
  13. package/dist/index.d.ts +5 -3
  14. package/dist/index.js +2 -1
  15. package/dist/manifest.d.ts +29 -0
  16. package/dist/manifest.js +37 -0
  17. package/dist/markdown.d.ts +5 -1
  18. package/dist/markdown.js +67 -18
  19. package/dist/openforgePackageMetadataSchema.json +40 -1
  20. package/dist/pluginIcons.d.ts +7 -0
  21. package/dist/pluginIcons.js +104 -0
  22. package/dist/publicUiExports.d.mts +16 -0
  23. package/dist/publicUiExports.mjs +59 -0
  24. package/dist/svelteHostRuntimeContract.d.mts +1 -0
  25. package/dist/svelteHostRuntimeContract.mjs +16 -0
  26. package/dist/testing/backendServicesFake.d.ts +19 -0
  27. package/dist/testing/backendServicesFake.js +87 -0
  28. package/dist/testing/commonApiFake.d.ts +22 -0
  29. package/dist/testing/commonApiFake.js +274 -0
  30. package/dist/testing/contracts.d.ts +231 -0
  31. package/dist/testing/contracts.js +1 -0
  32. package/dist/testing/frontendContributionFake.d.ts +36 -0
  33. package/dist/testing/frontendContributionFake.js +195 -0
  34. package/dist/testing/registryFake.d.ts +40 -0
  35. package/dist/testing/registryFake.js +124 -0
  36. package/dist/testing/support.d.ts +40 -0
  37. package/dist/testing/support.js +242 -0
  38. package/dist/testing.d.ts +3 -223
  39. package/dist/testing.js +2 -625
  40. package/dist/types.d.ts +193 -7
  41. package/dist/types.js +8 -0
  42. package/dist/ui/Button.svelte +49 -0
  43. package/dist/ui/Checkbox.svelte +137 -0
  44. package/dist/ui/FileTypeIcon.svelte +37 -0
  45. package/dist/ui/MarkdownContent.svelte +71 -11
  46. package/dist/ui/Modal.svelte +157 -0
  47. package/dist/ui/PluginPageHeader.svelte +26 -0
  48. package/dist/ui/PluginViewState.svelte +76 -0
  49. package/dist/ui/ResizablePanel.svelte +10 -0
  50. package/dist/ui/icons/README.md +9 -0
  51. package/dist/ui/icons/c.svg +1 -0
  52. package/dist/ui/icons/console.svg +1 -0
  53. package/dist/ui/icons/cpp.svg +1 -0
  54. package/dist/ui/icons/csharp.svg +1 -0
  55. package/dist/ui/icons/css.svg +3 -0
  56. package/dist/ui/icons/database.svg +1 -0
  57. package/dist/ui/icons/docker.svg +1 -0
  58. package/dist/ui/icons/document.svg +4 -0
  59. package/dist/ui/icons/file.svg +1 -0
  60. package/dist/ui/icons/folder-open.svg +1 -0
  61. package/dist/ui/icons/folder.svg +1 -0
  62. package/dist/ui/icons/git.svg +3 -0
  63. package/dist/ui/icons/go.svg +1 -0
  64. package/dist/ui/icons/graphql.svg +20 -0
  65. package/dist/ui/icons/h.svg +3 -0
  66. package/dist/ui/icons/html.svg +3 -0
  67. package/dist/ui/icons/image.svg +3 -0
  68. package/dist/ui/icons/java.svg +6 -0
  69. package/dist/ui/icons/javascript.svg +3 -0
  70. package/dist/ui/icons/json.svg +3 -0
  71. package/dist/ui/icons/kotlin.svg +1 -0
  72. package/dist/ui/icons/lock.svg +3 -0
  73. package/dist/ui/icons/markdown.svg +6 -0
  74. package/dist/ui/icons/nodejs.svg +6 -0
  75. package/dist/ui/icons/npm.svg +3 -0
  76. package/dist/ui/icons/pdf.svg +1 -0
  77. package/dist/ui/icons/php.svg +1 -0
  78. package/dist/ui/icons/python.svg +1 -0
  79. package/dist/ui/icons/react.svg +8 -0
  80. package/dist/ui/icons/react_ts.svg +8 -0
  81. package/dist/ui/icons/readme.svg +4 -0
  82. package/dist/ui/icons/ruby.svg +1 -0
  83. package/dist/ui/icons/rust.svg +1 -0
  84. package/dist/ui/icons/sass.svg +3 -0
  85. package/dist/ui/icons/settings.svg +4 -0
  86. package/dist/ui/icons/svelte.svg +1 -0
  87. package/dist/ui/icons/svg.svg +3 -0
  88. package/dist/ui/icons/swift.svg +1 -0
  89. package/dist/ui/icons/tune.svg +11 -0
  90. package/dist/ui/icons/typescript-def.svg +6 -0
  91. package/dist/ui/icons/typescript.svg +3 -0
  92. package/dist/ui/icons/vue.svg +1 -0
  93. package/dist/ui/icons/xml.svg +1 -0
  94. package/dist/ui/icons/yaml.svg +3 -0
  95. package/dist/ui/icons/zip.svg +3 -0
  96. package/dist/vite.js +5 -2
  97. package/package.json +34 -7
@@ -0,0 +1,84 @@
1
+ export const DEFAULT_FILE_ICON = 'file';
2
+ export const DEFAULT_FOLDER_ICON = 'folder';
3
+ export const DEFAULT_FOLDER_OPEN_ICON = 'folder-open';
4
+ // Lowercased extension (no leading dot) -> Material icon base name.
5
+ // Only names that exist in vscode-material-icons/generated/icons are used.
6
+ const EXTENSION_ICONS = {
7
+ ts: 'typescript', mts: 'typescript', cts: 'typescript',
8
+ tsx: 'react_ts',
9
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript',
10
+ jsx: 'react',
11
+ json: 'json', jsonc: 'json',
12
+ yaml: 'yaml', yml: 'yaml',
13
+ md: 'markdown', markdown: 'markdown', mdx: 'markdown',
14
+ css: 'css', less: 'css',
15
+ scss: 'sass', sass: 'sass',
16
+ html: 'html', htm: 'html',
17
+ svelte: 'svelte',
18
+ vue: 'vue',
19
+ rs: 'rust',
20
+ py: 'python',
21
+ rb: 'ruby',
22
+ go: 'go',
23
+ java: 'java',
24
+ kt: 'kotlin', kts: 'kotlin',
25
+ swift: 'swift',
26
+ c: 'c',
27
+ h: 'h', hpp: 'h', hh: 'h',
28
+ cc: 'cpp', cpp: 'cpp', cxx: 'cpp',
29
+ cs: 'csharp',
30
+ php: 'php',
31
+ sh: 'console', bash: 'console', zsh: 'console', fish: 'console',
32
+ sql: 'database',
33
+ graphql: 'graphql', gql: 'graphql',
34
+ xml: 'xml',
35
+ svg: 'svg',
36
+ png: 'image', jpg: 'image', jpeg: 'image', gif: 'image',
37
+ webp: 'image', bmp: 'image', ico: 'image', avif: 'image',
38
+ pdf: 'pdf',
39
+ zip: 'zip', tar: 'zip', gz: 'zip', tgz: 'zip', rar: 'zip', '7z': 'zip',
40
+ lock: 'lock',
41
+ toml: 'settings',
42
+ txt: 'document', log: 'document',
43
+ env: 'tune',
44
+ };
45
+ // Exact lowercased filename -> Material icon base name (takes precedence over extension).
46
+ const FILENAME_ICONS = {
47
+ 'package.json': 'nodejs',
48
+ 'package-lock.json': 'lock',
49
+ 'pnpm-lock.yaml': 'lock',
50
+ 'yarn.lock': 'lock',
51
+ dockerfile: 'docker',
52
+ '.dockerignore': 'docker',
53
+ '.gitignore': 'git',
54
+ '.gitattributes': 'git',
55
+ '.gitmodules': 'git',
56
+ '.npmrc': 'npm',
57
+ '.npmignore': 'npm',
58
+ };
59
+ export function getFileIconName(filename) {
60
+ const base = (filename.split('/').pop() ?? filename).toLowerCase();
61
+ if (base in FILENAME_ICONS)
62
+ return FILENAME_ICONS[base];
63
+ if (base.endsWith('.d.ts'))
64
+ return 'typescript-def';
65
+ if (base === 'readme' || /^readme\.[^.]+$/.test(base))
66
+ return 'readme';
67
+ if (base === '.env' || base.startsWith('.env.'))
68
+ return 'tune';
69
+ const ext = base.includes('.') ? (base.split('.').pop() ?? '') : '';
70
+ return EXTENSION_ICONS[ext] ?? DEFAULT_FILE_ICON;
71
+ }
72
+ export function getFolderIconName(open) {
73
+ return open ? DEFAULT_FOLDER_OPEN_ICON : DEFAULT_FOLDER_ICON;
74
+ }
75
+ export const BUNDLED_ICON_NAMES = Object.freeze(Array.from(new Set([
76
+ DEFAULT_FILE_ICON,
77
+ DEFAULT_FOLDER_ICON,
78
+ DEFAULT_FOLDER_OPEN_ICON,
79
+ 'typescript-def',
80
+ 'readme',
81
+ 'tune',
82
+ ...Object.values(EXTENSION_ICONS),
83
+ ...Object.values(FILENAME_ICONS),
84
+ ])).sort());
@@ -1,7 +1,10 @@
1
- import type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendViewRegistry, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginStorageScope, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginViewProps, PluginViewRegistration } from './types';
1
+ import type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState } from './browserSurfaces';
2
+ import type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendInjectionPointRegistry, FrontendTaskStartRegistry, FrontendTaskUIRegistry, FrontendViewRegistry, InjectionPointLocation, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, PluginIcon, PluginInjectionPointProps, PluginInjectionPointRegistration, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsSectionScope, PluginStorageScope, PluginSvgIcon, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginTaskUISectionProps, PluginTaskUISectionRegistration, PluginViewProps, PluginViewRegistration, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol } from './types';
2
3
  export declare const OPENFORGE_FRONTEND_PLUGIN_MARKER = "__openforgeFrontendPlugin";
3
4
  export type MarkedFrontendPlugin<TPlugin extends FrontendPlugin = FrontendPlugin> = TPlugin & {
4
5
  readonly [OPENFORGE_FRONTEND_PLUGIN_MARKER]: true;
5
6
  };
6
7
  export declare function defineFrontendPlugin<const TPlugin extends FrontendPlugin>(plugin: TPlugin): MarkedFrontendPlugin<TPlugin>;
7
- export type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendViewRegistry, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginStorageScope, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginViewProps, PluginViewRegistration, };
8
+ export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
9
+ export type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState, };
10
+ export type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendInjectionPointRegistry, FrontendTaskStartRegistry, FrontendTaskUIRegistry, FrontendViewRegistry, InjectionPointLocation, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, PluginIcon, PluginInjectionPointProps, PluginInjectionPointRegistration, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsSectionScope, PluginStorageScope, PluginSvgIcon, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginTaskUISectionProps, PluginTaskUISectionRegistration, PluginViewProps, PluginViewRegistration, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol, };
package/dist/frontend.js CHANGED
@@ -7,3 +7,4 @@ export function defineFrontendPlugin(plugin) {
7
7
  });
8
8
  return plugin;
9
9
  }
10
+ export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
2
+ export type { BrowserSurfaceCapture, BrowserSurfaceFeedbackSelection, BrowserSurfaceVisualFeedback, BrowserSurfaceRegion, BrowserSurfaceErrorCode, BrowserSurfaceNavigationError, BrowserSurfacesAPI, GetOrCreateBrowserSurfaceRequest, TaskBrowserSurfaceController, TaskBrowserSurfaceState, } from './browserSurfaces';
1
3
  export { OPENFORGE_PACKAGE_METADATA_SCHEMA, OPENFORGE_PLUGIN_CAPABILITIES, isOpenForgePackageMetadata, isPluginPackageMetadata, isSupportedOpenForgeApiVersion, validateOpenForgePackageMetadata, validatePluginPackageMetadata, } from './manifest';
2
4
  export { createMemoryPluginStorage, createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, createTestingCalls, TestingOpenForgeRegistryFake, TestingSubscriptionSink, } from './testing';
3
- export { MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
4
- export type { AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, FileSystemAPI, ImplementationRun, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, OpenForgePluginCapability, OpenForgePluginContext, OpenForgePluginPackageJson, PluginComponentLoader, PluginComponentModule, PluginEntry, PluginSettingsSectionProps, PluginState, PluginTaskPaneProps, PluginViewProps, PluginStorage, PluginStorageScope, PluginViewKey, ProjectsAPI, ShellAPI, ShellResizeRequest, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, StartTaskImplementationRequest, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TasksAPI, ValidationError, } from './types';
5
- export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingEventListenerContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingViewContribution, } from './testing';
5
+ export { TaskFollowUpError, MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
6
+ export type { AgentCommandDescriptor, AgentCommandMetadata, AgentCommandRuntime, AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ComposeTaskRequest, ComposeTaskResult, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, FileSystemAPI, ImplementationRun, InjectionPointLocation, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, OpenForgePluginCapability, OpenForgePluginContext, OpenForgePluginPackageJson, PluginCommandInvocationContext, PluginCommandInvocationSource, PluginComponentLoader, PluginComponentModule, PluginEntry, PluginIcon, PluginInjectionPointProps, PluginSettingsSectionProps, PluginState, PluginSvgIcon, PluginTaskPaneProps, PluginTaskUISectionProps, PluginViewProps, PluginStorage, PluginStorageScope, PluginViewKey, ProjectsAPI, ShellAPI, ShellResizeRequest, TaskStartPrefixContext, TaskStartPrefixProviderRegistration, TerminalImageProtocol, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, SendTaskFollowUpRequest, StartTaskImplementationRequest, TaskFollowUpDisposition, TaskFollowUpErrorCode, TaskFollowUpReceipt, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TaskLinkHandler, TaskLinkHandlerResult, TaskLinkOpenRequest, TaskLinksAPI, TasksAPI, ValidationError, } from './types';
7
+ export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingEventListenerContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingTaskUISectionContribution, TestingViewContribution, } from './testing';
6
8
  export { parseStrictFiniteNumber } from './numberParsing';
7
9
  export { buildProjectFileTree, flattenVisibleProjectFileTree, formatProjectFileTreeSize, getProjectFileTreeDepth, getProjectFileTreeItemAccessibility, getProjectFileTreeKeyboardAction, getProjectFileTreeParentPath, hasProjectFileTreeShortcutModifier, projectFileTreePathToId, } from './projectFileTree';
8
10
  export type { ProjectFileTreeEntry, ProjectFileTreeItemAccessibility, ProjectFileTreeKeyboardAction, ProjectFileTreeNode, } from './projectFileTree';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
+ export { BrowserSurfaceError, isAllowedBrowserSurfaceUrl } from './browserSurfaces';
1
2
  export { OPENFORGE_PACKAGE_METADATA_SCHEMA, OPENFORGE_PLUGIN_CAPABILITIES, isOpenForgePackageMetadata, isPluginPackageMetadata, isSupportedOpenForgeApiVersion, validateOpenForgePackageMetadata, validatePluginPackageMetadata, } from './manifest';
2
3
  export { createMemoryPluginStorage, createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, createTestingCalls, TestingOpenForgeRegistryFake, TestingSubscriptionSink, } from './testing';
3
- export { MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
4
+ export { TaskFollowUpError, MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
4
5
  export { parseStrictFiniteNumber } from './numberParsing';
5
6
  export { buildProjectFileTree, flattenVisibleProjectFileTree, formatProjectFileTreeSize, getProjectFileTreeDepth, getProjectFileTreeItemAccessibility, getProjectFileTreeKeyboardAction, getProjectFileTreeParentPath, hasProjectFileTreeShortcutModifier, projectFileTreePathToId, } from './projectFileTree';
6
7
  export { canMergePullRequest, getMergeReadiness, hasMergeConflicts, isClosedUnmergedPullRequest, isMergedPullRequest, isQueuedForMerge, isReadyToMerge, parseCheckRuns, preservePullRequestState, splitCheckRuns, } from './domain';
@@ -8,6 +8,24 @@ export declare const OPENFORGE_PACKAGE_METADATA_SCHEMA: {
8
8
  type: string;
9
9
  additionalProperties: boolean;
10
10
  required: string[];
11
+ dependentRequired: {
12
+ frontendStyles: string[];
13
+ };
14
+ allOf: {
15
+ if: {
16
+ properties: {
17
+ requires: {
18
+ contains: {
19
+ const: string;
20
+ };
21
+ };
22
+ };
23
+ required: string[];
24
+ };
25
+ then: {
26
+ required: string[];
27
+ };
28
+ }[];
11
29
  properties: {
12
30
  id: {
13
31
  type: string;
@@ -36,6 +54,17 @@ export declare const OPENFORGE_PACKAGE_METADATA_SCHEMA: {
36
54
  minLength: number;
37
55
  description: string;
38
56
  };
57
+ frontendStyles: {
58
+ type: string;
59
+ minItems: number;
60
+ uniqueItems: boolean;
61
+ items: {
62
+ type: string;
63
+ minLength: number;
64
+ pattern: string;
65
+ };
66
+ description: string;
67
+ };
39
68
  backend: {
40
69
  type: string;
41
70
  minLength: number;
package/dist/manifest.js CHANGED
@@ -27,6 +27,33 @@ function validateOptionalString(value, path) {
27
27
  }
28
28
  return [];
29
29
  }
30
+ function validateFrontendStyles(value) {
31
+ if (value === undefined)
32
+ return [];
33
+ if (!Array.isArray(value)) {
34
+ return [{ path: 'frontendStyles', message: 'Must be an array' }];
35
+ }
36
+ const errors = [];
37
+ if (value.length === 0) {
38
+ errors.push({ path: 'frontendStyles', message: 'Must contain at least one stylesheet path' });
39
+ }
40
+ const seen = new Set();
41
+ value.forEach((item, index) => {
42
+ if (!isNonEmptyString(item)) {
43
+ errors.push({ path: `frontendStyles[${index}]`, message: 'Must be a non-empty string' });
44
+ }
45
+ else if (!item.endsWith('.css')) {
46
+ errors.push({ path: `frontendStyles[${index}]`, message: 'Must point to a built CSS artifact' });
47
+ }
48
+ else if (seen.has(item)) {
49
+ errors.push({ path: `frontendStyles[${index}]`, message: 'Duplicate stylesheet path' });
50
+ }
51
+ else {
52
+ seen.add(item);
53
+ }
54
+ });
55
+ return errors;
56
+ }
30
57
  export function isSupportedOpenForgeApiVersion(apiVersion) {
31
58
  return typeof apiVersion === 'number'
32
59
  && Number.isInteger(apiVersion)
@@ -72,8 +99,18 @@ export function validateOpenForgePackageMetadata(data) {
72
99
  errors.push(...validateRequiredString(data.description, 'description'));
73
100
  errors.push(...validateOptionalString(data.icon, 'icon'));
74
101
  errors.push(...validateOptionalString(data.frontend, 'frontend'));
102
+ errors.push(...validateFrontendStyles(data.frontendStyles));
103
+ if (data.frontendStyles !== undefined && !isNonEmptyString(data.frontend)) {
104
+ errors.push({ path: 'frontendStyles', message: 'Requires a frontend entry' });
105
+ }
75
106
  errors.push(...validateOptionalString(data.backend, 'backend'));
76
107
  errors.push(...validateRequires(data.requires));
108
+ if (Array.isArray(data.requires) && data.requires.includes('browserSurfaces') && !isNonEmptyString(data.frontend)) {
109
+ errors.push({ path: 'requires', message: 'browserSurfaces capability requires a frontend entry' });
110
+ }
111
+ if (Array.isArray(data.requires) && data.requires.includes('taskLinks') && !isNonEmptyString(data.frontend)) {
112
+ errors.push({ path: 'requires', message: 'taskLinks capability requires a frontend entry' });
113
+ }
77
114
  if (data.contributes !== undefined) {
78
115
  errors.push({ path: 'contributes', message: 'Manifest contribution arrays are not supported; register contributions at runtime' });
79
116
  }
@@ -1,5 +1,9 @@
1
1
  export interface RenderMarkdownOptions {
2
2
  imageBaseUrl?: string | null;
3
+ markdownFilePath?: string | null;
4
+ deferRepositoryImages?: boolean;
3
5
  }
4
- export declare function resolveMarkdownImageSrc(src: string | null, imageBaseUrl: string | null | undefined): string | null;
6
+ export declare function resolveMarkdownRepositoryPath(value: string | null, markdownFilePath: string): string | null;
7
+ export declare function getMarkdownRepositoryLinkSuffix(value: string): string;
8
+ export declare function resolveMarkdownImageSrc(src: string | null, imageBaseUrl: string | null | undefined, markdownFilePath?: string): string | null;
5
9
  export declare function renderMarkdownHtml(content: string, options?: RenderMarkdownOptions): string;
package/dist/markdown.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { marked } from 'marked';
2
2
  import { sanitizeHtml } from './sanitize';
3
- const RELATIVE_PARENT_SEGMENT = /^\.\.\//;
4
- const RELATIVE_CURRENT_SEGMENT = /^\.\//;
5
3
  const markedOptions = {
6
4
  gfm: true,
7
5
  breaks: true,
@@ -9,39 +7,90 @@ const markedOptions = {
9
7
  function hasAbsoluteOrSpecialUrl(value) {
10
8
  return /^[a-z][a-z\d+.-]*:/i.test(value) || value.startsWith('//') || value.startsWith('#');
11
9
  }
12
- function normalizeRepoRelativeImagePath(value) {
13
- let normalized = value.startsWith('/') ? value.slice(1) : value;
14
- while (RELATIVE_CURRENT_SEGMENT.test(normalized)) {
15
- normalized = normalized.replace(RELATIVE_CURRENT_SEGMENT, '');
10
+ function decodeMarkdownPath(value) {
11
+ try {
12
+ return decodeURI(value);
16
13
  }
17
- while (RELATIVE_PARENT_SEGMENT.test(normalized)) {
18
- normalized = normalized.replace(RELATIVE_PARENT_SEGMENT, '');
14
+ catch {
15
+ return value;
19
16
  }
20
- return normalized;
17
+ }
18
+ function splitMarkdownRepositoryReference(value) {
19
+ const suffixIndex = value.search(/[?#]/);
20
+ return suffixIndex < 0
21
+ ? { pathValue: value, suffix: '' }
22
+ : { pathValue: value.slice(0, suffixIndex), suffix: value.slice(suffixIndex) };
23
+ }
24
+ export function resolveMarkdownRepositoryPath(value, markdownFilePath) {
25
+ if (!value)
26
+ return null;
27
+ const trimmedValue = value.trim();
28
+ if (!trimmedValue || hasAbsoluteOrSpecialUrl(trimmedValue))
29
+ return null;
30
+ const { pathValue } = splitMarkdownRepositoryReference(trimmedValue);
31
+ if (!pathValue)
32
+ return null;
33
+ const repositoryPath = decodeMarkdownPath(pathValue);
34
+ if (repositoryPath.includes('\\'))
35
+ return null;
36
+ const markdownDirectory = markdownFilePath.split('/').slice(0, -1).join('/');
37
+ const candidate = repositoryPath.startsWith('/')
38
+ ? repositoryPath.slice(1)
39
+ : [markdownDirectory, repositoryPath].filter(Boolean).join('/');
40
+ const parts = [];
41
+ for (const segment of candidate.split('/')) {
42
+ if (!segment || segment === '.')
43
+ continue;
44
+ if (segment === '..') {
45
+ if (parts.length === 0)
46
+ return null;
47
+ parts.pop();
48
+ }
49
+ else {
50
+ parts.push(segment);
51
+ }
52
+ }
53
+ return parts.length > 0 ? parts.join('/') : null;
54
+ }
55
+ export function getMarkdownRepositoryLinkSuffix(value) {
56
+ return splitMarkdownRepositoryReference(value.trim()).suffix;
21
57
  }
22
58
  function withTrailingSlash(value) {
23
59
  return value.endsWith('/') ? value : `${value}/`;
24
60
  }
25
- export function resolveMarkdownImageSrc(src, imageBaseUrl) {
26
- if (!src || !imageBaseUrl)
61
+ export function resolveMarkdownImageSrc(src, imageBaseUrl, markdownFilePath = '') {
62
+ if (!imageBaseUrl)
27
63
  return null;
28
- const trimmedSrc = src.trim();
29
- if (!trimmedSrc || hasAbsoluteOrSpecialUrl(trimmedSrc))
64
+ const repositoryPath = resolveMarkdownRepositoryPath(src, markdownFilePath);
65
+ if (!repositoryPath)
30
66
  return null;
31
67
  try {
32
- return new URL(normalizeRepoRelativeImagePath(trimmedSrc), withTrailingSlash(imageBaseUrl)).href;
68
+ return new URL(repositoryPath, withTrailingSlash(imageBaseUrl)).href;
33
69
  }
34
70
  catch {
35
71
  return null;
36
72
  }
37
73
  }
38
- function resolveMarkdownImageSources(html, imageBaseUrl) {
39
- if (!imageBaseUrl || typeof document === 'undefined')
74
+ function prepareMarkdownImageSources(html, options) {
75
+ if (typeof document === 'undefined' || (!options.imageBaseUrl && !options.deferRepositoryImages))
40
76
  return html;
41
77
  const template = document.createElement('template');
42
78
  template.innerHTML = html;
43
79
  for (const image of template.content.querySelectorAll('img[src]')) {
44
- const resolvedSrc = resolveMarkdownImageSrc(image.getAttribute('src'), imageBaseUrl);
80
+ const source = image.getAttribute('src')?.trim() ?? '';
81
+ const shouldDefer = options.deferRepositoryImages && source.length > 0 && !hasAbsoluteOrSpecialUrl(source);
82
+ if (shouldDefer) {
83
+ image.removeAttribute('src');
84
+ const repositoryPath = resolveMarkdownRepositoryPath(source, options.markdownFilePath ?? '');
85
+ if (repositoryPath) {
86
+ image.setAttribute('data-markdown-repository-path', repositoryPath);
87
+ }
88
+ continue;
89
+ }
90
+ const repositoryPath = resolveMarkdownRepositoryPath(source, options.markdownFilePath ?? '');
91
+ if (!repositoryPath)
92
+ continue;
93
+ const resolvedSrc = resolveMarkdownImageSrc(repositoryPath, options.imageBaseUrl);
45
94
  if (resolvedSrc) {
46
95
  image.setAttribute('src', resolvedSrc);
47
96
  }
@@ -50,5 +99,5 @@ function resolveMarkdownImageSources(html, imageBaseUrl) {
50
99
  }
51
100
  export function renderMarkdownHtml(content, options = {}) {
52
101
  const rawHtml = marked.parse(content, markedOptions);
53
- return sanitizeHtml(resolveMarkdownImageSources(rawHtml, options.imageBaseUrl));
102
+ return sanitizeHtml(prepareMarkdownImageSources(rawHtml, options));
54
103
  }
@@ -6,6 +6,29 @@
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["id", "apiVersion", "displayName", "description"],
9
+ "dependentRequired": {
10
+ "frontendStyles": ["frontend"]
11
+ },
12
+ "allOf": [
13
+ {
14
+ "if": {
15
+ "properties": {
16
+ "requires": { "contains": { "const": "browserSurfaces" } }
17
+ },
18
+ "required": ["requires"]
19
+ },
20
+ "then": { "required": ["frontend"] }
21
+ },
22
+ {
23
+ "if": {
24
+ "properties": {
25
+ "requires": { "contains": { "const": "taskLinks" } }
26
+ },
27
+ "required": ["requires"]
28
+ },
29
+ "then": { "required": ["frontend"] }
30
+ }
31
+ ],
9
32
  "properties": {
10
33
  "id": {
11
34
  "type": "string",
@@ -34,6 +57,17 @@
34
57
  "minLength": 1,
35
58
  "description": "Path to the built frontend JavaScript entry artifact."
36
59
  },
60
+ "frontendStyles": {
61
+ "type": "array",
62
+ "minItems": 1,
63
+ "uniqueItems": true,
64
+ "items": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "pattern": "\\.css$"
68
+ },
69
+ "description": "Paths to built frontend CSS artifacts. The renderer attaches them before importing the frontend entry and removes them when the plugin deactivates."
70
+ },
37
71
  "backend": {
38
72
  "type": "string",
39
73
  "minLength": 1,
@@ -47,7 +81,9 @@
47
81
  "commands",
48
82
  "events",
49
83
  "views",
84
+ "injectionPoints",
50
85
  "taskPane",
86
+ "taskStart",
51
87
  "settings",
52
88
  "background",
53
89
  "backend",
@@ -61,8 +97,11 @@
61
97
  "notifications",
62
98
  "attention",
63
99
  "system.openUrl",
100
+ "system.writeClipboardText",
64
101
  "config",
65
- "projectConfig"
102
+ "projectConfig",
103
+ "browserSurfaces",
104
+ "taskLinks"
66
105
  ]
67
106
  }
68
107
  }
@@ -0,0 +1,7 @@
1
+ import type { PluginIcon } from './types';
2
+ export declare const MAX_PLUGIN_SVG_ICON_CHARACTERS = 10000;
3
+ /**
4
+ * Validates and sanitizes a plugin icon before it crosses a host contribution boundary.
5
+ * Named icons are preserved so unsupported names can retain the host's existing fallback.
6
+ */
7
+ export declare function sanitizePluginIcon(icon: unknown): PluginIcon;
@@ -0,0 +1,104 @@
1
+ import DOMPurify from 'dompurify';
2
+ export const MAX_PLUGIN_SVG_ICON_CHARACTERS = 10_000;
3
+ const ALLOWED_SVG_TAGS = ['svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon'];
4
+ const ALLOWED_SVG_ATTRIBUTES = [
5
+ 'viewBox',
6
+ 'd',
7
+ 'fill',
8
+ 'fill-rule',
9
+ 'fill-opacity',
10
+ 'stroke',
11
+ 'stroke-width',
12
+ 'stroke-linecap',
13
+ 'stroke-linejoin',
14
+ 'stroke-miterlimit',
15
+ 'stroke-dasharray',
16
+ 'stroke-dashoffset',
17
+ 'stroke-opacity',
18
+ 'opacity',
19
+ 'transform',
20
+ 'x',
21
+ 'y',
22
+ 'x1',
23
+ 'y1',
24
+ 'x2',
25
+ 'y2',
26
+ 'cx',
27
+ 'cy',
28
+ 'r',
29
+ 'rx',
30
+ 'ry',
31
+ 'width',
32
+ 'height',
33
+ 'points',
34
+ ];
35
+ const SAFE_PAINT_VALUE = /^(?:none|currentColor|transparent|#[0-9a-f]{3,8}|[a-z]+|(?:rgb|rgba|hsl|hsla)\([\d\s.,%+\-/]+\))$/iu;
36
+ function isPluginSvgIcon(value) {
37
+ return value !== null
38
+ && typeof value === 'object'
39
+ && value.type === 'svg'
40
+ && typeof value.svg === 'string';
41
+ }
42
+ function parseSvgRoot(markup) {
43
+ const template = document.createElement('template');
44
+ template.innerHTML = markup.trim();
45
+ const children = template.content.children;
46
+ const root = children.item(0);
47
+ if (children.length !== 1 || !(root instanceof SVGSVGElement)) {
48
+ throw new TypeError('Custom plugin icons require exactly one <svg> root');
49
+ }
50
+ return root;
51
+ }
52
+ function hasValidViewBox(root) {
53
+ const values = root.getAttribute('viewBox')?.trim().split(/[\s,]+/u).map(Number) ?? [];
54
+ return values.length === 4
55
+ && values.every(Number.isFinite)
56
+ && (values[2] ?? 0) > 0
57
+ && (values[3] ?? 0) > 0;
58
+ }
59
+ function removeUnsafePaint(root) {
60
+ for (const element of [root, ...root.querySelectorAll('*')]) {
61
+ for (const attribute of ['fill', 'stroke']) {
62
+ const value = element.getAttribute(attribute);
63
+ if (value !== null && !SAFE_PAINT_VALUE.test(value.trim())) {
64
+ element.removeAttribute(attribute);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Validates and sanitizes a plugin icon before it crosses a host contribution boundary.
71
+ * Named icons are preserved so unsupported names can retain the host's existing fallback.
72
+ */
73
+ export function sanitizePluginIcon(icon) {
74
+ if (typeof icon === 'string') {
75
+ if (icon.trim().length === 0) {
76
+ throw new TypeError('Plugin icon names must be non-empty');
77
+ }
78
+ return icon;
79
+ }
80
+ if (!isPluginSvgIcon(icon) || icon.svg.trim().length === 0) {
81
+ throw new TypeError('Plugin icons must be a non-empty name or { type: "svg", svg }');
82
+ }
83
+ if (icon.svg.length > MAX_PLUGIN_SVG_ICON_CHARACTERS) {
84
+ throw new TypeError(`Custom plugin icon SVG must not exceed ${MAX_PLUGIN_SVG_ICON_CHARACTERS.toLocaleString('en-US')} characters`);
85
+ }
86
+ const sanitizedMarkup = DOMPurify.sanitize(icon.svg, {
87
+ ALLOWED_TAGS: ALLOWED_SVG_TAGS,
88
+ ALLOWED_ATTR: ALLOWED_SVG_ATTRIBUTES,
89
+ ALLOW_ARIA_ATTR: false,
90
+ ALLOW_DATA_ATTR: false,
91
+ KEEP_CONTENT: false,
92
+ });
93
+ const root = parseSvgRoot(sanitizedMarkup);
94
+ if (!hasValidViewBox(root)) {
95
+ throw new TypeError('Custom plugin icons require a viewBox with positive width and height');
96
+ }
97
+ if (!root.querySelector('path, rect, circle, ellipse, line, polyline, polygon')) {
98
+ throw new TypeError('Custom plugin icons require visible geometry');
99
+ }
100
+ root.removeAttribute('width');
101
+ root.removeAttribute('height');
102
+ removeUnsafePaint(root);
103
+ return { type: 'svg', svg: root.outerHTML };
104
+ }
@@ -0,0 +1,16 @@
1
+ export type OpenForgePluginSdkPublicUiExport = Readonly<{
2
+ componentName: string
3
+ packageSubpath: `./ui/${string}.svelte`
4
+ importSpecifier: `@openforge-app/plugin-sdk/ui/${string}.svelte`
5
+ sourcePath: `src/ui/${string}.svelte`
6
+ workspaceSourcePath: `packages/plugin-sdk/src/ui/${string}.svelte`
7
+ distPath: `./dist/ui/${string}.svelte`
8
+ }>
9
+
10
+ export const OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS: readonly OpenForgePluginSdkPublicUiExport[]
11
+
12
+ export function createOpenForgePluginSdkPublicUiPackageExports(): Record<string, string>
13
+
14
+ export function assertOpenForgePluginSdkPublicUiPackageExports(
15
+ packageExports: unknown,
16
+ ): asserts packageExports is Record<string, unknown>
@@ -0,0 +1,59 @@
1
+ const PLUGIN_SDK_PACKAGE_NAME = '@openforge-app/plugin-sdk'
2
+
3
+ const PUBLIC_UI_COMPONENT_NAMES = Object.freeze([
4
+ 'Button',
5
+ 'Checkbox',
6
+ 'MarkdownContent',
7
+ 'ResizablePanel',
8
+ 'Modal',
9
+ 'PluginPageHeader',
10
+ 'PluginViewState',
11
+ 'FileTypeIcon',
12
+ ])
13
+
14
+ /**
15
+ * Canonical registrations for public Svelte components shipped by the plugin SDK.
16
+ * Package exports are validated against this list; asset copying, source aliases,
17
+ * root Vite aliases, and import-boundary fallbacks are derived from it.
18
+ */
19
+ export const OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS = Object.freeze(
20
+ PUBLIC_UI_COMPONENT_NAMES.map((componentName) => Object.freeze({
21
+ componentName,
22
+ packageSubpath: `./ui/${componentName}.svelte`,
23
+ importSpecifier: `${PLUGIN_SDK_PACKAGE_NAME}/ui/${componentName}.svelte`,
24
+ sourcePath: `src/ui/${componentName}.svelte`,
25
+ workspaceSourcePath: `packages/plugin-sdk/src/ui/${componentName}.svelte`,
26
+ distPath: `./dist/ui/${componentName}.svelte`,
27
+ })),
28
+ )
29
+
30
+ export function createOpenForgePluginSdkPublicUiPackageExports() {
31
+ return Object.fromEntries(
32
+ OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS.map(({ packageSubpath, distPath }) => [packageSubpath, distPath]),
33
+ )
34
+ }
35
+
36
+ export function assertOpenForgePluginSdkPublicUiPackageExports(packageExports) {
37
+ if (!packageExports || typeof packageExports !== 'object' || Array.isArray(packageExports)) {
38
+ throw new Error('Plugin SDK package.json must define an exports object')
39
+ }
40
+
41
+ const expected = createOpenForgePluginSdkPublicUiPackageExports()
42
+ const actual = Object.fromEntries(
43
+ Object.entries(packageExports).filter(([subpath]) => subpath.startsWith('./ui/')),
44
+ )
45
+ const expectedEntries = Object.entries(expected)
46
+ const missingOrMismatched = expectedEntries
47
+ .filter(([subpath, distPath]) => actual[subpath] !== distPath)
48
+ .map(([subpath, distPath]) => `${subpath} -> ${distPath}`)
49
+ const unexpected = Object.keys(actual).filter((subpath) => !(subpath in expected))
50
+
51
+ if (missingOrMismatched.length === 0 && unexpected.length === 0) return
52
+
53
+ const details = [
54
+ missingOrMismatched.length > 0 ? `missing or mismatched: ${missingOrMismatched.join(', ')}` : null,
55
+ unexpected.length > 0 ? `not in the canonical manifest: ${unexpected.join(', ')}` : null,
56
+ ].filter(Boolean)
57
+
58
+ throw new Error(`Plugin SDK public UI exports drifted from the canonical manifest (${details.join('; ')})`)
59
+ }
@@ -6,6 +6,7 @@ export type SvelteHostRuntimeModule = Readonly<{
6
6
 
7
7
  export const SVELTE_HOST_RUNTIME_MODULES: readonly SvelteHostRuntimeModule[]
8
8
  export const OPENFORGE_HOST_RUNTIME_SVELTE_SPECIFIERS: readonly string[]
9
+ export function isOpenForgeHostRuntimeSvelteExternal(id: string): boolean
9
10
  export const SVELTE_HOST_RUNTIME_IMPORTS: Readonly<Record<string, string>>
10
11
  export function svelteHostRuntimeImportUrl(specifier: string): string | null
11
12
  export function svelteHostRuntimeBuildEntries(): Record<string, string>