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

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 (91) hide show
  1. package/package.json +8 -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/templates/.studiorc.yaml +0 -16
  50. package/templates/projects/blank/metadata.json +0 -7
  51. package/templates/studio-config.yaml +0 -14
  52. package/tests/__stubs__/studio-runner.ts +0 -104
  53. package/tests/commands/api.test.ts +0 -110
  54. package/tests/commands/config.test.ts +0 -221
  55. package/tests/commands/init.test.ts +0 -919
  56. package/tests/commands/install.test.ts +0 -52
  57. package/tests/commands/integrations.test.ts +0 -158
  58. package/tests/commands/ollama.test.ts +0 -139
  59. package/tests/commands/project.test.ts +0 -179
  60. package/tests/commands/registry/audit.test.ts +0 -56
  61. package/tests/commands/registry/install.test.ts +0 -200
  62. package/tests/commands/registry/publish.test.ts +0 -56
  63. package/tests/commands/registry/remove.test.ts +0 -103
  64. package/tests/commands/registry/search.test.ts +0 -44
  65. package/tests/commands/registry/sync.test.ts +0 -37
  66. package/tests/commands/registry/update.test.ts +0 -62
  67. package/tests/commands/replay.test.ts +0 -283
  68. package/tests/commands/template/validate.test.ts +0 -150
  69. package/tests/commands/templates.test.ts +0 -42
  70. package/tests/commands/tools.test.ts +0 -106
  71. package/tests/config.test.ts +0 -142
  72. package/tests/formatter.test.ts +0 -158
  73. package/tests/integration/sigint.test.ts +0 -188
  74. package/tests/models-cache.test.ts +0 -250
  75. package/tests/output/file-changes.test.ts +0 -178
  76. package/tests/output/formatters.test.ts +0 -448
  77. package/tests/output/progress-spinner.test.ts +0 -232
  78. package/tests/output/progress-timer.test.ts +0 -230
  79. package/tests/provider-validator.test.ts +0 -182
  80. package/tests/registry/cache.test.ts +0 -66
  81. package/tests/registry/client.test.ts +0 -70
  82. package/tests/registry/lockfile.test.ts +0 -87
  83. package/tests/registry/resolver.test.ts +0 -122
  84. package/tests/run-logger-events.test.ts +0 -326
  85. package/tests/run-logger.test.ts +0 -64
  86. package/tests/run-store-factory.test.ts +0 -51
  87. package/tests/studio-dir.test.ts +0 -31
  88. package/tests/utils/input-wizard.test.ts +0 -153
  89. package/tests/utils/placeholders.test.ts +0 -36
  90. package/tsconfig.json +0 -24
  91. 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
- });