@workbench-kit/workbench-config 0.0.2-prototype.0.1.4 → 0.0.2-prototype.0.2.11

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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@workbench-kit/workbench-config",
3
- "version": "0.0.2-prototype.0.1.4",
3
+ "version": "0.0.2-prototype.0.2.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@workbench-kit/base": "0.0.2-prototype.0.1.4",
11
- "@workbench-kit/platform": "0.0.2-prototype.0.1.4"
10
+ "@workbench-kit/base": "0.0.2-prototype.0.2.11",
11
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.11"
12
12
  },
13
13
  "files": [
14
14
  "src",
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { assertKnownKeys, assertRecord } from './parse-helpers.js';
2
+ import { WorkbenchConfigValidationError } from './validation-error.js';
3
+
1
4
  export const WORKBENCH_KIT_WORKBENCH_CONFIG_VERSION = '0.0.0' as const;
2
5
 
3
6
  export const WORKBENCH_CONFIG_DIR = '.workbench' as const;
@@ -6,6 +9,7 @@ export type WorkbenchConfigFileName =
6
9
  | 'workspace.json'
7
10
  | 'settings.json'
8
11
  | 'keybindings.json'
12
+ | 'user-commands.json'
9
13
  | 'extensions.json'
10
14
  | 'extensions.lock.json'
11
15
  | 'layout.default.json'
@@ -16,8 +20,23 @@ export interface WorkbenchExtensionsConfig {
16
20
  recommendations: readonly string[];
17
21
  }
18
22
 
23
+ export interface WorkbenchExtensionsLockEntry {
24
+ readonly integrity?: string | undefined;
25
+ readonly version: string;
26
+ }
27
+
28
+ export interface WorkbenchExtensionsLock {
29
+ readonly extensions: Readonly<Record<string, WorkbenchExtensionsLockEntry>>;
30
+ readonly lockfileVersion: number;
31
+ }
32
+
19
33
  export interface WorkbenchLayoutConfig {
20
34
  readonly activityBar: {
35
+ readonly hiddenItemIds?: readonly string[];
36
+ readonly itemOrder?: readonly string[];
37
+ readonly visible: boolean;
38
+ };
39
+ readonly auxiliaryBar: {
21
40
  readonly visible: boolean;
22
41
  };
23
42
  readonly panel: {
@@ -25,12 +44,14 @@ export interface WorkbenchLayoutConfig {
25
44
  };
26
45
  readonly sideBar: {
27
46
  readonly activeViewContainer?: string;
47
+ readonly sizePercent?: number;
28
48
  readonly visible: boolean;
29
49
  };
30
50
  }
31
51
 
32
52
  export type WorkbenchLayoutConfigInput = Partial<{
33
53
  activityBar: Partial<WorkbenchLayoutConfig['activityBar']>;
54
+ auxiliaryBar: Partial<WorkbenchLayoutConfig['auxiliaryBar']>;
34
55
  panel: Partial<WorkbenchLayoutConfig['panel']>;
35
56
  sideBar: Partial<WorkbenchLayoutConfig['sideBar']>;
36
57
  }>;
@@ -39,6 +60,9 @@ export const DEFAULT_WORKBENCH_LAYOUT_CONFIG: WorkbenchLayoutConfig = {
39
60
  activityBar: {
40
61
  visible: true,
41
62
  },
63
+ auxiliaryBar: {
64
+ visible: false,
65
+ },
42
66
  panel: {
43
67
  visible: false,
44
68
  },
@@ -47,19 +71,14 @@ export const DEFAULT_WORKBENCH_LAYOUT_CONFIG: WorkbenchLayoutConfig = {
47
71
  },
48
72
  };
49
73
 
50
- export class WorkbenchConfigValidationError extends Error {
51
- constructor(message: string) {
52
- super(message);
53
- this.name = 'WorkbenchConfigValidationError';
54
- }
55
- }
74
+ export { WorkbenchConfigValidationError };
56
75
 
57
76
  export function parseWorkbenchExtensionsConfig(input: unknown): WorkbenchExtensionsConfig {
58
77
  const record = assertRecord(input, 'extensions config');
59
78
 
60
79
  return {
61
- enabled: readOptionalStringArray(record, 'enabled'),
62
- recommendations: readOptionalStringArray(record, 'recommendations'),
80
+ enabled: readOptionalStringArrayFromExtensionsConfig(record, 'enabled'),
81
+ recommendations: readOptionalStringArrayFromExtensionsConfig(record, 'recommendations'),
63
82
  };
64
83
  }
65
84
 
@@ -75,31 +94,102 @@ export function parseWorkbenchExtensionsConfigJson(jsonText: string): WorkbenchE
75
94
  }
76
95
  }
