@agentconnect/cli 0.1.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.
- package/README.md +58 -0
- package/dist/fs-utils.d.ts +9 -0
- package/dist/fs-utils.js +43 -0
- package/dist/host.d.ts +7 -0
- package/dist/host.js +663 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3020 -0
- package/dist/manifest.d.ts +10 -0
- package/dist/manifest.js +34 -0
- package/dist/observed.d.ts +7 -0
- package/dist/observed.js +69 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +36 -0
- package/dist/providers/claude.d.ts +10 -0
- package/dist/providers/claude.js +672 -0
- package/dist/providers/codex.d.ts +9 -0
- package/dist/providers/codex.js +509 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +90 -0
- package/dist/providers/local.d.ts +8 -0
- package/dist/providers/local.js +111 -0
- package/dist/providers/utils.d.ts +32 -0
- package/dist/providers/utils.js +256 -0
- package/dist/registry-validate.d.ts +6 -0
- package/dist/registry-validate.js +209 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +66 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.js +1 -0
- package/dist/zip.d.ts +9 -0
- package/dist/zip.js +71 -0
- package/package.json +45 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function getLocalBaseUrl() {
|
|
2
|
+
const base = process.env.AGENTCONNECT_LOCAL_BASE_URL || 'http://localhost:11434/v1';
|
|
3
|
+
return base.replace(/\/+$/, '');
|
|
4
|
+
}
|
|
5
|
+
function getLocalApiKey() {
|
|
6
|
+
return process.env.AGENTCONNECT_LOCAL_API_KEY || '';
|
|
7
|
+
}
|
|
8
|
+
function resolveLocalModel(model, fallback) {
|
|
9
|
+
if (!model)
|
|
10
|
+
return fallback;
|
|
11
|
+
const raw = String(model);
|
|
12
|
+
if (raw === 'local')
|
|
13
|
+
return fallback;
|
|
14
|
+
if (raw.startsWith('local:'))
|
|
15
|
+
return raw.slice('local:'.length);
|
|
16
|
+
if (raw.startsWith('local/'))
|
|
17
|
+
return raw.slice('local/'.length);
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
async function fetchJson(url, options = {}) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), 4000);
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
return { ok: false, status: res.status, data: null };
|
|
27
|
+
}
|
|
28
|
+
const data = (await res.json());
|
|
29
|
+
return { ok: true, status: res.status, data };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, status: 0, data: null };
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function ensureLocalInstalled() {
|
|
39
|
+
const base = getLocalBaseUrl();
|
|
40
|
+
const res = await fetchJson(`${base}/models`);
|
|
41
|
+
return { installed: res.ok };
|
|
42
|
+
}
|
|
43
|
+
export async function getLocalStatus() {
|
|
44
|
+
const base = getLocalBaseUrl();
|
|
45
|
+
const res = await fetchJson(`${base}/models`);
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
return { installed: false, loggedIn: false };
|
|
48
|
+
return { installed: true, loggedIn: true };
|
|
49
|
+
}
|
|
50
|
+
export async function loginLocal(options = {}) {
|
|
51
|
+
if (typeof options.baseUrl === 'string') {
|
|
52
|
+
process.env.AGENTCONNECT_LOCAL_BASE_URL = options.baseUrl;
|
|
53
|
+
}
|
|
54
|
+
if (typeof options.apiKey === 'string') {
|
|
55
|
+
process.env.AGENTCONNECT_LOCAL_API_KEY = options.apiKey;
|
|
56
|
+
}
|
|
57
|
+
if (typeof options.model === 'string') {
|
|
58
|
+
process.env.AGENTCONNECT_LOCAL_MODEL = options.model;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(options.models)) {
|
|
61
|
+
process.env.AGENTCONNECT_LOCAL_MODELS = JSON.stringify(options.models.filter(Boolean));
|
|
62
|
+
}
|
|
63
|
+
const status = await getLocalStatus();
|
|
64
|
+
return { loggedIn: status.installed };
|
|
65
|
+
}
|
|
66
|
+
export async function listLocalModels() {
|
|
67
|
+
const base = getLocalBaseUrl();
|
|
68
|
+
const res = await fetchJson(`${base}/models`);
|
|
69
|
+
if (!res.ok || !res.data || !Array.isArray(res.data.data))
|
|
70
|
+
return [];
|
|
71
|
+
return res.data.data
|
|
72
|
+
.map((entry) => ({ id: entry.id, provider: 'local', displayName: entry.id }))
|
|
73
|
+
.filter((entry) => entry.id);
|
|
74
|
+
}
|
|
75
|
+
export async function runLocalPrompt({ prompt, model, onEvent, }) {
|
|
76
|
+
const base = getLocalBaseUrl();
|
|
77
|
+
const fallback = process.env.AGENTCONNECT_LOCAL_MODEL || '';
|
|
78
|
+
const resolvedModel = resolveLocalModel(model, fallback);
|
|
79
|
+
if (!resolvedModel) {
|
|
80
|
+
onEvent({ type: 'error', message: 'Local provider model is not configured.' });
|
|
81
|
+
return { sessionId: null };
|
|
82
|
+
}
|
|
83
|
+
const payload = {
|
|
84
|
+
model: resolvedModel,
|
|
85
|
+
messages: [{ role: 'user', content: prompt }],
|
|
86
|
+
stream: false,
|
|
87
|
+
};
|
|
88
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
89
|
+
const apiKey = getLocalApiKey();
|
|
90
|
+
if (apiKey)
|
|
91
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
92
|
+
const res = await fetchJson(`${base}/chat/completions`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers,
|
|
95
|
+
body: JSON.stringify(payload),
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
onEvent({ type: 'error', message: 'Local provider request failed.' });
|
|
99
|
+
return { sessionId: null };
|
|
100
|
+
}
|
|
101
|
+
const message = res.data?.choices?.[0]?.message?.content;
|
|
102
|
+
const text = typeof message === 'string' ? message : '';
|
|
103
|
+
if (text) {
|
|
104
|
+
onEvent({ type: 'delta', text });
|
|
105
|
+
onEvent({ type: 'final', text });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
onEvent({ type: 'error', message: 'Local provider returned no content.' });
|
|
109
|
+
}
|
|
110
|
+
return { sessionId: null };
|
|
111
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SpawnOptions } from 'child_process';
|
|
2
|
+
import type { CommandResult } from '../types.js';
|
|
3
|
+
export declare function debugLog(scope: string, message: string, details?: Record<string, unknown>): void;
|
|
4
|
+
export interface SplitCommandResult {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function splitCommand(value: string | string[] | undefined): SplitCommandResult;
|
|
9
|
+
export declare function resolveWindowsCommand(command: string): string;
|
|
10
|
+
export declare function resolveCommandPath(command: string): string | null;
|
|
11
|
+
export declare function commandExists(command: string): boolean;
|
|
12
|
+
export interface RunCommandOptions extends SpawnOptions {
|
|
13
|
+
input?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function runCommand(command: string, args: string[], options?: RunCommandOptions): Promise<CommandResult>;
|
|
17
|
+
export declare function createLineParser(onLine: (line: string) => void): (chunk: Buffer | string) => void;
|
|
18
|
+
export interface CheckVersionResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
version: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function checkCommandVersion(command: string, argsList: string[][]): Promise<CheckVersionResult>;
|
|
23
|
+
export type PackageManager = 'bun' | 'pnpm' | 'npm' | 'brew' | 'unknown';
|
|
24
|
+
export interface InstallCommandResult extends SplitCommandResult {
|
|
25
|
+
packageManager: PackageManager;
|
|
26
|
+
}
|
|
27
|
+
export declare function detectPackageManager(): Promise<PackageManager>;
|
|
28
|
+
export declare function getInstallCommand(packageManager: PackageManager, packageName: string): SplitCommandResult;
|
|
29
|
+
export declare function buildInstallCommandAuto(packageName: string): Promise<InstallCommandResult>;
|
|
30
|
+
export declare function buildInstallCommand(envVar: string, fallback: string): SplitCommandResult;
|
|
31
|
+
export declare function buildLoginCommand(envVar: string, fallback: string): SplitCommandResult;
|
|
32
|
+
export declare function buildStatusCommand(envVar: string, fallback: string): SplitCommandResult;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const DEBUG_ENABLED = Boolean(process.env.AGENTCONNECT_DEBUG?.trim());
|
|
6
|
+
export function debugLog(scope, message, details) {
|
|
7
|
+
if (!DEBUG_ENABLED)
|
|
8
|
+
return;
|
|
9
|
+
let suffix = '';
|
|
10
|
+
if (details) {
|
|
11
|
+
try {
|
|
12
|
+
suffix = ` ${JSON.stringify(details)}`;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
suffix = '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
console.log(`[AgentConnect][${scope}] ${message}${suffix}`);
|
|
19
|
+
}
|
|
20
|
+
export function splitCommand(value) {
|
|
21
|
+
if (!value)
|
|
22
|
+
return { command: '', args: [] };
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return { command: value[0] ?? '', args: value.slice(1) };
|
|
25
|
+
const input = String(value).trim();
|
|
26
|
+
const parts = [];
|
|
27
|
+
let current = '';
|
|
28
|
+
let quote = null;
|
|
29
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
30
|
+
const char = input[i];
|
|
31
|
+
if (quote) {
|
|
32
|
+
if (char === quote) {
|
|
33
|
+
quote = null;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
current += char;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (char === '"' || char === "'") {
|
|
41
|
+
quote = char;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === ' ') {
|
|
45
|
+
if (current) {
|
|
46
|
+
parts.push(current);
|
|
47
|
+
current = '';
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
current += char;
|
|
52
|
+
}
|
|
53
|
+
if (current)
|
|
54
|
+
parts.push(current);
|
|
55
|
+
return { command: parts[0] ?? '', args: parts.slice(1) };
|
|
56
|
+
}
|
|
57
|
+
export function resolveWindowsCommand(command) {
|
|
58
|
+
if (process.platform !== 'win32')
|
|
59
|
+
return command;
|
|
60
|
+
if (!command)
|
|
61
|
+
return command;
|
|
62
|
+
if (command.endsWith('.cmd') || command.endsWith('.exe') || command.includes('\\')) {
|
|
63
|
+
return command;
|
|
64
|
+
}
|
|
65
|
+
return `${command}.cmd`;
|
|
66
|
+
}
|
|
67
|
+
function getCommonBinPaths() {
|
|
68
|
+
const home = os.homedir();
|
|
69
|
+
const bunInstall = process.env.BUN_INSTALL || path.join(home, '.bun');
|
|
70
|
+
const pnpmHome = process.env.PNPM_HOME || path.join(home, 'Library', 'pnpm');
|
|
71
|
+
const npmPrefix = process.env.NPM_CONFIG_PREFIX;
|
|
72
|
+
const npmBin = npmPrefix ? path.join(npmPrefix, 'bin') : '';
|
|
73
|
+
const claudeLocal = process.env.CLAUDE_CONFIG_DIR
|
|
74
|
+
? path.join(process.env.CLAUDE_CONFIG_DIR, 'local')
|
|
75
|
+
: path.join(home, '.claude', 'local');
|
|
76
|
+
return [
|
|
77
|
+
path.join(bunInstall, 'bin'),
|
|
78
|
+
pnpmHome,
|
|
79
|
+
path.join(home, '.local', 'bin'),
|
|
80
|
+
claudeLocal,
|
|
81
|
+
path.join(home, '.claude', 'bin'),
|
|
82
|
+
npmBin,
|
|
83
|
+
'/opt/homebrew/bin',
|
|
84
|
+
'/usr/local/bin',
|
|
85
|
+
'/usr/bin',
|
|
86
|
+
'/bin',
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
function getCommandCandidates(command) {
|
|
90
|
+
if (process.platform !== 'win32')
|
|
91
|
+
return [command];
|
|
92
|
+
if (command.endsWith('.cmd') || command.endsWith('.exe') || command.endsWith('.bat')) {
|
|
93
|
+
return [command];
|
|
94
|
+
}
|
|
95
|
+
return [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`];
|
|
96
|
+
}
|
|
97
|
+
export function resolveCommandPath(command) {
|
|
98
|
+
if (!command)
|
|
99
|
+
return null;
|
|
100
|
+
if (command.includes('/') || command.includes('\\')) {
|
|
101
|
+
return existsSync(command) ? command : null;
|
|
102
|
+
}
|
|
103
|
+
const candidates = getCommandCandidates(command);
|
|
104
|
+
const searchPaths = new Set();
|
|
105
|
+
const pathEntries = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
|
106
|
+
for (const entry of pathEntries) {
|
|
107
|
+
if (entry)
|
|
108
|
+
searchPaths.add(entry);
|
|
109
|
+
}
|
|
110
|
+
for (const entry of getCommonBinPaths()) {
|
|
111
|
+
if (entry)
|
|
112
|
+
searchPaths.add(entry);
|
|
113
|
+
}
|
|
114
|
+
for (const dir of searchPaths) {
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
const fullPath = path.join(dir, candidate);
|
|
117
|
+
if (existsSync(fullPath))
|
|
118
|
+
return fullPath;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
export function commandExists(command) {
|
|
124
|
+
return Boolean(resolveCommandPath(command));
|
|
125
|
+
}
|
|
126
|
+
export function runCommand(command, args, options = {}) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const { input, timeoutMs, ...spawnOptions } = options;
|
|
129
|
+
if (!command) {
|
|
130
|
+
resolve({ code: -1, stdout: '', stderr: 'Command is empty' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const resolved = resolveCommandPath(command);
|
|
134
|
+
if (!resolved) {
|
|
135
|
+
resolve({ code: 127, stdout: '', stderr: `Executable not found in PATH: "${command}"` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const child = spawn(resolved, args, {
|
|
139
|
+
...spawnOptions,
|
|
140
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
141
|
+
});
|
|
142
|
+
let stdout = '';
|
|
143
|
+
let stderr = '';
|
|
144
|
+
let timeout;
|
|
145
|
+
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
|
146
|
+
timeout = setTimeout(() => {
|
|
147
|
+
child.kill();
|
|
148
|
+
resolve({ code: -1, stdout, stderr: `${stderr}Command timed out` });
|
|
149
|
+
}, timeoutMs);
|
|
150
|
+
}
|
|
151
|
+
if (input) {
|
|
152
|
+
child.stdin?.write(input);
|
|
153
|
+
}
|
|
154
|
+
child.stdin?.end();
|
|
155
|
+
child.stdout?.on('data', (chunk) => {
|
|
156
|
+
stdout += chunk.toString('utf8');
|
|
157
|
+
});
|
|
158
|
+
child.stderr?.on('data', (chunk) => {
|
|
159
|
+
stderr += chunk.toString('utf8');
|
|
160
|
+
});
|
|
161
|
+
child.on('error', (err) => {
|
|
162
|
+
if (timeout)
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
resolve({ code: -1, stdout, stderr: `${stderr}${err.message}` });
|
|
165
|
+
});
|
|
166
|
+
child.on('close', (code) => {
|
|
167
|
+
if (timeout)
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
export function createLineParser(onLine) {
|
|
174
|
+
let buffer = '';
|
|
175
|
+
return (chunk) => {
|
|
176
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
177
|
+
buffer += text;
|
|
178
|
+
let idx;
|
|
179
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
180
|
+
const line = buffer.slice(0, idx).trim();
|
|
181
|
+
buffer = buffer.slice(idx + 1);
|
|
182
|
+
if (line)
|
|
183
|
+
onLine(line);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export async function checkCommandVersion(command, argsList) {
|
|
188
|
+
for (const args of argsList) {
|
|
189
|
+
const result = await runCommand(command, args);
|
|
190
|
+
if (result.code === 0) {
|
|
191
|
+
const version = result.stdout.trim().split('\n')[0] ?? '';
|
|
192
|
+
return { ok: true, version };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { ok: false, version: '' };
|
|
196
|
+
}
|
|
197
|
+
const packageManagerCache = { detected: null };
|
|
198
|
+
async function isCommandAvailable(command) {
|
|
199
|
+
const result = await runCommand(command, ['--version']);
|
|
200
|
+
return result.code === 0;
|
|
201
|
+
}
|
|
202
|
+
export async function detectPackageManager() {
|
|
203
|
+
if (packageManagerCache.detected) {
|
|
204
|
+
return packageManagerCache.detected;
|
|
205
|
+
}
|
|
206
|
+
// Priority: bun > pnpm > npm > brew
|
|
207
|
+
if (await isCommandAvailable('bun')) {
|
|
208
|
+
packageManagerCache.detected = 'bun';
|
|
209
|
+
return 'bun';
|
|
210
|
+
}
|
|
211
|
+
if (await isCommandAvailable('pnpm')) {
|
|
212
|
+
packageManagerCache.detected = 'pnpm';
|
|
213
|
+
return 'pnpm';
|
|
214
|
+
}
|
|
215
|
+
if (await isCommandAvailable('npm')) {
|
|
216
|
+
packageManagerCache.detected = 'npm';
|
|
217
|
+
return 'npm';
|
|
218
|
+
}
|
|
219
|
+
if (process.platform === 'darwin' && (await isCommandAvailable('brew'))) {
|
|
220
|
+
packageManagerCache.detected = 'brew';
|
|
221
|
+
return 'brew';
|
|
222
|
+
}
|
|
223
|
+
packageManagerCache.detected = 'unknown';
|
|
224
|
+
return 'unknown';
|
|
225
|
+
}
|
|
226
|
+
export function getInstallCommand(packageManager, packageName) {
|
|
227
|
+
switch (packageManager) {
|
|
228
|
+
case 'bun':
|
|
229
|
+
return { command: 'bun', args: ['add', '-g', packageName] };
|
|
230
|
+
case 'pnpm':
|
|
231
|
+
return { command: 'pnpm', args: ['add', '-g', packageName] };
|
|
232
|
+
case 'npm':
|
|
233
|
+
return { command: 'npm', args: ['install', '-g', packageName] };
|
|
234
|
+
case 'brew':
|
|
235
|
+
return { command: 'brew', args: ['install', packageName] };
|
|
236
|
+
default:
|
|
237
|
+
return { command: '', args: [] };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export async function buildInstallCommandAuto(packageName) {
|
|
241
|
+
const pm = await detectPackageManager();
|
|
242
|
+
const cmd = getInstallCommand(pm, packageName);
|
|
243
|
+
return { ...cmd, packageManager: pm };
|
|
244
|
+
}
|
|
245
|
+
export function buildInstallCommand(envVar, fallback) {
|
|
246
|
+
const value = process.env[envVar] || fallback;
|
|
247
|
+
return splitCommand(value);
|
|
248
|
+
}
|
|
249
|
+
export function buildLoginCommand(envVar, fallback) {
|
|
250
|
+
const value = process.env[envVar] || fallback;
|
|
251
|
+
return splitCommand(value);
|
|
252
|
+
}
|
|
253
|
+
export function buildStatusCommand(envVar, fallback) {
|
|
254
|
+
const value = process.env[envVar] || fallback;
|
|
255
|
+
return splitCommand(value);
|
|
256
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RegistryValidationResult } from './types.js';
|
|
2
|
+
export interface ValidateRegistryOptions {
|
|
3
|
+
registryPath: string;
|
|
4
|
+
requireSignature?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function validateRegistry({ registryPath, requireSignature, }: ValidateRegistryOptions): Promise<RegistryValidationResult>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { createPublicKey, verify as verifySignature } from 'crypto';
|
|
3
|
+
import { readJson, fileExists } from './fs-utils.js';
|
|
4
|
+
import { hashFile } from './zip.js';
|
|
5
|
+
import { validateManifest } from './manifest.js';
|
|
6
|
+
function compareSemver(a, b) {
|
|
7
|
+
const parse = (v) => v
|
|
8
|
+
.split('-')[0]
|
|
9
|
+
.split('.')
|
|
10
|
+
.map((n) => Number(n));
|
|
11
|
+
const pa = parse(a);
|
|
12
|
+
const pb = parse(b);
|
|
13
|
+
for (let i = 0; i < 3; i += 1) {
|
|
14
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
15
|
+
if (diff !== 0)
|
|
16
|
+
return diff;
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
function resolveEntry(registryPath, entryPath) {
|
|
21
|
+
if (!entryPath || typeof entryPath !== 'string')
|
|
22
|
+
return null;
|
|
23
|
+
return path.resolve(registryPath, entryPath);
|
|
24
|
+
}
|
|
25
|
+
function normalizeSignatureAlg(signatureAlg) {
|
|
26
|
+
const value = String(signatureAlg || '').toLowerCase();
|
|
27
|
+
if (value === 'ed25519')
|
|
28
|
+
return null;
|
|
29
|
+
if (value === 'rsa-sha256')
|
|
30
|
+
return 'sha256';
|
|
31
|
+
if (value === 'ecdsa-sha256')
|
|
32
|
+
return 'sha256';
|
|
33
|
+
return 'sha256';
|
|
34
|
+
}
|
|
35
|
+
async function verifySignatureFile({ signaturePath, hash, }) {
|
|
36
|
+
const signature = await readJson(signaturePath);
|
|
37
|
+
if (!signature || typeof signature !== 'object') {
|
|
38
|
+
return { ok: false, message: 'Signature payload is not valid JSON.' };
|
|
39
|
+
}
|
|
40
|
+
if (signature.hash !== hash) {
|
|
41
|
+
return { ok: false, message: 'Signature hash does not match app hash.' };
|
|
42
|
+
}
|
|
43
|
+
const publicKey = signature.publicKey;
|
|
44
|
+
if (!publicKey || typeof publicKey !== 'string') {
|
|
45
|
+
return { ok: false, message: 'Signature is missing public key.' };
|
|
46
|
+
}
|
|
47
|
+
const signatureValue = signature.signature;
|
|
48
|
+
if (!signatureValue || typeof signatureValue !== 'string') {
|
|
49
|
+
return { ok: false, message: 'Signature is missing signature bytes.' };
|
|
50
|
+
}
|
|
51
|
+
const algorithm = normalizeSignatureAlg(signature.signatureAlg);
|
|
52
|
+
const key = createPublicKey(publicKey);
|
|
53
|
+
const payload = Buffer.from(hash, 'hex');
|
|
54
|
+
const sigBuffer = Buffer.from(signatureValue, 'base64');
|
|
55
|
+
const ok = verifySignature(algorithm, payload, key, sigBuffer);
|
|
56
|
+
return { ok, message: ok ? '' : 'Signature verification failed.' };
|
|
57
|
+
}
|
|
58
|
+
export async function validateRegistry({ registryPath, requireSignature = false, }) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
const warnings = [];
|
|
61
|
+
const indexPath = path.join(registryPath, 'index.json');
|
|
62
|
+
if (!(await fileExists(indexPath))) {
|
|
63
|
+
return {
|
|
64
|
+
valid: false,
|
|
65
|
+
errors: [{ path: 'index.json', message: 'index.json not found.' }],
|
|
66
|
+
warnings,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const index = await readJson(indexPath).catch(() => null);
|
|
70
|
+
if (!index || typeof index !== 'object') {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
errors: [{ path: 'index.json', message: 'index.json is not valid JSON.' }],
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const apps = index.apps;
|
|
78
|
+
if (!apps || typeof apps !== 'object') {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
errors: [{ path: 'index.json', message: 'index.json missing apps map.' }],
|
|
82
|
+
warnings,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
for (const [appId, appEntry] of Object.entries(apps)) {
|
|
86
|
+
if (!appEntry || typeof appEntry !== 'object') {
|
|
87
|
+
errors.push({
|
|
88
|
+
path: `apps.${appId}`,
|
|
89
|
+
message: `App entry for ${appId} is invalid.`,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const versions = appEntry.versions;
|
|
94
|
+
if (!versions || typeof versions !== 'object') {
|
|
95
|
+
errors.push({
|
|
96
|
+
path: `apps.${appId}`,
|
|
97
|
+
message: `App entry for ${appId} missing versions.`,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const versionKeys = Object.keys(versions);
|
|
102
|
+
if (!versionKeys.length) {
|
|
103
|
+
errors.push({
|
|
104
|
+
path: `apps.${appId}`,
|
|
105
|
+
message: `App entry for ${appId} has no versions.`,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const latest = appEntry.latest;
|
|
110
|
+
if (latest && !versions[latest]) {
|
|
111
|
+
errors.push({
|
|
112
|
+
path: `apps.${appId}`,
|
|
113
|
+
message: `App ${appId} latest (${latest}) not found in versions.`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const sorted = [...versionKeys].sort(compareSemver);
|
|
117
|
+
const expectedLatest = sorted[sorted.length - 1];
|
|
118
|
+
if (latest && expectedLatest && compareSemver(latest, expectedLatest) !== 0) {
|
|
119
|
+
warnings.push({
|
|
120
|
+
path: `apps.${appId}`,
|
|
121
|
+
message: `App ${appId} latest (${latest}) is not the newest (${expectedLatest}).`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
for (const [version, entry] of Object.entries(versions)) {
|
|
125
|
+
if (!entry || typeof entry !== 'object') {
|
|
126
|
+
errors.push({
|
|
127
|
+
path: `apps.${appId}.versions.${version}`,
|
|
128
|
+
message: `App ${appId}@${version} entry is invalid.`,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const zipPath = resolveEntry(registryPath, entry.path);
|
|
133
|
+
const manifestPath = resolveEntry(registryPath, entry.manifest);
|
|
134
|
+
const signaturePath = entry.signature ? resolveEntry(registryPath, entry.signature) : null;
|
|
135
|
+
if (!zipPath || !(await fileExists(zipPath))) {
|
|
136
|
+
errors.push({
|
|
137
|
+
path: `apps.${appId}.versions.${version}`,
|
|
138
|
+
message: `App ${appId}@${version} app.zip missing.`,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!manifestPath || !(await fileExists(manifestPath))) {
|
|
143
|
+
errors.push({
|
|
144
|
+
path: `apps.${appId}.versions.${version}`,
|
|
145
|
+
message: `App ${appId}@${version} manifest missing.`,
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const hash = await hashFile(zipPath);
|
|
150
|
+
if (entry.hash && entry.hash !== hash) {
|
|
151
|
+
errors.push({
|
|
152
|
+
path: `apps.${appId}.versions.${version}`,
|
|
153
|
+
message: `App ${appId}@${version} hash mismatch.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const manifest = await readJson(manifestPath).catch(() => null);
|
|
157
|
+
if (!manifest) {
|
|
158
|
+
errors.push({
|
|
159
|
+
path: `apps.${appId}.versions.${version}`,
|
|
160
|
+
message: `App ${appId}@${version} manifest invalid JSON.`,
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const manifestValidation = await validateManifest(manifest);
|
|
165
|
+
if (!manifestValidation.valid) {
|
|
166
|
+
errors.push({
|
|
167
|
+
path: `apps.${appId}.versions.${version}`,
|
|
168
|
+
message: `App ${appId}@${version} manifest failed schema validation.`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (manifest.id !== appId || manifest.version !== version) {
|
|
172
|
+
errors.push({
|
|
173
|
+
path: `apps.${appId}.versions.${version}`,
|
|
174
|
+
message: `App ${appId}@${version} manifest id/version mismatch.`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (signaturePath) {
|
|
178
|
+
if (!(await fileExists(signaturePath))) {
|
|
179
|
+
errors.push({
|
|
180
|
+
path: `apps.${appId}.versions.${version}`,
|
|
181
|
+
message: `App ${appId}@${version} signature file missing.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const verification = await verifySignatureFile({ signaturePath, hash });
|
|
186
|
+
if (!verification.ok) {
|
|
187
|
+
errors.push({
|
|
188
|
+
path: `apps.${appId}.versions.${version}`,
|
|
189
|
+
message: `App ${appId}@${version} signature invalid: ${verification.message}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (requireSignature) {
|
|
195
|
+
errors.push({
|
|
196
|
+
path: `apps.${appId}.versions.${version}`,
|
|
197
|
+
message: `App ${appId}@${version} is missing a signature.`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
warnings.push({
|
|
202
|
+
path: `apps.${appId}.versions.${version}`,
|
|
203
|
+
message: `App ${appId}@${version} has no signature.`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
209
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AppManifest } from './types.js';
|
|
2
|
+
export interface PublishPackageOptions {
|
|
3
|
+
zipPath: string;
|
|
4
|
+
signaturePath?: string;
|
|
5
|
+
registryPath: string;
|
|
6
|
+
manifest?: AppManifest;
|
|
7
|
+
}
|
|
8
|
+
export interface PublishPackageResult {
|
|
9
|
+
appId: string;
|
|
10
|
+
version: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
targetZip: string;
|
|
13
|
+
manifestPath: string;
|
|
14
|
+
signaturePath: string | null;
|
|
15
|
+
indexPath: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function publishPackage({ zipPath, signaturePath, registryPath, manifest, }: PublishPackageOptions): Promise<PublishPackageResult>;
|