cli4ai 1.2.0 → 1.2.2
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 +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +464 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +382 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +125 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +162 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- package/src/server/service.ts +0 -434
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PATH linking for global packages
|
|
3
|
+
*
|
|
4
|
+
* Creates executable symlinks in ~/.cli4ai/bin/ that can be added to PATH
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync, chmodSync } from 'fs';
|
|
7
|
+
import { resolve, join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// SECURITY: Shell escaping for safe script generation
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
/**
|
|
13
|
+
* Validate that a string is safe for shell script embedding.
|
|
14
|
+
* Only allows alphanumeric, hyphens, underscores, and dots.
|
|
15
|
+
* This is a strict allowlist approach for defense in depth.
|
|
16
|
+
*/
|
|
17
|
+
function isShellSafe(value) {
|
|
18
|
+
return /^[a-zA-Z0-9._-]+$/.test(value);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Escape a string for safe embedding in shell scripts.
|
|
22
|
+
* Uses single quotes which prevent all shell interpretation except for single quotes themselves.
|
|
23
|
+
*/
|
|
24
|
+
function shellEscape(value) {
|
|
25
|
+
// Single quotes prevent shell interpretation; escape any single quotes in the value
|
|
26
|
+
// by ending the single-quoted string, adding an escaped single quote, and starting a new single-quoted string
|
|
27
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
28
|
+
}
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// PATHS
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
export const C4AI_BIN = resolve(homedir(), '.cli4ai', 'bin');
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// FUNCTIONS
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
/**
|
|
37
|
+
* Ensure bin directory exists
|
|
38
|
+
*/
|
|
39
|
+
export function ensureBinDir() {
|
|
40
|
+
if (!existsSync(C4AI_BIN)) {
|
|
41
|
+
mkdirSync(C4AI_BIN, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create executable wrapper script for a package
|
|
46
|
+
*
|
|
47
|
+
* Creates a shell script that invokes `cli4ai run <package> [args]`
|
|
48
|
+
*/
|
|
49
|
+
export function linkPackage(manifest, packagePath) {
|
|
50
|
+
ensureBinDir();
|
|
51
|
+
// SECURITY: Validate manifest values before embedding in shell script
|
|
52
|
+
if (!isShellSafe(manifest.name)) {
|
|
53
|
+
throw new Error(`Invalid package name for shell script: ${manifest.name}`);
|
|
54
|
+
}
|
|
55
|
+
if (!isShellSafe(manifest.version)) {
|
|
56
|
+
throw new Error(`Invalid package version for shell script: ${manifest.version}`);
|
|
57
|
+
}
|
|
58
|
+
const binPath = join(C4AI_BIN, manifest.name);
|
|
59
|
+
// Create wrapper script with escaped values for defense in depth
|
|
60
|
+
const safeName = shellEscape(manifest.name);
|
|
61
|
+
const safeVersion = shellEscape(manifest.version);
|
|
62
|
+
const script = `#!/bin/sh
|
|
63
|
+
# cli4ai wrapper for ${safeName}@${safeVersion}
|
|
64
|
+
# Auto-generated - do not edit
|
|
65
|
+
|
|
66
|
+
exec cli4ai run ${safeName} "$@"
|
|
67
|
+
`;
|
|
68
|
+
writeFileSync(binPath, script);
|
|
69
|
+
if (process.platform !== 'win32') {
|
|
70
|
+
chmodSync(binPath, 0o755);
|
|
71
|
+
}
|
|
72
|
+
// Windows compatibility: generate .cmd and .ps1 launchers
|
|
73
|
+
if (process.platform === 'win32') {
|
|
74
|
+
const cmdContent = `@echo off\r\ncli4ai run ${manifest.name} %*\r\nexit /b %errorlevel%\r\n`;
|
|
75
|
+
writeFileSync(binPath + '.cmd', cmdContent);
|
|
76
|
+
const ps1Content = `& cli4ai run ${manifest.name} @args\nexit $LASTEXITCODE\n`;
|
|
77
|
+
writeFileSync(binPath + '.ps1', ps1Content);
|
|
78
|
+
}
|
|
79
|
+
return binPath;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create direct executable wrapper that runs the tool directly
|
|
83
|
+
*
|
|
84
|
+
* This is faster than going through cli4ai run
|
|
85
|
+
*/
|
|
86
|
+
export function linkPackageDirect(manifest, packagePath) {
|
|
87
|
+
ensureBinDir();
|
|
88
|
+
// SECURITY: Validate manifest values before embedding in shell script
|
|
89
|
+
if (!isShellSafe(manifest.name)) {
|
|
90
|
+
throw new Error(`Invalid package name for shell script: ${manifest.name}`);
|
|
91
|
+
}
|
|
92
|
+
if (!isShellSafe(manifest.version)) {
|
|
93
|
+
throw new Error(`Invalid package version for shell script: ${manifest.version}`);
|
|
94
|
+
}
|
|
95
|
+
const binPath = join(C4AI_BIN, manifest.name);
|
|
96
|
+
const entryPath = resolve(packagePath, manifest.entry);
|
|
97
|
+
// SECURITY: Shell-escape the entry path to prevent injection
|
|
98
|
+
const safeEntryPath = shellEscape(entryPath);
|
|
99
|
+
const safeName = shellEscape(manifest.name);
|
|
100
|
+
const safeVersion = shellEscape(manifest.version);
|
|
101
|
+
// Build runtime command - use tsx for TypeScript, node for JavaScript
|
|
102
|
+
let execCommand;
|
|
103
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
104
|
+
execCommand = `npx tsx ${safeEntryPath}`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
execCommand = `node ${safeEntryPath}`;
|
|
108
|
+
}
|
|
109
|
+
// Create wrapper script that runs the tool directly
|
|
110
|
+
const script = `#!/bin/sh
|
|
111
|
+
# cli4ai wrapper for ${safeName}@${safeVersion}
|
|
112
|
+
# Auto-generated - do not edit
|
|
113
|
+
|
|
114
|
+
exec ${execCommand} "$@"
|
|
115
|
+
`;
|
|
116
|
+
writeFileSync(binPath, script);
|
|
117
|
+
if (process.platform !== 'win32') {
|
|
118
|
+
chmodSync(binPath, 0o755);
|
|
119
|
+
}
|
|
120
|
+
// Windows compatibility: generate .cmd and .ps1 launchers
|
|
121
|
+
if (process.platform === 'win32') {
|
|
122
|
+
const quotedEntry = `"${entryPath.replaceAll('"', '""')}"`;
|
|
123
|
+
let cmdContent;
|
|
124
|
+
let ps1Content;
|
|
125
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
126
|
+
cmdContent = `@echo off\r\nnpx tsx ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
|
|
127
|
+
ps1Content = `& npx tsx ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
cmdContent = `@echo off\r\nnode ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
|
|
131
|
+
ps1Content = `& node ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
|
|
132
|
+
}
|
|
133
|
+
writeFileSync(binPath + '.cmd', cmdContent);
|
|
134
|
+
writeFileSync(binPath + '.ps1', ps1Content);
|
|
135
|
+
}
|
|
136
|
+
return binPath;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Remove executable link for a package
|
|
140
|
+
*/
|
|
141
|
+
export function unlinkPackage(packageName) {
|
|
142
|
+
// SECURITY: Validate package name to prevent path traversal
|
|
143
|
+
if (!isShellSafe(packageName) || packageName.includes('..')) {
|
|
144
|
+
throw new Error(`Invalid package name: ${packageName}`);
|
|
145
|
+
}
|
|
146
|
+
const basePath = join(C4AI_BIN, packageName);
|
|
147
|
+
const candidates = process.platform === 'win32'
|
|
148
|
+
? [basePath, basePath + '.cmd', basePath + '.ps1']
|
|
149
|
+
: [basePath];
|
|
150
|
+
let removed = false;
|
|
151
|
+
for (const path of candidates) {
|
|
152
|
+
if (existsSync(path)) {
|
|
153
|
+
unlinkSync(path);
|
|
154
|
+
removed = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return removed;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if a package is linked
|
|
161
|
+
*/
|
|
162
|
+
export function isPackageLinked(packageName) {
|
|
163
|
+
const basePath = join(C4AI_BIN, packageName);
|
|
164
|
+
if (existsSync(basePath))
|
|
165
|
+
return true;
|
|
166
|
+
if (process.platform === 'win32') {
|
|
167
|
+
if (existsSync(basePath + '.cmd'))
|
|
168
|
+
return true;
|
|
169
|
+
if (existsSync(basePath + '.ps1'))
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get PATH setup instructions
|
|
176
|
+
*/
|
|
177
|
+
export function getPathInstructions() {
|
|
178
|
+
if (process.platform === 'win32') {
|
|
179
|
+
return `
|
|
180
|
+
To use globally installed cli4ai packages from anywhere, add the bin directory to your PATH:
|
|
181
|
+
|
|
182
|
+
${C4AI_BIN}
|
|
183
|
+
|
|
184
|
+
On Windows, you can:
|
|
185
|
+
1. Use System Settings > Edit environment variables (GUI)
|
|
186
|
+
2. Or run in PowerShell (current session): $env:PATH += ";${C4AI_BIN}"
|
|
187
|
+
3. Or run in PowerShell (permanent): [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";${C4AI_BIN}", "User")
|
|
188
|
+
|
|
189
|
+
Then restart your terminal.
|
|
190
|
+
`.trim();
|
|
191
|
+
}
|
|
192
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
193
|
+
const isZsh = shell.includes('zsh');
|
|
194
|
+
const rcFile = isZsh ? '~/.zshrc' : '~/.bashrc';
|
|
195
|
+
return `
|
|
196
|
+
To use globally installed cli4ai packages from anywhere, add this to your ${rcFile}:
|
|
197
|
+
|
|
198
|
+
export PATH="${C4AI_BIN}:$PATH"
|
|
199
|
+
|
|
200
|
+
Then reload your shell:
|
|
201
|
+
|
|
202
|
+
source ${rcFile}
|
|
203
|
+
|
|
204
|
+
Or start a new terminal session.
|
|
205
|
+
`.trim();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if bin directory is in PATH
|
|
209
|
+
*/
|
|
210
|
+
export function isBinInPath() {
|
|
211
|
+
const pathEnv = process.env.PATH || '';
|
|
212
|
+
const separator = process.platform === 'win32' ? ';' : ':';
|
|
213
|
+
return pathEnv.split(separator).some(p => resolve(p) === C4AI_BIN);
|
|
214
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock file management (cli4ai.lock)
|
|
3
|
+
*
|
|
4
|
+
* Tracks installed packages with exact versions and sources
|
|
5
|
+
* for reproducible installations.
|
|
6
|
+
*/
|
|
7
|
+
export interface LockedPackage {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
resolved: string;
|
|
11
|
+
integrity?: string;
|
|
12
|
+
dependencies?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface Lockfile {
|
|
15
|
+
lockfileVersion: number;
|
|
16
|
+
packages: Record<string, LockedPackage>;
|
|
17
|
+
}
|
|
18
|
+
export declare const LOCKFILE_NAME = "cli4ai.lock";
|
|
19
|
+
export declare const LOCKFILE_VERSION = 1;
|
|
20
|
+
/**
|
|
21
|
+
* Get lock file path for a project
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLockfilePath(projectDir: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Check if lock file exists
|
|
26
|
+
*/
|
|
27
|
+
export declare function lockfileExists(projectDir: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Load lock file, returns empty lockfile if doesn't exist
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadLockfile(projectDir: string): Lockfile;
|
|
32
|
+
/**
|
|
33
|
+
* Save lock file
|
|
34
|
+
*/
|
|
35
|
+
export declare function saveLockfile(projectDir: string, lockfile: Lockfile): void;
|
|
36
|
+
/**
|
|
37
|
+
* Add or update a package in the lock file
|
|
38
|
+
*/
|
|
39
|
+
export declare function lockPackage(projectDir: string, pkg: LockedPackage): void;
|
|
40
|
+
/**
|
|
41
|
+
* Remove a package from the lock file
|
|
42
|
+
*/
|
|
43
|
+
export declare function unlockPackage(projectDir: string, packageName: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get a locked package by name
|
|
46
|
+
*/
|
|
47
|
+
export declare function getLockedPackage(projectDir: string, packageName: string): LockedPackage | null;
|
|
48
|
+
/**
|
|
49
|
+
* Get all locked packages
|
|
50
|
+
*/
|
|
51
|
+
export declare function getLockedPackages(projectDir: string): LockedPackage[];
|
|
52
|
+
/**
|
|
53
|
+
* Check if a package is locked
|
|
54
|
+
*/
|
|
55
|
+
export declare function isPackageLocked(projectDir: string, packageName: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Clear all packages from lock file
|
|
58
|
+
*/
|
|
59
|
+
export declare function clearLockfile(projectDir: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Format lock file as human-readable string
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatLockfile(lockfile: Lockfile): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock file management (cli4ai.lock)
|
|
3
|
+
*
|
|
4
|
+
* Tracks installed packages with exact versions and sources
|
|
5
|
+
* for reproducible installations.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// CONSTANTS
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
export const LOCKFILE_NAME = 'cli4ai.lock';
|
|
13
|
+
export const LOCKFILE_VERSION = 1;
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
// FUNCTIONS
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
/**
|
|
18
|
+
* Get lock file path for a project
|
|
19
|
+
*/
|
|
20
|
+
export function getLockfilePath(projectDir) {
|
|
21
|
+
return resolve(projectDir, LOCKFILE_NAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if lock file exists
|
|
25
|
+
*/
|
|
26
|
+
export function lockfileExists(projectDir) {
|
|
27
|
+
return existsSync(getLockfilePath(projectDir));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load lock file, returns empty lockfile if doesn't exist
|
|
31
|
+
*/
|
|
32
|
+
export function loadLockfile(projectDir) {
|
|
33
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
34
|
+
if (!existsSync(lockfilePath)) {
|
|
35
|
+
return {
|
|
36
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
37
|
+
packages: {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(lockfilePath, 'utf-8');
|
|
42
|
+
const data = JSON.parse(content);
|
|
43
|
+
// Validate version
|
|
44
|
+
if (data.lockfileVersion !== LOCKFILE_VERSION) {
|
|
45
|
+
console.error(`Warning: Lock file version mismatch (${data.lockfileVersion} vs ${LOCKFILE_VERSION})`);
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error('Warning: Could not parse lock file, starting fresh');
|
|
51
|
+
return {
|
|
52
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
53
|
+
packages: {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Save lock file
|
|
59
|
+
*/
|
|
60
|
+
export function saveLockfile(projectDir, lockfile) {
|
|
61
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
62
|
+
const content = JSON.stringify(lockfile, null, 2) + '\n';
|
|
63
|
+
const tmpPath = lockfilePath + '.tmp';
|
|
64
|
+
writeFileSync(tmpPath, content);
|
|
65
|
+
renameSync(tmpPath, lockfilePath);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Add or update a package in the lock file
|
|
69
|
+
*/
|
|
70
|
+
export function lockPackage(projectDir, pkg) {
|
|
71
|
+
const lockfile = loadLockfile(projectDir);
|
|
72
|
+
lockfile.packages[pkg.name] = pkg;
|
|
73
|
+
saveLockfile(projectDir, lockfile);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Remove a package from the lock file
|
|
77
|
+
*/
|
|
78
|
+
export function unlockPackage(projectDir, packageName) {
|
|
79
|
+
const lockfile = loadLockfile(projectDir);
|
|
80
|
+
if (lockfile.packages[packageName]) {
|
|
81
|
+
delete lockfile.packages[packageName];
|
|
82
|
+
saveLockfile(projectDir, lockfile);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get a locked package by name
|
|
87
|
+
*/
|
|
88
|
+
export function getLockedPackage(projectDir, packageName) {
|
|
89
|
+
const lockfile = loadLockfile(projectDir);
|
|
90
|
+
return lockfile.packages[packageName] || null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get all locked packages
|
|
94
|
+
*/
|
|
95
|
+
export function getLockedPackages(projectDir) {
|
|
96
|
+
const lockfile = loadLockfile(projectDir);
|
|
97
|
+
return Object.values(lockfile.packages);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a package is locked
|
|
101
|
+
*/
|
|
102
|
+
export function isPackageLocked(projectDir, packageName) {
|
|
103
|
+
const lockfile = loadLockfile(projectDir);
|
|
104
|
+
return packageName in lockfile.packages;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Clear all packages from lock file
|
|
108
|
+
*/
|
|
109
|
+
export function clearLockfile(projectDir) {
|
|
110
|
+
saveLockfile(projectDir, {
|
|
111
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
112
|
+
packages: {}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Format lock file as human-readable string
|
|
117
|
+
*/
|
|
118
|
+
export function formatLockfile(lockfile) {
|
|
119
|
+
const lines = [
|
|
120
|
+
`# cli4ai.lock - Auto-generated, do not edit`,
|
|
121
|
+
`# lockfileVersion: ${lockfile.lockfileVersion}`,
|
|
122
|
+
``
|
|
123
|
+
];
|
|
124
|
+
const sortedPackages = Object.values(lockfile.packages).sort((a, b) => a.name.localeCompare(b.name));
|
|
125
|
+
for (const pkg of sortedPackages) {
|
|
126
|
+
lines.push(`${pkg.name}@${pkg.version}:`);
|
|
127
|
+
lines.push(` resolved: "${pkg.resolved}"`);
|
|
128
|
+
if (pkg.integrity) {
|
|
129
|
+
lines.push(` integrity: ${pkg.integrity}`);
|
|
130
|
+
}
|
|
131
|
+
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
|
|
132
|
+
lines.push(` dependencies:`);
|
|
133
|
+
for (const [depName, depVersion] of Object.entries(pkg.dependencies)) {
|
|
134
|
+
lines.push(` ${depName}: "${depVersion}"`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
lines.push(``);
|
|
138
|
+
}
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai.json manifest validation and parsing
|
|
3
|
+
*/
|
|
4
|
+
export declare class ManifestValidationError extends Error {
|
|
5
|
+
details?: Record<string, unknown> | undefined;
|
|
6
|
+
constructor(message: string, details?: Record<string, unknown> | undefined);
|
|
7
|
+
}
|
|
8
|
+
export interface CommandArg {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
type?: 'string' | 'number' | 'boolean';
|
|
13
|
+
default?: string | number | boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface CommandOption {
|
|
16
|
+
name: string;
|
|
17
|
+
short?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
type?: 'string' | 'number' | 'boolean';
|
|
20
|
+
default?: string | number | boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface CommandDef {
|
|
23
|
+
description: string;
|
|
24
|
+
args?: CommandArg[];
|
|
25
|
+
options?: CommandOption[];
|
|
26
|
+
}
|
|
27
|
+
export interface EnvDef {
|
|
28
|
+
required?: boolean;
|
|
29
|
+
description?: string;
|
|
30
|
+
default?: string;
|
|
31
|
+
secret?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface McpConfig {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
transport?: 'stdio' | 'http';
|
|
36
|
+
port?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Manifest {
|
|
39
|
+
name: string;
|
|
40
|
+
version: string;
|
|
41
|
+
entry: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
author?: string;
|
|
44
|
+
license?: string;
|
|
45
|
+
repository?: string;
|
|
46
|
+
homepage?: string;
|
|
47
|
+
keywords?: string[];
|
|
48
|
+
runtime?: 'node' | 'bun';
|
|
49
|
+
commands?: Record<string, CommandDef>;
|
|
50
|
+
env?: Record<string, EnvDef>;
|
|
51
|
+
dependencies?: Record<string, string>;
|
|
52
|
+
peerDependencies?: Record<string, string>;
|
|
53
|
+
mcp?: McpConfig;
|
|
54
|
+
scripts?: Record<string, string>;
|
|
55
|
+
files?: string[];
|
|
56
|
+
}
|
|
57
|
+
export declare function validateManifest(manifest: unknown, source?: string): Manifest;
|
|
58
|
+
export declare const MANIFEST_FILENAME = "cli4ai.json";
|
|
59
|
+
export declare class ManifestNotFoundError extends Error {
|
|
60
|
+
path: string;
|
|
61
|
+
constructor(path: string);
|
|
62
|
+
}
|
|
63
|
+
export declare class ManifestParseError extends Error {
|
|
64
|
+
path: string;
|
|
65
|
+
constructor(path: string, message: string);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load and validate cli4ai.json from a directory (throws on error)
|
|
69
|
+
* Use this for programmatic/testable usage
|
|
70
|
+
*/
|
|
71
|
+
export declare function loadManifestOrThrow(dir: string): Manifest;
|
|
72
|
+
/**
|
|
73
|
+
* Load manifest from package.json (fallback for npm packages)
|
|
74
|
+
*/
|
|
75
|
+
export declare function loadFromPackageJson(dir: string): Manifest | null;
|
|
76
|
+
/**
|
|
77
|
+
* Load and validate cli4ai.json from a directory
|
|
78
|
+
* Falls back to package.json for npm packages
|
|
79
|
+
* Exits with error on failure (for CLI usage)
|
|
80
|
+
*/
|
|
81
|
+
export declare function loadManifest(dir: string): Manifest;
|
|
82
|
+
/**
|
|
83
|
+
* Try to load manifest, return null if not found
|
|
84
|
+
*/
|
|
85
|
+
export declare function tryLoadManifest(dir: string): Manifest | null;
|
|
86
|
+
/**
|
|
87
|
+
* Find manifest by walking up directory tree
|
|
88
|
+
*/
|
|
89
|
+
export declare function findManifest(startDir?: string): {
|
|
90
|
+
manifest: Manifest;
|
|
91
|
+
dir: string;
|
|
92
|
+
} | null;
|
|
93
|
+
/**
|
|
94
|
+
* Create a minimal manifest
|
|
95
|
+
*/
|
|
96
|
+
export declare function createManifest(name: string, options?: Partial<Manifest>): Manifest;
|