slicejs-cli 3.4.0 → 3.5.0

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 (35) hide show
  1. package/AGENTS.md +247 -0
  2. package/client.js +63 -64
  3. package/commands/Print.js +11 -15
  4. package/commands/Validations.js +12 -23
  5. package/commands/buildProduction/buildProduction.js +23 -26
  6. package/commands/bundle/bundle.js +10 -11
  7. package/commands/createComponent/createComponent.js +14 -16
  8. package/commands/deleteComponent/deleteComponent.js +6 -6
  9. package/commands/doctor/doctor.js +11 -14
  10. package/commands/getComponent/getComponent.js +99 -162
  11. package/commands/init/init.js +77 -26
  12. package/commands/listComponents/listComponents.js +18 -21
  13. package/commands/startServer/startServer.js +21 -24
  14. package/commands/startServer/watchServer.js +7 -7
  15. package/commands/types/types.js +53 -18
  16. package/commands/utils/PathHelper.js +9 -2
  17. package/commands/utils/VersionChecker.js +3 -3
  18. package/commands/utils/bundling/DependencyAnalyzer.js +8 -16
  19. package/commands/utils/loadConfig.js +31 -0
  20. package/commands/utils/updateManager.js +3 -4
  21. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +105 -105
  22. package/package.json +14 -2
  23. package/post.js +2 -2
  24. package/tests/bundle-generator.test.js +3 -20
  25. package/tests/component-registry-parse.test.js +34 -0
  26. package/tests/fixtures/components.js +8 -0
  27. package/tests/fixtures/sliceConfig.json +74 -0
  28. package/tests/getcomponent.test.js +407 -0
  29. package/tests/helpers/setup.js +97 -0
  30. package/tests/init-command-contract.test.js +46 -0
  31. package/tests/local-cli-delegation.test.js +7 -5
  32. package/tests/path-helper.test.js +206 -0
  33. package/tests/types-breakage.test.js +491 -0
  34. package/tests/types-generator-errors.test.js +361 -0
  35. package/tests/types-generator.test.js +172 -184
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-cli",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Command client for developing web applications with Slice.js framework",
5
5
  "main": "client.js",
6
6
  "bin": {
@@ -12,7 +12,19 @@
12
12
  },
13
13
  "scripts": {
14
14
  "test": "node --test",
15
- "postinstall": "node post.js"
15
+ "postinstall": "node post.js",
16
+ "slice:dev": "slice dev",
17
+ "slice:start": "slice start",
18
+ "slice:create": "slice component create",
19
+ "slice:list": "slice component list",
20
+ "slice:delete": "slice component delete",
21
+ "slice:init": "slice init",
22
+ "slice:get": "slice get",
23
+ "slice:browse": "slice browse",
24
+ "slice:sync": "slice sync",
25
+ "slice:version": "slice version",
26
+ "slice:update": "slice update",
27
+ "slice:types": "slice types generate"
16
28
  },
