@volter/editor-sdk 0.5.57

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 (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +19 -0
  4. package/package.json +90 -0
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +83 -0
  7. package/src/client.ts +1547 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +985 -0
  10. package/src/document-probe.ts +237 -0
  11. package/src/editor-view.ts +220 -0
  12. package/src/extension.ts +40 -0
  13. package/src/generations.ts +178 -0
  14. package/src/host.ts +1167 -0
  15. package/src/http-transport.browser.ts +14 -0
  16. package/src/http-transport.node.ts +19 -0
  17. package/src/index.ts +128 -0
  18. package/src/layout-arrangements.ts +5 -0
  19. package/src/layouts.tsx +108 -0
  20. package/src/looks.ts +14 -0
  21. package/src/project/output-roots.ts +73 -0
  22. package/src/project/tab-census.ts +149 -0
  23. package/src/project-tool-catalog.ts +96 -0
  24. package/src/selection.tsx +108 -0
  25. package/src/services.ts +18 -0
  26. package/src/session/build-report.ts +19 -0
  27. package/src/session/collaboration-types.ts +262 -0
  28. package/src/session/command-table.ts +333 -0
  29. package/src/session/discovery.ts +90 -0
  30. package/src/session/editor-brand.ts +73 -0
  31. package/src/session/editor-compatibility.ts +248 -0
  32. package/src/session/editor-control-lifecycle.ts +68 -0
  33. package/src/session/editor-control-protocol.ts +5 -0
  34. package/src/session/entrypoint-selection-readers.ts +66 -0
  35. package/src/session/entrypoint-selection-source.ts +120 -0
  36. package/src/session/game-css-scope.ts +30 -0
  37. package/src/session/product-create.ts +24 -0
  38. package/src/session/product-locator.ts +389 -0
  39. package/src/session/project-module-url.ts +245 -0
  40. package/src/session/registry-format.ts +203 -0
  41. package/src/session/relative-path-guard.ts +56 -0
  42. package/src/session/source-glob.ts +15 -0
  43. package/src/session/tool-contribution-convention.ts +116 -0
  44. package/src/session/workbench-locator.ts +650 -0
  45. package/src/session.ts +41 -0
  46. package/src/share.ts +160 -0
  47. package/src/tools/errors.ts +91 -0
  48. package/src/tools/provider-execution.ts +70 -0
  49. package/src/tools/registry.ts +341 -0
  50. package/src/tools/types.ts +159 -0
  51. package/src/transport.ts +97 -0
  52. package/src/types.ts +1581 -0
  53. package/src/views.ts +164 -0
  54. package/src/widgets/design-system.ts +93 -0
  55. package/src/widgets/editor-appearance.ts +149 -0
  56. package/src/widgets/editor-material.ts +83 -0
  57. package/src/widgets/icon-set-registry.ts +105 -0
  58. package/src/widgets/index.ts +71 -0
  59. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  60. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  61. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  62. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  63. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  64. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  65. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  66. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  67. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  68. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  69. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  70. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  71. package/src/widgets/inspector-widgets/index.ts +24 -0
  72. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  73. package/src/widgets/interactive-edit-scope.ts +33 -0
  74. package/src/widgets/patterns/Dialog.tsx +129 -0
  75. package/src/widgets/patterns/Fields.tsx +44 -0
  76. package/src/widgets/patterns/List.tsx +25 -0
  77. package/src/widgets/patterns/StateSurface.tsx +40 -0
  78. package/src/widgets/patterns/Surfaces.tsx +122 -0
  79. package/src/widgets/patterns/Tabs.tsx +80 -0
  80. package/src/widgets/patterns/Toolbar.tsx +72 -0
  81. package/src/widgets/patterns/Tree.tsx +72 -0
  82. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  83. package/src/widgets/primitives/Button.tsx +62 -0
  84. package/src/widgets/primitives/ColorInput.tsx +78 -0
  85. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  86. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  87. package/src/widgets/primitives/FormControls.tsx +88 -0
  88. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  89. package/src/widgets/primitives/JsonInput.tsx +113 -0
  90. package/src/widgets/primitives/Layout.tsx +100 -0
  91. package/src/widgets/primitives/Menu.tsx +140 -0
  92. package/src/widgets/primitives/NumberInput.tsx +169 -0
  93. package/src/widgets/primitives/Panel.tsx +80 -0
  94. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  95. package/src/widgets/primitives/Text.tsx +54 -0
  96. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  97. package/src/widgets/primitives/Tooltip.tsx +204 -0
  98. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  99. package/src/widgets/primitives/banner-tones.ts +32 -0
  100. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  101. package/src/widgets/primitives/editor-icons.ts +245 -0
  102. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  103. package/src/widgets/theme.ts +2633 -0
  104. package/src/widgets/z-index.ts +25 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `~/.vgai/editor-sessions.json` — the ONE spelling of the editor-session
3
+ * registry contract, and the shared machinery every reader was hand-copying.
4
+ *
5
+ * The registry's WRITE half stays with its owner
6
+ * (`packages/editor/server/session-registry.ts`); this module owns the
7
+ * FORMAT: the entry shape, the shape guards, the file path, the
8
+ * liveness-filtered read, the `/__editor/project` answer parser, and the
9
+ * pending-launch coordination. It existed as FOUR drifting copies
10
+ * (server registry, the CLI, and both vgai-sdk transports) held together by
11
+ * a "the CLI has no editor dependency" premise that had stopped being true —
12
+ * and the drift was already real: one copy's `/__editor/project` parser
13
+ * dropped `manifestError`, reporting a degraded session as belonging to no
14
+ * project.
15
+ */
16
+
17
+ import {
18
+ mkdirSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ realpathSync,
22
+ unlinkSync,
23
+ writeFileSync,
24
+ } from 'node:fs';
25
+ import { homedir } from 'node:os';
26
+ import { join, resolve } from 'node:path';
27
+ import { setTimeout as delay } from 'node:timers/promises';
28
+
29
+ /** One registry entry, exactly as the server's write half records it. */
30
+ export interface EditorSessionEntry {
31
+ /** Canonical project root currently open, or null when none. */
32
+ project: string | null;
33
+ port: number;
34
+ /** PID of the dev-server process. */
35
+ pid: number;
36
+ /** ISO timestamp of server start. */
37
+ startedAt: string;
38
+ sessionId: string | null;
39
+ /** Private loopback control credential — never returned by an editor HTTP
40
+ * route; exists only in this user-private registry. */
41
+ controlSecret?: string | null;
42
+ repositoryId: string | null;
43
+ worktreeId: string | null;
44
+ worktreeRoot: string | null;
45
+ projectRelativePath: string | null;
46
+ branch: string | null;
47
+ headCommit: string | null;
48
+ baseCommit: string | null;
49
+ }
50
+
51
+ export const EDITOR_SESSIONS_REGISTRY_FILE = join(homedir(), '.vgai', 'editor-sessions.json');
52
+
53
+ export function isEditorSessionEntry(v: unknown): v is EditorSessionEntry {
54
+ if (typeof v !== 'object' || v === null) return false;
55
+ const s = v as Record<string, unknown>;
56
+ const optionalIdentity = (value: unknown) =>
57
+ value === undefined || value === null || typeof value === 'string';
58
+ return (
59
+ (typeof s['project'] === 'string' || s['project'] === null) &&
60
+ typeof s['port'] === 'number' &&
61
+ typeof s['pid'] === 'number' &&
62
+ typeof s['startedAt'] === 'string' &&
63
+ optionalIdentity(s['sessionId']) &&
64
+ optionalIdentity(s['controlSecret']) &&
65
+ optionalIdentity(s['repositoryId']) &&
66
+ optionalIdentity(s['worktreeId']) &&
67
+ optionalIdentity(s['worktreeRoot']) &&
68
+ optionalIdentity(s['projectRelativePath']) &&
69
+ optionalIdentity(s['branch']) &&
70
+ optionalIdentity(s['headCommit']) &&
71
+ optionalIdentity(s['baseCommit'])
72
+ );
73
+ }
74
+
75
+ export function normalizeEditorSessionEntry(session: EditorSessionEntry): EditorSessionEntry {
76
+ return {
77
+ ...session,
78
+ sessionId: session.sessionId ?? null,
79
+ controlSecret: session.controlSecret ?? null,
80
+ repositoryId: session.repositoryId ?? null,
81
+ worktreeId: session.worktreeId ?? null,
82
+ worktreeRoot: session.worktreeRoot ?? null,
83
+ projectRelativePath: session.projectRelativePath ?? null,
84
+ branch: session.branch ?? null,
85
+ headCommit: session.headCommit ?? null,
86
+ baseCommit: session.baseCommit ?? null,
87
+ };
88
+ }
89
+
90
+ export function pidAlive(pid: number): boolean {
91
+ try {
92
+ process.kill(pid, 0);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /** The registry's live entries — shape-validated and PID-liveness-filtered. */
100
+ export function readLiveRegisteredSessions(): EditorSessionEntry[] {
101
+ try {
102
+ const raw: unknown = JSON.parse(readFileSync(EDITOR_SESSIONS_REGISTRY_FILE, 'utf8'));
103
+ return Array.isArray(raw)
104
+ ? raw
105
+ .filter(isEditorSessionEntry)
106
+ .map(normalizeEditorSessionEntry)
107
+ .filter((s) => pidAlive(s.pid))
108
+ : [];
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ /** Announce a launch before the server can register. Each launcher owns its file. */
115
+ export function announceEditorLaunch(project: string): () => void {
116
+ const directory = join(project, '.vgai', 'editor-launches');
117
+ mkdirSync(directory, { recursive: true });
118
+ const file = join(directory, `${process.pid}.json`);
119
+ writeFileSync(file, JSON.stringify({ pid: process.pid }));
120
+ const clear = () => {
121
+ process.removeListener('exit', clear);
122
+ try {
123
+ unlinkSync(file);
124
+ } catch {
125
+ // Already cleared when the server became ready.
126
+ }
127
+ };
128
+ process.once('exit', clear);
129
+ return clear;
130
+ }
131
+
132
+ function hasPendingEditorLaunch(project: string): boolean {
133
+ const directory = join(project, '.vgai', 'editor-launches');
134
+ try {
135
+ return readdirSync(directory).some((name) => {
136
+ if (!/^\d+\.json$/.test(name)) return false;
137
+ const pid = Number(name.slice(0, -5));
138
+ return pid !== process.pid && pidAlive(pid);
139
+ });
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ /** Wait only for an observed live launcher, never for a missing editor. */
146
+ export async function waitForPendingEditorLaunch(project: string): Promise<void> {
147
+ const canon = canonicalizeProjectPath(project);
148
+ const ready = () =>
149
+ readLiveRegisteredSessions().some(
150
+ (session) => session.project !== null && canonicalizeProjectPath(session.project) === canon,
151
+ );
152
+ if (ready() || !hasPendingEditorLaunch(canon)) return;
153
+ const deadline = Date.now() + 180_000;
154
+ while (!ready()) {
155
+ if (!hasPendingEditorLaunch(canon)) {
156
+ // The launcher clears its announcement only after registration.
157
+ if (ready()) return;
158
+ throw new Error(`The editor launch for ${canon} ended before registering a session.`);
159
+ }
160
+ if (Date.now() >= deadline) {
161
+ throw new Error(
162
+ `The editor launch for ${canon} is still running but has not registered within 180s.`,
163
+ );
164
+ }
165
+ await delay(100);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * WHICH PROJECT a `/__editor/project` body says its server is serving.
171
+ *
172
+ * `serving` is that server's own statement of "I AM serving this project, I
173
+ * just cannot describe it" (its manifest is unparseable or fails strict
174
+ * validation) — added to the route precisely because a bare
175
+ * `{ project: null }` is indistinguishable from "no project open". Reading
176
+ * only `project.path` collapses the two, and the cost is that every
177
+ * project-matched command loses a live session the moment a save breaks its
178
+ * manifest, reporting it as belonging to no project rather than as this
179
+ * project's degraded session.
180
+ */
181
+ export function servedProjectAnswer(body: unknown): {
182
+ path: string | null;
183
+ manifestError: string | null;
184
+ } {
185
+ const b = body as {
186
+ project?: { path?: string } | null;
187
+ serving?: { path?: string; error?: string } | null;
188
+ };
189
+ return {
190
+ path: b.project?.path ?? b.serving?.path ?? null,
191
+ manifestError: b.project ? null : (b.serving?.error ?? null),
192
+ };
193
+ }
194
+
195
+ /** Canonical (realpathed when possible) absolute form of a project path. */
196
+ function canonicalizeProjectPath(p: string): string {
197
+ const absolute = resolve(p);
198
+ try {
199
+ return realpathSync(absolute);
200
+ } catch {
201
+ return absolute;
202
+ }
203
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * THE spelling of "this string is a project-relative path that cannot escape
3
+ * its root". One helper, because the codebase had four incompatible ones and
4
+ * the weakest of them was load-bearing on a write route.
5
+ *
6
+ * What each of the four missed, and why a single predicate is the fix:
7
+ *
8
+ * - `path.includes('..')` (substring) is both too weak and too strong: it
9
+ * rejects the innocent `my..file.glb` while accepting nothing extra — but
10
+ * its real defect is that it says NOTHING about an ABSOLUTE path. A route
11
+ * that then does `resolve(root, 'public', filePath)` gets `filePath` back
12
+ * verbatim when it is absolute (that is `resolve`'s contract), so the write
13
+ * destination was never inside `root` at all and no `..` ever appeared.
14
+ * - `rel.startsWith('..')` false-negatives on a real directory named `..bak`.
15
+ * - `path.split('/').includes('..')` misses `a\..\b` on a Windows-shaped
16
+ * input; `path.split(/[\\/]/)` is the form that covers both.
17
+ * - none of them rejected a NUL byte or a `C:`-rooted path.
18
+ *
19
+ * Isomorphic on purpose — no `node:path` — so the browser-side resolver
20
+ * (`binding-resolver.ts`'s `guardModulePath`) and the Node-side route handlers
21
+ * enforce the SAME rule instead of two hand-rolled approximations of it. Its
22
+ * Node-side partner is `server/server-utils.ts`'s `isPathInside`, which
23
+ * answers the other half of the question (does the RESOLVED absolute path
24
+ * still land under the root); a write boundary wants both, because this one
25
+ * cannot see symlinks and that one cannot see intent.
26
+ */
27
+
28
+ /** Why a candidate path is not a contained project-relative path. */
29
+ export type RelativePathRejection = 'empty' | 'nul' | 'absolute' | 'traversal';
30
+
31
+ /**
32
+ * `null` when `path` is a safe project-relative path, otherwise the reason it
33
+ * is not. Callers that want a bespoke error message (see `guardModulePath`)
34
+ * switch on the reason; callers that only gate switch on
35
+ * {@link isContainedRelativePath}.
36
+ */
37
+ export function relativePathRejection(path: string): RelativePathRejection | null {
38
+ if (!path || path.trim() === '') return 'empty';
39
+ if (path.includes('\0')) return 'nul';
40
+ // Leading separator (POSIX absolute, UNC) or a Windows drive root. This is
41
+ // the case a `..` scan structurally cannot see.
42
+ // The drive form is `C:/…` / `C:\…` specifically — a POSIX file literally
43
+ // named `C:notes.txt` is relative and stays legal.
44
+ if (path.startsWith('/') || path.startsWith('\\') || /^[a-zA-Z]:[\\/]/.test(path)) {
45
+ return 'absolute';
46
+ }
47
+ // Segment-wise, over BOTH separators — `..bak` is a legitimate directory
48
+ // name and must survive; `a\..\b` must not.
49
+ if (path.split(/[\\/]/).includes('..')) return 'traversal';
50
+ return null;
51
+ }
52
+
53
+ /** True when `path` is a non-empty, relative, traversal-free project path. */
54
+ export function isContainedRelativePath(path: string): boolean {
55
+ return relativePathRejection(path) === null;
56
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One glob → one anchored RegExp, for a finder selection's `include`
3
+ * (ARCHITECTURE-CORE §The project model): `**` matches any depth, `*` one
4
+ * segment. No imports, so the editor server and the browser share it.
5
+ */
6
+ export function globToRegExp(glob: string): RegExp {
7
+ const GLOBSTAR = '__VGAI_GLOBSTAR__';
8
+ const escaped = glob
9
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
10
+ .replace(/\*\*\//g, GLOBSTAR)
11
+ .replace(/\*/g, '[^/]*')
12
+ .split(GLOBSTAR)
13
+ .join('(?:.*/)?');
14
+ return new RegExp(`^${escaped}$`);
15
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The tool-contribution FILENAME CONVENTION — the one definition for every
3
+ * program that asks "is this file a contribution module?": the dev server's
4
+ * scanner and `src/`-watcher classifier (`server/server-utils.ts`,
5
+ * `server/project-tools.ts`), and the page-side readers that must reach the
6
+ * same verdict about the same files. Lives in `src/` and names no DOM so the
7
+ * server TypeScript program can import it — the same split
8
+ * `game-globals-prelude.ts` documents for the globals prelude.
9
+ */
10
+
11
+ /**
12
+ * The two editor-side folders of a project (ARCHITECTURE-CORE §The project
13
+ * model): `src/contributions/` — what the project adds to the editor
14
+ * (documents, inspectors, utilities, analytics, kinds, finders) — and
15
+ * `src/tools/` — the tools a person or an agent invokes (`*.tool.ts`).
16
+ * Neither is bundled at Play; both reload the editor, never the game.
17
+ */
18
+ export const CONTRIBUTIONS_DIR = 'src/contributions';
19
+ export const TOOLS_DIR = 'src/tools';
20
+ export const EDITOR_LANE_DIRS = [CONTRIBUTIONS_DIR, TOOLS_DIR] as const;
21
+
22
+ /** Whether a project-relative (or absolute, `/`-normalized) path sits in the editor's lane. */
23
+ export function isEditorLanePath(path: string): boolean {
24
+ const normalized = path.replaceAll('\\', '/');
25
+ return EDITOR_LANE_DIRS.some(
26
+ (dir) => normalized.startsWith(`${dir}/`) || normalized.includes(`/${dir}/`),
27
+ );
28
+ }
29
+
30
+ export const TOOL_CONTRIBUTION_SUFFIXES = [
31
+ '.document',
32
+ '.inspector',
33
+ '.asset-inspector',
34
+ '.result',
35
+ '.utility',
36
+ '.analytics',
37
+ // A STATUS-BAR item (`workspace-status-registry.ts`): compact, passive,
38
+ // registered under `tool:<id>` like a utility tab.
39
+ '.status',
40
+ // A configuration KIND contribution (ARCHITECTURE-CORE §The project model);
41
+ // read by the hosts that load the manifest, never mounted as UI.
42
+ '.kind',
43
+ // A FINDER contribution — registered into the finder registry by the host
44
+ // that resolves the document table (the browser), never mounted as UI.
45
+ '.finder',
46
+ // The LOOK points (`@volter/editor-sdk/looks`): a workspace layout, a keymap
47
+ // and a style bundle are DATA — registered by the host, mounted by no UI
48
+ // point. What makes "the Blender" a build is a package contributing these.
49
+ '.layout',
50
+ '.keymap',
51
+ '.style',
52
+ // A COMMAND table (`@volter/editor-sdk/commands`): session verbs a package
53
+ // answers, registered with the host's relay; data, no UI.
54
+ '.command',
55
+ // The CHROME points (`@volter/editor-sdk/chrome`): palette actions and
56
+ // application-menu items; data the host renders in its own chrome.
57
+ '.action',
58
+ '.menu',
59
+ // A HEADER item: a component in the project header's transport cluster.
60
+ '.header',
61
+ // A SERVICE (`@volter/editor-sdk/services`): code a package runs while its
62
+ // contributions are loaded — started after a pass, stopped before the next.
63
+ '.service',
64
+ ] as const;
65
+
66
+ const TOOL_CONTRIBUTION_EXTENSIONS = ['.tsx', '.jsx', '.ts'] as const;
67
+
68
+ export type ChromeContributionKind = 'action' | 'menu' | 'header';
69
+
70
+ /** Which chrome point a contribution file names, or null. */
71
+ export function chromeContributionKind(fileName: string): ChromeContributionKind | null {
72
+ for (const kind of ['action', 'menu', 'header'] as const)
73
+ if (TOOL_CONTRIBUTION_EXTENSIONS.some((extension) => fileName.endsWith(`.${kind}${extension}`)))
74
+ return kind;
75
+ return null;
76
+ }
77
+
78
+ export function isServiceContribution(fileName: string): boolean {
79
+ return TOOL_CONTRIBUTION_EXTENSIONS.some((extension) =>
80
+ fileName.endsWith(`.service${extension}`),
81
+ );
82
+ }
83
+
84
+ export function isCommandContribution(fileName: string): boolean {
85
+ return TOOL_CONTRIBUTION_EXTENSIONS.some((extension) =>
86
+ fileName.endsWith(`.command${extension}`),
87
+ );
88
+ }
89
+
90
+ export type LookContributionKind = 'layout' | 'keymap' | 'style';
91
+
92
+ /** Which look point a contribution file names, or null for a module of
93
+ * another kind. */
94
+ export function lookContributionKind(fileName: string): LookContributionKind | null {
95
+ for (const kind of ['layout', 'keymap', 'style'] as const)
96
+ if (TOOL_CONTRIBUTION_EXTENSIONS.some((extension) => fileName.endsWith(`.${kind}${extension}`)))
97
+ return kind;
98
+ return null;
99
+ }
100
+
101
+ /** A kind contribution: scanned with the rest, mounted by no UI point. */
102
+ /** A finder contribution: registered by the document-table host, mounted by no UI point. */
103
+ export function isFinderContribution(fileName: string): boolean {
104
+ return TOOL_CONTRIBUTION_EXTENSIONS.some((extension) => fileName.endsWith(`.finder${extension}`));
105
+ }
106
+
107
+ export function isConfigurationKindContribution(fileName: string): boolean {
108
+ return TOOL_CONTRIBUTION_EXTENSIONS.some((extension) => fileName.endsWith(`.kind${extension}`));
109
+ }
110
+
111
+ /** Whether one file name is a contribution module by the convention above. */
112
+ export function isToolContributionModule(fileName: string): boolean {
113
+ return TOOL_CONTRIBUTION_EXTENSIONS.some((extension) =>
114
+ TOOL_CONTRIBUTION_SUFFIXES.some((suffix) => fileName.endsWith(`${suffix}${extension}`)),
115
+ );
116
+ }