@studio-foundation/cli 0.3.0-beta.1
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/ARCHITECTURE.md +40 -0
- package/LICENSE +663 -0
- package/README.md +165 -0
- package/dist/commands/api.d.ts +16 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +118 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/config.d.ts +32 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +390 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +93 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +728 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/integrations.d.ts +14 -0
- package/dist/commands/integrations.d.ts.map +1 -0
- package/dist/commands/integrations.js +282 -0
- package/dist/commands/integrations.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +161 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +7 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +98 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/ollama.d.ts +6 -0
- package/dist/commands/ollama.d.ts.map +1 -0
- package/dist/commands/ollama.js +161 -0
- package/dist/commands/ollama.js.map +1 -0
- package/dist/commands/project.d.ts +38 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +137 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/registry/audit.d.ts +13 -0
- package/dist/commands/registry/audit.d.ts.map +1 -0
- package/dist/commands/registry/audit.js +65 -0
- package/dist/commands/registry/audit.js.map +1 -0
- package/dist/commands/registry/index.d.ts +3 -0
- package/dist/commands/registry/index.d.ts.map +1 -0
- package/dist/commands/registry/index.js +53 -0
- package/dist/commands/registry/index.js.map +1 -0
- package/dist/commands/registry/install.d.ts +17 -0
- package/dist/commands/registry/install.d.ts.map +1 -0
- package/dist/commands/registry/install.js +153 -0
- package/dist/commands/registry/install.js.map +1 -0
- package/dist/commands/registry/publish.d.ts +8 -0
- package/dist/commands/registry/publish.d.ts.map +1 -0
- package/dist/commands/registry/publish.js +94 -0
- package/dist/commands/registry/publish.js.map +1 -0
- package/dist/commands/registry/remove.d.ts +8 -0
- package/dist/commands/registry/remove.d.ts.map +1 -0
- package/dist/commands/registry/remove.js +82 -0
- package/dist/commands/registry/remove.js.map +1 -0
- package/dist/commands/registry/search.d.ts +9 -0
- package/dist/commands/registry/search.d.ts.map +1 -0
- package/dist/commands/registry/search.js +68 -0
- package/dist/commands/registry/search.js.map +1 -0
- package/dist/commands/registry/sync.d.ts +8 -0
- package/dist/commands/registry/sync.d.ts.map +1 -0
- package/dist/commands/registry/sync.js +19 -0
- package/dist/commands/registry/sync.js.map +1 -0
- package/dist/commands/registry/update.d.ts +15 -0
- package/dist/commands/registry/update.d.ts.map +1 -0
- package/dist/commands/registry/update.js +48 -0
- package/dist/commands/registry/update.js.map +1 -0
- package/dist/commands/replay.d.ts +36 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +453 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/run.d.ts +18 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +406 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +144 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/template/index.d.ts +2 -0
- package/dist/commands/template/index.d.ts.map +1 -0
- package/dist/commands/template/index.js +57 -0
- package/dist/commands/template/index.js.map +1 -0
- package/dist/commands/template/validate.d.ts +8 -0
- package/dist/commands/template/validate.d.ts.map +1 -0
- package/dist/commands/template/validate.js +135 -0
- package/dist/commands/template/validate.js.map +1 -0
- package/dist/commands/templates.d.ts +14 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +86 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +200 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/users.d.ts +4 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +123 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +39 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -0
- package/dist/models-cache.d.ts +13 -0
- package/dist/models-cache.d.ts.map +1 -0
- package/dist/models-cache.js +106 -0
- package/dist/models-cache.js.map +1 -0
- package/dist/output/file-changes.d.ts +15 -0
- package/dist/output/file-changes.d.ts.map +1 -0
- package/dist/output/file-changes.js +77 -0
- package/dist/output/file-changes.js.map +1 -0
- package/dist/output/formatter.d.ts +6 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +70 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/output/formatters.d.ts +42 -0
- package/dist/output/formatters.d.ts.map +1 -0
- package/dist/output/formatters.js +280 -0
- package/dist/output/formatters.js.map +1 -0
- package/dist/output/logger.d.ts +3 -0
- package/dist/output/logger.d.ts.map +1 -0
- package/dist/output/logger.js +15 -0
- package/dist/output/logger.js.map +1 -0
- package/dist/output/parallel-progress.d.ts +33 -0
- package/dist/output/parallel-progress.d.ts.map +1 -0
- package/dist/output/parallel-progress.js +85 -0
- package/dist/output/parallel-progress.js.map +1 -0
- package/dist/output/progress.d.ts +32 -0
- package/dist/output/progress.d.ts.map +1 -0
- package/dist/output/progress.js +393 -0
- package/dist/output/progress.js.map +1 -0
- package/dist/provider-validator.d.ts +15 -0
- package/dist/provider-validator.d.ts.map +1 -0
- package/dist/provider-validator.js +85 -0
- package/dist/provider-validator.js.map +1 -0
- package/dist/registry/cache.d.ts +10 -0
- package/dist/registry/cache.d.ts.map +1 -0
- package/dist/registry/cache.js +36 -0
- package/dist/registry/cache.js.map +1 -0
- package/dist/registry/client.d.ts +19 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +63 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/lockfile.d.ts +17 -0
- package/dist/registry/lockfile.d.ts.map +1 -0
- package/dist/registry/lockfile.js +61 -0
- package/dist/registry/lockfile.js.map +1 -0
- package/dist/registry/resolver.d.ts +13 -0
- package/dist/registry/resolver.d.ts.map +1 -0
- package/dist/registry/resolver.js +79 -0
- package/dist/registry/resolver.js.map +1 -0
- package/dist/registry/types.d.ts +59 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +14 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/run-logger.d.ts +8 -0
- package/dist/run-logger.d.ts.map +1 -0
- package/dist/run-logger.js +53 -0
- package/dist/run-logger.js.map +1 -0
- package/dist/run-store-factory.d.ts +9 -0
- package/dist/run-store-factory.d.ts.map +1 -0
- package/dist/run-store-factory.js +26 -0
- package/dist/run-store-factory.js.map +1 -0
- package/dist/studio-dir.d.ts +7 -0
- package/dist/studio-dir.d.ts.map +1 -0
- package/dist/studio-dir.js +27 -0
- package/dist/studio-dir.js.map +1 -0
- package/dist/utils/input-wizard.d.ts +4 -0
- package/dist/utils/input-wizard.d.ts.map +1 -0
- package/dist/utils/input-wizard.js +56 -0
- package/dist/utils/input-wizard.js.map +1 -0
- package/dist/utils/placeholders.d.ts +6 -0
- package/dist/utils/placeholders.d.ts.map +1 -0
- package/dist/utils/placeholders.js +12 -0
- package/dist/utils/placeholders.js.map +1 -0
- package/package.json +43 -0
- package/src/commands/api.ts +126 -0
- package/src/commands/config.ts +433 -0
- package/src/commands/init.ts +879 -0
- package/src/commands/install.ts +23 -0
- package/src/commands/integrations.ts +332 -0
- package/src/commands/list.ts +197 -0
- package/src/commands/logs.ts +119 -0
- package/src/commands/ollama.ts +168 -0
- package/src/commands/project.ts +167 -0
- package/src/commands/registry/audit.ts +87 -0
- package/src/commands/registry/index.ts +63 -0
- package/src/commands/registry/install.ts +222 -0
- package/src/commands/registry/publish.ts +112 -0
- package/src/commands/registry/remove.ts +89 -0
- package/src/commands/registry/search.ts +93 -0
- package/src/commands/registry/sync.ts +24 -0
- package/src/commands/registry/update.ts +63 -0
- package/src/commands/replay.ts +559 -0
- package/src/commands/run.ts +454 -0
- package/src/commands/status.ts +163 -0
- package/src/commands/template/index.ts +59 -0
- package/src/commands/template/validate.ts +175 -0
- package/src/commands/templates.ts +99 -0
- package/src/commands/tools.ts +227 -0
- package/src/commands/users.ts +127 -0
- package/src/commands/validate.ts +46 -0
- package/src/config.ts +101 -0
- package/src/index.ts +201 -0
- package/src/models-cache.ts +127 -0
- package/src/output/file-changes.ts +97 -0
- package/src/output/formatter.ts +85 -0
- package/src/output/formatters.ts +303 -0
- package/src/output/logger.ts +18 -0
- package/src/output/parallel-progress.ts +103 -0
- package/src/output/progress.ts +414 -0
- package/src/provider-validator.ts +109 -0
- package/src/registry/cache.ts +46 -0
- package/src/registry/client.ts +84 -0
- package/src/registry/lockfile.ts +68 -0
- package/src/registry/resolver.ts +113 -0
- package/src/registry/types.ts +66 -0
- package/src/run-logger.ts +66 -0
- package/src/run-store-factory.ts +30 -0
- package/src/studio-dir.ts +28 -0
- package/src/utils/input-wizard.ts +73 -0
- package/src/utils/placeholders.ts +10 -0
- package/templates/.studiorc.yaml +16 -0
- package/templates/projects/blank/metadata.json +7 -0
- package/templates/studio-config.yaml +14 -0
- package/tests/__stubs__/studio-runner.ts +104 -0
- package/tests/commands/api.test.ts +110 -0
- package/tests/commands/config.test.ts +221 -0
- package/tests/commands/init.test.ts +919 -0
- package/tests/commands/install.test.ts +52 -0
- package/tests/commands/integrations.test.ts +158 -0
- package/tests/commands/ollama.test.ts +139 -0
- package/tests/commands/project.test.ts +179 -0
- package/tests/commands/registry/audit.test.ts +56 -0
- package/tests/commands/registry/install.test.ts +200 -0
- package/tests/commands/registry/publish.test.ts +56 -0
- package/tests/commands/registry/remove.test.ts +103 -0
- package/tests/commands/registry/search.test.ts +44 -0
- package/tests/commands/registry/sync.test.ts +37 -0
- package/tests/commands/registry/update.test.ts +62 -0
- package/tests/commands/replay.test.ts +283 -0
- package/tests/commands/template/validate.test.ts +150 -0
- package/tests/commands/templates.test.ts +42 -0
- package/tests/commands/tools.test.ts +106 -0
- package/tests/config.test.ts +142 -0
- package/tests/formatter.test.ts +158 -0
- package/tests/integration/sigint.test.ts +188 -0
- package/tests/models-cache.test.ts +250 -0
- package/tests/output/file-changes.test.ts +178 -0
- package/tests/output/formatters.test.ts +448 -0
- package/tests/output/progress-spinner.test.ts +232 -0
- package/tests/output/progress-timer.test.ts +230 -0
- package/tests/provider-validator.test.ts +182 -0
- package/tests/registry/cache.test.ts +66 -0
- package/tests/registry/client.test.ts +70 -0
- package/tests/registry/lockfile.test.ts +87 -0
- package/tests/registry/resolver.test.ts +122 -0
- package/tests/run-logger-events.test.ts +326 -0
- package/tests/run-logger.test.ts +64 -0
- package/tests/run-store-factory.test.ts +51 -0
- package/tests/studio-dir.test.ts +31 -0
- package/tests/utils/input-wizard.test.ts +153 -0
- package/tests/utils/placeholders.test.ts +36 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|