cognitive-modules-cli 2.2.1 → 2.2.7
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -29
- package/dist/cli.js +519 -23
- package/dist/commands/add.d.ts +33 -14
- package/dist/commands/add.js +383 -16
- package/dist/commands/compose.js +60 -23
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/init.js +23 -1
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.js +650 -0
- package/dist/commands/pipe.d.ts +1 -0
- package/dist/commands/pipe.js +31 -11
- package/dist/commands/remove.js +33 -2
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +61 -28
- package/dist/commands/search.d.ts +28 -0
- package/dist/commands/search.js +143 -0
- package/dist/commands/test.d.ts +65 -0
- package/dist/commands/test.js +454 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +106 -14
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.js +97 -0
- package/dist/errors/index.d.ts +225 -0
- package/dist/errors/index.js +420 -0
- package/dist/mcp/server.js +84 -79
- package/dist/modules/composition.js +97 -32
- package/dist/modules/loader.js +4 -2
- package/dist/modules/runner.d.ts +72 -5
- package/dist/modules/runner.js +306 -59
- package/dist/modules/subagent.d.ts +6 -1
- package/dist/modules/subagent.js +18 -13
- package/dist/modules/validator.js +14 -6
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +147 -5
- package/dist/providers/base.d.ts +11 -0
- package/dist/providers/base.js +18 -0
- package/dist/providers/gemini.d.ts +15 -0
- package/dist/providers/gemini.js +122 -5
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.js +111 -3
- package/dist/providers/openai.d.ts +11 -0
- package/dist/providers/openai.js +133 -0
- package/dist/registry/client.d.ts +212 -0
- package/dist/registry/client.js +359 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/tar.d.ts +8 -0
- package/dist/registry/tar.js +353 -0
- package/dist/server/http.js +301 -45
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/sse.d.ts +13 -0
- package/dist/server/sse.js +22 -0
- package/dist/types.d.ts +32 -1
- package/dist/types.js +4 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +31 -7
- package/dist/modules/composition.test.d.ts +0 -11
- package/dist/modules/composition.test.js +0 -450
- package/dist/modules/policy.test.d.ts +0 -10
- package/dist/modules/policy.test.js +0 -369
- package/src/cli.ts +0 -471
- package/src/commands/add.ts +0 -315
- package/src/commands/compose.ts +0 -185
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -94
- package/src/commands/list.ts +0 -33
- package/src/commands/pipe.ts +0 -76
- package/src/commands/remove.ts +0 -57
- package/src/commands/run.ts +0 -80
- package/src/commands/update.ts +0 -130
- package/src/commands/versions.ts +0 -79
- package/src/index.ts +0 -90
- package/src/mcp/index.ts +0 -5
- package/src/mcp/server.ts +0 -403
- package/src/modules/composition.test.ts +0 -558
- package/src/modules/composition.ts +0 -1674
- package/src/modules/index.ts +0 -9
- package/src/modules/loader.ts +0 -508
- package/src/modules/policy.test.ts +0 -455
- package/src/modules/runner.ts +0 -1983
- package/src/modules/subagent.ts +0 -277
- package/src/modules/validator.ts +0 -700
- package/src/providers/anthropic.ts +0 -89
- package/src/providers/base.ts +0 -29
- package/src/providers/deepseek.ts +0 -83
- package/src/providers/gemini.ts +0 -117
- package/src/providers/index.ts +0 -78
- package/src/providers/minimax.ts +0 -81
- package/src/providers/moonshot.ts +0 -82
- package/src/providers/ollama.ts +0 -83
- package/src/providers/openai.ts +0 -84
- package/src/providers/qwen.ts +0 -82
- package/src/server/http.ts +0 -316
- package/src/server/index.ts +0 -6
- package/src/types.ts +0 -599
- package/tsconfig.json +0 -17
package/dist/commands/add.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Add command - Install modules from GitHub
|
|
2
|
+
* Add command - Install modules from GitHub or Registry
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* From Registry:
|
|
5
|
+
* cog add code-reviewer
|
|
6
|
+
* cog add code-reviewer@2.0.0
|
|
7
|
+
*
|
|
8
|
+
* From GitHub:
|
|
9
|
+
* cog add ziel-io/cognitive-modules -m code-simplifier
|
|
10
|
+
* cog add https://github.com/org/repo --module name --tag v1.0.0
|
|
6
11
|
*/
|
|
7
12
|
import type { CommandContext, CommandResult } from '../types.js';
|
|
8
13
|
export interface AddOptions {
|
|
@@ -10,25 +15,39 @@ export interface AddOptions {
|
|
|
10
15
|
name?: string;
|
|
11
16
|
branch?: string;
|
|
12
17
|
tag?: string;
|
|
18
|
+
registry?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface InstallInfo {
|
|
21
|
+
source: string;
|
|
22
|
+
githubUrl: string;
|
|
23
|
+
modulePath?: string;
|
|
24
|
+
tag?: string;
|
|
25
|
+
branch?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
installedAt: string;
|
|
28
|
+
installedTime: string;
|
|
29
|
+
/** Registry module name if installed from registry */
|
|
30
|
+
registryModule?: string;
|
|
31
|
+
/** Registry URL if installed from a custom registry */
|
|
32
|
+
registryUrl?: string;
|
|
13
33
|
}
|
|
14
34
|
interface InstallManifest {
|
|
15
|
-
[moduleName: string]:
|
|
16
|
-
source: string;
|
|
17
|
-
githubUrl: string;
|
|
18
|
-
modulePath?: string;
|
|
19
|
-
tag?: string;
|
|
20
|
-
branch?: string;
|
|
21
|
-
version?: string;
|
|
22
|
-
installedAt: string;
|
|
23
|
-
installedTime: string;
|
|
24
|
-
};
|
|
35
|
+
[moduleName: string]: InstallInfo;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Get installation info for a module
|
|
28
39
|
*/
|
|
29
40
|
export declare function getInstallInfo(moduleName: string): Promise<InstallManifest[string] | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Add a module from the registry
|
|
43
|
+
*/
|
|
44
|
+
export declare function addFromRegistry(moduleSpec: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
|
|
30
45
|
/**
|
|
31
46
|
* Add a module from GitHub
|
|
32
47
|
*/
|
|
33
|
-
export declare function
|
|
48
|
+
export declare function addFromGitHub(url: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Add a module from GitHub or Registry (auto-detect source)
|
|
51
|
+
*/
|
|
52
|
+
export declare function add(source: string, ctx: CommandContext, options?: AddOptions): Promise<CommandResult>;
|
|
34
53
|
export {};
|
package/dist/commands/add.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Add command - Install modules from GitHub
|
|
2
|
+
* Add command - Install modules from GitHub or Registry
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* From Registry:
|
|
5
|
+
* cog add code-reviewer
|
|
6
|
+
* cog add code-reviewer@2.0.0
|
|
7
|
+
*
|
|
8
|
+
* From GitHub:
|
|
9
|
+
* cog add ziel-io/cognitive-modules -m code-simplifier
|
|
10
|
+
* cog add https://github.com/org/repo --module name --tag v1.0.0
|
|
6
11
|
*/
|
|
7
|
-
import { createWriteStream, existsSync, mkdirSync, rmSync, readdirSync, statSync, copyFileSync } from 'node:fs';
|
|
12
|
+
import { createWriteStream, existsSync, mkdirSync, rmSync, readdirSync, statSync, copyFileSync, lstatSync } from 'node:fs';
|
|
8
13
|
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
9
|
-
import { pipeline } from 'node:stream/promises';
|
|
14
|
+
import { pipeline, finished } from 'node:stream/promises';
|
|
10
15
|
import { Readable } from 'node:stream';
|
|
11
|
-
import { join, basename } from 'node:path';
|
|
16
|
+
import { join, basename, dirname, resolve, sep, isAbsolute } from 'node:path';
|
|
12
17
|
import { homedir, tmpdir } from 'node:os';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { RegistryClient } from '../registry/client.js';
|
|
20
|
+
import { extractTarGzFile } from '../registry/tar.js';
|
|
13
21
|
// Module storage paths
|
|
14
22
|
const USER_MODULES_DIR = join(homedir(), '.cognitive', 'modules');
|
|
15
23
|
const INSTALLED_MANIFEST = join(homedir(), '.cognitive', 'installed.json');
|
|
@@ -33,6 +41,33 @@ function parseGitHubUrl(url) {
|
|
|
33
41
|
fullUrl: `https://github.com/${org}/${repo}`,
|
|
34
42
|
};
|
|
35
43
|
}
|
|
44
|
+
function assertSafeModuleName(name) {
|
|
45
|
+
const trimmed = name.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
throw new Error('Invalid module name: empty');
|
|
48
|
+
}
|
|
49
|
+
if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
|
|
50
|
+
throw new Error(`Invalid module name: ${name}`);
|
|
51
|
+
}
|
|
52
|
+
if (isAbsolute(trimmed)) {
|
|
53
|
+
throw new Error(`Invalid module name (absolute path not allowed): ${name}`);
|
|
54
|
+
}
|
|
55
|
+
return trimmed;
|
|
56
|
+
}
|
|
57
|
+
function resolveModuleTarget(moduleName) {
|
|
58
|
+
const safeName = assertSafeModuleName(moduleName);
|
|
59
|
+
const targetPath = resolve(USER_MODULES_DIR, safeName);
|
|
60
|
+
const root = resolve(USER_MODULES_DIR) + sep;
|
|
61
|
+
if (!targetPath.startsWith(root)) {
|
|
62
|
+
throw new Error(`Invalid module name (path traversal): ${moduleName}`);
|
|
63
|
+
}
|
|
64
|
+
return targetPath;
|
|
65
|
+
}
|
|
66
|
+
function isPathWithinRoot(rootDir, targetPath) {
|
|
67
|
+
const root = resolve(rootDir);
|
|
68
|
+
const target = resolve(targetPath);
|
|
69
|
+
return target === root || target.startsWith(root + sep);
|
|
70
|
+
}
|
|
36
71
|
/**
|
|
37
72
|
* Download and extract ZIP from GitHub
|
|
38
73
|
*/
|
|
@@ -56,6 +91,9 @@ async function downloadAndExtract(org, repo, ref, isTag) {
|
|
|
56
91
|
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
57
92
|
// Extract using built-in unzip (available on most systems)
|
|
58
93
|
const { execSync } = await import('node:child_process');
|
|
94
|
+
// Validate ZIP entries to avoid path traversal (Zip Slip)
|
|
95
|
+
const listing = execSync(`unzip -Z1 "${zipPath}"`, { stdio: 'pipe' }).toString('utf-8');
|
|
96
|
+
assertSafeZipEntries(listing);
|
|
59
97
|
execSync(`unzip -q "${zipPath}" -d "${tempDir}"`, { stdio: 'pipe' });
|
|
60
98
|
// Find extracted directory
|
|
61
99
|
const entries = readdirSync(tempDir).filter(e => e !== 'repo.zip' && statSync(join(tempDir, e)).isDirectory());
|
|
@@ -64,6 +102,22 @@ async function downloadAndExtract(org, repo, ref, isTag) {
|
|
|
64
102
|
}
|
|
65
103
|
return join(tempDir, entries[0]);
|
|
66
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate ZIP listing to prevent path traversal.
|
|
107
|
+
*/
|
|
108
|
+
function assertSafeZipEntries(listing) {
|
|
109
|
+
const entries = listing.split(/\r?\n/).map(e => e.trim()).filter(Boolean);
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const normalized = entry.replace(/\\/g, '/');
|
|
112
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) {
|
|
113
|
+
throw new Error(`Unsafe ZIP entry (absolute path): ${entry}`);
|
|
114
|
+
}
|
|
115
|
+
const parts = normalized.split('/');
|
|
116
|
+
if (parts.includes('..')) {
|
|
117
|
+
throw new Error(`Unsafe ZIP entry (path traversal): ${entry}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
67
121
|
/**
|
|
68
122
|
* Check if a directory is a valid module
|
|
69
123
|
*/
|
|
@@ -76,11 +130,15 @@ function isValidModule(path) {
|
|
|
76
130
|
* Find module within repository
|
|
77
131
|
*/
|
|
78
132
|
function findModuleInRepo(repoRoot, modulePath) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
133
|
+
const candidatePaths = [
|
|
134
|
+
resolve(repoRoot, modulePath),
|
|
135
|
+
resolve(repoRoot, 'cognitive', 'modules', modulePath),
|
|
136
|
+
resolve(repoRoot, 'modules', modulePath),
|
|
83
137
|
];
|
|
138
|
+
const possiblePaths = candidatePaths.filter((p) => isPathWithinRoot(repoRoot, p));
|
|
139
|
+
if (possiblePaths.length === 0) {
|
|
140
|
+
throw new Error(`Invalid module path (outside repository root): ${modulePath}`);
|
|
141
|
+
}
|
|
84
142
|
for (const p of possiblePaths) {
|
|
85
143
|
if (existsSync(p) && isValidModule(p)) {
|
|
86
144
|
return p;
|
|
@@ -97,7 +155,11 @@ function copyDir(src, dest) {
|
|
|
97
155
|
for (const entry of readdirSync(src)) {
|
|
98
156
|
const srcPath = join(src, entry);
|
|
99
157
|
const destPath = join(dest, entry);
|
|
100
|
-
|
|
158
|
+
const st = lstatSync(srcPath);
|
|
159
|
+
if (st.isSymbolicLink()) {
|
|
160
|
+
throw new Error(`Refusing to install module containing symlink: ${srcPath}`);
|
|
161
|
+
}
|
|
162
|
+
if (st.isDirectory()) {
|
|
101
163
|
copyDir(srcPath, destPath);
|
|
102
164
|
}
|
|
103
165
|
else {
|
|
@@ -105,6 +167,74 @@ function copyDir(src, dest) {
|
|
|
105
167
|
}
|
|
106
168
|
}
|
|
107
169
|
}
|
|
170
|
+
async function downloadTarballWithSha256(url, outPath, maxBytes) {
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
173
|
+
const hash = createHash('sha256');
|
|
174
|
+
let received = 0;
|
|
175
|
+
const fileStream = createWriteStream(outPath);
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(url, {
|
|
178
|
+
headers: { 'User-Agent': 'cognitive-runtime/2.2' },
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
|
|
183
|
+
}
|
|
184
|
+
const contentLengthHeader = response.headers?.get?.('content-length');
|
|
185
|
+
if (contentLengthHeader) {
|
|
186
|
+
const contentLength = Number(contentLengthHeader);
|
|
187
|
+
if (!Number.isNaN(contentLength) && contentLength > maxBytes) {
|
|
188
|
+
throw new Error(`Tarball too large: ${contentLength} bytes (max ${maxBytes})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!response.body) {
|
|
192
|
+
throw new Error('Tarball response has no body');
|
|
193
|
+
}
|
|
194
|
+
const reader = response.body.getReader?.();
|
|
195
|
+
if (!reader) {
|
|
196
|
+
// Fallback: stream pipeline without incremental hash.
|
|
197
|
+
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
198
|
+
const content = await readFile(outPath);
|
|
199
|
+
return createHash('sha256').update(content).digest('hex');
|
|
200
|
+
}
|
|
201
|
+
while (true) {
|
|
202
|
+
const { done, value } = await reader.read();
|
|
203
|
+
if (done)
|
|
204
|
+
break;
|
|
205
|
+
if (value) {
|
|
206
|
+
received += value.byteLength;
|
|
207
|
+
if (received > maxBytes) {
|
|
208
|
+
controller.abort();
|
|
209
|
+
throw new Error(`Tarball too large: ${received} bytes (max ${maxBytes})`);
|
|
210
|
+
}
|
|
211
|
+
const chunk = Buffer.from(value);
|
|
212
|
+
hash.update(chunk);
|
|
213
|
+
if (!fileStream.write(chunk)) {
|
|
214
|
+
await new Promise((resolve) => fileStream.once('drain', () => resolve()));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
fileStream.end();
|
|
219
|
+
await finished(fileStream);
|
|
220
|
+
return hash.digest('hex');
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
try {
|
|
224
|
+
fileStream.destroy();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// ignore
|
|
228
|
+
}
|
|
229
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
230
|
+
throw new Error('Tarball download timed out after 10000ms');
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
clearTimeout(timeout);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
108
238
|
/**
|
|
109
239
|
* Get module version from module.yaml or MODULE.md
|
|
110
240
|
*/
|
|
@@ -157,10 +287,219 @@ export async function getInstallInfo(moduleName) {
|
|
|
157
287
|
const manifest = JSON.parse(content);
|
|
158
288
|
return manifest[moduleName] || null;
|
|
159
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Check if input looks like a GitHub URL or org/repo format
|
|
292
|
+
*
|
|
293
|
+
* GitHub formats:
|
|
294
|
+
* - https://github.com/org/repo
|
|
295
|
+
* - http://github.com/org/repo
|
|
296
|
+
* - github:org/repo
|
|
297
|
+
* - org/repo (shorthand, must be exactly two parts)
|
|
298
|
+
*
|
|
299
|
+
* NOT GitHub (registry module names):
|
|
300
|
+
* - code-reviewer
|
|
301
|
+
* - code-reviewer@1.0.0
|
|
302
|
+
* - my-module (no slash)
|
|
303
|
+
*/
|
|
304
|
+
function isGitHubSource(input) {
|
|
305
|
+
// URL formats
|
|
306
|
+
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
// github: prefix format
|
|
310
|
+
if (input.startsWith('github:')) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
// Contains github.com
|
|
314
|
+
if (input.includes('github.com')) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
// Shorthand format: org/repo (exactly two parts, no @ version suffix)
|
|
318
|
+
// Must match pattern like: owner/repo or owner/repo but NOT module@version
|
|
319
|
+
const shorthandMatch = input.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
|
|
320
|
+
if (shorthandMatch) {
|
|
321
|
+
// Additional check: if it contains @, it's likely a scoped npm package or versioned module
|
|
322
|
+
// But GitHub shorthand shouldn't have @ in the middle
|
|
323
|
+
return !input.includes('@');
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Parse module name with optional version: "module-name@1.0.0"
|
|
329
|
+
*/
|
|
330
|
+
function parseModuleSpec(spec) {
|
|
331
|
+
const match = spec.match(/^([^@]+)(?:@(.+))?$/);
|
|
332
|
+
if (!match) {
|
|
333
|
+
return { name: spec };
|
|
334
|
+
}
|
|
335
|
+
return { name: match[1], version: match[2] };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Add a module from the registry
|
|
339
|
+
*/
|
|
340
|
+
export async function addFromRegistry(moduleSpec, ctx, options = {}) {
|
|
341
|
+
const { name: moduleName, version: requestedVersion } = parseModuleSpec(moduleSpec);
|
|
342
|
+
const { name: customName, registry: registryUrl } = options;
|
|
343
|
+
let tempDir;
|
|
344
|
+
try {
|
|
345
|
+
const client = new RegistryClient(registryUrl);
|
|
346
|
+
const moduleInfo = await client.getModule(moduleName);
|
|
347
|
+
if (!moduleInfo) {
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
error: `Module not found in registry: ${moduleName}\nUse 'cog search' to find available modules.`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// Check if deprecated
|
|
354
|
+
if (moduleInfo.deprecated) {
|
|
355
|
+
console.error(`Warning: Module '${moduleName}' is deprecated.`);
|
|
356
|
+
}
|
|
357
|
+
// Get download info
|
|
358
|
+
const downloadInfo = await client.getDownloadUrl(moduleName);
|
|
359
|
+
if (downloadInfo.isGitHub && downloadInfo.githubInfo) {
|
|
360
|
+
const { org, repo, path, ref } = downloadInfo.githubInfo;
|
|
361
|
+
// Use addFromGitHub() directly (not add() to avoid recursion)
|
|
362
|
+
const result = await addFromGitHub(`${org}/${repo}`, ctx, {
|
|
363
|
+
module: path,
|
|
364
|
+
name: customName || moduleName,
|
|
365
|
+
tag: requestedVersion || ref,
|
|
366
|
+
branch: ref || 'main',
|
|
367
|
+
});
|
|
368
|
+
// If successful, update the install manifest to track registry info
|
|
369
|
+
if (result.success && result.data) {
|
|
370
|
+
const data = result.data;
|
|
371
|
+
const installName = data.name;
|
|
372
|
+
// Update manifest with registry info
|
|
373
|
+
const existingInfo = await getInstallInfo(installName);
|
|
374
|
+
if (existingInfo) {
|
|
375
|
+
await recordInstall(installName, {
|
|
376
|
+
...existingInfo,
|
|
377
|
+
registryModule: moduleName,
|
|
378
|
+
registryUrl: registryUrl,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Fallback: create minimal install info if not found (shouldn't happen but safety first)
|
|
383
|
+
await recordInstall(installName, {
|
|
384
|
+
source: 'registry',
|
|
385
|
+
githubUrl: `${org}/${repo}`,
|
|
386
|
+
modulePath: path,
|
|
387
|
+
version: data.version,
|
|
388
|
+
installedAt: data.location || '',
|
|
389
|
+
installedTime: new Date().toISOString(),
|
|
390
|
+
registryModule: moduleName,
|
|
391
|
+
registryUrl: registryUrl,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// Update result to indicate registry source
|
|
395
|
+
result.data = {
|
|
396
|
+
...result.data,
|
|
397
|
+
source: 'registry',
|
|
398
|
+
registryModule: moduleName,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
// Tarball sources: require checksum, verify, and extract safely.
|
|
404
|
+
if (!downloadInfo.url.startsWith('http')) {
|
|
405
|
+
return { success: false, error: `Unsupported registry download URL: ${downloadInfo.url}` };
|
|
406
|
+
}
|
|
407
|
+
if (!downloadInfo.checksum) {
|
|
408
|
+
return {
|
|
409
|
+
success: false,
|
|
410
|
+
error: `Registry tarball missing checksum (required for safe install): ${moduleName}`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
tempDir = join(tmpdir(), `cog-reg-${Date.now()}`);
|
|
414
|
+
mkdirSync(tempDir, { recursive: true });
|
|
415
|
+
const tarPath = join(tempDir, 'module.tar.gz');
|
|
416
|
+
const MAX_TARBALL_BYTES = 20 * 1024 * 1024; // 20MB
|
|
417
|
+
const actualSha256 = await downloadTarballWithSha256(downloadInfo.url, tarPath, MAX_TARBALL_BYTES);
|
|
418
|
+
const expected = downloadInfo.checksum;
|
|
419
|
+
const checksumMatch = expected.match(/^sha256:([a-f0-9]{64})$/);
|
|
420
|
+
if (!checksumMatch) {
|
|
421
|
+
throw new Error(`Unsupported checksum format (expected sha256:<64hex>): ${expected}`);
|
|
422
|
+
}
|
|
423
|
+
const expectedHash = checksumMatch[1];
|
|
424
|
+
if (actualSha256 !== expectedHash) {
|
|
425
|
+
throw new Error(`Checksum mismatch for ${moduleName}: expected ${expectedHash}, got ${actualSha256}`);
|
|
426
|
+
}
|
|
427
|
+
const extractedRoot = join(tempDir, 'pkg');
|
|
428
|
+
mkdirSync(extractedRoot, { recursive: true });
|
|
429
|
+
await extractTarGzFile(tarPath, extractedRoot, {
|
|
430
|
+
maxFiles: 5_000,
|
|
431
|
+
maxTotalBytes: 50 * 1024 * 1024,
|
|
432
|
+
maxSingleFileBytes: 20 * 1024 * 1024,
|
|
433
|
+
maxTarBytes: 100 * 1024 * 1024,
|
|
434
|
+
});
|
|
435
|
+
// Find module directory inside extractedRoot.
|
|
436
|
+
const rootNames = readdirSync(extractedRoot).filter((e) => e !== '__MACOSX' && e !== '.DS_Store');
|
|
437
|
+
const rootPaths = rootNames.map((e) => join(extractedRoot, e)).filter((p) => existsSync(p));
|
|
438
|
+
const rootDirs = rootPaths.filter((p) => statSync(p).isDirectory());
|
|
439
|
+
const rootFiles = rootPaths.filter((p) => !statSync(p).isDirectory());
|
|
440
|
+
if (rootDirs.length === 0) {
|
|
441
|
+
throw new Error('Tarball extraction produced no root directory');
|
|
442
|
+
}
|
|
443
|
+
if (rootDirs.length !== 1 || rootFiles.length > 0) {
|
|
444
|
+
throw new Error(`Tarball must contain exactly one module root directory and no other top-level entries. ` +
|
|
445
|
+
`dirs=${rootDirs.map((p) => basename(p)).join(',') || '(none)'} files=${rootFiles.map((p) => basename(p)).join(',') || '(none)'}`);
|
|
446
|
+
}
|
|
447
|
+
// Strict mode: require root dir itself to be a valid module.
|
|
448
|
+
const sourcePath = rootDirs[0];
|
|
449
|
+
if (!isValidModule(sourcePath)) {
|
|
450
|
+
throw new Error('Root directory in tarball is not a valid module');
|
|
451
|
+
}
|
|
452
|
+
const installName = (customName || moduleName);
|
|
453
|
+
const safeInstallName = assertSafeModuleName(installName);
|
|
454
|
+
const targetPath = resolveModuleTarget(safeInstallName);
|
|
455
|
+
if (existsSync(targetPath)) {
|
|
456
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
457
|
+
}
|
|
458
|
+
await mkdir(USER_MODULES_DIR, { recursive: true });
|
|
459
|
+
copyDir(sourcePath, targetPath);
|
|
460
|
+
const version = await getModuleVersion(sourcePath);
|
|
461
|
+
await recordInstall(safeInstallName, {
|
|
462
|
+
source: downloadInfo.url,
|
|
463
|
+
githubUrl: downloadInfo.url,
|
|
464
|
+
version,
|
|
465
|
+
installedAt: targetPath,
|
|
466
|
+
installedTime: new Date().toISOString(),
|
|
467
|
+
registryModule: moduleName,
|
|
468
|
+
registryUrl,
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
success: true,
|
|
472
|
+
data: {
|
|
473
|
+
message: `Added: ${safeInstallName}${version ? ` v${version}` : ''} (registry tarball)`,
|
|
474
|
+
name: safeInstallName,
|
|
475
|
+
version,
|
|
476
|
+
location: targetPath,
|
|
477
|
+
source: 'registry',
|
|
478
|
+
registryModule: moduleName,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
return {
|
|
484
|
+
success: false,
|
|
485
|
+
error: error instanceof Error ? error.message : String(error),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
if (tempDir) {
|
|
490
|
+
try {
|
|
491
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// ignore
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
160
499
|
/**
|
|
161
500
|
* Add a module from GitHub
|
|
162
501
|
*/
|
|
163
|
-
export async function
|
|
502
|
+
export async function addFromGitHub(url, ctx, options = {}) {
|
|
164
503
|
const { org, repo, fullUrl } = parseGitHubUrl(url);
|
|
165
504
|
const { module: modulePath, name, branch = 'main', tag } = options;
|
|
166
505
|
// Determine ref (tag takes priority)
|
|
@@ -184,11 +523,11 @@ export async function add(url, ctx, options = {}) {
|
|
|
184
523
|
sourcePath = repoRoot;
|
|
185
524
|
}
|
|
186
525
|
// Determine module name
|
|
187
|
-
moduleName = name
|
|
526
|
+
moduleName = name ? assertSafeModuleName(name) : basename(sourcePath);
|
|
188
527
|
// Get version
|
|
189
528
|
const version = await getModuleVersion(sourcePath);
|
|
190
529
|
// Install to user modules dir
|
|
191
|
-
const targetPath =
|
|
530
|
+
const targetPath = resolveModuleTarget(moduleName);
|
|
192
531
|
// Remove existing
|
|
193
532
|
if (existsSync(targetPath)) {
|
|
194
533
|
rmSync(targetPath, { recursive: true, force: true });
|
|
@@ -208,8 +547,10 @@ export async function add(url, ctx, options = {}) {
|
|
|
208
547
|
installedTime: new Date().toISOString(),
|
|
209
548
|
});
|
|
210
549
|
// Cleanup temp directory
|
|
211
|
-
const tempDir = repoRoot
|
|
212
|
-
|
|
550
|
+
const tempDir = dirname(repoRoot);
|
|
551
|
+
if (tempDir && tempDir !== '/' && tempDir !== '.' && tempDir !== USER_MODULES_DIR) {
|
|
552
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
553
|
+
}
|
|
213
554
|
return {
|
|
214
555
|
success: true,
|
|
215
556
|
data: {
|
|
@@ -217,13 +558,39 @@ export async function add(url, ctx, options = {}) {
|
|
|
217
558
|
name: moduleName,
|
|
218
559
|
version,
|
|
219
560
|
location: targetPath,
|
|
561
|
+
source: 'github',
|
|
220
562
|
},
|
|
221
563
|
};
|
|
222
564
|
}
|
|
223
565
|
catch (error) {
|
|
566
|
+
// Cleanup temp directory on error
|
|
567
|
+
if (repoRoot) {
|
|
568
|
+
try {
|
|
569
|
+
const tempDir = dirname(repoRoot);
|
|
570
|
+
if (tempDir && tempDir !== '/' && tempDir !== '.' && tempDir !== USER_MODULES_DIR) {
|
|
571
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Ignore cleanup errors
|
|
576
|
+
}
|
|
577
|
+
}
|
|
224
578
|
return {
|
|
225
579
|
success: false,
|
|
226
580
|
error: error instanceof Error ? error.message : String(error),
|
|
227
581
|
};
|
|
228
582
|
}
|
|
229
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Add a module from GitHub or Registry (auto-detect source)
|
|
586
|
+
*/
|
|
587
|
+
export async function add(source, ctx, options = {}) {
|
|
588
|
+
// Determine source type
|
|
589
|
+
if (isGitHubSource(source)) {
|
|
590
|
+
return addFromGitHub(source, ctx, options);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// Treat as registry module name
|
|
594
|
+
return addFromRegistry(source, ctx, options);
|
|
595
|
+
}
|
|
596
|
+
}
|
package/dist/commands/compose.js
CHANGED
|
@@ -8,14 +8,30 @@
|
|
|
8
8
|
* - Iterative: A → (check) → A → ... → Done
|
|
9
9
|
*/
|
|
10
10
|
import { findModule, getDefaultSearchPaths, executeComposition } from '../modules/index.js';
|
|
11
|
+
import { ErrorCodes, attachContext, makeErrorEnvelope } from '../errors/index.js';
|
|
12
|
+
function looksLikeCode(str) {
|
|
13
|
+
const codeIndicators = [
|
|
14
|
+
/^(def|function|class|const|let|var|import|export|public|private)\s/,
|
|
15
|
+
/[{};()]/,
|
|
16
|
+
/=>/,
|
|
17
|
+
/\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
|
|
18
|
+
];
|
|
19
|
+
return codeIndicators.some(re => re.test(str));
|
|
20
|
+
}
|
|
11
21
|
export async function compose(moduleName, ctx, options = {}) {
|
|
12
22
|
const searchPaths = getDefaultSearchPaths(ctx.cwd);
|
|
13
23
|
// Find module
|
|
14
24
|
const module = await findModule(moduleName, searchPaths);
|
|
15
25
|
if (!module) {
|
|
26
|
+
const errorEnvelope = attachContext(makeErrorEnvelope({
|
|
27
|
+
code: ErrorCodes.MODULE_NOT_FOUND,
|
|
28
|
+
message: `Module not found: ${moduleName}\nSearch paths: ${searchPaths.join(', ')}`,
|
|
29
|
+
suggestion: "Use 'cog list' to see installed modules or 'cog search' to find modules in registry",
|
|
30
|
+
}), { module: moduleName, provider: ctx.provider.name });
|
|
16
31
|
return {
|
|
17
32
|
success: false,
|
|
18
|
-
error:
|
|
33
|
+
error: errorEnvelope.error.message,
|
|
34
|
+
data: errorEnvelope,
|
|
19
35
|
};
|
|
20
36
|
}
|
|
21
37
|
try {
|
|
@@ -26,16 +42,26 @@ export async function compose(moduleName, ctx, options = {}) {
|
|
|
26
42
|
inputData = JSON.parse(options.input);
|
|
27
43
|
}
|
|
28
44
|
catch {
|
|
45
|
+
const errorEnvelope = attachContext(makeErrorEnvelope({
|
|
46
|
+
code: ErrorCodes.INVALID_INPUT,
|
|
47
|
+
message: `Invalid JSON input: ${options.input}`,
|
|
48
|
+
suggestion: 'Ensure input is valid JSON format',
|
|
49
|
+
}), { module: moduleName, provider: ctx.provider.name });
|
|
29
50
|
return {
|
|
30
51
|
success: false,
|
|
31
|
-
error:
|
|
52
|
+
error: errorEnvelope.error.message,
|
|
53
|
+
data: errorEnvelope,
|
|
32
54
|
};
|
|
33
55
|
}
|
|
34
56
|
}
|
|
35
57
|
// Handle --args as text input
|
|
36
58
|
if (options.args) {
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
if (looksLikeCode(options.args)) {
|
|
60
|
+
inputData.code = options.args;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
inputData.query = options.args;
|
|
64
|
+
}
|
|
39
65
|
}
|
|
40
66
|
// Execute composition
|
|
41
67
|
const result = await executeComposition(moduleName, inputData, ctx.provider, {
|
|
@@ -56,11 +82,27 @@ export async function compose(moduleName, ctx, options = {}) {
|
|
|
56
82
|
}
|
|
57
83
|
console.error(`--- Total: ${result.totalTimeMs}ms ---`);
|
|
58
84
|
}
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
const partialData = options.trace
|
|
87
|
+
? { moduleResults: result.moduleResults, trace: result.trace }
|
|
88
|
+
: { moduleResults: result.moduleResults };
|
|
89
|
+
const errorEnvelope = attachContext(makeErrorEnvelope({
|
|
90
|
+
code: result.error?.code ?? ErrorCodes.INTERNAL_ERROR,
|
|
91
|
+
message: result.error?.message ?? 'Composition failed',
|
|
92
|
+
details: result.error?.module ? { module: result.error.module } : undefined,
|
|
93
|
+
partial_data: partialData,
|
|
94
|
+
}), { module: moduleName, provider: ctx.provider.name });
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: errorEnvelope.error.message,
|
|
98
|
+
data: errorEnvelope,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
59
101
|
// Return result
|
|
60
102
|
if (options.trace) {
|
|
61
103
|
// Include full result with trace
|
|
62
104
|
return {
|
|
63
|
-
success:
|
|
105
|
+
success: true,
|
|
64
106
|
data: {
|
|
65
107
|
ok: result.ok,
|
|
66
108
|
result: result.result,
|
|
@@ -73,33 +115,28 @@ export async function compose(moduleName, ctx, options = {}) {
|
|
|
73
115
|
}
|
|
74
116
|
else if (options.pretty) {
|
|
75
117
|
return {
|
|
76
|
-
success:
|
|
118
|
+
success: true,
|
|
77
119
|
data: result.result,
|
|
78
120
|
};
|
|
79
121
|
}
|
|
80
122
|
else {
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
return {
|
|
90
|
-
success: false,
|
|
91
|
-
error: result.error
|
|
92
|
-
? `${result.error.code}: ${result.error.message}`
|
|
93
|
-
: 'Composition failed',
|
|
94
|
-
data: result.moduleResults,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
123
|
+
// Keep compose output consistent with run/pipe: always return full v2.2 envelope
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
data: result.result,
|
|
127
|
+
};
|
|
97
128
|
}
|
|
98
129
|
}
|
|
99
130
|
catch (e) {
|
|
131
|
+
const errorEnvelope = attachContext(makeErrorEnvelope({
|
|
132
|
+
code: ErrorCodes.INTERNAL_ERROR,
|
|
133
|
+
message: e instanceof Error ? e.message : String(e),
|
|
134
|
+
recoverable: false,
|
|
135
|
+
}), { module: moduleName, provider: ctx.provider.name });
|
|
100
136
|
return {
|
|
101
137
|
success: false,
|
|
102
|
-
error:
|
|
138
|
+
error: errorEnvelope.error.message,
|
|
139
|
+
data: errorEnvelope,
|
|
103
140
|
};
|
|
104
141
|
}
|
|
105
142
|
}
|