@workbench-kit/workbench-config 0.0.2-prototype.0.1.3 → 0.0.2-prototype.0.2.10

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