77
96
 
97
+ export function parseWorkbenchExtensionsLock(input: unknown): WorkbenchExtensionsLock {
98
+ const record = assertRecord(input, 'extensions lock');
99
+ const lockfileVersion = record.lockfileVersion;
100
+ if (
101
+ typeof lockfileVersion !== 'number' ||
102
+ !Number.isInteger(lockfileVersion) ||
103
+ lockfileVersion < 1
104
+ ) {
105
+ throw new WorkbenchConfigValidationError(
106
+ 'Expected extensions lock "lockfileVersion" to be a positive integer.',
107
+ );
108
+ }
109
+
110
+ const extensionsRecord = assertRecord(record.extensions ?? {}, 'extensions lock extensions');
111
+ const extensions: Record<string, WorkbenchExtensionsLockEntry> = {};
112
+ for (const [extensionId, rawEntry] of Object.entries(extensionsRecord)) {
113
+ const entry = assertRecord(rawEntry, `extensions lock entry "${extensionId}"`);
114
+ if (typeof entry.version !== 'string' || entry.version.trim().length === 0) {
115
+ throw new WorkbenchConfigValidationError(
116
+ `Expected extensions lock entry "${extensionId}" to include a non-empty version.`,
117
+ );
118
+ }
119
+ if (entry.integrity !== undefined && typeof entry.integrity !== 'string') {
120
+ throw new WorkbenchConfigValidationError(
121
+ `Expected extensions lock entry "${extensionId}" integrity to be a string.`,
122
+ );
123
+ }
124
+ extensions[extensionId] = {
125
+ integrity: typeof entry.integrity === 'string' ? entry.integrity : undefined,
126
+ version: entry.version.trim(),
127
+ };
128
+ }
129
+
130
+ return {
131
+ extensions,
132
+ lockfileVersion,
133
+ };
134
+ }
135
+
136
+ export function parseWorkbenchExtensionsLockJson(jsonText: string): WorkbenchExtensionsLock {
137
+ try {
138
+ return parseWorkbenchExtensionsLock(JSON.parse(jsonText) as unknown);
139
+ } catch (error) {
140
+ if (error instanceof WorkbenchConfigValidationError) {
141
+ throw error;
142
+ }
143
+
144
+ throw new WorkbenchConfigValidationError('Expected extensions lock to be valid JSON.');
145
+ }
146
+ }
147
+
78
148
  export function parseWorkbenchLayoutConfig(input: unknown): WorkbenchLayoutConfig {
79
149
  const record = assertRecord(input, 'layout config');
80
- assertKnownKeys(record, ['activityBar', 'panel', 'sideBar'], 'layout config');
150
+ assertKnownKeys(record, ['activityBar', 'auxiliaryBar', 'panel', 'sideBar'], 'layout config');
81
151
 
82
152
  const activityBar = readOptionalRecord(record, 'activityBar');
153
+ const auxiliaryBar = readOptionalRecord(record, 'auxiliaryBar');
83
154
  const panel = readOptionalRecord(record, 'panel');
84
155
  const sideBar = readOptionalRecord(record, 'sideBar');
85
156
 
86
- assertKnownKeys(activityBar, ['visible'], 'layout config activityBar');
157
+ assertKnownKeys(
158
+ activityBar,
159
+ ['hiddenItemIds', 'itemOrder', 'visible'],
160
+ 'layout config activityBar',
161
+ );
162
+ assertKnownKeys(auxiliaryBar, ['visible'], 'layout config auxiliaryBar');
87
163
  assertKnownKeys(panel, ['visible'], 'layout config panel');
88
- assertKnownKeys(sideBar, ['activeViewContainer', 'visible'], 'layout config sideBar');
164
+ assertKnownKeys(
165
+ sideBar,
166
+ ['activeViewContainer', 'sizePercent', 'visible'],
167
+ 'layout config sideBar',
168
+ );
89
169
 
90
170
  return {
91
171
  activityBar: {
172
+ hiddenItemIds: readOptionalStringArray(activityBar, 'hiddenItemIds'),
173
+ itemOrder: readOptionalStringArray(activityBar, 'itemOrder'),
92
174
  visible: readOptionalBoolean(
93
175
  activityBar,
94
176
  'visible',
95
177
  DEFAULT_WORKBENCH_LAYOUT_CONFIG.activityBar.visible,
96
178
  ),
97
179
  },
180
+ auxiliaryBar: {
181
+ visible: readOptionalBoolean(
182
+ auxiliaryBar,
183
+ 'visible',
184
+ DEFAULT_WORKBENCH_LAYOUT_CONFIG.auxiliaryBar.visible,
185
+ ),
186
+ },
98
187
  panel: {
99
188
  visible: readOptionalBoolean(panel, 'visible', DEFAULT_WORKBENCH_LAYOUT_CONFIG.panel.visible),
100
189
  },
101
190
  sideBar: {
102
191
  ...readOptionalLayoutId(sideBar, 'activeViewContainer'),
192
+ ...readOptionalSizePercent(sideBar, 'sizePercent'),
103
193
  visible: readOptionalBoolean(
104
194
  sideBar,
105
195
  'visible',
@@ -121,13 +211,37 @@ export function parseWorkbenchLayoutConfigJson(jsonText: string): WorkbenchLayou
121
211
  }
122
212
  }
123
213
 
124
- function assertRecord(value: unknown, label: string): Record<string, unknown> {
125
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
126
- throw new WorkbenchConfigValidationError(`Expected ${label} to be an object.`);
127
- }
128
-
129
- return value as Record<string, unknown>;
130
- }
214
+ export {
215
+ parseWorkbenchKeybindingsConfig,
216
+ parseWorkbenchKeybindingsConfigJson,
217
+ type WorkbenchKeybindingDefinition,
218
+ } from './keybindings-config.js';
219
+ export {
220
+ parseWorkbenchSettingsConfig,
221
+ parseWorkbenchSettingsConfigJson,
222
+ type WorkbenchSettingsConfig,
223
+ } from './settings-config.js';
224
+ export {
225
+ createEmptyPreferenceValuesByScope,
226
+ FUTURE_PREFERENCE_SCOPES,
227
+ isPreferenceScope,
228
+ mergePreferenceValuesByScope,
229
+ mergeScopedPreferences,
230
+ PREFERENCE_SCOPE_MERGE_ORDER,
231
+ type FuturePreferenceScope,
232
+ type PreferenceScope,
233
+ type PreferenceValuesByScope,
234
+ type ScopedPreferenceLayer,
235
+ } from './preference-scopes.js';
236
+ export {
237
+ parseWorkbenchUserCommandsConfig,
238
+ parseWorkbenchUserCommandsConfigJson,
239
+ type WorkbenchUserCommandAction,
240
+ type WorkbenchUserCommandDefinition,
241
+ type WorkbenchUserCommandExecuteAction,
242
+ type WorkbenchUserCommandSequenceAction,
243
+ type WorkbenchUserCommandsConfig,
244
+ } from './user-commands-config.js';
131
245
 
132
246
  function readOptionalRecord(record: Record<string, unknown>, key: string): Record<string, unknown> {
133
247
  const value = record[key];
@@ -143,6 +257,22 @@ function readOptionalRecord(record: Record<string, unknown>, key: string): Recor
143
257
  }
144
258
 
145
259
  function readOptionalStringArray(
260
+ record: Record<string, unknown>,
261
+ key: string,
262
+ ): readonly string[] | undefined {
263
+ const value = record[key];
264
+ if (value === undefined) {
265
+ return undefined;
266
+ }
267
+
268
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
269
+ throw new WorkbenchConfigValidationError(`Expected "${key}" to be an array of strings.`);
270
+ }
271
+
272
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))];
273
+ }
274
+
275
+ function readOptionalStringArrayFromExtensionsConfig(
146
276
  record: Record<string, unknown>,
147
277
  key: keyof WorkbenchExtensionsConfig,
148
278
  ): readonly string[] {
@@ -193,10 +323,24 @@ function readOptionalLayoutId(
193
323
  };
194
324
  }
