@42ailab/42plugin 0.1.0-beta.1 → 0.1.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.
package/src/db/client.ts DELETED
@@ -1,180 +0,0 @@
1
- import { createClient as createLibsqlClient } from '@libsql/client';
2
- import { Pool } from 'pg';
3
- import { getDbPath } from '../config';
4
-
5
- type Query = string | { sql: string; args?: unknown[] };
6
-
7
- export interface DbClient {
8
- execute(query: Query): Promise<{ rows: unknown[] }>;
9
- close(): Promise<void> | void;
10
- }
11
-
12
- let client: DbClient | null = null;
13
-
14
- function normalizeQuery(query: Query): { sql: string; args: unknown[] } {
15
- if (typeof query === 'string') {
16
- return { sql: query, args: [] };
17
- }
18
- return {
19
- sql: query.sql,
20
- args: query.args || [],
21
- };
22
- }
23
-
24
- function convertParamsToPg(sql: string): string {
25
- let paramIndex = 0;
26
- return sql.replace(/\?/g, () => {
27
- paramIndex += 1;
28
- return `$${paramIndex}`;
29
- });
30
- }
31
-
32
- function createSqliteClient(): DbClient {
33
- const sqliteClient = createLibsqlClient({
34
- url: `file:${getDbPath()}`,
35
- });
36
-
37
- return {
38
- execute: (query) => sqliteClient.execute(query as any),
39
- close: () => sqliteClient.close(),
40
- };
41
- }
42
-
43
- function createPostgresClient(): DbClient {
44
- const connectionString =
45
- process.env.CLI_DATABASE_URL ||
46
- process.env.LOCAL_DATABASE_URL ||
47
- process.env.DATABASE_URL ||
48
- process.env.POSTGRES_URL;
49
-
50
- if (!connectionString) {
51
- throw new Error(
52
- 'Postgres connection string not provided. Set CLI_DATABASE_URL, LOCAL_DATABASE_URL, or DATABASE_URL.'
53
- );
54
- }
55
-
56
- const pool = new Pool({ connectionString });
57
-
58
- return {
59
- async execute(query) {
60
- const { sql, args } = normalizeQuery(query);
61
- const text = convertParamsToPg(sql);
62
- const res = await pool.query(text, args);
63
- return { rows: res.rows };
64
- },
65
- async close() {
66
- await pool.end();
67
- },
68
- };
69
- }
70
-
71
- function createDbClient(): DbClient {
72
- const driver = (process.env.CLI_DB_DRIVER || process.env.DB_DRIVER || 'sqlite').toLowerCase();
73
- if (driver === 'postgres' || driver === 'postgresql' || driver === 'pg') {
74
- return createPostgresClient();
75
- }
76
- return createSqliteClient();
77
- }
78
-
79
- export function getDb(): DbClient {
80
- if (!client) {
81
- client = createDbClient();
82
- }
83
- return client;
84
- }
85
-
86
- // Future extension: Support remote sync (Turso); fallback to current driver for Postgres
87
- export function getDbWithSync(authToken: string): DbClient {
88
- const driver = (process.env.CLI_DB_DRIVER || process.env.DB_DRIVER || 'sqlite').toLowerCase();
89
-
90
- if (driver === 'postgres' || driver === 'postgresql' || driver === 'pg') {
91
- return getDb();
92
- }
93
-
94
- const sqliteClient = createLibsqlClient({
95
- url: `file:${getDbPath()}`,
96
- syncUrl: process.env.TURSO_DATABASE_URL,
97
- authToken,
98
- syncInterval: 60, // Sync every 60 seconds
99
- });
100
-
101
- return {
102
- execute: (query) => sqliteClient.execute(query as any),
103
- close: () => sqliteClient.close(),
104
- };
105
- }
106
-
107
- export async function initDb(): Promise<void> {
108
- const db = getDb();
109
-
110
- // Create projects table
111
- await db.execute(`
112
- CREATE TABLE IF NOT EXISTS projects (
113
- id TEXT PRIMARY KEY,
114
- path TEXT NOT NULL UNIQUE,
115
- name TEXT,
116
- registered_at TEXT NOT NULL,
117
- last_used_at TEXT NOT NULL
118
- )
119
- `);
120
-
121
- await db.execute(`
122
- CREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path)
123
- `);
124
-
125
- // Create plugin_cache table
126
- await db.execute(`
127
- CREATE TABLE IF NOT EXISTS plugin_cache (
128
- id TEXT PRIMARY KEY,
129
- full_name TEXT NOT NULL,
130
- type TEXT NOT NULL,
131
- version TEXT NOT NULL,
132
- cache_path TEXT NOT NULL,
133
- checksum TEXT NOT NULL,
134
- size_bytes INTEGER,
135
- downloaded_at TEXT NOT NULL,
136
- UNIQUE(full_name, version)
137
- )
138
- `);
139
-
140
- await db.execute(`
141
- CREATE INDEX IF NOT EXISTS idx_cache_name ON plugin_cache(full_name)
142
- `);
143
-
144
- // Create project_plugins table
145
- await db.execute(`
146
- CREATE TABLE IF NOT EXISTS project_plugins (
147
- id TEXT PRIMARY KEY,
148
- project_id TEXT NOT NULL,
149
- full_name TEXT NOT NULL,
150
- type TEXT NOT NULL,
151
- version TEXT NOT NULL,
152
- cache_path TEXT NOT NULL,
153
- link_path TEXT NOT NULL,
154
- installed_at TEXT NOT NULL,
155
- source TEXT DEFAULT 'direct',
156
- source_list TEXT,
157
- UNIQUE(project_id, full_name),
158
- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
159
- )
160
- `);
161
-
162
- await db.execute(`
163
- CREATE INDEX IF NOT EXISTS idx_pp_project ON project_plugins(project_id)
164
- `);
165
-
166
- // Create config table
167
- await db.execute(`
168
- CREATE TABLE IF NOT EXISTS config (
169
- key TEXT PRIMARY KEY,
170
- value TEXT NOT NULL
171
- )
172
- `);
173
- }
174
-
175
- export async function closeDb(): Promise<void> {
176
- if (client) {
177
- await client.close();
178
- client = null;
179
- }
180
- }
@@ -1,128 +0,0 @@
1
- import { loadSecrets } from './auth';
2
- import { config } from '../config';
3
- import type {
4
- AuthStartResponse,
5
- AuthPollResponse,
6
- UserProfile,
7
- CapabilityDownload,
8
- PluginDetail,
9
- ListDownload,
10
- SearchResult,
11
- SearchOptions,
12
- } from '../types/api';
13
-
14
- class ApiClient {
15
- private accessToken: string | null = null;
16
- private initialized = false;
17
-
18
- async init(): Promise<void> {
19
- if (this.initialized) return;
20
- const secrets = await loadSecrets();
21
- this.accessToken = secrets?.access_token || null;
22
- this.initialized = true;
23
- }
24
-
25
- isAuthenticated(): boolean {
26
- return !!this.accessToken;
27
- }
28
-
29
- private async request<T>(
30
- method: string,
31
- path: string,
32
- options: {
33
- body?: unknown;
34
- auth?: boolean;
35
- } = {}
36
- ): Promise<T> {
37
- if (!this.initialized) {
38
- await this.init();
39
- }
40
-
41
- const headers: Record<string, string> = {
42
- 'Content-Type': 'application/json',
43
- };
44
-
45
- if (options.auth !== false && this.accessToken) {
46
- headers['Authorization'] = `Bearer ${this.accessToken}`;
47
- }
48
-
49
- const response = await fetch(`${config.apiBase}${path}`, {
50
- method,
51
- headers,
52
- body: method === 'POST' || method === 'PUT' || method === 'PATCH'
53
- ? JSON.stringify(options.body ?? {})
54
- : undefined,
55
- });
56
-
57
- const data = await response.json() as { data?: T; error?: { message: string } };
58
-
59
- if (!response.ok) {
60
- throw new Error(data.error?.message || 'API request failed');
61
- }
62
-
63
- return data.data as T;
64
- }
65
-
66
- // Auth
67
- async startAuth(): Promise<AuthStartResponse> {
68
- return this.request('POST', '/v1/auth/cli/start', { auth: false });
69
- }
70
-
71
- async pollAuth(code: string): Promise<AuthPollResponse> {
72
- return this.request('GET', `/v1/auth/cli/poll?code=${code}`, { auth: false });
73
- }
74
-
75
- async getMe(): Promise<UserProfile> {
76
- return this.request('GET', '/v1/auth/me');
77
- }
78
-
79
- // Capabilities
80
- async getCapabilityDownload(fullName: string): Promise<CapabilityDownload> {
81
- const encoded = encodeURIComponent(fullName);
82
- return this.request('GET', `/v1/capabilities/by-name/download?full_name=${encoded}`);
83
- }
84
-
85
- async getCapabilityDownloadBySlug(author: string, slug: string): Promise<CapabilityDownload> {
86
- return this.request('GET', `/v1/capabilities/${author}/${slug}/download`);
87
- }
88
-
89
- // Plugins
90
- async getPluginByName(fullName: string): Promise<PluginDetail> {
91
- const encoded = encodeURIComponent(fullName);
92
- return this.request('GET', `/v1/plugins/by-name?full_name=${encoded}`);
93
- }
94
-
95
- async getPluginDownload(user: string, plugin: string, version?: string): Promise<CapabilityDownload> {
96
- let path = `/v1/plugins/${user}/${plugin}/download`;
97
- if (version) {
98
- path += `?version=${version}`;
99
- }
100
- return this.request('GET', path);
101
- }
102
-
103
- // Lists
104
- async getListDownload(username: string, slugOrId: string): Promise<ListDownload> {
105
- return this.request('GET', `/v1/lists/${username}/list/${slugOrId}/download`);
106
- }
107
-
108
- // Search
109
- async search(query: string, options?: SearchOptions): Promise<SearchResult> {
110
- const params = new URLSearchParams({ q: query });
111
- if (options?.type) params.set('type', options.type);
112
- if (options?.per_page) params.set('per_page', String(options.per_page));
113
- return this.request('GET', `/v1/search?${params}`);
114
- }
115
-
116
- // Reset token (for logout)
117
- resetToken(): void {
118
- this.accessToken = null;
119
- this.initialized = false;
120
- }
121
-
122
- // Set token (for login)
123
- setToken(token: string): void {
124
- this.accessToken = token;
125
- }
126
- }
127
-
128
- export const api = new ApiClient();
@@ -1,46 +0,0 @@
1
- import fs from 'fs/promises';
2
- import { getSecretsPath } from '../config';
3
-
4
- const SECRETS_FILE = 'secrets.json';
5
-
6
- export interface Secrets {
7
- access_token: string;
8
- refresh_token: string;
9
- created_at: string;
10
- }
11
-
12
- export async function saveSecrets(secrets: Secrets): Promise<void> {
13
- const filePath = getSecretsPath();
14
-
15
- await fs.writeFile(
16
- filePath,
17
- JSON.stringify(secrets, null, 2),
18
- { mode: 0o600 } // Only current user can read/write
19
- );
20
- }
21
-
22
- export async function loadSecrets(): Promise<Secrets | null> {
23
- const filePath = getSecretsPath();
24
-
25
- try {
26
- const content = await fs.readFile(filePath, 'utf-8');
27
- return JSON.parse(content);
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- export async function clearSecrets(): Promise<void> {
34
- const filePath = getSecretsPath();
35
-
36
- try {
37
- await fs.rm(filePath);
38
- } catch {
39
- // File doesn't exist, ignore
40
- }
41
- }
42
-
43
- export async function getAccessToken(): Promise<string | null> {
44
- const secrets = await loadSecrets();
45
- return secrets?.access_token || null;
46
- }
@@ -1,101 +0,0 @@
1
- import fs from 'fs/promises';
2
- import { nanoid } from 'nanoid';
3
- import { getDb } from '../db/client';
4
- import type { CacheEntry } from '../types/db';
5
-
6
- export async function getCachedPlugin(fullName: string, version: string): Promise<CacheEntry | null> {
7
- const db = getDb();
8
- const result = await db.execute({
9
- sql: 'SELECT * FROM plugin_cache WHERE full_name = ? AND version = ?',
10
- args: [fullName, version],
11
- });
12
-
13
- if (result.rows.length === 0) return null;
14
- return result.rows[0] as unknown as CacheEntry;
15
- }
16
-
17
- export async function cachePlugin(entry: {
18
- full_name: string;
19
- type: string;
20
- version: string;
21
- cache_path: string;
22
- checksum: string;
23
- }): Promise<void> {
24
- const db = getDb();
25
- const id = nanoid();
26
- const now = new Date().toISOString();
27
-
28
- // Calculate directory size
29
- let sizeBytes: number | null = null;
30
- try {
31
- sizeBytes = await getDirSize(entry.cache_path);
32
- } catch {
33
- // Ignore
34
- }
35
-
36
- await db.execute({
37
- sql: `INSERT INTO plugin_cache
38
- (id, full_name, type, version, cache_path, checksum, size_bytes, downloaded_at)
39
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
40
- ON CONFLICT(full_name, version) DO UPDATE SET
41
- id = excluded.id,
42
- type = excluded.type,
43
- cache_path = excluded.cache_path,
44
- checksum = excluded.checksum,
45
- size_bytes = excluded.size_bytes,
46
- downloaded_at = excluded.downloaded_at`,
47
- args: [
48
- id,
49
- entry.full_name,
50
- entry.type,
51
- entry.version,
52
- entry.cache_path,
53
- entry.checksum,
54
- sizeBytes,
55
- now,
56
- ],
57
- });
58
- }
59
-
60
- export async function removeFromCache(fullName: string, version: string): Promise<void> {
61
- const entry = await getCachedPlugin(fullName, version);
62
-
63
- if (entry) {
64
- // Delete files
65
- try {
66
- await fs.rm(entry.cache_path, { recursive: true, force: true });
67
- } catch {
68
- // Ignore
69
- }
70
-
71
- // Delete record
72
- const db = getDb();
73
- await db.execute({
74
- sql: 'DELETE FROM plugin_cache WHERE id = ?',
75
- args: [entry.id],
76
- });
77
- }
78
- }
79
-
80
- export async function getAllCachedPlugins(): Promise<CacheEntry[]> {
81
- const db = getDb();
82
- const result = await db.execute('SELECT * FROM plugin_cache ORDER BY downloaded_at DESC');
83
- return result.rows as unknown as CacheEntry[];
84
- }
85
-
86
- async function getDirSize(dirPath: string): Promise<number> {
87
- let size = 0;
88
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
89
-
90
- for (const entry of entries) {
91
- const fullPath = `${dirPath}/${entry.name}`;
92
- if (entry.isDirectory()) {
93
- size += await getDirSize(fullPath);
94
- } else {
95
- const stats = await fs.stat(fullPath);
96
- size += stats.size;
97
- }
98
- }
99
-
100
- return size;
101
- }
@@ -1,148 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { createWriteStream } from 'fs';
4
- import { pipeline } from 'stream/promises';
5
- import * as tar from 'tar';
6
- import { createHash } from 'crypto';
7
- import { getCacheDir } from '../config';
8
-
9
- export async function downloadAndExtract(
10
- url: string,
11
- expectedChecksum: string,
12
- fullName: string,
13
- version: string
14
- ): Promise<string> {
15
- // Build cache path
16
- const cachePath = buildCachePath(fullName, version);
17
- await fs.mkdir(cachePath, { recursive: true });
18
-
19
- // Download file
20
- const tempPath = path.join(cachePath, '_download.tar.gz');
21
- await downloadFile(url, tempPath);
22
-
23
- // Verify checksum
24
- const actualChecksum = await computeFileChecksum(tempPath);
25
- const expectedHash = expectedChecksum.replace('sha256:', '');
26
- if (actualChecksum !== expectedHash) {
27
- await fs.rm(tempPath);
28
- throw new Error('Checksum verification failed');
29
- }
30
-
31
- // Extract
32
- await tar.extract({
33
- file: tempPath,
34
- cwd: cachePath,
35
- });
36
-
37
- // Delete temp file
38
- await fs.rm(tempPath);
39
-
40
- // Check if tarball extracted to a single subdirectory (common pattern)
41
- // If so, return that subdirectory as the content path
42
- const entries = await fs.readdir(cachePath);
43
- if (entries.length === 1) {
44
- const subPath = path.join(cachePath, entries[0]);
45
- const stat = await fs.stat(subPath);
46
- if (stat.isDirectory()) {
47
- return subPath;
48
- }
49
- }
50
-
51
- return cachePath;
52
- }
53
-
54
- export function buildCachePath(fullName: string, version: string): string {
55
- // fullName formats:
56
- // - owner/repo:plugin:type:name (capability from GitHub)
57
- // - owner:custom:type:name (custom/curated capability)
58
- // - owner/repo:plugin (plugin)
59
- // - owner/plugin (simple)
60
-
61
- const parts = fullName.split(':');
62
- const [ownerRepo] = parts;
63
- const [owner, repo] = ownerRepo.split('/');
64
-
65
- if (parts.length === 4) {
66
- const [, pluginName, capType, capName] = parts;
67
-
68
- // Check if this is a custom capability (no slash in first part)
69
- if (!repo) {
70
- // owner:custom:type:name format (custom capability)
71
- return path.join(
72
- getCacheDir(),
73
- owner,
74
- pluginName, // 'custom'
75
- capType,
76
- capName,
77
- version
78
- );
79
- }
80
-
81
- // owner/repo:plugin:type:name format (GitHub capability)
82
- return path.join(
83
- getCacheDir(),
84
- owner,
85
- repo,
86
- pluginName,
87
- capType,
88
- capName,
89
- version
90
- );
91
- } else if (parts.length === 2) {
92
- // owner/repo:plugin
93
- const [, pluginName] = parts;
94
- return path.join(
95
- getCacheDir(),
96
- owner,
97
- repo || 'default',
98
- pluginName,
99
- version
100
- );
101
- } else {
102
- // owner/plugin (simple format)
103
- return path.join(
104
- getCacheDir(),
105
- owner,
106
- repo || 'default',
107
- version
108
- );
109
- }
110
- }
111
-
112
- async function downloadFile(url: string, destPath: string): Promise<void> {
113
- const response = await fetch(url);
114
-
115
- if (!response.ok) {
116
- throw new Error(`Download failed: ${response.status}`);
117
- }
118
-
119
- if (!response.body) {
120
- throw new Error('No response body');
121
- }
122
-
123
- const fileStream = createWriteStream(destPath);
124
- // Convert Web ReadableStream to Node stream
125
- const reader = response.body.getReader();
126
-
127
- try {
128
- while (true) {
129
- const { done, value } = await reader.read();
130
- if (done) break;
131
- fileStream.write(value);
132
- }
133
- } finally {
134
- reader.releaseLock();
135
- fileStream.end();
136
- }
137
-
138
- // Wait for file to be fully written
139
- await new Promise<void>((resolve, reject) => {
140
- fileStream.on('finish', resolve);
141
- fileStream.on('error', reject);
142
- });
143
- }
144
-
145
- async function computeFileChecksum(filePath: string): Promise<string> {
146
- const content = await fs.readFile(filePath);
147
- return createHash('sha256').update(content).digest('hex');
148
- }
@@ -1,86 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { exec } from 'child_process';
4
- import { promisify } from 'util';
5
- import { platform } from '../utils/platform';
6
-
7
- const execAsync = promisify(exec);
8
-
9
- export async function createLink(source: string, target: string): Promise<void> {
10
- // Normalize paths and remove trailing slashes (important for symlink to work correctly)
11
- const normalizedSource = source.replace(/\/+$/, '');
12
- const normalizedTarget = target.replace(/\/+$/, '');
13
-
14
- // Ensure target directory exists
15
- await fs.mkdir(path.dirname(normalizedTarget), { recursive: true });
16
-
17
- // If target already exists, remove it first
18
- try {
19
- await fs.rm(normalizedTarget, { recursive: true, force: true });
20
- } catch {
21
- // Ignore
22
- }
23
-
24
- // Choose link strategy based on platform
25
- if (platform.isWindows) {
26
- await createWindowsLink(normalizedSource, normalizedTarget);
27
- } else {
28
- await createPosixLink(normalizedSource, normalizedTarget);
29
- }
30
- }
31
-
32
- async function createPosixLink(source: string, target: string): Promise<void> {
33
- try {
34
- await fs.symlink(source, target, 'dir');
35
- } catch (error) {
36
- // symlink failed, fall back to copy
37
- const errorMsg = error instanceof Error ? error.message : String(error);
38
- console.warn(`Symlink failed (${errorMsg}), falling back to copy`);
39
- await copyDir(source, target);
40
- }
41
- }
42
-
43
- async function createWindowsLink(source: string, target: string): Promise<void> {
44
- try {
45
- // Try to create junction
46
- await execAsync(`mklink /J "${target}" "${source}"`);
47
- } catch {
48
- // junction failed, fall back to copy
49
- console.warn('Junction failed, falling back to copy');
50
- await copyDir(source, target);
51
- }
52
- }
53
-
54
- async function copyDir(source: string, target: string): Promise<void> {
55
- await fs.mkdir(target, { recursive: true });
56
-
57
- const entries = await fs.readdir(source, { withFileTypes: true });
58
-
59
- for (const entry of entries) {
60
- const srcPath = path.join(source, entry.name);
61
- const destPath = path.join(target, entry.name);
62
-
63
- if (entry.isDirectory()) {
64
- await copyDir(srcPath, destPath);
65
- } else {
66
- await fs.copyFile(srcPath, destPath);
67
- }
68
- }
69
- }
70
-
71
- export async function removeLink(linkPath: string): Promise<void> {
72
- try {
73
- await fs.rm(linkPath, { recursive: true, force: true });
74
- } catch {
75
- // Ignore errors
76
- }
77
- }
78
-
79
- export async function isSymlink(linkPath: string): Promise<boolean> {
80
- try {
81
- const stats = await fs.lstat(linkPath);
82
- return stats.isSymbolicLink();
83
- } catch {
84
- return false;
85
- }
86
- }