opc-agent 1.2.0 → 1.3.0

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.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * OPC Agent Traces — Structured logging for agent actions.
3
+ *
4
+ * Collects traces that can be fed to DeepBrain's learn() API.
5
+ * Inspired by OpenTelemetry spans.
6
+ */
7
+
8
+ export interface Span {
9
+ traceId: string;
10
+ spanId: string;
11
+ name: string;
12
+ startTime: Date;
13
+ endTime?: Date;
14
+ attributes: Record<string, string | number | boolean>;
15
+ events: SpanEvent[];
16
+ status: 'ok' | 'error' | 'unset';
17
+ }
18
+
19
+ export interface SpanEvent {
20
+ name: string;
21
+ timestamp: Date;
22
+ attributes?: Record<string, string | number | boolean>;
23
+ }
24
+
25
+ export interface TraceExporter {
26
+ export(spans: Span[]): Promise<void>;
27
+ }
28
+
29
+ /** In-memory buffer that holds spans until flushed */
30
+ export class TraceCollector {
31
+ private spans: Span[] = [];
32
+ private exporters: TraceExporter[] = [];
33
+ private maxBufferSize: number;
34
+
35
+ constructor(maxBufferSize = 100) {
36
+ this.maxBufferSize = maxBufferSize;
37
+ }
38
+
39
+ addExporter(exporter: TraceExporter): void {
40
+ this.exporters.push(exporter);
41
+ }
42
+
43
+ startSpan(name: string, attributes: Record<string, string | number | boolean> = {}): Span {
44
+ const span: Span = {
45
+ traceId: crypto.randomUUID(),
46
+ spanId: crypto.randomUUID().slice(0, 16),
47
+ name,
48
+ startTime: new Date(),
49
+ attributes,
50
+ events: [],
51
+ status: 'unset',
52
+ };
53
+ this.spans.push(span);
54
+
55
+ if (this.spans.length >= this.maxBufferSize) {
56
+ this.flush().catch(() => {}); // Best effort
57
+ }
58
+
59
+ return span;
60
+ }
61
+
62
+ endSpan(span: Span, status: 'ok' | 'error' = 'ok'): void {
63
+ span.endTime = new Date();
64
+ span.status = status;
65
+ }
66
+
67
+ addEvent(span: Span, name: string, attributes?: Record<string, string | number | boolean>): void {
68
+ span.events.push({ name, timestamp: new Date(), attributes });
69
+ }
70
+
71
+ async flush(): Promise<number> {
72
+ const toExport = [...this.spans];
73
+ this.spans = [];
74
+
75
+ for (const exporter of this.exporters) {
76
+ await exporter.export(toExport);
77
+ }
78
+
79
+ return toExport.length;
80
+ }
81
+
82
+ getBufferedSpans(): readonly Span[] {
83
+ return this.spans;
84
+ }
85
+
86
+ get bufferedCount(): number {
87
+ return this.spans.length;
88
+ }
89
+ }
90
+
91
+ /** Console exporter for development */
92
+ export class ConsoleExporter implements TraceExporter {
93
+ async export(spans: Span[]): Promise<void> {
94
+ for (const span of spans) {
95
+ const duration = span.endTime
96
+ ? `${span.endTime.getTime() - span.startTime.getTime()}ms`
97
+ : 'ongoing';
98
+ console.log(`[TRACE] ${span.name} (${duration}) [${span.status}]`);
99
+ }
100
+ }
101
+ }
102
+
103
+ /** DeepBrain exporter — sends traces to DeepBrain learn() */
104
+ export class DeepBrainExporter implements TraceExporter {
105
+ private learnEndpoint: string;
106
+
107
+ constructor(deepbrainUrl: string = 'http://localhost:3333') {
108
+ this.learnEndpoint = `${deepbrainUrl}/api/learn`;
109
+ }
110
+
111
+ async export(spans: Span[]): Promise<void> {
112
+ for (const span of spans) {
113
+ try {
114
+ await fetch(this.learnEndpoint, {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({
118
+ action: span.name,
119
+ result: span.status === 'ok' ? 'success' : 'error',
120
+ context: {
121
+ ...span.attributes,
122
+ duration: span.endTime ? span.endTime.getTime() - span.startTime.getTime() : null,
123
+ events: span.events.map(e => e.name),
124
+ },
125
+ }),
126
+ });
127
+ } catch {
128
+ // Best effort — don't break agent if brain is down
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ToolGateway } from '../src/tools/gateway';
3
+ import type { ToolGatewayConfig } from '../src/tools/gateway';
4
+
5
+ const baseConfig: ToolGatewayConfig = {
6
+ enabled: true,
7
+ endpoint: 'https://gateway.example.com',
8
+ apiKey: 'test-key',
9
+ };
10
+
11
+ describe('ToolGateway', () => {
12
+ it('should load default tools when connect fails', async () => {
13
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
14
+ const gw = new ToolGateway(baseConfig);
15
+ await gw.connect();
16
+ expect(gw.isConnected).toBe(false);
17
+ expect(gw.toolCount).toBe(4);
18
+ expect(gw.listTools().map((t) => t.name)).toContain('gateway:web-search');
19
+ vi.unstubAllGlobals();
20
+ });
21
+
22
+ it('should filter tools by enabledTools config', async () => {
23
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
24
+ const gw = new ToolGateway({ ...baseConfig, enabledTools: ['web-search', 'tts'] });
25
+ await gw.connect();
26
+ expect(gw.toolCount).toBe(2);
27
+ vi.unstubAllGlobals();
28
+ });
29
+
30
+ it('should parse gateway discovery response', async () => {
31
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
32
+ ok: true,
33
+ json: async () => ({
34
+ tools: [
35
+ { name: 'web-search', description: 'Search', inputSchema: {}, available: true },
36
+ ],
37
+ }),
38
+ }));
39
+ const gw = new ToolGateway(baseConfig);
40
+ await gw.connect();
41
+ expect(gw.isConnected).toBe(true);
42
+ expect(gw.toolCount).toBe(1);
43
+ vi.unstubAllGlobals();
44
+ });
45
+
46
+ it('should return MCPTool instances from getTools()', async () => {
47
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
48
+ const gw = new ToolGateway(baseConfig);
49
+ await gw.connect();
50
+ const tools = gw.getTools();
51
+ expect(tools.length).toBe(4);
52
+ expect(tools[0]).toHaveProperty('execute');
53
+ expect(tools[0]).toHaveProperty('name');
54
+ vi.unstubAllGlobals();
55
+ });
56
+
57
+ it('should handle invoke errors gracefully', async () => {
58
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('timeout')));
59
+ const gw = new ToolGateway(baseConfig);
60
+ const result = await gw.invokeTool('web-search', { query: 'test' });
61
+ expect(result.isError).toBe(true);
62
+ expect(result.content).toContain('timeout');
63
+ vi.unstubAllGlobals();
64
+ });
65
+
66
+ it('should not discover tools when disabled', async () => {
67
+ const gw = new ToolGateway({ ...baseConfig, enabled: false });
68
+ await gw.connect();
69
+ expect(gw.toolCount).toBe(0);
70
+ });
71
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { StreamingManager, StreamableResponse } from '../src/core/streaming';
3
+ import type { StreamChunk } from '../src/core/streaming';
4
+
5
+ describe('StreamableResponse', () => {
6
+ it('should collect chunks and build text', () => {
7
+ const stream = new StreamableResponse('test-1');
8
+ stream.push({ id: '0', type: 'text', data: 'Hello ', timestamp: Date.now() });
9
+ stream.push({ id: '1', type: 'text', data: 'World', timestamp: Date.now() });
10
+ expect(stream.getText()).toBe('Hello World');
11
+ expect(stream.length).toBe(2);
12
+ });
13
+
14
+ it('should emit chunk events', () => {
15
+ const stream = new StreamableResponse('test-2');
16
+ const received: StreamChunk[] = [];
17
+ stream.on('chunk', (c: StreamChunk) => received.push(c));
18
+ stream.push({ id: '0', type: 'text', data: 'hi', timestamp: Date.now() });
19
+ expect(received.length).toBe(1);
20
+ });
21
+
22
+ it('should emit end event', () => {
23
+ const stream = new StreamableResponse('test-3');
24
+ let ended = false;
25
+ stream.on('end', () => { ended = true; });
26
+ stream.end();
27
+ expect(ended).toBe(true);
28
+ expect(stream.isEnded).toBe(true);
29
+ });
30
+
31
+ it('should apply backpressure at highWaterMark', () => {
32
+ const stream = new StreamableResponse('test-4', { highWaterMark: 2 });
33
+ const chunk = (): StreamChunk => ({ id: 'c', type: 'text', data: 'x', timestamp: Date.now() });
34
+ stream.push(chunk()); // 1 — ok
35
+ const ok = stream.push(chunk()); // 2 — triggers backpressure
36
+ expect(ok).toBe(false);
37
+ expect(stream.isPaused).toBe(true);
38
+ });
39
+
40
+ it('should flush buffer on resume', () => {
41
+ const stream = new StreamableResponse('test-5', { highWaterMark: 1 });
42
+ const received: StreamChunk[] = [];
43
+ stream.on('chunk', (c: StreamChunk) => received.push(c));
44
+ stream.push({ id: '0', type: 'text', data: 'a', timestamp: Date.now() });
45
+ // Now paused — next chunk goes to buffer
46
+ stream.push({ id: '1', type: 'text', data: 'b', timestamp: Date.now() });
47
+ expect(received.length).toBe(1);
48
+ stream.resume();
49
+ expect(received.length).toBe(2);
50
+ expect(stream.isPaused).toBe(false);
51
+ });
52
+
53
+ it('should reject pushes after end', () => {
54
+ const stream = new StreamableResponse('test-6');
55
+ stream.end();
56
+ const ok = stream.push({ id: '0', type: 'text', data: 'late', timestamp: Date.now() });
57
+ expect(ok).toBe(false);
58
+ expect(stream.length).toBe(0);
59
+ });
60
+ });
61
+
62
+ describe('StreamingManager', () => {
63
+ it('should create and manage streams', () => {
64
+ const mgr = new StreamingManager();
65
+ const stream = mgr.createStream();
66
+ expect(stream.id).toMatch(/^stream_/);
67
+ expect(mgr.activeCount).toBe(1);
68
+ });
69
+
70
+ it('should write chunks and end stream', () => {
71
+ const mgr = new StreamingManager();
72
+ const stream = mgr.createStream();
73
+ mgr.writeChunk(stream.id, 'hello');
74
+ mgr.writeChunk(stream.id, ' world');
75
+ mgr.endStream(stream.id);
76
+ expect(stream.getText()).toBe('hello world');
77
+ expect(stream.isEnded).toBe(true);
78
+ });
79
+
80
+ it('should return false for unknown stream writes', () => {
81
+ const mgr = new StreamingManager();
82
+ expect(mgr.writeChunk('nonexistent', 'data')).toBe(false);
83
+ });
84
+
85
+ it('should format SSE correctly', () => {
86
+ const chunk: StreamChunk = { id: 'c1', type: 'text', data: 'hi', timestamp: 123 };
87
+ const sse = StreamingManager.formatSSE(chunk);
88
+ expect(sse).toContain('event: text');
89
+ expect(sse).toContain('id: c1');
90
+ expect(sse).toContain('"data":"hi"');
91
+ });
92
+
93
+ it('should pipe to SSE response', () => {
94
+ const mgr = new StreamingManager();
95
+ const stream = mgr.createStream();
96
+ const written: string[] = [];
97
+ const mockRes = {
98
+ write: (d: string) => { written.push(d); return true; },
99
+ end: vi.fn(),
100
+ setHeader: vi.fn(),
101
+ };
102
+ StreamingManager.pipeSSE(stream, mockRes, { heartbeatInterval: 100_000 });
103
+ mgr.writeChunk(stream.id, 'data');
104
+ mgr.endStream(stream.id);
105
+ expect(written.length).toBeGreaterThan(0);
106
+ expect(mockRes.end).toHaveBeenCalled();
107
+ expect(mockRes.setHeader).toHaveBeenCalledWith('Content-Type', 'text/event-stream');
108
+ });
109
+ });
package/src/dtv/data.ts DELETED
@@ -1,29 +0,0 @@
1
- export interface DataSource {
2
- readonly name: string;
3
- readonly type: string;
4
- read(key: string): Promise<unknown>;
5
- }
6
-
7
- /**
8
- * MRGConfig reader — read-only data layer for agents.
9
- * Agents can read business data but cannot modify source systems.
10
- */
11
- export class MRGConfigReader implements DataSource {
12
- readonly name = 'mrg-config';
13
- readonly type = 'config';
14
- private data: Map<string, unknown>;
15
-
16
- constructor(initial?: Record<string, unknown>) {
17
- this.data = new Map(Object.entries(initial ?? {}));
18
- }
19
-
20
- async read(key: string): Promise<unknown> {
21
- return this.data.get(key);
22
- }
23
-
24
- load(data: Record<string, unknown>): void {
25
- for (const [k, v] of Object.entries(data)) {
26
- this.data.set(k, v);
27
- }
28
- }
29
- }
package/src/dtv/trust.ts DELETED
@@ -1,43 +0,0 @@
1
- import type { TrustLevelType } from '../schema/oad';
2
-
3
- /**
4
- * Trust levels: sandbox → verified → certified → listed
5
- *
6
- * - sandbox: No network, no file system, limited capabilities
7
- * - verified: Identity verified, basic capabilities
8
- * - certified: Passed security audit, full capabilities
9
- * - listed: Published in OPC marketplace
10
- */
11
- export class TrustManager {
12
- private level: TrustLevelType;
13
-
14
- constructor(level: TrustLevelType = 'sandbox') {
15
- this.level = level;
16
- }
17
-
18
- getLevel(): TrustLevelType {
19
- return this.level;
20
- }
21
-
22
- canAccessNetwork(): boolean {
23
- return this.level !== 'sandbox';
24
- }
25
-
26
- canAccessFileSystem(): boolean {
27
- return this.level === 'certified' || this.level === 'listed';
28
- }
29
-
30
- canPublish(): boolean {
31
- return this.level === 'listed';
32
- }
33
-
34
- upgrade(to: TrustLevelType): void {
35
- const order: TrustLevelType[] = ['sandbox', 'verified', 'certified', 'listed'];
36
- const currentIdx = order.indexOf(this.level);
37
- const targetIdx = order.indexOf(to);
38
- if (targetIdx <= currentIdx) {
39
- throw new Error(`Cannot downgrade trust from ${this.level} to ${to}`);
40
- }
41
- this.level = to;
42
- }
43
- }
package/src/dtv/value.ts DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * Value tracking — metrics and ROI for agent operations.
3
- */
4
- export interface ValueMetric {
5
- name: string;
6
- value: number;
7
- unit: string;
8
- timestamp: number;
9
- }
10
-
11
- export class ValueTracker {
12
- private metrics: Map<string, ValueMetric[]> = new Map();
13
- private trackedNames: Set<string>;
14
-
15
- constructor(metricNames: string[] = []) {
16
- this.trackedNames = new Set(metricNames);
17
- }
18
-
19
- record(name: string, value: number, unit: string = ''): void {
20
- if (!this.metrics.has(name)) {
21
- this.metrics.set(name, []);
22
- }
23
- this.metrics.get(name)!.push({ name, value, unit, timestamp: Date.now() });
24
- }
25
-
26
- getMetrics(name: string): ValueMetric[] {
27
- return this.metrics.get(name) ?? [];
28
- }
29
-
30
- getAverage(name: string): number {
31
- const m = this.getMetrics(name);
32
- if (m.length === 0) return 0;
33
- return m.reduce((sum, v) => sum + v.value, 0) / m.length;
34
- }
35
-
36
- getSummary(): Record<string, { count: number; average: number; last: number }> {
37
- const result: Record<string, { count: number; average: number; last: number }> = {};
38
- for (const [name, values] of this.metrics) {
39
- result[name] = {
40
- count: values.length,
41
- average: this.getAverage(name),
42
- last: values[values.length - 1]?.value ?? 0,
43
- };
44
- }
45
- return result;
46
- }
47
- }
@@ -1,223 +0,0 @@
1
- /**
2
- * Agent Marketplace - Package, publish, and install agents
3
- */
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import * as crypto from 'crypto';
7
- import { execSync } from 'child_process';
8
-
9
- export interface AgentManifest {
10
- name: string;
11
- version: string;
12
- description: string;
13
- author: string;
14
- license: string;
15
- oadVersion: string;
16
- channels: string[];
17
- skills: string[];
18
- files: string[];
19
- checksum: string;
20
- publishedAt: string;
21
- homepage?: string;
22
- repository?: string;
23
- tags?: string[];
24
- }
25
-
26
- export interface PublishOptions {
27
- oadPath: string;
28
- outputDir?: string;
29
- includeKnowledge?: boolean;
30
- }
31
-
32
- export interface InstallOptions {
33
- source: string; // local path or URL
34
- targetDir?: string;
35
- }
36
-
37
- function computeChecksum(filePath: string): string {
38
- const content = fs.readFileSync(filePath);
39
- return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
40
- }
41
-
42
- export async function publishAgent(options: PublishOptions): Promise<{ archivePath: string; manifest: AgentManifest }> {
43
- const { oadPath, outputDir = '.', includeKnowledge = false } = options;
44
- const absOad = path.resolve(oadPath);
45
- const baseDir = path.dirname(absOad);
46
-
47
- if (!fs.existsSync(absOad)) {
48
- throw new Error(`OAD file not found: ${absOad}`);
49
- }
50
-
51
- // Dynamic import yaml
52
- const yaml = await import('js-yaml');
53
- const oadContent = fs.readFileSync(absOad, 'utf-8');
54
- const oad = yaml.load(oadContent) as any;
55
-
56
- const name = oad.metadata?.name ?? 'unnamed-agent';
57
- const version = oad.metadata?.version ?? '0.0.0';
58
- const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
59
-
60
- // Collect files to package
61
- const filesToPack: { rel: string; abs: string }[] = [
62
- { rel: path.basename(absOad), abs: absOad },
63
- ];
64
-
65
- // Include common files
66
- const extras = ['.env.example', 'README.md', 'package.json'];
67
- for (const f of extras) {
68
- const fp = path.join(baseDir, f);
69
- if (fs.existsSync(fp)) {
70
- filesToPack.push({ rel: f, abs: fp });
71
- }
72
- }
73
-
74
- // Include knowledge base if requested
75
- if (includeKnowledge) {
76
- const kbFile = path.join(baseDir, '.opc-knowledge.json');
77
- if (fs.existsSync(kbFile)) {
78
- filesToPack.push({ rel: '.opc-knowledge.json', abs: kbFile });
79
- }
80
- }
81
-
82
- // Include prompts directory if exists
83
- const promptsDir = path.join(baseDir, 'prompts');
84
- if (fs.existsSync(promptsDir) && fs.statSync(promptsDir).isDirectory()) {
85
- const promptFiles = fs.readdirSync(promptsDir);
86
- for (const pf of promptFiles) {
87
- filesToPack.push({ rel: `prompts/${pf}`, abs: path.join(promptsDir, pf) });
88
- }
89
- }
90
-
91
- // Build manifest
92
- const manifest: AgentManifest = {
93
- name: safeName,
94
- version,
95
- description: oad.metadata?.description ?? '',
96
- author: oad.metadata?.author ?? '',
97
- license: oad.metadata?.license ?? 'Apache-2.0',
98
- oadVersion: 'opc/v1',
99
- channels: (oad.spec?.channels ?? []).map((c: any) => c.type),
100
- skills: (oad.spec?.skills ?? []).map((s: any) => s.name),
101
- files: filesToPack.map(f => f.rel),
102
- checksum: '',
103
- publishedAt: new Date().toISOString(),
104
- tags: oad.metadata?.marketplace?.tags,
105
- };
106
-
107
- // Create staging directory
108
- const stageDir = path.join(outputDir, `.opc-stage-${safeName}`);
109
- fs.mkdirSync(stageDir, { recursive: true });
110
-
111
- for (const f of filesToPack) {
112
- const dest = path.join(stageDir, f.rel);
113
- fs.mkdirSync(path.dirname(dest), { recursive: true });
114
- fs.copyFileSync(f.abs, dest);
115
- }
116
-
117
- // Write manifest
118
- fs.writeFileSync(path.join(stageDir, 'opc-manifest.json'), JSON.stringify(manifest, null, 2));
119
-
120
- // Create tar.gz
121
- const archiveName = `${safeName}-${version}.tar.gz`;
122
- const archivePath = path.join(outputDir, archiveName);
123
-
124
- try {
125
- execSync(`tar -czf "${archivePath}" -C "${stageDir}" .`, { stdio: 'pipe' });
126
- } catch {
127
- // Fallback: just zip the directory content list
128
- // On Windows without tar, create a simple zip-like package
129
- const packageData = {
130
- manifest,
131
- files: filesToPack.map(f => ({
132
- path: f.rel,
133
- content: fs.readFileSync(f.abs, 'utf-8'),
134
- })),
135
- };
136
- fs.writeFileSync(
137
- archivePath.replace('.tar.gz', '.opc.json'),
138
- JSON.stringify(packageData, null, 2),
139
- );
140
- }
141
-
142
- // Compute checksum
143
- if (fs.existsSync(archivePath)) {
144
- manifest.checksum = computeChecksum(archivePath);
145
- }
146
-
147
- // Cleanup staging
148
- fs.rmSync(stageDir, { recursive: true, force: true });
149
-
150
- // Write final manifest
151
- fs.writeFileSync(
152
- path.join(outputDir, 'opc-manifest.json'),
153
- JSON.stringify(manifest, null, 2),
154
- );
155
-
156
- return { archivePath: fs.existsSync(archivePath) ? archivePath : archivePath.replace('.tar.gz', '.opc.json'), manifest };
157
- }
158
-
159
- export async function installAgent(options: InstallOptions): Promise<{ dir: string; manifest: AgentManifest }> {
160
- const { source, targetDir } = options;
161
- const absSource = path.resolve(source);
162
-
163
- if (!fs.existsSync(absSource)) {
164
- throw new Error(`Package not found: ${absSource}`);
165
- }
166
-
167
- let manifest: AgentManifest;
168
- let installDir: string;
169
-
170
- if (absSource.endsWith('.opc.json')) {
171
- // JSON package format
172
- const pkg = JSON.parse(fs.readFileSync(absSource, 'utf-8'));
173
- manifest = pkg.manifest;
174
- installDir = targetDir ?? path.join('.', manifest.name);
175
- fs.mkdirSync(installDir, { recursive: true });
176
-
177
- for (const f of pkg.files) {
178
- const dest = path.join(installDir, f.path);
179
- fs.mkdirSync(path.dirname(dest), { recursive: true });
180
- fs.writeFileSync(dest, f.content, 'utf-8');
181
- }
182
- fs.writeFileSync(path.join(installDir, 'opc-manifest.json'), JSON.stringify(manifest, null, 2));
183
- } else {
184
- // tar.gz format
185
- const tmpDir = path.join(path.dirname(absSource), '.opc-extract-tmp');
186
- fs.mkdirSync(tmpDir, { recursive: true });
187
-
188
- try {
189
- execSync(`tar -xzf "${absSource}" -C "${tmpDir}"`, { stdio: 'pipe' });
190
- } catch {
191
- throw new Error('Failed to extract package. Ensure tar is available.');
192
- }
193
-
194
- const manifestPath = path.join(tmpDir, 'opc-manifest.json');
195
- if (!fs.existsSync(manifestPath)) {
196
- fs.rmSync(tmpDir, { recursive: true, force: true });
197
- throw new Error('Invalid package: missing opc-manifest.json');
198
- }
199
-
200
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
201
- installDir = targetDir ?? path.join('.', manifest.name);
202
-
203
- // Move files
204
- fs.mkdirSync(installDir, { recursive: true });
205
- const copyRecursive = (src: string, dest: string) => {
206
- const entries = fs.readdirSync(src, { withFileTypes: true });
207
- for (const entry of entries) {
208
- const srcPath = path.join(src, entry.name);
209
- const destPath = path.join(dest, entry.name);
210
- if (entry.isDirectory()) {
211
- fs.mkdirSync(destPath, { recursive: true });
212
- copyRecursive(srcPath, destPath);
213
- } else {
214
- fs.copyFileSync(srcPath, destPath);
215
- }
216
- }
217
- };
218
- copyRecursive(tmpDir, installDir);
219
- fs.rmSync(tmpDir, { recursive: true, force: true });
220
- }
221
-
222
- return { dir: installDir, manifest };
223
- }