195
325
 
196
- function assertKnownKeys(record: Record<string, unknown>, keys: readonly string[], label: string) {
197
- const knownKeys = new Set(keys);
198
- const unknownKeys = Object.keys(record).filter((key) => !knownKeys.has(key));
199
- if (unknownKeys.length > 0) {
200
- throw new WorkbenchConfigValidationError(`Unexpected ${label} field "${unknownKeys[0]}".`);
326
+ function readOptionalSizePercent(
327
+ record: Record<string, unknown>,
328
+ key: string,
329
+ ): { readonly sizePercent?: number } {
330
+ const value = record[key];
331
+ if (value === undefined) {
332
+ return {};
201
333
  }
334
+
335
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
336
+ throw new WorkbenchConfigValidationError(`Expected "${key}" to be a finite number.`);
337
+ }
338
+
339
+ return {
340
+ sizePercent: clampLayoutSizePercent(value),
341
+ };
342
+ }
343
+
344
+ function clampLayoutSizePercent(value: number): number {
345
+ return Math.min(90, Math.max(10, value));
202
346
  }
@@ -0,0 +1,68 @@
1
+ import {
2
+ assertKnownKeys,
3
+ assertRecord,
4
+ readOptionalString,
5
+ readRequiredString,
6
+ } from './parse-helpers.js';
7
+ import { WorkbenchConfigValidationError } from './validation-error.js';
8
+
9
+ export interface WorkbenchKeybindingDefinition {
10
+ readonly args?: readonly unknown[];
11
+ readonly command: string;
12
+ readonly key: string;
13
+ readonly when?: string;
14
+ }
15
+
16
+ export function parseWorkbenchKeybindingsConfig(
17
+ input: unknown,
18
+ ): readonly WorkbenchKeybindingDefinition[] {
19
+ if (!Array.isArray(input)) {
20
+ throw new WorkbenchConfigValidationError('Expected keybindings config to be an array.');
21
+ }
22
+
23
+ return input.map((entry, index) => parseWorkbenchKeybindingDefinition(entry, index));
24
+ }
25
+
26
+ export function parseWorkbenchKeybindingsConfigJson(
27
+ jsonText: string,
28
+ ): readonly WorkbenchKeybindingDefinition[] {
29
+ try {
30
+ return parseWorkbenchKeybindingsConfig(JSON.parse(jsonText) as unknown);
31
+ } catch (error) {
32
+ if (error instanceof WorkbenchConfigValidationError) {
33
+ throw error;
34
+ }
35
+
36
+ throw new WorkbenchConfigValidationError('Expected keybindings config to be valid JSON.');
37
+ }
38
+ }
39
+
40
+ function parseWorkbenchKeybindingDefinition(
41
+ input: unknown,
42
+ index: number,
43
+ ): WorkbenchKeybindingDefinition {
44
+ const record = assertRecord(input, `keybindings[${index}]`);
45
+ assertKnownKeys(record, ['args', 'command', 'key', 'when'], `keybindings[${index}]`);
46
+
47
+ const args = readOptionalArgs(record);
48
+
49
+ return {
50
+ ...(args ? { args } : {}),
51
+ command: readRequiredString(record, 'command'),
52
+ key: readRequiredString(record, 'key'),
53
+ ...(readOptionalString(record, 'when') ? { when: readOptionalString(record, 'when') } : {}),
54
+ };
55
+ }
56
+
57
+ function readOptionalArgs(record: Record<string, unknown>): readonly unknown[] | undefined {
58
+ const value = record.args;
59
+ if (value === undefined) {
60
+ return undefined;
61
+ }
62
+
63
+ if (!Array.isArray(value)) {
64
+ throw new WorkbenchConfigValidationError('Expected "args" to be an array.');
65
+ }
66
+
67
+ return [...value];
68
+ }
@@ -0,0 +1,46 @@
1
+ import { WorkbenchConfigValidationError } from './validation-error.js';
2
+
3
+ export function assertRecord(value: unknown, label: string): Record<string, unknown> {
4
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
5
+ throw new WorkbenchConfigValidationError(`Expected ${label} to be an object.`);
6
+ }
7
+
8
+ return value as Record<string, unknown>;
9
+ }
10
+
11
+ export function readRequiredString(record: Record<string, unknown>, key: string): string {
12
+ const value = record[key];
13
+ if (typeof value !== 'string' || value.trim().length === 0) {
14
+ throw new WorkbenchConfigValidationError(`Expected "${key}" to be a non-empty string.`);
15
+ }
16
+
17
+ return value.trim();
18
+ }
19
+
20
+ export function readOptionalString(
21
+ record: Record<string, unknown>,
22
+ key: string,
23
+ ): string | undefined {
24
+ const value = record[key];
25
+ if (value === undefined) {
26
+ return undefined;
27
+ }
28
+
29
+ if (typeof value !== 'string' || value.trim().length === 0) {
30
+ throw new WorkbenchConfigValidationError(`Expected "${key}" to be a non-empty string.`);
31
+ }
32
+
33
+ return value.trim();
34
+ }
35
+
36
+ export function assertKnownKeys(
37
+ record: Record<string, unknown>,
38
+ keys: readonly string[],
39
+ label: string,
40
+ ) {
41
+ const knownKeys = new Set(keys);
42
+ const unknownKeys = Object.keys(record).filter((key) => !knownKeys.has(key));
43
+ if (unknownKeys.length > 0) {
44
+ throw new WorkbenchConfigValidationError(`Unexpected ${label} field "${unknownKeys[0]}".`);
45
+ }
46
+ }
@@ -0,0 +1,65 @@
1
+ import type { WorkbenchSettingsConfig } from './settings-config.js';
2
+
3
+ /** Runtime preference scopes supported in v1. */
4
+ export type PreferenceScope = 'default' | 'workspace' | 'local';
5
+
6
+ /** Documented future scopes — not merged or persisted yet. */
7
+ export const FUTURE_PREFERENCE_SCOPES = ['user', 'resource', 'secret'] as const;
8
+
9
+ export type FuturePreferenceScope = (typeof FUTURE_PREFERENCE_SCOPES)[number];
10
+
11
+ /** Lower index = lower precedence when merging effective values. */
12
+ export const PREFERENCE_SCOPE_MERGE_ORDER: readonly PreferenceScope[] = [
13
+ 'default',
14
+ 'workspace',
15
+ 'local',
16
+ ];
17
+
18
+ export type PreferenceValuesByScope = Partial<Record<PreferenceScope, WorkbenchSettingsConfig>>;
19
+
20
+ export interface ScopedPreferenceLayer {
21
+ readonly scope: PreferenceScope;
22
+ readonly values: WorkbenchSettingsConfig;
23
+ }
24
+
25
+ export function isPreferenceScope(value: unknown): value is PreferenceScope {
26
+ return value === 'default' || value === 'workspace' || value === 'local';
27
+ }
28
+
29
+ export function mergeScopedPreferences(
30
+ layers: readonly ScopedPreferenceLayer[],
31
+ ): WorkbenchSettingsConfig {
32
+ const merged: Record<string, unknown> = {};
33
+
34
+ for (const scope of PREFERENCE_SCOPE_MERGE_ORDER) {
35
+ const layer = layers.find((candidate) => candidate.scope === scope);
36
+ if (!layer) {
37
+ continue;
38
+ }
39
+
40
+ for (const [key, value] of Object.entries(layer.values)) {
41
+ merged[key] = value;
42
+ }
43
+ }
44
+
45
+ return merged;
46
+ }
47
+
48
+ export function mergePreferenceValuesByScope(
49
+ valuesByScope: PreferenceValuesByScope,
50
+ ): WorkbenchSettingsConfig {
51
+ return mergeScopedPreferences(
52
+ PREFERENCE_SCOPE_MERGE_ORDER.flatMap((scope) => {
53
+ const values = valuesByScope[scope];
54
+ return values ? [{ scope, values }] : [];
55
+ }),
56
+ );
57
+ }
58
+
59
+ export function createEmptyPreferenceValuesByScope(): PreferenceValuesByScope {
60
+ return {
61
+ default: {},
62
+ local: {},
63
+ workspace: {},
64
+ };
65
+ }
@@ -0,0 +1,23 @@
1
+ import { WorkbenchConfigValidationError } from './validation-error.js';
2
+
3
+ export type WorkbenchSettingsConfig = Readonly<Record<string, unknown>>;
4
+
5
+ export function parseWorkbenchSettingsConfig(input: unknown): WorkbenchSettingsConfig {
6
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
7
+ throw new WorkbenchConfigValidationError('Expected settings config to be an object.');
8
+ }
9
+
10
+ return { ...(input as Record<string, unknown>) };
11
+ }
12
+
13
+ export function parseWorkbenchSettingsConfigJson(jsonText: string): WorkbenchSettingsConfig {
14
+ try {
15
+ return parseWorkbenchSettingsConfig(JSON.parse(jsonText) as unknown);
16
+ } catch (error) {
17
+ if (error instanceof WorkbenchConfigValidationError) {
18
+ throw error;
19
+ }
20
+
21
+ throw new WorkbenchConfigValidationError('Expected settings config to be valid JSON.');
22
+ }
23
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ assertKnownKeys,
3
+ assertRecord,
4
+ readOptionalString,
5
+ readRequiredString,
6
+ } from './parse-helpers.js';
7
+ import { WorkbenchConfigValidationError } from './validation-error.js';
8
+
9
+ export interface WorkbenchUserCommandExecuteAction {
10
+ readonly args?: unknown;
11
+ readonly command: string;
12
+ readonly type: 'executeCommand';
13
+ }
14
+
15
+ export interface WorkbenchUserCommandSequenceAction {
16
+ readonly steps: readonly WorkbenchUserCommandAction[];
17
+ readonly type: 'sequence';
18
+ }
19
+
20
+ export type WorkbenchUserCommandAction =
21
+ WorkbenchUserCommandExecuteAction | WorkbenchUserCommandSequenceAction;
22
+
23
+ export interface WorkbenchUserCommandDefinition {
24
+ readonly action: WorkbenchUserCommandAction;
25
+ readonly category?: string | undefined;
26
+ readonly command: string;
27
+ readonly title: string;
28
+ }
29
+
30
+ export interface WorkbenchUserCommandsConfig {
31
+ readonly commands: readonly WorkbenchUserCommandDefinition[];
32
+ readonly version?: number | undefined;
33
+ }
34
+
35
+ export function parseWorkbenchUserCommandsConfig(input: unknown): WorkbenchUserCommandsConfig {
36
+ const record = assertRecord(input, 'user commands config');
37
+ assertKnownKeys(record, ['commands', 'version'], 'user commands config');
38
+
39
+ const commands = record.commands;
40
+ if (!Array.isArray(commands)) {
41
+ throw new WorkbenchConfigValidationError('Expected "commands" to be an array.');
42
+ }
43
+
44
+ return {
45
+ commands: commands.map((entry, index) => parseUserCommandDefinition(entry, index)),
46
+ ...(readOptionalNumber(record, 'version') !== undefined
47
+ ? { version: readOptionalNumber(record, 'version') }
48
+ : {}),
49
+ };
50
+ }
51
+
52
+ export function parseWorkbenchUserCommandsConfigJson(
53
+ jsonText: string,
54
+ ): WorkbenchUserCommandsConfig {
55
+ try {
56
+ return parseWorkbenchUserCommandsConfig(JSON.parse(jsonText) as unknown);
57
+ } catch (error) {
58
+ if (error instanceof WorkbenchConfigValidationError) {
59
+ throw error;
60
+ }
61
+
62
+ throw new WorkbenchConfigValidationError('Expected user commands config to be valid JSON.');
63
+ }
64
+ }
65
+
66
+ function parseUserCommandDefinition(input: unknown, index: number): WorkbenchUserCommandDefinition {
67
+ const record = assertRecord(input, `user commands[${index}]`);
68
+ assertKnownKeys(record, ['action', 'category', 'command', 'title'], `user commands[${index}]`);
69
+
70
+ return {
71
+ action: parseUserCommandAction(record.action, `user commands[${index}].action`),
72
+ ...(readOptionalString(record, 'category')
73
+ ? { category: readOptionalString(record, 'category') }
74
+ : {}),
75
+ command: readRequiredString(record, 'command'),
76
+ title: readRequiredString(record, 'title'),
77
+ };
78
+ }
79
+
80
+ function parseUserCommandAction(input: unknown, label: string): WorkbenchUserCommandAction {
81
+ const record = assertRecord(input, label);
82
+ const type = readRequiredString(record, 'type');
83
+
84
+ if (type === 'executeCommand') {
85
+ assertKnownKeys(record, ['args', 'command', 'type'], label);
86
+ return {
87
+ ...(record.args !== undefined ? { args: record.args } : {}),
88
+ command: readRequiredString(record, 'command'),
89
+ type,
90
+ };
91
+ }
92
+
93
+ if (type === 'sequence') {
94
+ assertKnownKeys(record, ['steps', 'type'], label);
95
+ const steps = record.steps;
96
+ if (!Array.isArray(steps) || steps.length === 0) {
97
+ throw new WorkbenchConfigValidationError(
98
+ `Expected "${label}.steps" to be a non-empty array.`,
99
+ );
100
+ }
101
+
102
+ return {
103
+ steps: steps.map((step, index) => parseUserCommandAction(step, `${label}.steps[${index}]`)),
104
+ type,
105
+ };
106
+ }
107
+
108
+ throw new WorkbenchConfigValidationError(`Unexpected ${label} type "${type}".`);
109
+ }
110
+
111
+ function readOptionalNumber(record: Record<string, unknown>, key: string): number | undefined {
112
+ const value = record[key];
113
+ if (value === undefined) {
114
+ return undefined;
115
+ }
116
+
117
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
118
+ throw new WorkbenchConfigValidationError(`Expected "${key}" to be a finite number.`);
119
+ }
120
+
121
+ return value;
122
+ }
@@ -0,0 +1,6 @@
1
+ export class WorkbenchConfigValidationError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'WorkbenchConfigValidationError';
5
+ }
6
+ }