@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,110 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest';
2
- import { writeFile, rm } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
-
6
- // Use a unique fake home dir so tests don't collide with other test files using /tmp
7
- const FAKE_HOME = join(tmpdir(), `.studio-api-test-${process.pid}`);
8
-
9
- vi.mock('node:os', async (importOriginal) => {
10
- const actual = await importOriginal<typeof import('node:os')>();
11
- return { ...actual, homedir: () => FAKE_HOME };
12
- });
13
-
14
- // Import after mocking
15
- const { apiStopCommand, apiStatusCommand, writePid, readPid, clearPid, isProcessAlive } = await import('../../src/commands/api.js');
16
-
17
- afterEach(async () => {
18
- await clearPid();
19
- // Remove the .studio dir created by writePid
20
- await rm(join(FAKE_HOME, '.studio'), { recursive: true, force: true });
21
- });
22
-
23
- describe('writePid / readPid / clearPid', () => {
24
- it('writes and reads PID with port', async () => {
25
- await writePid(3700);
26
- const entry = await readPid();
27
- expect(entry).not.toBeNull();
28
- expect(entry!.pid).toBe(process.pid);
29
- expect(entry!.port).toBe(3700);
30
- });
31
-
32
- it('readPid returns null when no file exists', async () => {
33
- const entry = await readPid();
34
- expect(entry).toBeNull();
35
- });
36
-
37
- it('clearPid removes the file', async () => {
38
- await writePid(3700);
39
- await clearPid();
40
- const entry = await readPid();
41
- expect(entry).toBeNull();
42
- });
43
- });
44
-
45
- describe('isProcessAlive', () => {
46
- it('returns true for the current process', () => {
47
- expect(isProcessAlive(process.pid)).toBe(true);
48
- });
49
-
50
- it('returns false for a non-existent PID', () => {
51
- // PID 999999999 is very unlikely to exist
52
- expect(isProcessAlive(999999999)).toBe(false);
53
- });
54
- });
55
-
56
- describe('apiStopCommand', () => {
57
- it('prints "not running" when no PID file', async () => {
58
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
59
- await apiStopCommand();
60
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
61
- consoleSpy.mockRestore();
62
- });
63
-
64
- it('prints "stale PID" and clears file when process is dead', async () => {
65
- await writePid(3700);
66
- // Overwrite with a dead PID
67
- const pidFile = join(FAKE_HOME, '.studio', 'api.pid');
68
- await writeFile(pidFile, '999999999:3700', 'utf-8');
69
-
70
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
71
- await apiStopCommand();
72
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('stale'));
73
- expect(await readPid()).toBeNull();
74
- consoleSpy.mockRestore();
75
- });
76
-
77
- it('sends SIGTERM and clears PID when process is the current process', async () => {
78
- await writePid(3700);
79
- const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
80
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
81
-
82
- await apiStopCommand();
83
-
84
- expect(killSpy).toHaveBeenCalledWith(process.pid, 'SIGTERM');
85
- expect(await readPid()).toBeNull();
86
- killSpy.mockRestore();
87
- consoleSpy.mockRestore();
88
- });
89
- });
90
-
91
- describe('apiStatusCommand', () => {
92
- it('prints "not running" when no PID file', async () => {
93
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
94
- await apiStatusCommand({});
95
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
96
- consoleSpy.mockRestore();
97
- });
98
-
99
- it('prints "running" when process is alive and health check succeeds', async () => {
100
- await writePid(3700);
101
- // Override with current PID (alive) and mock fetch
102
- global.fetch = vi.fn().mockResolvedValue({ ok: true }) as unknown as typeof fetch;
103
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
104
-
105
- await apiStatusCommand({});
106
-
107
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
108
- consoleSpy.mockRestore();
109
- });
110
- });
@@ -1,221 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mkdir, rm, readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
- import * as yaml from 'js-yaml';
5
- import {
6
- getConfigValue,
7
- setConfigValue,
8
- maskSecrets,
9
- PROVIDERS,
10
- validateApiKeyForProvider,
11
- addProviderConfig,
12
- isProviderConfigured,
13
- } from '../../src/commands/config.js';
14
-
15
- const TMP = resolve(import.meta.dirname, '.tmp-config-cmd-test');
16
- const STUDIO_DIR = resolve(TMP, '.studio');
17
-
18
- beforeEach(async () => { await mkdir(STUDIO_DIR, { recursive: true }); });
19
- afterEach(async () => { await rm(TMP, { recursive: true, force: true }); });
20
-
21
- describe('getConfigValue', () => {
22
- it('gets a nested value by dotted path', () => {
23
- const config = { defaults: { model: 'claude-haiku' } };
24
- expect(getConfigValue(config, 'defaults.model')).toBe('claude-haiku');
25
- });
26
-
27
- it('returns undefined for missing path', () => {
28
- expect(getConfigValue({}, 'defaults.model')).toBeUndefined();
29
- });
30
- });
31
-
32
- describe('setConfigValue', () => {
33
- it('sets a nested value by dotted path', () => {
34
- const config: Record<string, unknown> = {};
35
- setConfigValue(config, 'defaults.model', 'claude-sonnet');
36
- expect((config as any).defaults.model).toBe('claude-sonnet');
37
- });
38
-
39
- it('merges without destroying sibling keys', () => {
40
- const config = { defaults: { provider: 'anthropic', model: 'old' } };
41
- setConfigValue(config, 'defaults.model', 'new');
42
- expect((config as any).defaults.provider).toBe('anthropic');
43
- expect((config as any).defaults.model).toBe('new');
44
- });
45
- });
46
-
47
- describe('maskSecrets', () => {
48
- it('masks apiKey values', () => {
49
- const config = { providers: { anthropic: { apiKey: 'sk-ant-longkey' } } };
50
- const masked = maskSecrets(config);
51
- expect((masked as any).providers.anthropic.apiKey).toMatch(/\*\*\*/);
52
- expect((masked as any).providers.anthropic.apiKey).not.toContain('longkey');
53
- });
54
-
55
- it('preserves non-secret values', () => {
56
- const config = { defaults: { model: 'claude-haiku' } };
57
- expect((maskSecrets(config) as any).defaults.model).toBe('claude-haiku');
58
- });
59
- });
60
-
61
- describe('PROVIDERS', () => {
62
- it('includes anthropic, openai, google, ollama', () => {
63
- const ids = PROVIDERS.map((p) => p.id);
64
- expect(ids).toContain('anthropic');
65
- expect(ids).toContain('openai');
66
- expect(ids).toContain('google');
67
- expect(ids).toContain('ollama');
68
- });
69
-
70
- it('each provider has id, label, and defaultModel', () => {
71
- for (const p of PROVIDERS) {
72
- expect(typeof p.id).toBe('string');
73
- expect(typeof p.label).toBe('string');
74
- expect(typeof p.defaultModel).toBe('string');
75
- }
76
- });
77
- });
78
-
79
- describe('validateApiKeyForProvider', () => {
80
- it('accepts a valid Anthropic key (sk-ant-...)', () => {
81
- expect(validateApiKeyForProvider('anthropic', 'sk-ant-api03-abc123')).toBe(true);
82
- });
83
-
84
- it('rejects an Anthropic key with wrong prefix', () => {
85
- const result = validateApiKeyForProvider('anthropic', 'sk-wrong');
86
- expect(typeof result).toBe('string');
87
- expect(result).toContain('sk-ant-');
88
- });
89
-
90
- it('accepts a valid OpenAI key (sk-...)', () => {
91
- expect(validateApiKeyForProvider('openai', 'sk-proj-abc123')).toBe(true);
92
- });
93
-
94
- it('rejects an OpenAI key with wrong prefix', () => {
95
- const result = validateApiKeyForProvider('openai', 'wrong-key');
96
- expect(typeof result).toBe('string');
97
- expect(result).toContain('sk-');
98
- });
99
-
100
- it('rejects an Anthropic-prefixed key when provider is openai', () => {
101
- const result = validateApiKeyForProvider('openai', 'sk-ant-api03-abc');
102
- expect(typeof result).toBe('string');
103
- });
104
-
105
- it('accepts a valid Google key (AIza...)', () => {
106
- expect(validateApiKeyForProvider('google', 'AIzaSyABC123')).toBe(true);
107
- });
108
-
109
- it('rejects a Google key with wrong prefix', () => {
110
- const result = validateApiKeyForProvider('google', 'wrong-key');
111
- expect(typeof result).toBe('string');
112
- expect(result).toContain('AIza');
113
- });
114
-
115
- it('accepts any value for ollama provider (no validation)', () => {
116
- expect(validateApiKeyForProvider('ollama', 'http://localhost:11434')).toBe(true);
117
- expect(validateApiKeyForProvider('ollama', '')).toBe(true);
118
- });
119
-
120
- it('accepts any value for unknown providers', () => {
121
- expect(validateApiKeyForProvider('future-provider', 'any-key')).toBe(true);
122
- });
123
- });
124
-
125
- describe('addProviderConfig', () => {
126
- const CONFIG_FILE = resolve(STUDIO_DIR, 'config.yaml');
127
-
128
- it('writes provider apiKey to config.yaml', async () => {
129
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-key', false);
130
-
131
- const raw = await readFile(CONFIG_FILE, 'utf-8');
132
- const parsed = yaml.load(raw) as Record<string, unknown>;
133
- const providers = parsed.providers as Record<string, { apiKey: string }>;
134
- expect(providers.anthropic.apiKey).toBe('sk-ant-key');
135
- });
136
-
137
- it('sets defaults when setDefault=true', async () => {
138
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-key', true);
139
-
140
- const raw = await readFile(CONFIG_FILE, 'utf-8');
141
- const parsed = yaml.load(raw) as Record<string, unknown>;
142
- const defaults = parsed.defaults as { provider: string; model: string };
143
- expect(defaults.provider).toBe('anthropic');
144
- expect(defaults.model).toBe('claude-sonnet-4-20250514');
145
- });
146
-
147
- it('does not touch defaults when setDefault=false', async () => {
148
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-key', false);
149
-
150
- const raw = await readFile(CONFIG_FILE, 'utf-8');
151
- const parsed = yaml.load(raw) as Record<string, unknown>;
152
- expect(parsed.defaults).toBeUndefined();
153
- });
154
-
155
- it('preserves existing provider when adding a second', async () => {
156
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-key', false);
157
- await addProviderConfig(CONFIG_FILE, 'openai', 'sk-openai-key', false);
158
-
159
- const raw = await readFile(CONFIG_FILE, 'utf-8');
160
- const parsed = yaml.load(raw) as Record<string, unknown>;
161
- const providers = parsed.providers as Record<string, { apiKey: string }>;
162
- expect(providers.anthropic.apiKey).toBe('sk-ant-key');
163
- expect(providers.openai.apiKey).toBe('sk-openai-key');
164
- });
165
-
166
- it('overwrites existing provider key', async () => {
167
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-first', false);
168
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-second', false);
169
-
170
- const raw = await readFile(CONFIG_FILE, 'utf-8');
171
- const parsed = yaml.load(raw) as Record<string, unknown>;
172
- const providers = parsed.providers as Record<string, { apiKey: string }>;
173
- expect(providers.anthropic.apiKey).toBe('sk-ant-second');
174
- });
175
-
176
- it('creates config.yaml if it does not exist', async () => {
177
- // STUDIO_DIR exists (from beforeEach) but config.yaml does not
178
- await addProviderConfig(CONFIG_FILE, 'openai', 'sk-new-key', false);
179
-
180
- const raw = await readFile(CONFIG_FILE, 'utf-8');
181
- const parsed = yaml.load(raw) as Record<string, unknown>;
182
- const providers = parsed.providers as Record<string, { apiKey: string }>;
183
- expect(providers.openai.apiKey).toBe('sk-new-key');
184
- });
185
-
186
- it('uses correct defaultModel for openai', async () => {
187
- await addProviderConfig(CONFIG_FILE, 'openai', 'sk-openai-key', true);
188
-
189
- const raw = await readFile(CONFIG_FILE, 'utf-8');
190
- const parsed = yaml.load(raw) as Record<string, unknown>;
191
- const defaults = parsed.defaults as { model: string };
192
- expect(defaults.model).toBe('gpt-4o');
193
- });
194
-
195
- it('uses claude-sonnet fallback for unknown provider', async () => {
196
- await addProviderConfig(CONFIG_FILE, 'unknown-provider', 'some-key', true);
197
-
198
- const raw = await readFile(CONFIG_FILE, 'utf-8');
199
- const parsed = yaml.load(raw) as Record<string, unknown>;
200
- const defaults = parsed.defaults as { model: string };
201
- expect(defaults.model).toBe('claude-sonnet-4-20250514');
202
- });
203
- });
204
-
205
- describe('isProviderConfigured', () => {
206
- const CONFIG_FILE = resolve(STUDIO_DIR, 'config.yaml');
207
-
208
- it('returns false when config.yaml does not exist', async () => {
209
- expect(await isProviderConfigured(CONFIG_FILE, 'anthropic')).toBe(false);
210
- });
211
-
212
- it('returns false when provider not in config', async () => {
213
- await addProviderConfig(CONFIG_FILE, 'openai', 'sk-key', false);
214
- expect(await isProviderConfigured(CONFIG_FILE, 'anthropic')).toBe(false);
215
- });
216
-
217
- it('returns true when provider is in config', async () => {
218
- await addProviderConfig(CONFIG_FILE, 'anthropic', 'sk-ant-key', false);
219
- expect(await isProviderConfigured(CONFIG_FILE, 'anthropic')).toBe(true);
220
- });
221
- });