17
29
  "keywords": [
18
30
  "framework",
package/post.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { getProjectRoot } from './commands/utils/PathHelper.js';
4
+ import { getProjectRoot, getPath } from './commands/utils/PathHelper.js';
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
 
@@ -15,7 +15,7 @@ if (isGlobal) {
15
15
  }
16
16
 
17
17
  const projectRoot = getProjectRoot(import.meta.url);
18
- const pkgPath = path.join(projectRoot, 'package.json');
18
+ const pkgPath = getPath(import.meta.url, 'package.json');
19
19
 
20
20
  const sliceScripts = {
21
21
  'slice:dev': 'slice dev',
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import BundleGenerator from '../commands/utils/bundling/BundleGenerator.js';
7
+ import { withTestProject } from './helpers/setup.js';
7
8
 
8
9
  const createComponent = (name, deps = []) => ({
9
10
  name,
@@ -78,18 +79,7 @@ test('loading policy is enabled when sliceConfig loading.enabled is true', () =>
78
79
  });
79
80
 
80
81
  test('loading policy falls back to project sliceConfig when analysisData lacks sliceConfig', async () => {
81
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-bundle-test-'));
82
- const srcDir = path.join(tempRoot, 'src');
83
- const previousInitCwd = process.env.INIT_CWD;
84
-
85
- await fs.ensureDir(srcDir);
86
- await fs.writeJson(path.join(srcDir, 'sliceConfig.json'), {
87
- loading: { enabled: true }
88
- });
89
-
90
- process.env.INIT_CWD = tempRoot;
91
-
92
- try {
82
+ await withTestProject(async () => {
93
83
  const generator = new BundleGenerator(import.meta.url, {
94
84
  components: [],
95
85
  routes: [],
@@ -103,14 +93,7 @@ test('loading policy falls back to project sliceConfig when analysisData lacks s
103
93
 
104
94
  const config = generator.generateBundleConfig(null);
105
95
  assert.equal(config.loadingPolicy, 'enabled');
106
- } finally {
107
- if (previousInitCwd === undefined) {
108
- delete process.env.INIT_CWD;
109
- } else {
110
- process.env.INIT_CWD = previousInitCwd;
111
- }
112
- await fs.remove(tempRoot);
113
- }
96
+ });
114
97
  });
115
98
 
116
99
  test('loading enabled always includes Loading component in critical bundle', () => {
@@ -0,0 +1,34 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createTestProject, cleanupTestProject, withTestProject } from './helpers/setup.js';
6
+
7
+ test('generateTypesFile parses JSON from components.js (no eval)', async () => {
8
+ const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
9
+
10
+ try {
11
+ const srcDir = path.join(tmpRoot, 'src');
12
+ const { generateTypesFile } = await import('../commands/types/types.js');
13
+ const result = await generateTypesFile({
14
+ projectRoot: tmpRoot,
15
+ outputPath: path.join(srcDir, 'slice-build.generated.d.ts')
16
+ });
17
+
18
+ assert.ok(result.componentsProcessed > 0);
19
+ assert.equal(fs.existsSync(result.outputPath), true);
20
+
21
+ const declaration = fs.readFileSync(result.outputPath, 'utf8');
22
+ assert.match(declaration, /export interface ButtonProps/);
23
+ } finally {
24
+ await cleanupTestProject(tmpRoot);
25
+ }
26
+ });
27
+
28
+ test('Validations componentExists with JSON.parse (no eval)', async () => {
29
+ await withTestProject(async (tmpDir) => {
30
+ const validations = (await import('../commands/Validations.js')).default;
31
+ assert.equal(validations.componentExists('Button'), true);
32
+ assert.equal(validations.componentExists('NonExistent'), false);
33
+ });
34
+ });
@@ -0,0 +1,8 @@
1
+ const components = {
2
+ "Button": "Visual",
3
+ "Link": "Visual",
4
+ "Loading": "Visual",
5
+ "Navbar": "Visual",
6
+ "NotFound": "Visual",
7
+ "FetchManager": "Service"
8
+ }; export default components;
@@ -0,0 +1,74 @@
1
+ {
2
+ "server": {
3
+ "port": 3001,
4
+ "host": "localhost"
5
+ },
6
+ "debugger": {
7
+ "enabled": false,
8
+ "click": "right"
9
+ },
10
+ "events": {
11
+ "enabled": true,
12
+ "ui": {
13
+ "enabled": true,
14
+ "shortcut": "alt+shift+e"
15
+ }
16
+ },
17
+ "context": {
18
+ "enabled": true,
19
+ "ui": {
20
+ "enabled": true,
21
+ "shortcut": "alt+shift+c"
22
+ }
23
+ },
24
+ "stylesManager": {
25
+ "requestedStyles": ["sliceStyles"]
26
+ },
27
+ "themeManager": {
28
+ "enabled": true,
29
+ "defaultTheme": "Slice",
30
+ "saveThemeLocally": false,
31
+ "useBrowserTheme": false
32
+ },
33
+ "logger": {
34
+ "enabled": true,
35
+ "showLogs": {
36
+ "console": {
37
+ "error": true,
38
+ "warning": true,
39
+ "info": false
40
+ }
41
+ }
42
+ },
43
+ "paths": {
44
+ "components": {
45
+ "AppComponents": {
46
+ "path": "/Components/AppComponents",
47
+ "type": "Visual"
48
+ },
49
+ "Visual": {
50
+ "path": "/Components/Visual",
51
+ "type": "Visual"
52
+ },
53
+ "Service": {
54
+ "path": "/Components/Service",
55
+ "type": "Service"
56
+ }
57
+ },
58
+ "themes": "/Themes",
59
+ "styles": "/Styles",
60
+ "routesFile": "/routes.js"
61
+ },
62
+ "router": {
63
+ "defaultRoute": "/"
64
+ },
65
+ "loading": {
66
+ "enabled": true
67
+ },
68
+ "publicFolders": [
69
+ "/Themes",
70
+ "/Styles",
71
+ "/assets",
72
+ "/images"
73
+ ]
74
+ }
@@ -0,0 +1,407 @@
1
+ import { test, describe, before, after, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { createTestProject, cleanupTestProject, withTestProject } from './helpers/setup.js';
6
+
7
+ describe('runConcurrent', () => {
8
+ let runConcurrent;
9
+
10
+ before(async () => {
11
+ ({ runConcurrent } = await import('../commands/getComponent/getComponent.js'));
12
+ });
13
+
14
+ test('runs all items', async () => {
15
+ const results = [];
16
+ const worker = async (item) => { results.push(item); };
17
+ await runConcurrent([1, 2, 3], worker, 5);
18
+ assert.deepEqual(results.sort(), [1, 2, 3]);
19
+ });
20
+
21
+ test('respects concurrency limit', async () => {
22
+ let concurrent = 0;
23
+ let maxConcurrent = 0;
24
+ const worker = async (item) => {
25
+ concurrent++;
26
+ maxConcurrent = Math.max(maxConcurrent, concurrent);
27
+ await new Promise(r => setTimeout(r, 10));
28
+ concurrent--;
29
+ };
30
+ await runConcurrent([1, 2, 3, 4, 5], worker, 2);
31
+ assert.equal(maxConcurrent <= 2, true);
32
+ });
33
+
34
+ test('handles empty array', async () => {
35
+ const worker = async () => {};
36
+ await runConcurrent([], worker, 3);
37
+ assert.ok(true);
38
+ });
39
+
40
+ test('propagates worker errors', async () => {
41
+ const worker = async () => { throw new Error('worker fail'); };
42
+ await assert.rejects(() => runConcurrent([1], worker, 3), /worker fail/);
43
+ });
44
+
45
+ test('default concurrency is 3', async () => {
46
+ let maxConcurrent = 0;
47
+ let running = 0;
48
+ const worker = async () => {
49
+ running++;
50
+ maxConcurrent = Math.max(maxConcurrent, running);
51
+ await new Promise(r => setTimeout(r, 10));
52
+ running--;
53
+ };
54
+ await runConcurrent([1, 2, 3, 4, 5], worker);
55
+ assert.equal(maxConcurrent <= 3, true);
56
+ });
57
+ });
58
+
59
+ describe('loadConfig', () => {
60
+ test('returns config object when sliceConfig.json exists', async () => {
61
+ await withTestProject(async (tmpDir) => {
62
+ const { loadConfig } = await import('../commands/getComponent/getComponent.js');
63
+ const config = await loadConfig();
64
+ assert.notEqual(config, null);
65
+ assert.equal(typeof config, 'object');
66
+ assert.equal(config.server?.port, 3001);
67
+ });
68
+ });
69
+
70
+ test('returns null when sliceConfig.json is missing', async () => {
71
+ const tmpDir = await createTestProject();
72
+ try {
73
+ process.env.INIT_CWD = tmpDir;
74
+ const fixtureConfig = new URL('../tests/fixtures/sliceConfig.json', import.meta.url);
75
+ const fs = await import('node:fs/promises');
76
+ const path = await import('node:path');
77
+ const destPath = path.join(tmpDir, 'src', 'sliceConfig.json');
78
+ try { await fs.unlink(destPath); } catch {}
79
+ const { loadConfig } = await import('../commands/getComponent/getComponent.js');
80
+ const config = await loadConfig();
81
+ assert.equal(config, null);
82
+ } finally {
83
+ delete process.env.INIT_CWD;
84
+ await cleanupTestProject(tmpDir);
85
+ }
86
+ });
87
+ });
88
+
89
+ describe('ComponentRegistry.getAvailableComponents', () => {
90
+ let ComponentRegistry;
91
+
92
+ before(async () => {
93
+ ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
94
+ });
95
+
96
+ const makeRegistry = (entries) => {
97
+ const registry = new ComponentRegistry();
98
+ registry.componentsRegistry = { ...entries };
99
+ return registry;
100
+ };
101
+
102
+ test('returns empty object when registry not loaded', () => {
103
+ const registry = new ComponentRegistry();
104
+ const result = registry.getAvailableComponents();
105
+ assert.deepEqual(result, {});
106
+ });
107
+
108
+ test('routing components (Route, MultiRoute, Link) return only .js', () => {
109
+ const registry = makeRegistry({
110
+ Route: 'Visual',
111
+ MultiRoute: 'Visual',
112
+ Link: 'Visual'
113
+ });
114
+ const result = registry.getAvailableComponents('Visual');
115
+ assert.deepEqual(result.Route.files, ['Route.js']);
116
+ assert.deepEqual(result.MultiRoute.files, ['MultiRoute.js']);
117
+ assert.deepEqual(result.Link.files, ['Link.js']);
118
+ });
119
+
120
+ test('NotFound returns .js, .html, .css like other visual components', () => {
121
+ const registry = makeRegistry({
122
+ NotFound: 'Visual',
123
+ Button: 'Visual',
124
+ Navbar: 'Visual'
125
+ });
126
+ const result = registry.getAvailableComponents('Visual');
127
+ assert.deepEqual(result.NotFound.files, ['NotFound.js', 'NotFound.html', 'NotFound.css']);
128
+ assert.deepEqual(result.Button.files, ['Button.js', 'Button.html', 'Button.css']);
129
+ assert.deepEqual(result.Navbar.files, ['Navbar.js', 'Navbar.html', 'Navbar.css']);
130
+ });
131
+
132
+ test('Service components return only .js', () => {
133
+ const registry = makeRegistry({
134
+ FetchManager: 'Service'
135
+ });
136
+ const result = registry.getAvailableComponents('Service');
137
+ assert.deepEqual(result.FetchManager.files, ['FetchManager.js']);
138
+ });
139
+
140
+ test('filters by category - Visual', () => {
141
+ const registry = makeRegistry({
142
+ Button: 'Visual',
143
+ FetchManager: 'Service'
144
+ });
145
+ const result = registry.getAvailableComponents('Visual');
146
+ assert.ok(result.Button);
147
+ assert.ok(!result.FetchManager);
148
+ });
149
+
150
+ test('filters by category - Service', () => {
151
+ const registry = makeRegistry({
152
+ Button: 'Visual',
153
+ FetchManager: 'Service'
154
+ });
155
+ const result = registry.getAvailableComponents('Service');
156
+ assert.ok(!result.Button);
157
+ assert.ok(result.FetchManager);
158
+ });
159
+
160
+ test('returns all categories when no filter', () => {
161
+ const registry = makeRegistry({
162
+ Button: 'Visual',
163
+ FetchManager: 'Service'
164
+ });
165
+ const result = registry.getAvailableComponents();
166
+ assert.ok(result.Button);
167
+ assert.ok(result.FetchManager);
168
+ });
169
+ });
170
+
171
+ describe('fetchWithRetry', () => {
172
+ let fetchWithRetry;
173
+ let originalFetch;
174
+
175
+ before(async () => {
176
+ ({ fetchWithRetry } = await import('../commands/getComponent/getComponent.js'));
177
+ });
178
+
179
+ after(() => {
180
+ if (originalFetch) globalThis.fetch = originalFetch;
181
+ });
182
+
183
+ test('succeeds on first attempt', async () => {
184
+ originalFetch = globalThis.fetch;
185
+ globalThis.fetch = mock.fn(async () => ({
186
+ ok: true,
187
+ text: async () => 'response body'
188
+ }));
189
+ const result = await fetchWithRetry('https://example.com');
190
+ assert.equal(result, 'response body');
191
+ assert.equal(globalThis.fetch.mock.calls.length, 1);
192
+ globalThis.fetch = originalFetch;
193
+ });
194
+
195
+ test('retries and eventually succeeds', async () => {
196
+ let attempts = 0;
197
+ originalFetch = globalThis.fetch;
198
+ globalThis.fetch = mock.fn(async () => {
199
+ attempts++;
200
+ if (attempts < 3) throw new Error('network error');
201
+ return { ok: true, text: async () => 'success after retry' };
202
+ });
203
+ const result = await fetchWithRetry('https://example.com', 3, 5);
204
+ assert.equal(result, 'success after retry');
205
+ assert.equal(attempts, 3);
206
+ globalThis.fetch = originalFetch;
207
+ });
208
+
209
+ test('fails after exhausting retries', async () => {
210
+ originalFetch = globalThis.fetch;
211
+ globalThis.fetch = mock.fn(async () => { throw new Error('persistent error'); });
212
+ await assert.rejects(() => fetchWithRetry('https://example.com', 2, 5), /persistent error/);
213
+ globalThis.fetch = originalFetch;
214
+ });
215
+
216
+ test('rejects non-ok response', async () => {
217
+ originalFetch = globalThis.fetch;
218
+ globalThis.fetch = mock.fn(async () => ({
219
+ ok: false,
220
+ status: 404,
221
+ statusText: 'Not Found'
222
+ }));
223
+ await assert.rejects(() => fetchWithRetry('https://example.com', 2, 5), /HTTP 404/);
224
+ globalThis.fetch = originalFetch;
225
+ });
226
+ });
227
+
228
+ describe('filterOfficialComponents', () => {
229
+ let ComponentRegistry;
230
+
231
+ before(async () => {
232
+ ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
233
+ });
234
+
235
+ test('includes Visual and Service components', () => {
236
+ const registry = new ComponentRegistry();
237
+ const result = registry.filterOfficialComponents({
238
+ Button: 'Visual',
239
+ FetchManager: 'Service'
240
+ });
241
+ assert.deepEqual(result, { Button: 'Visual', FetchManager: 'Service' });
242
+ });
243
+
244
+ test('excludes AppComponents and other categories', () => {
245
+ const registry = new ComponentRegistry();
246
+ const result = registry.filterOfficialComponents({
247
+ Button: 'Visual',
248
+ AppShell: 'AppComponents',
249
+ SomeOther: 'Unknown'
250
+ });
251
+ assert.deepEqual(result, { Button: 'Visual' });
252
+ });
253
+
254
+ test('returns empty object for empty input', () => {
255
+ const registry = new ComponentRegistry();
256
+ const result = registry.filterOfficialComponents({});
257
+ assert.deepEqual(result, {});
258
+ });
259
+
260
+ test('returns empty when no Visual or Service components', () => {
261
+ const registry = new ComponentRegistry();
262
+ const result = registry.filterOfficialComponents({
263
+ AppShell: 'AppComponents',
264
+ Custom: 'Other'
265
+ });
266
+ assert.deepEqual(result, {});
267
+ });
268
+ });
269
+
270
+ describe('findComponentInRegistry', () => {
271
+ let ComponentRegistry;
272
+
273
+ before(async () => {
274
+ ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
275
+ });
276
+
277
+ const makeRegistry = (entries) => {
278
+ const registry = new ComponentRegistry();
279
+ registry.componentsRegistry = { ...entries };
280
+ return registry;
281
+ };
282
+
283
+ test('finds component by name (case-insensitive)', () => {
284
+ const registry = makeRegistry({ Button: 'Visual' });
285
+ const result = registry.findComponentInRegistry('button');
286
+ assert.deepEqual(result, { name: 'Button', category: 'Visual' });
287
+ });
288
+
289
+ test('returns null for non-existent component', () => {
290
+ const registry = makeRegistry({ Button: 'Visual' });
291
+ const result = registry.findComponentInRegistry('NonExistent');
292
+ assert.equal(result, null);
293
+ });
294
+
295
+ test('returns null when registry not loaded', () => {
296
+ const registry = new ComponentRegistry();
297
+ const result = registry.findComponentInRegistry('Button');
298
+ assert.equal(result, null);
299
+ });
300
+
301
+ test('finds component with exact case', () => {
302
+ const registry = makeRegistry({ FetchManager: 'Service' });
303
+ const result = registry.findComponentInRegistry('FetchManager');
304
+ assert.deepEqual(result, { name: 'FetchManager', category: 'Service' });
305
+ });
306
+ });
307
+
308
+ describe('getLocalComponents', () => {
309
+ let ComponentRegistry;
310
+
311
+ before(async () => {
312
+ ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
313
+ });
314
+
315
+ test('returns components from local components.js', async () => {
316
+ await withTestProject(async (tmpDir) => {
317
+ const registry = new ComponentRegistry();
318
+ const result = await registry.getLocalComponents();
319
+ assert.deepEqual(result, { Button: 'Visual' });
320
+ }, { visualComponents: ['Button'] });
321
+ });
322
+
323
+ test('returns empty object when no components.js exists', async () => {
324
+ const tmpDir = await createTestProject();
325
+ try {
326
+ process.env.INIT_CWD = tmpDir;
327
+ const componentsPath = path.join(tmpDir, 'src', 'Components', 'components.js');
328
+ await fs.remove(componentsPath);
329
+ const registry = new ComponentRegistry();
330
+ const result = await registry.getLocalComponents();
331
+ assert.deepEqual(result, {});
332
+ } finally {
333
+ delete process.env.INIT_CWD;
334
+ await cleanupTestProject(tmpDir);
335
+ }
336
+ });
337
+
338
+ test('returns empty object when components.js has invalid format', async () => {
339
+ const tmpDir = await createTestProject();
340
+ try {
341
+ process.env.INIT_CWD = tmpDir;
342
+ const componentsPath = path.join(tmpDir, 'src', 'Components', 'components.js');
343
+ await fs.writeFile(componentsPath, 'not valid javascript', 'utf8');
344
+ const registry = new ComponentRegistry();
345
+ const result = await registry.getLocalComponents();
346
+ assert.deepEqual(result, {});
347
+ } finally {
348
+ delete process.env.INIT_CWD;
349
+ await cleanupTestProject(tmpDir);
350
+ }
351
+ });
352
+ });
353
+
354
+ describe('updateLocalRegistrySafe', () => {
355
+ let ComponentRegistry;
356
+
357
+ before(async () => {
358
+ ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
359
+ });
360
+
361
+ test('registers a new component in components.js', async () => {
362
+ await withTestProject(async (tmpDir) => {
363
+ const registry = new ComponentRegistry();
364
+ await registry.updateLocalRegistrySafe('NewComp', 'Visual');
365
+ const components = await registry.getLocalComponents();
366
+ assert.equal(components.NewComp, 'Visual');
367
+ }, { visualComponents: [] });
368
+ });
369
+
370
+ test('does not duplicate existing component', async () => {
371
+ await withTestProject(async (tmpDir) => {
372
+ const registry = new ComponentRegistry();
373
+ await registry.updateLocalRegistrySafe('Button', 'Visual');
374
+ const components = await registry.getLocalComponents();
375
+ assert.equal(components.Button, 'Visual');
376
+ assert.deepEqual(Object.keys(components).filter(k => k === 'Button'), ['Button']);
377
+ }, { visualComponents: ['Button'] });
378
+ });
379
+ });
380
+
381
+ describe('findUpdatableComponents', () => {
382
+ let ComponentRegistry;
383
+
384
+ before(async () => {
385
+ ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
386
+ });
387
+
388
+ test('returns empty when no local components match remote registry', async () => {
389
+ await withTestProject(async (tmpDir) => {
390
+ const registry = new ComponentRegistry();
391
+ registry.componentsRegistry = { SomeRemote: 'Visual' };
392
+ const result = await registry.findUpdatableComponents();
393
+ assert.deepEqual(result, []);
394
+ }, { visualComponents: ['Button'] });
395
+ });
396
+
397
+ test('returns components that exist both locally and remotely', async () => {
398
+ await withTestProject(async (tmpDir) => {
399
+ const registry = new ComponentRegistry();
400
+ registry.componentsRegistry = { Button: 'Visual', FetchManager: 'Service' };
401
+ const result = await registry.findUpdatableComponents();
402
+ assert.equal(result.length, 1);
403
+ assert.equal(result[0].name, 'Button');
404
+ assert.equal(result[0].category, 'Visual');
405
+ }, { visualComponents: ['Button'] });
406
+ });
407
+ });