@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.
- package/package.json +8 -5
- package/ARCHITECTURE.md +0 -40
- package/src/commands/api.ts +0 -126
- package/src/commands/config.ts +0 -433
- package/src/commands/init.ts +0 -879
- package/src/commands/install.ts +0 -23
- package/src/commands/integrations.ts +0 -332
- package/src/commands/list.ts +0 -197
- package/src/commands/logs.ts +0 -119
- package/src/commands/ollama.ts +0 -168
- package/src/commands/project.ts +0 -167
- package/src/commands/registry/audit.ts +0 -87
- package/src/commands/registry/index.ts +0 -63
- package/src/commands/registry/install.ts +0 -222
- package/src/commands/registry/publish.ts +0 -112
- package/src/commands/registry/remove.ts +0 -89
- package/src/commands/registry/search.ts +0 -93
- package/src/commands/registry/sync.ts +0 -24
- package/src/commands/registry/update.ts +0 -63
- package/src/commands/replay.ts +0 -559
- package/src/commands/run.ts +0 -454
- package/src/commands/status.ts +0 -163
- package/src/commands/template/index.ts +0 -59
- package/src/commands/template/validate.ts +0 -175
- package/src/commands/templates.ts +0 -99
- package/src/commands/tools.ts +0 -227
- package/src/commands/users.ts +0 -127
- package/src/commands/validate.ts +0 -46
- package/src/config.ts +0 -101
- package/src/index.ts +0 -201
- package/src/models-cache.ts +0 -127
- package/src/output/file-changes.ts +0 -97
- package/src/output/formatter.ts +0 -85
- package/src/output/formatters.ts +0 -303
- package/src/output/logger.ts +0 -18
- package/src/output/parallel-progress.ts +0 -103
- package/src/output/progress.ts +0 -414
- package/src/provider-validator.ts +0 -109
- package/src/registry/cache.ts +0 -46
- package/src/registry/client.ts +0 -84
- package/src/registry/lockfile.ts +0 -68
- package/src/registry/resolver.ts +0 -113
- package/src/registry/types.ts +0 -66
- package/src/run-logger.ts +0 -66
- package/src/run-store-factory.ts +0 -30
- package/src/studio-dir.ts +0 -28
- package/src/utils/input-wizard.ts +0 -73
- package/src/utils/placeholders.ts +0 -10
- package/templates/.studiorc.yaml +0 -16
- package/templates/projects/blank/metadata.json +0 -7
- package/templates/studio-config.yaml +0 -14
- package/tests/__stubs__/studio-runner.ts +0 -104
- package/tests/commands/api.test.ts +0 -110
- package/tests/commands/config.test.ts +0 -221
- package/tests/commands/init.test.ts +0 -919
- package/tests/commands/install.test.ts +0 -52
- package/tests/commands/integrations.test.ts +0 -158
- package/tests/commands/ollama.test.ts +0 -139
- package/tests/commands/project.test.ts +0 -179
- package/tests/commands/registry/audit.test.ts +0 -56
- package/tests/commands/registry/install.test.ts +0 -200
- package/tests/commands/registry/publish.test.ts +0 -56
- package/tests/commands/registry/remove.test.ts +0 -103
- package/tests/commands/registry/search.test.ts +0 -44
- package/tests/commands/registry/sync.test.ts +0 -37
- package/tests/commands/registry/update.test.ts +0 -62
- package/tests/commands/replay.test.ts +0 -283
- package/tests/commands/template/validate.test.ts +0 -150
- package/tests/commands/templates.test.ts +0 -42
- package/tests/commands/tools.test.ts +0 -106
- package/tests/config.test.ts +0 -142
- package/tests/formatter.test.ts +0 -158
- package/tests/integration/sigint.test.ts +0 -188
- package/tests/models-cache.test.ts +0 -250
- package/tests/output/file-changes.test.ts +0 -178
- package/tests/output/formatters.test.ts +0 -448
- package/tests/output/progress-spinner.test.ts +0 -232
- package/tests/output/progress-timer.test.ts +0 -230
- package/tests/provider-validator.test.ts +0 -182
- package/tests/registry/cache.test.ts +0 -66
- package/tests/registry/client.test.ts +0 -70
- package/tests/registry/lockfile.test.ts +0 -87
- package/tests/registry/resolver.test.ts +0 -122
- package/tests/run-logger-events.test.ts +0 -326
- package/tests/run-logger.test.ts +0 -64
- package/tests/run-store-factory.test.ts +0 -51
- package/tests/studio-dir.test.ts +0 -31
- package/tests/utils/input-wizard.test.ts +0 -153
- package/tests/utils/placeholders.test.ts +0 -36
- package/tsconfig.json +0 -24
- 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
|
-
});
|