@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,175 +0,0 @@
1
- import { access, readFile, readdir } from 'node:fs/promises';
2
- import { join, basename } from 'node:path';
3
- import * as yaml from 'js-yaml';
4
- import { spawnSync } from 'node:child_process';
5
-
6
- export interface ValidationResult {
7
- valid: boolean;
8
- structuralErrors: string[];
9
- semanticErrors: string[];
10
- warnings: string[];
11
- }
12
-
13
- interface StageDefinition {
14
- name: string;
15
- agent?: string;
16
- contract?: string;
17
- }
18
-
19
- interface PipelineEntry {
20
- group?: string;
21
- stages?: StageDefinition[];
22
- name?: string;
23
- agent?: string;
24
- contract?: string;
25
- }
26
-
27
- function collectStages(entries: PipelineEntry[]): StageDefinition[] {
28
- const stages: StageDefinition[] = [];
29
- for (const entry of entries) {
30
- if (entry.group && Array.isArray(entry.stages)) {
31
- stages.push(...entry.stages);
32
- } else {
33
- stages.push(entry as StageDefinition);
34
- }
35
- }
36
- return stages;
37
- }
38
-
39
- async function pathExists(p: string): Promise<boolean> {
40
- try {
41
- await access(p);
42
- return true;
43
- } catch {
44
- return false;
45
- }
46
- }
47
-
48
- async function listYamlFiles(dir: string): Promise<string[]> {
49
- try {
50
- const entries = await readdir(dir);
51
- return entries.filter((e) => e.endsWith('.yaml') || e.endsWith('.yml'));
52
- } catch {
53
- return [];
54
- }
55
- }
56
-
57
- export async function validateTemplateDir(templatePath: string): Promise<ValidationResult> {
58
- const warnings: string[] = [];
59
-
60
- // ── Level 1: Structural ──────────────────────────────────────────────
61
-
62
- const structuralErrors: string[] = [];
63
-
64
- if (!(await pathExists(templatePath))) {
65
- return { valid: false, structuralErrors: [`Template directory not found: ${templatePath}`], semanticErrors: [], warnings };
66
- }
67
-
68
- const metaPath = join(templatePath, 'metadata.json');
69
- try {
70
- const raw = await readFile(metaPath, 'utf-8');
71
- const meta = JSON.parse(raw) as Record<string, unknown>;
72
- for (const field of ['name', 'version', 'description']) {
73
- if (!meta[field]) structuralErrors.push(`metadata.json: missing required field '${field}'`);
74
- }
75
- } catch (err) {
76
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
77
- structuralErrors.push('metadata.json: file not found');
78
- } else {
79
- structuralErrors.push(`metadata.json: ${(err as Error).message}`);
80
- }
81
- }
82
-
83
- const pipelinesDir = join(templatePath, 'pipelines');
84
-
85
- const agentsDir = join(templatePath, 'agents');
86
- const agentFiles = (await listYamlFiles(agentsDir)).filter((f) => f.endsWith('.agent.yaml'));
87
-
88
- const contractsDir = join(templatePath, 'contracts');
89
-
90
- if (structuralErrors.length > 0) {
91
- return { valid: false, structuralErrors, semanticErrors: [], warnings };
92
- }
93
-
94
- // ── Level 2: Semantic ────────────────────────────────────────────────
95
-
96
- const semanticErrors: string[] = [];
97
-
98
- const knownAgents = new Set(agentFiles.map((f) => basename(f, '.agent.yaml')));
99
- const contractFiles = (await listYamlFiles(contractsDir)).filter((f) => f.endsWith('.contract.yaml'));
100
- const knownContracts = new Set(contractFiles.map((f) => basename(f, '.contract.yaml')));
101
-
102
- const allYamlDirs: [string, string][] = [
103
- [pipelinesDir, 'pipelines'],
104
- [agentsDir, 'agents'],
105
- [contractsDir, 'contracts'],
106
- [join(templatePath, 'tools'), 'tools'],
107
- ];
108
-
109
- const parsedPipelines: Array<{ file: string; parsed: Record<string, unknown> }> = [];
110
-
111
- for (const [dir, label] of allYamlDirs) {
112
- const files = await listYamlFiles(dir);
113
- for (const file of files) {
114
- const filePath = join(dir, file);
115
- try {
116
- const content = await readFile(filePath, 'utf-8');
117
- const parsed = yaml.load(content);
118
- if (parsed === null || parsed === undefined) {
119
- semanticErrors.push(`${label}/${file}: YAML file is empty`);
120
- continue;
121
- }
122
- if (typeof parsed !== 'object' || Array.isArray(parsed)) {
123
- semanticErrors.push(`${label}/${file}: YAML file must be a mapping (object)`);
124
- continue;
125
- }
126
- if (label === 'pipelines') {
127
- parsedPipelines.push({ file, parsed: parsed as Record<string, unknown> });
128
- }
129
- } catch (err) {
130
- semanticErrors.push(`${label}/${file}: YAML parse error — ${(err as Error).message}`);
131
- }
132
- }
133
- }
134
-
135
- for (const { file, parsed } of parsedPipelines) {
136
- if (!parsed || !Array.isArray(parsed.stages)) continue;
137
- const stages = collectStages(parsed.stages as PipelineEntry[]);
138
- for (const stage of stages) {
139
- if (stage.contract && !knownContracts.has(stage.contract)) {
140
- semanticErrors.push(
141
- `pipelines/${file}: stage '${stage.name ?? '?'}' references contract '${stage.contract}' which does not exist in contracts/`
142
- );
143
- }
144
- if (stage.agent && !knownAgents.has(stage.agent)) {
145
- semanticErrors.push(
146
- `pipelines/${file}: stage '${stage.name ?? '?'}' references agent '${stage.agent}' which does not exist in agents/`
147
- );
148
- }
149
- }
150
- }
151
-
152
- if (semanticErrors.length > 0) {
153
- return { valid: false, structuralErrors: [], semanticErrors, warnings };
154
- }
155
-
156
- // ── Level 3: Optional TypeScript compilation ─────────────────────────
157
-
158
- const tsConfigPath = join(templatePath, 'tsconfig.json');
159
- if (await pathExists(tsConfigPath)) {
160
- // spawnSync is intentional: this is a CLI command, not a server.
161
- // Blocking during tsc --noEmit is acceptable for a validation step.
162
- const result = spawnSync('tsc', ['--noEmit'], { cwd: templatePath, encoding: 'utf-8' });
163
- if (result.status !== 0) {
164
- const output = (result.stdout ?? '') + (result.stderr ?? '');
165
- semanticErrors.push(`TypeScript compilation failed:\n${output.trim()}`);
166
- }
167
- }
168
-
169
- const prismaSchema = join(templatePath, 'prisma', 'schema.prisma');
170
- if (await pathExists(prismaSchema)) {
171
- warnings.push('prisma/schema.prisma found (migration testing not automated — run prisma validate manually)');
172
- }
173
-
174
- return { valid: semanticErrors.length === 0, structuralErrors: [], semanticErrors, warnings };
175
- }
@@ -1,99 +0,0 @@
1
- import { readdir, readFile } from 'node:fs/promises';
2
- import { resolve, join } from 'node:path';
3
- import chalk from 'chalk';
4
- import { RegistryCache } from '../registry/cache.js';
5
- import { syncRegistry } from './registry/sync.js';
6
-
7
- const PROJECTS_TEMPLATES_DIR = resolve(import.meta.dirname, '../../templates/projects');
8
-
9
- export interface TemplateMetadata {
10
- name: string;
11
- version: string;
12
- description: string;
13
- author?: string;
14
- tags?: string[];
15
- type?: string;
16
- studio_version?: string;
17
- pipelines?: string[];
18
- tools_included?: string[];
19
- }
20
-
21
- export async function listTemplates(): Promise<TemplateMetadata[]> {
22
- // 1. Load local (bundled) templates
23
- const localTemplates: TemplateMetadata[] = [];
24
- try {
25
- const entries = await readdir(PROJECTS_TEMPLATES_DIR, { withFileTypes: true });
26
- const dirs = entries
27
- .filter((e) => e.isDirectory())
28
- .map((e) => e.name)
29
- .sort();
30
-
31
- for (const dir of dirs) {
32
- try {
33
- const metaPath = join(PROJECTS_TEMPLATES_DIR, dir, 'metadata.json');
34
- const meta = JSON.parse(await readFile(metaPath, 'utf-8')) as TemplateMetadata;
35
- localTemplates.push(meta);
36
- } catch {
37
- // Skip malformed or missing metadata
38
- }
39
- }
40
- } catch {
41
- // templates dir missing — continue
42
- }
43
-
44
- // 2. Merge with registry templates (sync silently if stale, fall back gracefully)
45
- const registryTemplates: TemplateMetadata[] = [];
46
- try {
47
- await syncRegistry({ force: false, silent: true });
48
- const cache = new RegistryCache();
49
- const index = await cache.read();
50
- if (index) {
51
- const localNames = new Set(localTemplates.map((t) => t.name));
52
- for (const pkg of index.packages) {
53
- if (pkg.type === 'template' && !localNames.has(pkg.name)) {
54
- registryTemplates.push({
55
- name: pkg.name,
56
- version: pkg.version,
57
- description: pkg.description,
58
- author: pkg.author,
59
- tags: pkg.tags,
60
- studio_version: pkg.studio_version ?? undefined,
61
- });
62
- }
63
- }
64
- }
65
- } catch {
66
- // Registry unreachable — show local templates only
67
- }
68
-
69
- return [...localTemplates, ...registryTemplates];
70
- }
71
-
72
- export async function templatesCommand(action: string, _args: string[]): Promise<void> {
73
- try {
74
- switch (action) {
75
- case 'list': {
76
- const templates = await listTemplates();
77
- if (templates.length === 0) {
78
- console.log(chalk.yellow('No templates available.'));
79
- return;
80
- }
81
- console.log('\nAvailable templates:\n');
82
- const maxLen = Math.max(...templates.map((t) => t.name.length));
83
- for (const t of templates) {
84
- console.log(` ${t.name.padEnd(maxLen + 2)}${chalk.gray(t.description)}`);
85
- }
86
- console.log('');
87
- console.log(`Run: ${chalk.cyan('studio init --template <name>')}`);
88
- console.log('');
89
- break;
90
- }
91
- default:
92
- console.error(`Unknown templates action: ${action}. Available: list`);
93
- process.exit(1);
94
- }
95
- } catch (error) {
96
- console.error('Error:', error instanceof Error ? error.message : error);
97
- process.exit(1);
98
- }
99
- }
@@ -1,227 +0,0 @@
1
- import { readdir, readFile, writeFile, unlink, mkdir, access } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
3
- import chalk from 'chalk';
4
- import { checkbox } from '@inquirer/prompts';
5
- import { loadConfig } from '../config.js';
6
- import { listAvailableToolTemplates, getBundledToolTemplate } from '@studio-foundation/runner';
7
-
8
- export function getToolsDir(studioDir: string): string {
9
- return resolve(studioDir, 'tools');
10
- }
11
-
12
- export async function toolsAddDirect(
13
- studioDir: string,
14
- toolNames: string[]
15
- ): Promise<{ installed: string[]; skipped: string[] }> {
16
- const toolsDir = getToolsDir(studioDir);
17
- await mkdir(toolsDir, { recursive: true });
18
-
19
- const installed: string[] = [];
20
- const skipped: string[] = [];
21
-
22
- for (const name of toolNames) {
23
- const templateContent = await getBundledToolTemplate(name);
24
- if (!templateContent) {
25
- const available = await listAvailableToolTemplates();
26
- throw new Error(`Unknown tool '${name}'. Available: ${available.map((t) => t.name).join(', ')}`);
27
- }
28
-
29
- const destPath = resolve(toolsDir, `${name}.tool.yaml`);
30
- const alreadyInstalled = await access(destPath).then(() => true).catch(() => false);
31
- if (alreadyInstalled) {
32
- skipped.push(name);
33
- continue;
34
- }
35
-
36
- await writeFile(destPath, templateContent, 'utf-8');
37
- installed.push(name);
38
- }
39
-
40
- return { installed, skipped };
41
- }
42
-
43
- export async function listAvailableTools(): Promise<{ name: string; description: string }[]> {
44
- return listAvailableToolTemplates();
45
- }
46
-
47
- export async function listTools(toolsDir: string): Promise<string[]> {
48
- try {
49
- const entries = await readdir(toolsDir);
50
- return entries
51
- .filter((f) => f.endsWith('.tool.yaml'))
52
- .map((f) => f.replace('.tool.yaml', ''))
53
- .sort();
54
- } catch {
55
- return [];
56
- }
57
- }
58
-
59
- async function resolveToolsDir(): Promise<string> {
60
- const config = await loadConfig();
61
- const studioDir = config.resolvedStudioDir;
62
-
63
- if (!studioDir) {
64
- console.error("Error: No .studio/ directory found. Run 'studio init' first.");
65
- process.exit(1);
66
- }
67
-
68
- return getToolsDir(studioDir);
69
- }
70
-
71
- export async function toolsCommand(
72
- action: string,
73
- args: string[]
74
- ): Promise<void> {
75
- try {
76
- switch (action) {
77
- case 'list': {
78
- const config = await loadConfig();
79
- const studioDir = config.resolvedStudioDir;
80
- if (!studioDir) {
81
- console.error("Error: No .studio/ directory found. Run 'studio init' first.");
82
- process.exit(1);
83
- }
84
- const toolsDir = getToolsDir(studioDir);
85
- const tools = await listTools(toolsDir);
86
-
87
- if (tools.length === 0) {
88
- console.log(chalk.yellow('No tools installed.'));
89
- console.log(' Run: studio tools add <name>');
90
- } else {
91
- console.log('\nInstalled tools:');
92
- for (const t of tools) {
93
- console.log(` - ${t}`);
94
- }
95
- }
96
-
97
- // Show installed plugins (from .studio/plugins/)
98
- const { loadPlugins: loadPluginManifests } = await import('@studio-foundation/runner');
99
- const pluginsDir = resolve(studioDir, 'plugins');
100
- const manifests = await loadPluginManifests(pluginsDir);
101
- if (manifests.length > 0) {
102
- console.log('\nInstalled plugins:');
103
- for (const m of manifests) {
104
- const serverNames = Object.keys(m.mcpServers);
105
- const skillCount = m.skills.length;
106
- const parts: string[] = [];
107
- if (serverNames.length > 0) parts.push(`MCP: ${serverNames.join(', ')}`);
108
- if (skillCount > 0) parts.push(`${skillCount} skill${skillCount !== 1 ? 's' : ''}`);
109
- console.log(` - ${m.name}${parts.length > 0 ? ` (${parts.join('; ')})` : ''}`);
110
- }
111
- }
112
- console.log('');
113
- break;
114
- }
115
-
116
- case 'add': {
117
- if (args.length === 0) {
118
- // Wizard mode
119
- const config = await loadConfig();
120
- const studioDir = config.resolvedStudioDir;
121
- if (!studioDir) {
122
- console.error("Error: No .studio/ directory found. Run 'studio init' first.");
123
- process.exit(1);
124
- }
125
-
126
- console.log('');
127
- const available = await listAvailableToolTemplates();
128
- const alreadyInstalled = await listTools(getToolsDir(studioDir));
129
-
130
- const choices = available.map((t) => ({
131
- value: t.name,
132
- name: `${t.name} — ${t.description}`,
133
- disabled: alreadyInstalled.includes(t.name) ? '(already installed)' : false,
134
- }));
135
-
136
- const selected: string[] = await checkbox({
137
- message: 'Select tools to install:',
138
- choices,
139
- });
140
-
141
- if (selected.length === 0) {
142
- console.log('No tools selected.');
143
- break;
144
- }
145
-
146
- console.log('\nInstalling tools...');
147
- const { installed, skipped } = await toolsAddDirect(studioDir, selected);
148
-
149
- for (const name of installed) {
150
- console.log(chalk.green(` ✓ ${name}.tool.yaml`));
151
- }
152
- for (const name of skipped) {
153
- console.log(chalk.yellow(` ⚠ ${name} already installed, skipping`));
154
- }
155
- console.log('');
156
- console.log(`Done! ${installed.length} tool${installed.length !== 1 ? 's' : ''} installed.`);
157
- break;
158
- }
159
-
160
- // Direct mode
161
- const config = await loadConfig();
162
- const studioDir = config.resolvedStudioDir!;
163
-
164
- const { installed, skipped } = await toolsAddDirect(studioDir, args);
165
-
166
- for (const name of installed) {
167
- console.log(chalk.green(` ✓ ${name}.tool.yaml`));
168
- }
169
- for (const name of skipped) {
170
- console.log(chalk.yellow(` ⚠ ${name} already installed, skipping`));
171
- }
172
- console.log('');
173
- if (installed.length > 0) {
174
- console.log(`Done! ${installed.length} tool${installed.length > 1 ? 's' : ''} installed.`);
175
- } else {
176
- console.log('No new tools installed.');
177
- }
178
- break;
179
- }
180
-
181
- case 'remove': {
182
- const name = args[0];
183
- if (!name) {
184
- console.error('Usage: studio tools remove <name>');
185
- process.exit(1);
186
- }
187
- const toolsDir = await resolveToolsDir();
188
- const toolPath = resolve(toolsDir, `${name}.tool.yaml`);
189
- try {
190
- await unlink(toolPath);
191
- console.log(chalk.green(`✓ Removed tool '${name}'`));
192
- } catch {
193
- console.error(`Error: Tool '${name}' not found`);
194
- process.exit(1);
195
- }
196
- break;
197
- }
198
-
199
- case 'info': {
200
- const name = args[0];
201
- if (!name) {
202
- console.error('Usage: studio tools info <name>');
203
- process.exit(1);
204
- }
205
- const toolsDir = await resolveToolsDir();
206
- const toolPath = resolve(toolsDir, `${name}.tool.yaml`);
207
- try {
208
- const content = await readFile(toolPath, 'utf-8');
209
- console.log(content);
210
- } catch {
211
- console.error(`Error: Tool '${name}' not found.`);
212
- process.exit(1);
213
- }
214
- break;
215
- }
216
-
217
- default:
218
- console.error(
219
- `Unknown tools action: ${action}. Available: list, add, remove, info`
220
- );
221
- process.exit(1);
222
- }
223
- } catch (error) {
224
- console.error('Error:', error instanceof Error ? error.message : error);
225
- process.exit(1);
226
- }
227
- }
@@ -1,127 +0,0 @@
1
- // cli/src/commands/users.ts
2
- import { randomBytes, randomUUID } from 'node:crypto';
3
- import { join } from 'node:path';
4
- import { mkdir } from 'node:fs/promises';
5
- import chalk from 'chalk';
6
- import { findStudioDir } from '../studio-dir.js';
7
- import { UserStore } from '@studio-foundation/api/user-store';
8
-
9
- async function getStore(): Promise<{ store: UserStore; close: () => void }> {
10
- const studioDir = await findStudioDir(process.cwd());
11
- if (!studioDir) throw new Error('No .studio/ directory found. Run studio init first.');
12
-
13
- const dbPath = join(studioDir, 'runs', 'runs.db');
14
- await mkdir(join(studioDir, 'runs'), { recursive: true });
15
- const store = new UserStore(dbPath);
16
- return { store, close: () => store.close() };
17
- }
18
-
19
- export async function usersCommand(
20
- subcommand: string,
21
- args: string[],
22
- options: { plan?: string },
23
- ): Promise<void> {
24
- switch (subcommand) {
25
- case 'list': {
26
- const { store, close } = await getStore();
27
- try {
28
- const users = store.listUsers();
29
- if (users.length === 0) {
30
- console.log(chalk.gray('No users found.'));
31
- } else {
32
- console.log(chalk.bold('Users:'));
33
- for (const { email, plan, id } of users) {
34
- console.log(` ${chalk.cyan(email)} — plan: ${chalk.yellow(plan)} — id: ${chalk.gray(id)}`);
35
- }
36
- }
37
- } finally {
38
- close();
39
- }
40
- break;
41
- }
42
-
43
- case 'add': {
44
- const email = args[0];
45
- if (!email) {
46
- console.error(chalk.red('Usage: studio users add <email> [--plan pro]'));
47
- process.exit(1);
48
- }
49
- const { store, close } = await getStore();
50
- try {
51
- if (store.getUserByEmail(email)) {
52
- console.error(chalk.red(`User ${email} already exists.`));
53
- process.exit(1);
54
- }
55
- const apiKey = randomBytes(32).toString('hex');
56
- const user = {
57
- id: randomUUID(),
58
- email,
59
- plan: options.plan ?? 'free',
60
- api_key: apiKey,
61
- created_at: new Date().toISOString(),
62
- };
63
- store.saveUser(user);
64
- console.log(chalk.green(`✓ User created: ${email} (plan: ${user.plan})`));
65
- console.log(chalk.bold('API Key (shown only once):'));
66
- console.log(chalk.yellow(apiKey));
67
- } finally {
68
- close();
69
- }
70
- break;
71
- }
72
-
73
- case 'remove': {
74
- const email = args[0];
75
- if (!email) {
76
- console.error(chalk.red('Usage: studio users remove <email>'));
77
- process.exit(1);
78
- }
79
- const { store, close } = await getStore();
80
- try {
81
- const user = store.getUserByEmail(email);
82
- if (!user) {
83
- console.error(chalk.red(`User ${email} not found.`));
84
- process.exit(1);
85
- }
86
- store.deleteUser(user.id);
87
- console.log(chalk.green(`✓ User ${email} deleted.`));
88
- } finally {
89
- close();
90
- }
91
- break;
92
- }
93
-
94
- case 'info': {
95
- const email = args[0];
96
- if (!email) {
97
- console.error(chalk.red('Usage: studio users info <email>'));
98
- process.exit(1);
99
- }
100
- const { store, close } = await getStore();
101
- try {
102
- const user = store.getUserByEmail(email);
103
- if (!user) {
104
- console.error(chalk.red(`User ${email} not found.`));
105
- process.exit(1);
106
- }
107
- const today = new Date().toISOString().slice(0, 10);
108
- const usage = store.getDailyUsage(user.id, today);
109
- console.log(chalk.bold(`User: ${user.email}`));
110
- console.log(` Plan: ${chalk.yellow(user.plan)}`);
111
- console.log(` ID: ${chalk.gray(user.id)}`);
112
- console.log(` Created: ${user.created_at}`);
113
- console.log(chalk.bold(`Today (${today}):`));
114
- console.log(` Runs: ${usage.runs_count}`);
115
- console.log(` Tokens: ${usage.tokens_used}`);
116
- } finally {
117
- close();
118
- }
119
- break;
120
- }
121
-
122
- default:
123
- console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
124
- console.error('Usage: studio users <list|add|remove|info>');
125
- process.exit(1);
126
- }
127
- }
@@ -1,46 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
3
- import chalk from 'chalk';
4
- import yaml from 'js-yaml';
5
- import type { OutputContract } from '@studio-foundation/contracts';
6
- import { validateSchema } from '@studio-foundation/engine';
7
-
8
- export async function validateCommand(
9
- contractPath: string,
10
- outputPath: string
11
- ): Promise<void> {
12
- try {
13
- // Load contract YAML
14
- const contractRaw = await readFile(resolve(contractPath), 'utf-8');
15
- const contract = yaml.load(contractRaw) as OutputContract;
16
-
17
- // Load output JSON
18
- const outputRaw = await readFile(resolve(outputPath), 'utf-8');
19
- const output: unknown = JSON.parse(outputRaw);
20
-
21
- // Validate
22
- const result = validateSchema(output, contract);
23
-
24
- if (result.valid) {
25
- console.log(chalk.green('✓ Valid'));
26
- } else {
27
- console.log(chalk.red('✗ Invalid'));
28
- console.log('');
29
- for (const error of result.errors) {
30
- console.log(chalk.red(` - ${error}`));
31
- }
32
- }
33
-
34
- if (result.warnings.length > 0) {
35
- console.log('');
36
- for (const warning of result.warnings) {
37
- console.log(chalk.yellow(` ⚠ ${warning}`));
38
- }
39
- }
40
-
41
- process.exit(result.valid ? 0 : 1);
42
- } catch (error) {
43
- console.error('Error:', error instanceof Error ? error.message : error);
44
- process.exit(1);
45
- }
46
- }