@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,222 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
4
|
+
import { RegistryClient } from '../../registry/client.js';
|
|
5
|
+
import { RegistryLockfile } from '../../registry/lockfile.js';
|
|
6
|
+
import { RegistryCache } from '../../registry/cache.js';
|
|
7
|
+
import { syncRegistry } from './sync.js';
|
|
8
|
+
import { findStudioDir } from '../../studio-dir.js';
|
|
9
|
+
import { resolveDependencies } from '../../registry/resolver.js';
|
|
10
|
+
import type { PackageMetadata, PackageType, RegistryIndex, Lockfile } from '../../registry/types.js';
|
|
11
|
+
import { INSTALL_DIRS } from '../../registry/types.js';
|
|
12
|
+
|
|
13
|
+
const SINGLE_FILE_EXTENSIONS: Partial<Record<PackageType, string>> = {
|
|
14
|
+
tool: '.tool.yaml',
|
|
15
|
+
pipeline: '.pipeline.yaml',
|
|
16
|
+
integration: '.integration.yaml',
|
|
17
|
+
agent: '.agent.yaml',
|
|
18
|
+
skill: '.skill.md',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SHELL_EXEC_PATTERN = /execute:\s*\n\s+type:\s*shell/;
|
|
22
|
+
|
|
23
|
+
interface InstallOptions {
|
|
24
|
+
studioDir?: string;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
requiredBy?: string;
|
|
28
|
+
/** Skip interactive prompts (auto-accept). Use when called under a spinner. */
|
|
29
|
+
interactive?: boolean;
|
|
30
|
+
_depth?: number;
|
|
31
|
+
_metaCache?: Map<string, PackageMetadata>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function doInstallPackage(
|
|
35
|
+
nameAtVersion: string,
|
|
36
|
+
options: InstallOptions,
|
|
37
|
+
client: RegistryClient,
|
|
38
|
+
lockfile: RegistryLockfile,
|
|
39
|
+
index: RegistryIndex,
|
|
40
|
+
lockfileData: Lockfile,
|
|
41
|
+
metaCache: Map<string, PackageMetadata>,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const [name, requestedVersion] = nameAtVersion.split('@');
|
|
44
|
+
const studioDir = options.studioDir!;
|
|
45
|
+
const depth = options._depth ?? 0;
|
|
46
|
+
const indent = ' '.repeat(depth);
|
|
47
|
+
|
|
48
|
+
// Check already installed
|
|
49
|
+
const existing = await lockfile.get(name);
|
|
50
|
+
if (existing && !options.force) {
|
|
51
|
+
if (options.requiredBy) {
|
|
52
|
+
await lockfile.addRequiredBy(name, options.requiredBy);
|
|
53
|
+
}
|
|
54
|
+
if (depth === 0) {
|
|
55
|
+
console.log(chalk.yellow(`${name} v${existing.version} is already installed. Use --force to reinstall.`));
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const indexEntry = index.packages.find(p => p.name === name);
|
|
61
|
+
if (!indexEntry) throw new Error(`Package '${name}' not found in registry`);
|
|
62
|
+
|
|
63
|
+
const type = indexEntry.type as PackageType;
|
|
64
|
+
|
|
65
|
+
// Use cached metadata if available (populated by resolver's fetcher)
|
|
66
|
+
let meta = metaCache.get(name);
|
|
67
|
+
if (!meta) {
|
|
68
|
+
meta = await client.fetchMetadata(type, name) as PackageMetadata;
|
|
69
|
+
metaCache.set(name, meta);
|
|
70
|
+
}
|
|
71
|
+
const version = requestedVersion ?? meta.version;
|
|
72
|
+
|
|
73
|
+
console.log(`${indent}Installing ${depth > 0 ? 'dependency: ' : ''}${chalk.bold(name)} v${version} [${type}]...`);
|
|
74
|
+
|
|
75
|
+
let sha256: string;
|
|
76
|
+
const destBaseDir = resolve(studioDir, INSTALL_DIRS[type]);
|
|
77
|
+
await mkdir(destBaseDir, { recursive: true });
|
|
78
|
+
|
|
79
|
+
if (type === 'template' || type === 'plugin') {
|
|
80
|
+
const destDir = resolve(destBaseDir, name);
|
|
81
|
+
await mkdir(destDir, { recursive: true });
|
|
82
|
+
sha256 = await client.downloadDirectory(type, name, 'project', destDir);
|
|
83
|
+
} else {
|
|
84
|
+
const ext = SINGLE_FILE_EXTENSIONS[type] ?? '.yaml';
|
|
85
|
+
const filename = `${name}${ext}`;
|
|
86
|
+
const result = await client.downloadFile(type, name, filename, destBaseDir);
|
|
87
|
+
sha256 = result.sha256;
|
|
88
|
+
|
|
89
|
+
const content = await readFile(result.destPath, 'utf8');
|
|
90
|
+
if (SHELL_EXEC_PATTERN.test(content) && options.interactive !== false) {
|
|
91
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
92
|
+
const proceed = await confirm({
|
|
93
|
+
message: chalk.yellow(`⚠ This package executes shell commands. Review ${result.destPath} before use. Install anyway?`),
|
|
94
|
+
default: false,
|
|
95
|
+
});
|
|
96
|
+
if (!proceed) {
|
|
97
|
+
const { unlink } = await import('node:fs/promises');
|
|
98
|
+
await unlink(result.destPath);
|
|
99
|
+
console.log('Installation cancelled.');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (meta.requires_binaries?.length) {
|
|
106
|
+
const { spawnSync } = await import('node:child_process');
|
|
107
|
+
for (const bin of meta.requires_binaries) {
|
|
108
|
+
const check = spawnSync('which', [bin], { encoding: 'utf8' });
|
|
109
|
+
if (check.status !== 0) {
|
|
110
|
+
console.log(chalk.yellow(`⚠ Warning: required binary '${bin}' not found in PATH`));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await lockfile.add(name, {
|
|
116
|
+
version,
|
|
117
|
+
type,
|
|
118
|
+
installed_at: new Date().toISOString().split('T')[0],
|
|
119
|
+
sha256,
|
|
120
|
+
required_by: options.requiredBy ? [options.requiredBy] : [],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log(`${indent}${chalk.green(`✓ Installed ${name} v${version}`)}`);
|
|
124
|
+
|
|
125
|
+
// Resolve and install dependencies
|
|
126
|
+
if (meta.dependencies) {
|
|
127
|
+
const graph = await resolveDependencies(
|
|
128
|
+
name,
|
|
129
|
+
meta,
|
|
130
|
+
index,
|
|
131
|
+
lockfileData,
|
|
132
|
+
(depName) => {
|
|
133
|
+
const depEntry = index.packages.find(p => p.name === depName);
|
|
134
|
+
const depType = (depEntry?.type ?? 'tool') as PackageType;
|
|
135
|
+
const cached = metaCache.get(depName);
|
|
136
|
+
if (cached) return Promise.resolve(cached);
|
|
137
|
+
return client.fetchMetadata(depType, depName).then(m => {
|
|
138
|
+
metaCache.set(depName, m as PackageMetadata);
|
|
139
|
+
return m as PackageMetadata;
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
for (const dep of graph.required) {
|
|
145
|
+
await doInstallPackage(
|
|
146
|
+
dep.name,
|
|
147
|
+
{ studioDir, requiredBy: name, interactive: options.interactive, _depth: depth + 1 },
|
|
148
|
+
client,
|
|
149
|
+
lockfile,
|
|
150
|
+
index,
|
|
151
|
+
lockfileData,
|
|
152
|
+
metaCache,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (depth === 0 && graph.recommended.length > 0) {
|
|
157
|
+
let install = options.interactive !== false;
|
|
158
|
+
if (options.interactive !== false) {
|
|
159
|
+
const names = graph.recommended.map(d => d.name).join(', ');
|
|
160
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
161
|
+
install = await confirm({
|
|
162
|
+
message: `Install recommended packages? [${names}]`,
|
|
163
|
+
default: true,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (install) {
|
|
167
|
+
for (const dep of graph.recommended) {
|
|
168
|
+
await doInstallPackage(
|
|
169
|
+
dep.name,
|
|
170
|
+
{ studioDir, interactive: options.interactive, _depth: depth + 1 },
|
|
171
|
+
client,
|
|
172
|
+
lockfile,
|
|
173
|
+
index,
|
|
174
|
+
lockfileData,
|
|
175
|
+
metaCache,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function installPackage(nameAtVersion: string, options: InstallOptions = {}): Promise<void> {
|
|
184
|
+
const [name] = nameAtVersion.split('@');
|
|
185
|
+
|
|
186
|
+
const studioDir = options.studioDir ??
|
|
187
|
+
(await findStudioDir(options.cwd ?? process.cwd()) ?? resolve(process.cwd(), '.studio'));
|
|
188
|
+
const resolvedOptions = { ...options, studioDir };
|
|
189
|
+
|
|
190
|
+
const lockfile = new RegistryLockfile(studioDir);
|
|
191
|
+
|
|
192
|
+
// Sync cache and resolve package type
|
|
193
|
+
await syncRegistry({ force: false, silent: true });
|
|
194
|
+
const cache = new RegistryCache();
|
|
195
|
+
const index = await cache.read();
|
|
196
|
+
if (!index?.packages.find(p => p.name === name)) {
|
|
197
|
+
throw new Error(`Package '${name}' not found in registry`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const client = new RegistryClient();
|
|
201
|
+
const lockfileData = await lockfile.read();
|
|
202
|
+
const metaCache = options._metaCache ?? new Map<string, PackageMetadata>();
|
|
203
|
+
|
|
204
|
+
await doInstallPackage(
|
|
205
|
+
nameAtVersion,
|
|
206
|
+
resolvedOptions,
|
|
207
|
+
client,
|
|
208
|
+
lockfile,
|
|
209
|
+
index,
|
|
210
|
+
lockfileData,
|
|
211
|
+
metaCache,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function installCommand(nameAtVersion: string, options: { force?: boolean } = {}): Promise<void> {
|
|
216
|
+
try {
|
|
217
|
+
await installPackage(nameAtVersion, options);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error(chalk.red(`Install failed: ${err instanceof Error ? err.message : err}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import type { PackageMetadata } from '../../registry/types.js';
|
|
7
|
+
import { REGISTRY_REPO } from '../../registry/types.js';
|
|
8
|
+
|
|
9
|
+
const REQUIRED_METADATA_FIELDS = ['name', 'version', 'description', 'author', 'license', 'type'];
|
|
10
|
+
|
|
11
|
+
export async function validatePublishPayload(packagePath: string): Promise<PackageMetadata> {
|
|
12
|
+
const absPath = resolve(packagePath);
|
|
13
|
+
|
|
14
|
+
if (!existsSync(absPath)) {
|
|
15
|
+
throw new Error(`Path does not exist: ${absPath}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const packageDir = dirname(absPath);
|
|
19
|
+
const metadataPath = resolve(packageDir, 'metadata.json');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(metadataPath)) {
|
|
22
|
+
throw new Error(`metadata.json not found in ${packageDir}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const meta = JSON.parse(await readFile(metadataPath, 'utf8')) as Partial<PackageMetadata>;
|
|
26
|
+
const missing = REQUIRED_METADATA_FIELDS.filter(f => !(f in meta));
|
|
27
|
+
if (missing.length > 0) {
|
|
28
|
+
throw new Error(`Missing required metadata fields: ${missing.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return meta as PackageMetadata;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PublishOptions {
|
|
35
|
+
dryRun?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function publishCommand(packagePath: string, options: PublishOptions = {}): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
console.log('Validating package...');
|
|
41
|
+
const meta = await validatePublishPayload(packagePath);
|
|
42
|
+
console.log(chalk.green(`✓ ${meta.name} v${meta.version} [${meta.type}]`));
|
|
43
|
+
|
|
44
|
+
if (options.dryRun) {
|
|
45
|
+
console.log(chalk.gray('Dry run — skipping GitHub PR'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check gh CLI is available and authenticated
|
|
50
|
+
const ghCheck = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8' });
|
|
51
|
+
if (ghCheck.status !== 0) {
|
|
52
|
+
throw new Error('GitHub CLI not authenticated. Run: gh auth login');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packageDir = dirname(resolve(packagePath));
|
|
56
|
+
const branchName = `${meta.type}-${meta.name}-v${meta.version}`;
|
|
57
|
+
const registryPath = `${meta.type}s/${meta.name}`;
|
|
58
|
+
|
|
59
|
+
console.log('Creating GitHub PR...');
|
|
60
|
+
|
|
61
|
+
// Fork the registry (idempotent)
|
|
62
|
+
spawnSync('gh', ['repo', 'fork', REGISTRY_REPO, '--clone=false'], {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Get the authenticated user's login for the fork URL
|
|
67
|
+
const userResult = spawnSync('gh', ['api', 'user', '--jq', '.login'], { encoding: 'utf8' });
|
|
68
|
+
const userLogin = userResult.stdout.trim();
|
|
69
|
+
if (!userLogin) throw new Error('Failed to get GitHub username');
|
|
70
|
+
|
|
71
|
+
// Clone fork to temp dir
|
|
72
|
+
const tmp = `/tmp/studio-registry-publish-${Date.now()}`;
|
|
73
|
+
const cloneResult = spawnSync('git', ['clone', `https://github.com/${userLogin}/registry.git`, tmp], {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
});
|
|
76
|
+
if (cloneResult.status !== 0) throw new Error(`Failed to clone fork: ${cloneResult.stderr}`);
|
|
77
|
+
|
|
78
|
+
// Create branch, copy files, push
|
|
79
|
+
spawnSync('git', ['-C', tmp, 'checkout', '-b', branchName], { encoding: 'utf8' });
|
|
80
|
+
spawnSync('cp', ['-r', packageDir, resolve(tmp, registryPath)], { encoding: 'utf8' });
|
|
81
|
+
spawnSync('git', ['-C', tmp, 'add', registryPath], { encoding: 'utf8' });
|
|
82
|
+
spawnSync('git', ['-C', tmp, 'commit', '-m', `[${meta.type}] ${meta.name} v${meta.version}`], {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
});
|
|
85
|
+
const pushResult = spawnSync('git', ['-C', tmp, 'push', '-u', 'origin', branchName], {
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
});
|
|
88
|
+
if (pushResult.status !== 0) throw new Error(`Failed to push branch: ${pushResult.stderr}`);
|
|
89
|
+
|
|
90
|
+
// Open PR
|
|
91
|
+
const prResult = spawnSync('gh', [
|
|
92
|
+
'pr', 'create',
|
|
93
|
+
'--repo', REGISTRY_REPO,
|
|
94
|
+
'--title', `[${meta.type}] ${meta.name} v${meta.version}`,
|
|
95
|
+
'--body', `## ${meta.name}\n\n${meta.description}\n\n**Author:** ${meta.author}\n**License:** ${meta.license}\n**Version:** ${meta.version}`,
|
|
96
|
+
'--head', `${userLogin}:${branchName}`,
|
|
97
|
+
], { encoding: 'utf8', cwd: tmp });
|
|
98
|
+
|
|
99
|
+
// Cleanup
|
|
100
|
+
spawnSync('rm', ['-rf', tmp]);
|
|
101
|
+
|
|
102
|
+
if (prResult.status === 0) {
|
|
103
|
+
const prUrl = prResult.stdout.trim();
|
|
104
|
+
console.log(chalk.green(`✓ PR opened: ${prUrl}`));
|
|
105
|
+
} else {
|
|
106
|
+
throw new Error(`Failed to create PR: ${prResult.stderr}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(chalk.red(`Publish failed: ${err instanceof Error ? err.message : err}`));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { RegistryLockfile } from '../../registry/lockfile.js';
|
|
6
|
+
import { findStudioDir } from '../../studio-dir.js';
|
|
7
|
+
import { INSTALL_DIRS } from '../../registry/types.js';
|
|
8
|
+
import type { PackageType } from '../../registry/types.js';
|
|
9
|
+
|
|
10
|
+
interface RemoveOptions {
|
|
11
|
+
studioDir?: string;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FILE_EXTENSIONS: Partial<Record<PackageType, string>> = {
|
|
16
|
+
tool: '.tool.yaml',
|
|
17
|
+
pipeline: '.pipeline.yaml',
|
|
18
|
+
integration: '.integration.yaml',
|
|
19
|
+
agent: '.agent.yaml',
|
|
20
|
+
skill: '.skill.md',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function deletePackageFiles(studioDir: string, name: string, type: PackageType): Promise<void> {
|
|
24
|
+
const destDir = resolve(studioDir, INSTALL_DIRS[type]);
|
|
25
|
+
if (type === 'template' || type === 'plugin') {
|
|
26
|
+
const dirPath = resolve(destDir, name);
|
|
27
|
+
if (existsSync(dirPath)) await rm(dirPath, { recursive: true });
|
|
28
|
+
} else {
|
|
29
|
+
const ext = FILE_EXTENSIONS[type] ?? '.yaml';
|
|
30
|
+
const filePath = resolve(destDir, `${name}${ext}`);
|
|
31
|
+
if (existsSync(filePath)) await rm(filePath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function removePackage(name: string, options: RemoveOptions = {}): Promise<void> {
|
|
36
|
+
const studioDir = options.studioDir ??
|
|
37
|
+
(await findStudioDir(options.cwd ?? process.cwd()) ?? resolve(process.cwd(), '.studio'));
|
|
38
|
+
const lockfile = new RegistryLockfile(studioDir);
|
|
39
|
+
const entry = await lockfile.get(name);
|
|
40
|
+
if (!entry) throw new Error(`'${name}' is not installed`);
|
|
41
|
+
|
|
42
|
+
// Block removal if another package depends on this one
|
|
43
|
+
const dependents = (entry.required_by ?? []).filter(d => d !== '');
|
|
44
|
+
if (dependents.length > 0) {
|
|
45
|
+
throw new Error(`'${name}' is required by: ${dependents.join(', ')}. Remove them first.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const type = entry.type as PackageType;
|
|
49
|
+
await deletePackageFiles(studioDir, name, type);
|
|
50
|
+
await lockfile.remove(name);
|
|
51
|
+
console.log(chalk.green(`✓ Removed ${name}`));
|
|
52
|
+
|
|
53
|
+
// Find orphans: packages whose required_by only contained 'name'
|
|
54
|
+
const data = await lockfile.read();
|
|
55
|
+
const orphans: string[] = [];
|
|
56
|
+
for (const [pkgName, pkgEntry] of Object.entries(data.installed)) {
|
|
57
|
+
const wasRequired = pkgEntry.required_by?.includes(name);
|
|
58
|
+
const otherRequirers = (pkgEntry.required_by ?? []).filter(r => r !== name);
|
|
59
|
+
if (wasRequired && otherRequirers.length === 0) {
|
|
60
|
+
orphans.push(pkgName);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (orphans.length > 0) {
|
|
65
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
66
|
+
const cleanup = await confirm({
|
|
67
|
+
message: `These packages are no longer needed: [${orphans.join(', ')}]. Remove them?`,
|
|
68
|
+
default: true,
|
|
69
|
+
});
|
|
70
|
+
if (cleanup) {
|
|
71
|
+
for (const orphan of orphans) {
|
|
72
|
+
// Strip the requirer reference so the protection check passes
|
|
73
|
+
await lockfile.removeRequiredBy(orphan, name);
|
|
74
|
+
await removePackage(orphan, { studioDir });
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
console.log(chalk.yellow(`Packages left installed: [${orphans.join(', ')}]`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function removeCommand(name: string): Promise<void> {
|
|
83
|
+
try {
|
|
84
|
+
await removePackage(name);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { RegistryCache } from '../../registry/cache.js';
|
|
3
|
+
import { syncRegistry } from './sync.js';
|
|
4
|
+
import type { PackageEntry, PackageType } from '../../registry/types.js';
|
|
5
|
+
|
|
6
|
+
export function searchPackages(
|
|
7
|
+
packages: PackageEntry[],
|
|
8
|
+
query?: string,
|
|
9
|
+
type?: PackageType | string,
|
|
10
|
+
): PackageEntry[] {
|
|
11
|
+
let results = packages;
|
|
12
|
+
|
|
13
|
+
if (type) {
|
|
14
|
+
results = results.filter(p => p.type === type);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (query) {
|
|
18
|
+
const q = query.toLowerCase();
|
|
19
|
+
results = results.filter(p =>
|
|
20
|
+
p.name.toLowerCase().includes(q) ||
|
|
21
|
+
p.description.toLowerCase().includes(q) ||
|
|
22
|
+
p.tags.some(t => t.toLowerCase().includes(q)),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderPackage(pkg: PackageEntry): void {
|
|
30
|
+
console.log(
|
|
31
|
+
` ${chalk.bold(pkg.name)} ${chalk.gray(`v${pkg.version}`)} ${chalk.cyan(`[${pkg.type}]`)}`,
|
|
32
|
+
);
|
|
33
|
+
console.log(` ${pkg.description}`);
|
|
34
|
+
if (pkg.tags.length > 0) {
|
|
35
|
+
console.log(` ${chalk.gray(pkg.tags.join(', '))}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SearchOptions {
|
|
40
|
+
type?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function searchCommand(query: string, options: SearchOptions = {}): Promise<void> {
|
|
44
|
+
await syncRegistry({ force: false, silent: true });
|
|
45
|
+
const cache = new RegistryCache();
|
|
46
|
+
const index = await cache.read();
|
|
47
|
+
if (!index) {
|
|
48
|
+
console.error(chalk.red('Failed to load registry. Run: studio registry sync'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results = searchPackages(index.packages, query, options.type as PackageType | undefined);
|
|
53
|
+
|
|
54
|
+
if (results.length === 0) {
|
|
55
|
+
console.log(chalk.yellow(`No packages found for "${query}"`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.bold(`\n${results.length} package${results.length > 1 ? 's' : ''} found:\n`));
|
|
60
|
+
for (const pkg of results) {
|
|
61
|
+
renderPackage(pkg);
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
console.log(chalk.gray(`Install: studio registry install <name>`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function browseCommand(): Promise<void> {
|
|
68
|
+
await syncRegistry({ force: false, silent: true });
|
|
69
|
+
const cache = new RegistryCache();
|
|
70
|
+
const index = await cache.read();
|
|
71
|
+
if (!index) {
|
|
72
|
+
console.error(chalk.red('Failed to load registry. Run: studio registry sync'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sorted = [...index.packages].sort((a, b) => b.downloads - a.downloads);
|
|
77
|
+
|
|
78
|
+
console.log(chalk.bold(`\nStudio Community Registry — ${sorted.length} packages\n`));
|
|
79
|
+
|
|
80
|
+
const byType: Record<string, PackageEntry[]> = {};
|
|
81
|
+
for (const pkg of sorted) {
|
|
82
|
+
if (!byType[pkg.type]) byType[pkg.type] = [];
|
|
83
|
+
byType[pkg.type].push(pkg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [type, pkgs] of Object.entries(byType)) {
|
|
87
|
+
console.log(chalk.bold.underline(`${type}s`));
|
|
88
|
+
for (const pkg of pkgs) {
|
|
89
|
+
renderPackage(pkg);
|
|
90
|
+
console.log();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { RegistryCache } from '../../registry/cache.js';
|
|
3
|
+
import { RegistryClient } from '../../registry/client.js';
|
|
4
|
+
|
|
5
|
+
interface SyncOptions {
|
|
6
|
+
cacheDir?: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
silent?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function syncRegistry(options: SyncOptions = {}): Promise<void> {
|
|
12
|
+
const cache = new RegistryCache(options.cacheDir);
|
|
13
|
+
|
|
14
|
+
if (!options.force && await cache.isFresh()) {
|
|
15
|
+
if (!options.silent) console.log(chalk.gray('Registry index is up to date.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!options.silent) process.stdout.write('Syncing registry... ');
|
|
20
|
+
const client = new RegistryClient();
|
|
21
|
+
const index = await client.fetchIndex();
|
|
22
|
+
await cache.write(index);
|
|
23
|
+
if (!options.silent) console.log(chalk.green(`✓ ${index.packages.length} packages`));
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { RegistryLockfile } from '../../registry/lockfile.js';
|
|
3
|
+
import { RegistryCache } from '../../registry/cache.js';
|
|
4
|
+
import { syncRegistry } from './sync.js';
|
|
5
|
+
import { installPackage } from './install.js';
|
|
6
|
+
import { findStudioDir } from '../../studio-dir.js';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
interface OutdatedEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
installed: string;
|
|
12
|
+
latest: string;
|
|
13
|
+
type: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UpdateOptions {
|
|
17
|
+
studioDir?: string;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function outdatedPackages(options: UpdateOptions = {}): Promise<OutdatedEntry[]> {
|
|
22
|
+
const studioDir = options.studioDir ??
|
|
23
|
+
(await findStudioDir(options.cwd ?? process.cwd()) ?? resolve(process.cwd(), '.studio'));
|
|
24
|
+
await syncRegistry({ force: false, silent: true });
|
|
25
|
+
const cache = new RegistryCache();
|
|
26
|
+
const index = await cache.read();
|
|
27
|
+
if (!index) return [];
|
|
28
|
+
|
|
29
|
+
const lockfile = new RegistryLockfile(studioDir);
|
|
30
|
+
const installed = await lockfile.list();
|
|
31
|
+
const results: OutdatedEntry[] = [];
|
|
32
|
+
|
|
33
|
+
for (const entry of installed) {
|
|
34
|
+
const latest = index.packages.find(p => p.name === entry.name);
|
|
35
|
+
if (latest && latest.version !== entry.version) {
|
|
36
|
+
results.push({ name: entry.name, installed: entry.version, latest: latest.version, type: entry.type });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function outdatedCommand(options: UpdateOptions = {}): Promise<void> {
|
|
44
|
+
const outdated = await outdatedPackages(options);
|
|
45
|
+
if (outdated.length === 0) {
|
|
46
|
+
console.log(chalk.green('All packages are up to date.'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk.bold('\nOutdated packages:\n'));
|
|
50
|
+
for (const pkg of outdated) {
|
|
51
|
+
console.log(` ${chalk.bold(pkg.name)} ${chalk.red(pkg.installed)} → ${chalk.green(pkg.latest)} [${pkg.type}]`);
|
|
52
|
+
}
|
|
53
|
+
console.log(`\nRun: studio registry update <name>`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function updateCommand(name: string): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
await installPackage(name, { force: true });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|