@studio-foundation/cli 0.3.0-beta.1 → 0.3.0-beta.6

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 (88) hide show
  1. package/package.json +9 -5
  2. package/ARCHITECTURE.md +0 -40
  3. package/src/commands/api.ts +0 -126
  4. package/src/commands/config.ts +0 -433
  5. package/src/commands/init.ts +0 -879
  6. package/src/commands/install.ts +0 -23
  7. package/src/commands/integrations.ts +0 -332
  8. package/src/commands/list.ts +0 -197
  9. package/src/commands/logs.ts +0 -119
  10. package/src/commands/ollama.ts +0 -168
  11. package/src/commands/project.ts +0 -167
  12. package/src/commands/registry/audit.ts +0 -87
  13. package/src/commands/registry/index.ts +0 -63
  14. package/src/commands/registry/install.ts +0 -222
  15. package/src/commands/registry/publish.ts +0 -112
  16. package/src/commands/registry/remove.ts +0 -89
  17. package/src/commands/registry/search.ts +0 -93
  18. package/src/commands/registry/sync.ts +0 -24
  19. package/src/commands/registry/update.ts +0 -63
  20. package/src/commands/replay.ts +0 -559
  21. package/src/commands/run.ts +0 -454
  22. package/src/commands/status.ts +0 -163
  23. package/src/commands/template/index.ts +0 -59
  24. package/src/commands/template/validate.ts +0 -175
  25. package/src/commands/templates.ts +0 -99
  26. package/src/commands/tools.ts +0 -227
  27. package/src/commands/users.ts +0 -127
  28. package/src/commands/validate.ts +0 -46
  29. package/src/config.ts +0 -101
  30. package/src/index.ts +0 -201
  31. package/src/models-cache.ts +0 -127
  32. package/src/output/file-changes.ts +0 -97
  33. package/src/output/formatter.ts +0 -85
  34. package/src/output/formatters.ts +0 -303
  35. package/src/output/logger.ts +0 -18
  36. package/src/output/parallel-progress.ts +0 -103
  37. package/src/output/progress.ts +0 -414
  38. package/src/provider-validator.ts +0 -109
  39. package/src/registry/cache.ts +0 -46
  40. package/src/registry/client.ts +0 -84
  41. package/src/registry/lockfile.ts +0 -68
  42. package/src/registry/resolver.ts +0 -113
  43. package/src/registry/types.ts +0 -66
  44. package/src/run-logger.ts +0 -66
  45. package/src/run-store-factory.ts +0 -30
  46. package/src/studio-dir.ts +0 -28
  47. package/src/utils/input-wizard.ts +0 -73
  48. package/src/utils/placeholders.ts +0 -10
  49. package/tests/__stubs__/studio-runner.ts +0 -104
  50. package/tests/commands/api.test.ts +0 -110
  51. package/tests/commands/config.test.ts +0 -221
  52. package/tests/commands/init.test.ts +0 -919
  53. package/tests/commands/install.test.ts +0 -52
  54. package/tests/commands/integrations.test.ts +0 -158
  55. package/tests/commands/ollama.test.ts +0 -139
  56. package/tests/commands/project.test.ts +0 -179
  57. package/tests/commands/registry/audit.test.ts +0 -56
  58. package/tests/commands/registry/install.test.ts +0 -200
  59. package/tests/commands/registry/publish.test.ts +0 -56
  60. package/tests/commands/registry/remove.test.ts +0 -103
  61. package/tests/commands/registry/search.test.ts +0 -44
  62. package/tests/commands/registry/sync.test.ts +0 -37
  63. package/tests/commands/registry/update.test.ts +0 -62
  64. package/tests/commands/replay.test.ts +0 -283
  65. package/tests/commands/template/validate.test.ts +0 -150
  66. package/tests/commands/templates.test.ts +0 -42
  67. package/tests/commands/tools.test.ts +0 -106
  68. package/tests/config.test.ts +0 -142
  69. package/tests/formatter.test.ts +0 -158
  70. package/tests/integration/sigint.test.ts +0 -188
  71. package/tests/models-cache.test.ts +0 -250
  72. package/tests/output/file-changes.test.ts +0 -178
  73. package/tests/output/formatters.test.ts +0 -448
  74. package/tests/output/progress-spinner.test.ts +0 -232
  75. package/tests/output/progress-timer.test.ts +0 -230
  76. package/tests/provider-validator.test.ts +0 -182
  77. package/tests/registry/cache.test.ts +0 -66
  78. package/tests/registry/client.test.ts +0 -70
  79. package/tests/registry/lockfile.test.ts +0 -87
  80. package/tests/registry/resolver.test.ts +0 -122
  81. package/tests/run-logger-events.test.ts +0 -326
  82. package/tests/run-logger.test.ts +0 -64
  83. package/tests/run-store-factory.test.ts +0 -51
  84. package/tests/studio-dir.test.ts +0 -31
  85. package/tests/utils/input-wizard.test.ts +0 -153
  86. package/tests/utils/placeholders.test.ts +0 -36
  87. package/tsconfig.json +0 -24
  88. package/vitest.config.ts +0 -20
