@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,222 +0,0 @@
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
- }
@@ -1,112 +0,0 @@
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
- }
@@ -1,89 +0,0 @@
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
- }
@@ -1,93 +0,0 @@
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
- }
@@ -1,24 +0,0 @@
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
- }
@@ -1,63 +0,0 @@
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
- }