@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,68 +0,0 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
3
- import type { Lockfile, LockfileEntry } from './types.js';
4
-
5
- export class RegistryLockfile {
6
- private lockPath: string;
7
- private studioDir: string;
8
-
9
- constructor(studioDir: string) {
10
- this.studioDir = studioDir;
11
- this.lockPath = resolve(studioDir, 'registry.lock.json');
12
- }
13
-
14
- async read(): Promise<Lockfile> {
15
- try {
16
- const raw = await readFile(this.lockPath, 'utf8');
17
- return JSON.parse(raw) as Lockfile;
18
- } catch {
19
- return { installed: {} };
20
- }
21
- }
22
-
23
- private async write(data: Lockfile): Promise<void> {
24
- await mkdir(this.studioDir, { recursive: true });
25
- await writeFile(this.lockPath, JSON.stringify(data, null, 2) + '\n');
26
- }
27
-
28
- async add(name: string, entry: LockfileEntry): Promise<void> {
29
- const data = await this.read();
30
- data.installed[name] = entry;
31
- await this.write(data);
32
- }
33
-
34
- async remove(name: string): Promise<void> {
35
- const data = await this.read();
36
- delete data.installed[name];
37
- await this.write(data);
38
- }
39
-
40
- async get(name: string): Promise<LockfileEntry | null> {
41
- const data = await this.read();
42
- return data.installed[name] ?? null;
43
- }
44
-
45
- async list(): Promise<Array<{ name: string } & LockfileEntry>> {
46
- const data = await this.read();
47
- return Object.entries(data.installed).map(([name, entry]) => ({ name, ...entry }));
48
- }
49
-
50
- async addRequiredBy(name: string, requiredBy: string): Promise<void> {
51
- const data = await this.read();
52
- const entry = data.installed[name];
53
- if (!entry) return;
54
- const existing = entry.required_by ?? [];
55
- if (!existing.includes(requiredBy)) {
56
- data.installed[name] = { ...entry, required_by: [...existing, requiredBy] };
57
- await this.write(data);
58
- }
59
- }
60
-
61
- async removeRequiredBy(name: string, requiredBy: string): Promise<void> {
62
- const data = await this.read();
63
- const entry = data.installed[name];
64
- if (!entry) return;
65
- data.installed[name] = { ...entry, required_by: (entry.required_by ?? []).filter(r => r !== requiredBy) };
66
- await this.write(data);
67
- }
68
- }
@@ -1,113 +0,0 @@
1
- import type { PackageMetadata, PackageType, RegistryIndex, Lockfile, PackageDependencies } from './types.js';
2
-
3
- export interface DependencyNode {
4
- name: string;
5
- type: PackageType;
6
- }
7
-
8
- export interface ResolvedGraph {
9
- required: DependencyNode[];
10
- recommended: DependencyNode[];
11
- }
12
-
13
- type MetadataFetcher = (name: string) => Promise<PackageMetadata>;
14
-
15
- /** Extract all dependency names (regardless of whether they're in the index) for cycle detection. */
16
- function depNames(deps: PackageDependencies, kind: 'required' | 'recommended'): string[] {
17
- const names: string[] = [];
18
- for (const [, spec] of Object.entries(deps)) {
19
- const arr: string[] = (spec as Record<string, string[]>)[kind] ?? [];
20
- names.push(...arr);
21
- }
22
- return names;
23
- }
24
-
25
- function flattenDeps(
26
- deps: PackageDependencies,
27
- kind: 'required' | 'recommended',
28
- index: RegistryIndex,
29
- ): DependencyNode[] {
30
- const nodes: DependencyNode[] = [];
31
- for (const [, spec] of Object.entries(deps)) {
32
- const names: string[] = (spec as Record<string, string[]>)[kind] ?? [];
33
- for (const name of names) {
34
- const entry = index.packages.find(p => p.name === name);
35
- if (entry) {
36
- nodes.push({ name, type: entry.type as PackageType });
37
- }
38
- }
39
- }
40
- return nodes;
41
- }
42
-
43
- export async function resolveDependencies(
44
- rootPackageName: string,
45
- meta: PackageMetadata,
46
- index: RegistryIndex,
47
- _lockfile: Lockfile,
48
- fetchMeta: MetadataFetcher,
49
- ): Promise<ResolvedGraph> {
50
- const resolved = new Map<string, DependencyNode>();
51
- const visiting = new Set<string>();
52
-
53
- // Track the root package so cycles back to it are detected
54
- visiting.add(rootPackageName);
55
-
56
- async function visit(name: string, pkgMeta: PackageMetadata): Promise<void> {
57
- if (visiting.has(name)) {
58
- throw new Error(`Circular dependency detected: ${name} is part of a cycle`);
59
- }
60
- if (resolved.has(name)) return;
61
-
62
- visiting.add(name);
63
-
64
- if (pkgMeta.dependencies) {
65
- // Check for cycles on all required names (even those not in index)
66
- for (const depName of depNames(pkgMeta.dependencies, 'required')) {
67
- if (visiting.has(depName)) {
68
- throw new Error(`Circular dependency detected: ${depName} is part of a cycle`);
69
- }
70
- }
71
-
72
- const requiredNodes = flattenDeps(pkgMeta.dependencies, 'required', index);
73
- for (const node of requiredNodes) {
74
- if (!resolved.has(node.name)) {
75
- const subMeta = await fetchMeta(node.name);
76
- await visit(node.name, subMeta);
77
- if (!resolved.has(node.name)) {
78
- resolved.set(node.name, node);
79
- }
80
- }
81
- }
82
- }
83
-
84
- visiting.delete(name);
85
- }
86
-
87
- if (meta.dependencies) {
88
- // Check for cycles at the first level too (root → dep that cycles back to root)
89
- for (const depName of depNames(meta.dependencies, 'required')) {
90
- if (visiting.has(depName)) {
91
- throw new Error(`Circular dependency detected: ${depName} is part of a cycle`);
92
- }
93
- }
94
-
95
- const firstLevelRequired = flattenDeps(meta.dependencies, 'required', index);
96
- for (const node of firstLevelRequired) {
97
- const subMeta = await fetchMeta(node.name);
98
- await visit(node.name, subMeta);
99
- if (!resolved.has(node.name)) {
100
- resolved.set(node.name, node);
101
- }
102
- }
103
- }
104
-
105
- const recommended: DependencyNode[] = meta.dependencies
106
- ? flattenDeps(meta.dependencies, 'recommended', index)
107
- : [];
108
-
109
- return {
110
- required: Array.from(resolved.values()),
111
- recommended,
112
- };
113
- }
@@ -1,66 +0,0 @@
1
- export type PackageType =
2
- | 'tool'
3
- | 'template'
4
- | 'pipeline'
5
- | 'integration'
6
- | 'agent'
7
- | 'plugin'
8
- | 'skill';
9
-
10
- export interface PackageEntry {
11
- name: string;
12
- type: PackageType;
13
- version: string;
14
- description: string;
15
- author: string;
16
- license: string;
17
- tags: string[];
18
- studio_version: string | null;
19
- downloads: number;
20
- }
21
-
22
- export interface RegistryIndex {
23
- generated_at: string;
24
- version: string;
25
- packages: PackageEntry[];
26
- }
27
-
28
- export interface PackageDependencies {
29
- tools?: { required?: string[]; recommended?: string[] };
30
- agents?: { required?: string[]; recommended?: string[] };
31
- skills?: { required?: string[]; recommended?: string[] };
32
- templates?: { required?: string[]; recommended?: string[] };
33
- pipelines?: { required?: string[]; recommended?: string[] };
34
- }
35
-
36
- export interface PackageMetadata extends PackageEntry {
37
- requires_binaries?: string[];
38
- dependencies?: PackageDependencies;
39
- }
40
-
41
- export interface LockfileEntry {
42
- version: string;
43
- type: PackageType;
44
- installed_at: string;
45
- sha256: string;
46
- required_by?: string[];
47
- }
48
-
49
- export interface Lockfile {
50
- installed: Record<string, LockfileEntry>;
51
- }
52
-
53
- /** Where packages get installed relative to the project's .studio/ dir */
54
- export const INSTALL_DIRS: Record<PackageType, string> = {
55
- tool: 'tools',
56
- template: 'projects',
57
- pipeline: 'pipelines',
58
- integration: 'integrations',
59
- agent: 'agents',
60
- plugin: 'plugins',
61
- skill: 'skills',
62
- };
63
-
64
- export const REGISTRY_REPO = 'PipStudio/studio-community';
65
- export const REGISTRY_RAW_BASE = `https://raw.githubusercontent.com/${REGISTRY_REPO}/main`;
66
- export const REGISTRY_API_BASE = `https://api.github.com/repos/${REGISTRY_REPO}`;
package/src/run-logger.ts DELETED
@@ -1,66 +0,0 @@
1
- import { createWriteStream, mkdirSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
-
4
- const RUNS_DIR = '.studio/runs';
5
-
6
- function runIdShort(runId: string): string {
7
- return runId.slice(0, 8);
8
- }
9
-
10
- function dateForFilename(): string {
11
- const iso = new Date().toISOString();
12
- return iso.slice(0, 16).replace(/(\d{2}):(\d{2})$/, '$1h$2m');
13
- }
14
-
15
- export interface RunLogger {
16
- start(runId: string, pipeline: string): void;
17
- log(payload: Record<string, unknown>): void;
18
- close(): Promise<void>;
19
- getLogPath(): string;
20
- }
21
-
22
- export function createRunLogger(cwd: string = process.cwd()): RunLogger {
23
- let logPath = '';
24
- let stream: ReturnType<typeof createWriteStream> | null = null;
25
- let shortRunId = '';
26
-
27
- return {
28
- start(runId: string, pipeline: string): void {
29
- shortRunId = runIdShort(runId);
30
- const date = dateForFilename();
31
- const base = resolve(cwd, RUNS_DIR);
32
- logPath = resolve(base, `${date}-${pipeline}-${shortRunId}.jsonl`);
33
- mkdirSync(base, { recursive: true });
34
- stream = createWriteStream(logPath, { flags: 'a' });
35
- },
36
-
37
- log(payload: Record<string, unknown>): void {
38
- const line: Record<string, unknown> = {
39
- ts: new Date().toISOString(),
40
- ...payload,
41
- run_id: payload.run_id !== undefined ? runIdShort(String(payload.run_id)) : shortRunId,
42
- };
43
- const out = JSON.stringify(line) + '\n';
44
- if (stream?.writable) {
45
- stream.write(out);
46
- }
47
- },
48
-
49
- close(): Promise<void> {
50
- return new Promise((resolve) => {
51
- if (stream) {
52
- stream.end(() => {
53
- stream = null;
54
- resolve();
55
- });
56
- } else {
57
- resolve();
58
- }
59
- });
60
- },
61
-
62
- getLogPath(): string {
63
- return logPath;
64
- },
65
- };
66
- }
@@ -1,30 +0,0 @@
1
- import { join } from 'node:path';
2
- import { mkdirSync } from 'node:fs';
3
- import type { StudioConfig } from './config.js';
4
- import { SQLiteRunStore, InMemoryRunStore, PgRunStore } from '@studio-foundation/engine';
5
- import type { AnyRunStore } from '@studio-foundation/engine';
6
-
7
- /**
8
- * Create the production RunStore from config.
9
- * Returns AnyRunStore (sync RunStore or async AsyncRunStore) based on config.db.type.
10
- * Defaults to SQLite when no db config is present.
11
- */
12
- export async function createRunStore(config: StudioConfig): Promise<AnyRunStore> {
13
- const dbType = config.db?.type ?? 'sqlite';
14
-
15
- if (dbType === 'inmemory') {
16
- return new InMemoryRunStore();
17
- }
18
-
19
- if (dbType === 'postgres') {
20
- const url = config.db?.url;
21
- if (!url) throw new Error('db.url is required when db.type is postgres');
22
- return new PgRunStore(url);
23
- }
24
-
25
- // Default: sqlite
26
- const studioDir = config.resolvedStudioDir ?? join(process.cwd(), '.studio');
27
- mkdirSync(join(studioDir, 'runs'), { recursive: true });
28
- const dbPath = join(studioDir, 'runs', 'runs.db');
29
- return new SQLiteRunStore(dbPath);
30
- }
package/src/studio-dir.ts DELETED
@@ -1,28 +0,0 @@
1
- import { access } from 'node:fs/promises';
2
- import { dirname, join, resolve } from 'node:path';
3
-
4
- /**
5
- * Walk up the directory tree from `startDir` looking for a `.studio/` directory.
6
- * Like git looking for `.git/`.
7
- * Returns the absolute path to `.studio/`, or null if not found.
8
- */
9
- export async function findStudioDir(startDir: string): Promise<string | null> {
10
- let current = resolve(startDir);
11
-
12
- while (true) {
13
- const candidate = join(current, '.studio');
14
- try {
15
- await access(candidate);
16
- return candidate;
17
- } catch {
18
- // not found here, go up
19
- }
20
-
21
- const parent = dirname(current);
22
- if (parent === current) {
23
- // Reached filesystem root
24
- return null;
25
- }
26
- current = parent;
27
- }
28
- }
@@ -1,73 +0,0 @@
1
- import { input } from '@inquirer/prompts';
2
- import type { InputSchema } from '@studio-foundation/contracts';
3
-
4
- export function validateInputSchema(raw: unknown): InputSchema {
5
- const schema = raw as Record<string, unknown>;
6
-
7
- if (!Array.isArray(schema?.fields) || schema.fields.length === 0) {
8
- throw new Error('input_schema must have at least one field');
9
- }
10
-
11
- for (const field of schema.fields as any[]) {
12
- const name = field.name ?? '(unnamed)';
13
-
14
- if (!field.prompt || typeof field.prompt !== 'string' || !field.prompt.trim()) {
15
- throw new Error(`Field '${name}' must have a non-empty 'prompt'`);
16
- }
17
-
18
- if (field.type !== 'text' && field.type !== 'array') {
19
- throw new Error(
20
- `Field '${name}' has invalid type '${field.type}'. Must be 'text' or 'array'`
21
- );
22
- }
23
-
24
- if (field.type === 'array' && field.items !== 'text') {
25
- throw new Error(`Array field '${name}' must have items: 'text'`);
26
- }
27
- }
28
-
29
- return raw as InputSchema;
30
- }
31
-
32
- export async function collectStructuredInput(
33
- schema: InputSchema
34
- ): Promise<Record<string, unknown>> {
35
- const result: Record<string, unknown> = {};
36
-
37
- for (const field of schema.fields) {
38
- if (field.type === 'text') {
39
- result[field.name] = await input({
40
- message: field.prompt,
41
- required: field.required,
42
- default: field.default,
43
- });
44
- } else if (field.type === 'array') {
45
- const items: string[] = [];
46
- let index = 1;
47
-
48
- while (true) {
49
- const value = await input({
50
- message: `${field.prompt} (${index})`,
51
- required: index === 1 && field.required,
52
- default: '',
53
- });
54
-
55
- if (value === '') {
56
- if (index === 1 && field.required) {
57
- console.log('At least one value required.');
58
- continue;
59
- }
60
- break;
61
- }
62
-
63
- items.push(value);
64
- index++;
65
- }
66
-
67
- result[field.name] = items;
68
- }
69
- }
70
-
71
- console.log('\n✓ Input collected\n');
72
- return result;
73
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Replace {{ALL_CAPS}} placeholders in `content` with values from `vars`.
3
- * Throws if any placeholder has no corresponding entry in `vars`.
4
- */
5
- export function applyPlaceholders(content: string, vars: Record<string, string>): string {
6
- return content.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (match, key) => {
7
- if (key in vars) return vars[key];
8
- throw new Error(`Unresolved placeholder: ${match}`);
9
- });
10
- }
@@ -1,16 +0,0 @@
1
- # Studio v7 Configuration
2
-
3
- providers:
4
- anthropic:
5
- apiKey: ${ANTHROPIC_API_KEY}
6
- # openai:
7
- # apiKey: ${OPENAI_API_KEY}
8
-
9
- paths:
10
- pipelines: ./pipelines
11
- contracts: ./configs/contracts
12
- agents: ./configs/agents
13
-
14
- defaults:
15
- provider: anthropic
16
- model: claude-sonnet-4-20250514
@@ -1,7 +0,0 @@
1
- {
2
- "name": "blank",
3
- "version": "1.0.0",
4
- "description": "Empty project structure",
5
- "type": "template",
6
- "studio_version": ">=7.0.0"
7
- }
@@ -1,14 +0,0 @@
1
- # Studio Configuration
2
- # This file is gitignored — it contains secrets.
3
- # See .studio/projects/ for your pipeline configs (those ARE committed).
4
-
5
- providers:
6
- ollama: {}
7
- # anthropic:
8
- # apiKey: ${ANTHROPIC_API_KEY}
9
- # openai:
10
- # apiKey: ${OPENAI_API_KEY}
11
-
12
- defaults:
13
- provider: ollama
14
- model: llama3.3
@@ -1,104 +0,0 @@
1
- /**
2
- * Stub for @studio/runner used in tests when the package has not been built.
3
- * Reads from the actual runner template files so tool/integration tests work correctly.
4
- */
5
- import { readFile, readdir } from 'node:fs/promises';
6
- import { resolve } from 'node:path';
7
- import yaml from 'js-yaml';
8
-
9
- const RUNNER_TEMPLATES = resolve(import.meta.dirname, '../../../runner/templates');
10
- const TOOL_TEMPLATES_DIR = resolve(RUNNER_TEMPLATES, 'tools');
11
- const INTEGRATION_TEMPLATES_DIR = resolve(RUNNER_TEMPLATES, 'integrations');
12
-
13
- // ── Tool functions ────────────────────────────────────────────────────────────
14
-
15
- export async function listAvailableToolTemplates(): Promise<{ name: string; description: string }[]> {
16
- let files: string[];
17
- try {
18
- files = (await readdir(TOOL_TEMPLATES_DIR)).filter(f => f.endsWith('.tool.yaml')).sort();
19
- } catch {
20
- return [];
21
- }
22
- const result: { name: string; description: string }[] = [];
23
- for (const file of files) {
24
- const content = await readFile(resolve(TOOL_TEMPLATES_DIR, file), 'utf-8');
25
- const def = yaml.load(content) as { description?: string };
26
- result.push({ name: file.replace('.tool.yaml', ''), description: def.description ?? '' });
27
- }
28
- return result;
29
- }
30
-
31
- export async function getBundledToolTemplate(name: string): Promise<string | null> {
32
- const filePath = resolve(TOOL_TEMPLATES_DIR, `${name}.tool.yaml`);
33
- try {
34
- return await readFile(filePath, 'utf-8');
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- export const BUILTIN_TOOL_NAMES = new Set(['repo-manager', 'shell', 'search', 'git']);
41
-
42
- // ── Integration functions ─────────────────────────────────────────────────────
43
-
44
- export async function getBundledIntegrationTemplate(name: string): Promise<string | null> {
45
- const filePath = resolve(INTEGRATION_TEMPLATES_DIR, `${name}.integration.yaml`);
46
- try {
47
- return await readFile(filePath, 'utf-8');
48
- } catch {
49
- return null;
50
- }
51
- }
52
-
53
- export async function listAvailableIntegrationTemplates(): Promise<{ name: string; description: string }[]> {
54
- let files: string[];
55
- try {
56
- files = (await readdir(INTEGRATION_TEMPLATES_DIR)).filter(f => f.endsWith('.integration.yaml')).sort();
57
- } catch {
58
- return [];
59
- }
60
- const result: { name: string; description: string }[] = [];
61
- for (const file of files) {
62
- const content = await readFile(resolve(INTEGRATION_TEMPLATES_DIR, file), 'utf-8');
63
- const def = yaml.load(content) as { description?: string };
64
- result.push({ name: file.replace('.integration.yaml', ''), description: def.description ?? '' });
65
- }
66
- return result;
67
- }
68
-
69
- export async function loadProjectIntegrations(): Promise<unknown[]> {
70
- return [];
71
- }
72
-
73
- // ── Runtime stubs (used by run.ts — only needs to be importable) ──────────────
74
-
75
- export function createDefaultRegistry(..._args: unknown[]): unknown {
76
- return {};
77
- }
78
-
79
- export class ToolRegistry {
80
- addTool(..._args: unknown[]): void {}
81
- getTools(): unknown[] { return []; }
82
- }
83
-
84
- export function createRepoManagerTools(..._args: unknown[]): unknown[] {
85
- return [];
86
- }
87
-
88
- export async function loadProjectTools(..._args: unknown[]): Promise<unknown[]> {
89
- return [];
90
- }
91
-
92
- export async function loadPlugins(..._args: unknown[]): Promise<unknown[]> {
93
- return [];
94
- }
95
-
96
- export class MCPClient {
97
- constructor(..._args: unknown[]) {}
98
- async connect(): Promise<void> {}
99
- async disconnect(): Promise<void> {}
100
- }
101
-
102
- export class StudioOAuthProvider {
103
- constructor(..._args: unknown[]) {}
104
- }