@@ -1,200 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { rm, mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { resolve, join } from 'node:path';
4
-
5
- const TMP = resolve(import.meta.dirname, '.tmp-install');
6
- const STUDIO_DIR = join(TMP, '.studio');
7
-
8
- const MOCK_METADATA = {
9
- name: 'linear',
10
- type: 'integration',
11
- version: '1.0.0',
12
- description: 'Linear integration',
13
- author: 'studio-core',
14
- license: 'MIT',
15
- tags: ['linear'],
16
- studio_version: '>=7.0.0',
17
- };
18
-
19
- const MOCK_INDEX = {
20
- generated_at: '2026-02-28T00:00:00Z',
21
- version: '1',
22
- packages: [{ ...MOCK_METADATA, downloads: 0 }],
23
- };
24
-
25
- const FAKE_INTEGRATION_CONTENT = 'name: linear\ntype: integration\n';
26
-
27
- // Mock syncRegistry to be a no-op (sync already handled), and RegistryCache.read to return mock index
28
- vi.mock('../../../src/commands/registry/sync.js', () => ({
29
- syncRegistry: vi.fn().mockResolvedValue(undefined),
30
- }));
31
- vi.mock('../../../src/registry/cache.js', () => {
32
- class RegistryCache {
33
- read() { return Promise.resolve(MOCK_INDEX); }
34
- write() { return Promise.resolve(undefined); }
35
- isFresh() { return Promise.resolve(true); }
36
- }
37
- return { RegistryCache };
38
- });
39
-
40
- beforeEach(async () => {
41
- await mkdir(STUDIO_DIR, { recursive: true });
42
- vi.stubGlobal('fetch', vi.fn()
43
- .mockResolvedValueOnce({ ok: true, json: async () => MOCK_METADATA })
44
- .mockResolvedValueOnce({ ok: true, text: async () => FAKE_INTEGRATION_CONTENT }),
45
- );
46
- });
47
- afterEach(async () => {
48
- await rm(TMP, { recursive: true, force: true });
49
- vi.unstubAllGlobals();
50
- vi.resetModules();
51
- vi.restoreAllMocks();
52
- });
53
-
54
- describe('installPackage', () => {
55
- it('installs an integration to .studio/integrations/', async () => {
56
- const { installPackage } = await import('../../../src/commands/registry/install.js');
57
- await installPackage('linear', { studioDir: STUDIO_DIR, force: true });
58
-
59
- const dest = resolve(STUDIO_DIR, 'integrations', 'linear.integration.yaml');
60
- const content = await readFile(dest, 'utf8');
61
- expect(content).toBe(FAKE_INTEGRATION_CONTENT);
62
- });
63
-
64
- it('writes lockfile entry after install', async () => {
65
- const { installPackage } = await import('../../../src/commands/registry/install.js');
66
- await installPackage('linear', { studioDir: STUDIO_DIR, force: true });
67
-
68
- const lf = JSON.parse(await readFile(resolve(STUDIO_DIR, 'registry.lock.json'), 'utf8'));
69
- expect(lf.installed['linear']).toMatchObject({
70
- version: '1.0.0',
71
- type: 'integration',
72
- });
73
- expect(lf.installed['linear'].sha256).toBeTruthy();
74
- });
75
- });
76
-
77
- // --- ADDITIONAL CONSTANTS for dep resolution tests ---
78
-
79
- const MOCK_TOOL_META = {
80
- name: 'repo-manager',
81
- type: 'tool',
82
- version: '1.0.0',
83
- description: 'File manager',
84
- author: 'studio-core',
85
- license: 'MIT',
86
- tags: [] as string[],
87
- studio_version: '>=7.0.0',
88
- };
89
-
90
- const MOCK_INDEX_WITH_DEPS = {
91
- generated_at: '2026-02-28T00:00:00Z',
92
- version: '1',
93
- packages: [
94
- {
95
- name: 'software-full',
96
- type: 'template',
97
- version: '2.0.0',
98
- description: 'Full software template',
99
- author: 'studio-core',
100
- license: 'MIT',
101
- tags: [] as string[],
102
- studio_version: '>=7.0.0',
103
- downloads: 0,
104
- },
105
- { ...MOCK_TOOL_META, downloads: 0 },
106
- ],
107
- };
108
-
109
- const MOCK_TEMPLATE_META_WITH_DEPS = {
110
- ...MOCK_INDEX_WITH_DEPS.packages[0],
111
- dependencies: {
112
- tools: { required: ['repo-manager'] as string[] },
113
- },
114
- };
115
-
116
- describe('installPackage — with required dependencies', () => {
117
- beforeEach(async () => {
118
- await mkdir(STUDIO_DIR, { recursive: true });
119
- vi.resetModules();
120
- });
121
-
122
- afterEach(async () => {
123
- await rm(TMP, { recursive: true, force: true });
124
- vi.unstubAllGlobals();
125
- vi.resetModules();
126
- vi.restoreAllMocks();
127
- });
128
-
129
- it('auto-installs required deps without prompting', async () => {
130
- vi.doMock('../../../src/registry/cache.js', () => {
131
- class RegistryCache {
132
- read() { return Promise.resolve(MOCK_INDEX_WITH_DEPS); }
133
- write() { return Promise.resolve(undefined); }
134
- isFresh() { return Promise.resolve(true); }
135
- }
136
- return { RegistryCache };
137
- });
138
- vi.doMock('../../../src/commands/registry/sync.js', () => ({
139
- syncRegistry: vi.fn().mockResolvedValue(undefined),
140
- }));
141
-
142
- vi.stubGlobal('fetch', vi.fn()
143
- // 1. fetch template metadata (has dependencies)
144
- .mockResolvedValueOnce({ ok: true, json: async () => MOCK_TEMPLATE_META_WITH_DEPS })
145
- // 2. download template dir (GitHub API — empty dir for simplicity)
146
- .mockResolvedValueOnce({ ok: true, json: async () => [] })
147
- // 3. fetch repo-manager metadata (dep)
148
- .mockResolvedValueOnce({ ok: true, json: async () => MOCK_TOOL_META })
149
- // 4. download repo-manager file
150
- .mockResolvedValueOnce({ ok: true, text: async () => 'name: repo_manager\n' }),
151
- );
152
-
153
- const { installPackage } = await import('../../../src/commands/registry/install.js');
154
- await installPackage('software-full', { studioDir: STUDIO_DIR, force: true });
155
-
156
- const lf = JSON.parse(await readFile(resolve(STUDIO_DIR, 'registry.lock.json'), 'utf8'));
157
- expect(lf.installed['software-full']).toBeDefined();
158
- expect(lf.installed['repo-manager']).toBeDefined();
159
- expect(lf.installed['repo-manager'].required_by).toEqual(['software-full']);
160
- });
161
-
162
- it('updates required_by if dep already installed', async () => {
163
- // Pre-install repo-manager in the lockfile
164
- await mkdir(resolve(STUDIO_DIR, 'tools'), { recursive: true });
165
- await writeFile(resolve(STUDIO_DIR, 'tools', 'repo-manager.tool.yaml'), 'name: repo_manager\n');
166
- await writeFile(resolve(STUDIO_DIR, 'registry.lock.json'), JSON.stringify({
167
- installed: {
168
- 'repo-manager': { version: '1.0.0', type: 'tool', installed_at: '2026-02-28', sha256: 'abc' },
169
- },
170
- }));
171
-
172
- vi.doMock('../../../src/registry/cache.js', () => {
173
- class RegistryCache {
174
- read() { return Promise.resolve(MOCK_INDEX_WITH_DEPS); }
175
- write() { return Promise.resolve(undefined); }
176
- isFresh() { return Promise.resolve(true); }
177
- }
178
- return { RegistryCache };
179
- });
180
- vi.doMock('../../../src/commands/registry/sync.js', () => ({
181
- syncRegistry: vi.fn().mockResolvedValue(undefined),
182
- }));
183
-
184
- vi.stubGlobal('fetch', vi.fn()
185
- // 1. fetch template metadata
186
- .mockResolvedValueOnce({ ok: true, json: async () => MOCK_TEMPLATE_META_WITH_DEPS })
187
- // 2. download template dir
188
- .mockResolvedValueOnce({ ok: true, json: async () => [] })
189
- // 3. fetch repo-manager metadata for resolver
190
- .mockResolvedValueOnce({ ok: true, json: async () => MOCK_TOOL_META }),
191
- );
192
-
193
- const { installPackage } = await import('../../../src/commands/registry/install.js');
194
- await installPackage('software-full', { studioDir: STUDIO_DIR, force: true });
195
-
196
- const lf = JSON.parse(await readFile(resolve(STUDIO_DIR, 'registry.lock.json'), 'utf8'));
197
- // repo-manager was not reinstalled but required_by updated
198
- expect(lf.installed['repo-manager'].required_by).toContain('software-full');
199
- });
200
- });
@@ -1,56 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { rm, mkdir, writeFile } from 'node:fs/promises';
3
- import { resolve, join } from 'node:path';
4
-
5
- const TMP = resolve(import.meta.dirname, '.tmp-publish');
6
-
7
- const VALID_METADATA = JSON.stringify({
8
- name: 'my-tool',
9
- version: '1.0.0',
10
- description: 'My custom tool',
11
- author: 'test-user',
12
- license: 'MIT',
13
- type: 'tool',
14
- tags: ['test'],
15
- studio_version: '>=7.0.0',
16
- }, null, 2);
17
-
18
- beforeEach(async () => { await mkdir(TMP, { recursive: true }); });
19
- afterEach(async () => { await rm(TMP, { recursive: true, force: true }); });
20
-
21
- describe('validatePublishPayload', () => {
22
- it('accepts valid single-file package with metadata.json in same dir', async () => {
23
- const toolPath = join(TMP, 'my-tool.tool.yaml');
24
- await writeFile(toolPath, 'name: my-tool\n');
25
- await writeFile(join(TMP, 'metadata.json'), VALID_METADATA);
26
-
27
- const { validatePublishPayload } = await import('../../../src/commands/registry/publish.js');
28
- const result = await validatePublishPayload(toolPath);
29
- expect(result.name).toBe('my-tool');
30
- expect(result.type).toBe('tool');
31
- });
32
-
33
- it('rejects when metadata.json is missing', async () => {
34
- const toolPath = join(TMP, 'sub', 'my-tool.tool.yaml');
35
- await mkdir(join(TMP, 'sub'), { recursive: true });
36
- await writeFile(toolPath, 'name: my-tool\n');
37
- // No metadata.json
38
-
39
- const { validatePublishPayload } = await import('../../../src/commands/registry/publish.js');
40
- await expect(validatePublishPayload(toolPath)).rejects.toThrow('metadata.json');
41
- });
42
-
43
- it('rejects when metadata is missing required fields', async () => {
44
- const toolPath = join(TMP, 'my-tool.tool.yaml');
45
- await writeFile(toolPath, 'name: my-tool\n');
46
- await writeFile(join(TMP, 'metadata.json'), JSON.stringify({ name: 'my-tool' }));
47
-
48
- const { validatePublishPayload } = await import('../../../src/commands/registry/publish.js');
49
- await expect(validatePublishPayload(toolPath)).rejects.toThrow('Missing required');
50
- });
51
-
52
- it('rejects when package file does not exist', async () => {
53
- const { validatePublishPayload } = await import('../../../src/commands/registry/publish.js');
54
- await expect(validatePublishPayload(join(TMP, 'nonexistent.tool.yaml'))).rejects.toThrow('does not exist');
55
- });
56
- });
@@ -1,103 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { rm, mkdir, writeFile, readFile } from 'node:fs/promises';
3
- import { resolve, join } from 'node:path';
4
-
5
- const TMP = resolve(import.meta.dirname, '.tmp-remove');
6
- const STUDIO = join(TMP, '.studio');
7
-
8
- beforeEach(async () => {
9
- await mkdir(resolve(STUDIO, 'tools'), { recursive: true });
10
- await writeFile(resolve(STUDIO, 'tools', 'my-tool.tool.yaml'), 'name: my-tool\n');
11
- await writeFile(resolve(STUDIO, 'registry.lock.json'), JSON.stringify({
12
- installed: {
13
- 'my-tool': { version: '1.0.0', type: 'tool', installed_at: '2026-02-28', sha256: 'abc' },
14
- },
15
- }));
16
- });
17
- afterEach(async () => { await rm(TMP, { recursive: true, force: true }); });
18
-
19
- describe('removePackage', () => {
20
- it('removes the file and lockfile entry', async () => {
21
- const { removePackage } = await import('../../../src/commands/registry/remove.js');
22
- await removePackage('my-tool', { studioDir: STUDIO });
23
- const lf = JSON.parse(await readFile(resolve(STUDIO, 'registry.lock.json'), 'utf8'));
24
- expect(lf.installed['my-tool']).toBeUndefined();
25
- });
26
-
27
- it('errors if package not installed', async () => {
28
- const { removePackage } = await import('../../../src/commands/registry/remove.js');
29
- await expect(removePackage('nonexistent', { studioDir: STUDIO })).rejects.toThrow('not installed');
30
- });
31
- });
32
-
33
- describe('removePackage — required_by protection', () => {
34
- beforeEach(async () => {
35
- await mkdir(resolve(STUDIO, 'tools'), { recursive: true });
36
- await writeFile(resolve(STUDIO, 'tools', 'repo-manager.tool.yaml'), 'name: repo_manager\n');
37
- await writeFile(resolve(STUDIO, 'registry.lock.json'), JSON.stringify({
38
- installed: {
39
- 'repo-manager': {
40
- version: '1.0.0', type: 'tool', installed_at: '2026-02-28', sha256: 'abc',
41
- required_by: ['software-full'],
42
- },
43
- },
44
- }));
45
- });
46
- afterEach(async () => {
47
- await rm(TMP, { recursive: true, force: true });
48
- vi.resetModules();
49
- });
50
-
51
- it('throws if package is required by another installed package', async () => {
52
- const { removePackage } = await import('../../../src/commands/registry/remove.js');
53
- await expect(removePackage('repo-manager', { studioDir: STUDIO }))
54
- .rejects.toThrow(/required by.*software-full/i);
55
- });
56
- });
57
-
58
- describe('removePackage — orphan cleanup', () => {
59
- beforeEach(async () => {
60
- await mkdir(resolve(STUDIO, 'tools'), { recursive: true });
61
- await mkdir(resolve(STUDIO, 'projects', 'software-full'), { recursive: true });
62
- await writeFile(resolve(STUDIO, 'tools', 'repo-manager.tool.yaml'), 'name: repo_manager\n');
63
- await writeFile(resolve(STUDIO, 'registry.lock.json'), JSON.stringify({
64
- installed: {
65
- 'software-full': {
66
- version: '2.0.0', type: 'template', installed_at: '2026-02-28', sha256: 'def',
67
- required_by: [],
68
- },
69
- 'repo-manager': {
70
- version: '1.0.0', type: 'tool', installed_at: '2026-02-28', sha256: 'abc',
71
- required_by: ['software-full'],
72
- },
73
- },
74
- }));
75
- });
76
- afterEach(async () => {
77
- await rm(TMP, { recursive: true, force: true });
78
- vi.resetModules();
79
- vi.restoreAllMocks();
80
- });
81
-
82
- it('prompts about orphans and removes them when confirmed', async () => {
83
- vi.doMock('@inquirer/prompts', () => ({ confirm: vi.fn().mockResolvedValue(true) }));
84
- vi.resetModules();
85
- const { removePackage } = await import('../../../src/commands/registry/remove.js');
86
- await removePackage('software-full', { studioDir: STUDIO });
87
-
88
- const lf = JSON.parse(await readFile(resolve(STUDIO, 'registry.lock.json'), 'utf8'));
89
- expect(lf.installed['software-full']).toBeUndefined();
90
- expect(lf.installed['repo-manager']).toBeUndefined();
91
- });
92
-
93
- it('leaves orphans installed when user declines', async () => {
94
- vi.doMock('@inquirer/prompts', () => ({ confirm: vi.fn().mockResolvedValue(false) }));
95
- vi.resetModules();
96
- const { removePackage } = await import('../../../src/commands/registry/remove.js');
97
- await removePackage('software-full', { studioDir: STUDIO });
98
-
99
- const lf = JSON.parse(await readFile(resolve(STUDIO, 'registry.lock.json'), 'utf8'));
100
- expect(lf.installed['software-full']).toBeUndefined();
101
- expect(lf.installed['repo-manager']).toBeDefined();
102
- });
103
- });
@@ -1,44 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import type { PackageEntry } from '../../../src/registry/types.js';
3
-
4
- const MOCK_PACKAGES: PackageEntry[] = [
5
- { name: 'software', type: 'template', version: '1.0.0', description: 'Code generation', author: 'studio-core', license: 'MIT', tags: ['software', 'code'], studio_version: null, downloads: 10 },
6
- { name: 'content', type: 'template', version: '1.0.0', description: 'Content creation', author: 'studio-core', license: 'MIT', tags: ['content', 'writing'], studio_version: null, downloads: 5 },
7
- { name: 'linear', type: 'integration', version: '1.0.0', description: 'Linear integration', author: 'studio-core', license: 'MIT', tags: ['linear'], studio_version: null, downloads: 3 },
8
- ];
9
-
10
- describe('searchPackages', () => {
11
- it('filters by name match', async () => {
12
- const { searchPackages } = await import('../../../src/commands/registry/search.js');
13
- const results = searchPackages(MOCK_PACKAGES, 'software');
14
- expect(results).toHaveLength(1);
15
- expect(results[0].name).toBe('software');
16
- });
17
-
18
- it('filters by description match', async () => {
19
- const { searchPackages } = await import('../../../src/commands/registry/search.js');
20
- const results = searchPackages(MOCK_PACKAGES, 'creation');
21
- expect(results).toHaveLength(1);
22
- expect(results[0].name).toBe('content');
23
- });
24
-
25
- it('filters by type', async () => {
26
- const { searchPackages } = await import('../../../src/commands/registry/search.js');
27
- const results = searchPackages(MOCK_PACKAGES, undefined, 'integration');
28
- expect(results).toHaveLength(1);
29
- expect(results[0].name).toBe('linear');
30
- });
31
-
32
- it('returns all packages when no query and no type', async () => {
33
- const { searchPackages } = await import('../../../src/commands/registry/search.js');
34
- const results = searchPackages(MOCK_PACKAGES);
35
- expect(results).toHaveLength(3);
36
- });
37
-
38
- it('filters by tag match', async () => {
39
- const { searchPackages } = await import('../../../src/commands/registry/search.js');
40
- const results = searchPackages(MOCK_PACKAGES, 'writing');
41
- expect(results).toHaveLength(1);
42
- expect(results[0].name).toBe('content');
43
- });
44
- });
@@ -1,37 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { rm, mkdir, readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
-
5
- const TMP = resolve(import.meta.dirname, '.tmp-sync');
6
- const MOCK_INDEX = { generated_at: '2026-02-28T00:00:00Z', version: '1', packages: [] };
7
-
8
- beforeEach(async () => {
9
- await mkdir(TMP, { recursive: true });
10
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => MOCK_INDEX }));
11
- });
12
- afterEach(async () => {
13
- await rm(TMP, { recursive: true, force: true });
14
- vi.unstubAllGlobals();
15
- });
16
-
17
- describe('syncRegistry', () => {
18
- it('writes index.json to cache dir when forced', async () => {
19
- const { syncRegistry } = await import('../../../src/commands/registry/sync.js');
20
- await syncRegistry({ cacheDir: TMP, force: true });
21
- const raw = await readFile(resolve(TMP, 'index.json'), 'utf8');
22
- const data = JSON.parse(raw);
23
- expect(data.version).toBe('1');
24
- });
25
-
26
- it('skips sync if cache is fresh and force=false', async () => {
27
- // Write a fresh cache first
28
- const { RegistryCache } = await import('../../../src/registry/cache.js');
29
- const cache = new RegistryCache(TMP);
30
- await cache.write(MOCK_INDEX);
31
- vi.mocked(fetch).mockClear();
32
-
33
- const { syncRegistry } = await import('../../../src/commands/registry/sync.js');
34
- await syncRegistry({ cacheDir: TMP, force: false });
35
- expect(vi.mocked(fetch)).not.toHaveBeenCalled();
36
- });
37
- });
@@ -1,62 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { rm, mkdir, writeFile } from 'node:fs/promises';
3
- import { resolve, join } from 'node:path';
4
- import type { RegistryIndex } from '../../../src/registry/types.js';
5
-
6
- const TMP = resolve(import.meta.dirname, '.tmp-update');
7
- const STUDIO = join(TMP, '.studio');
8
-
9
- const MOCK_INDEX: RegistryIndex = {
10
- generated_at: '2026-02-28T00:00:00Z',
11
- version: '1',
12
- packages: [
13
- { name: 'linear', type: 'integration', version: '2.0.0', description: 'Linear', author: 'studio-core', license: 'MIT', tags: [], studio_version: null, downloads: 0 },
14
- ],
15
- };
16
-
17
- vi.mock('../../../src/commands/registry/sync.js', () => ({
18
- syncRegistry: vi.fn().mockResolvedValue(undefined),
19
- }));
20
- vi.mock('../../../src/registry/cache.js', () => {
21
- class RegistryCache {
22
- read() { return Promise.resolve(MOCK_INDEX); }
23
- write() { return Promise.resolve(undefined); }
24
- isFresh() { return Promise.resolve(true); }
25
- }
26
- return { RegistryCache };
27
- });
28
-
29
- beforeEach(async () => {
30
- await mkdir(STUDIO, { recursive: true });
31
- await writeFile(resolve(STUDIO, 'registry.lock.json'), JSON.stringify({
32
- installed: {
33
- 'linear': { version: '1.0.0', type: 'integration', installed_at: '2026-02-28', sha256: 'old' },
34
- },
35
- }));
36
- });
37
- afterEach(async () => {
38
- await rm(TMP, { recursive: true, force: true });
39
- vi.resetModules();
40
- vi.restoreAllMocks();
41
- });
42
-
43
- describe('outdatedPackages', () => {
44
- it('returns packages with newer versions in registry', async () => {
45
- const { outdatedPackages } = await import('../../../src/commands/registry/update.js');
46
- const result = await outdatedPackages({ studioDir: STUDIO });
47
- expect(result).toHaveLength(1);
48
- expect(result[0]).toMatchObject({ name: 'linear', installed: '1.0.0', latest: '2.0.0' });
49
- });
50
-
51
- it('returns empty when all packages up to date', async () => {
52
- // Update lockfile to match registry version
53
- await writeFile(resolve(STUDIO, 'registry.lock.json'), JSON.stringify({
54
- installed: {
55
- 'linear': { version: '2.0.0', type: 'integration', installed_at: '2026-02-28', sha256: 'new' },
56
- },
57
- }));
58
- const { outdatedPackages } = await import('../../../src/commands/registry/update.js');
59
- const result = await outdatedPackages({ studioDir: STUDIO });
60
- expect(result).toHaveLength(0);
61
- });
62
- });