linchpin-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 +109 -0
- package/dist/bin/vm.js +34 -0
- package/dist/src/commands/align.js +242 -0
- package/dist/src/commands/check.js +151 -0
- package/dist/src/commands/explain.js +54 -0
- package/dist/src/lib/ai.js +149 -0
- package/dist/src/lib/files.js +16 -0
- package/dist/src/lib/npm.js +46 -0
- package/dist/src/utils/logger.js +33 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Linchpin
|
|
2
|
+
|
|
3
|
+
**The "Don't Break My App" Tool**
|
|
4
|
+
|
|
5
|
+
AI-powered dependency management for solo founders who code but aren't DevOps experts.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
You built your app 6 months ago. Now your dependencies are showing red warnings everywhere. You're afraid to touch anything because:
|
|
10
|
+
|
|
11
|
+
- "If I run `npm install`, will my app stop working?"
|
|
12
|
+
- "ChatGPT told me to install X, but now Y is broken"
|
|
13
|
+
- Big companies have DevOps teams. You have... anxiety.
|
|
14
|
+
|
|
15
|
+
## The Solution
|
|
16
|
+
|
|
17
|
+
Linchpin scans your project and tells you what's safe to update in **plain English**.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx linchpin-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it. One command. No installation required.
|
|
24
|
+
|
|
25
|
+
## What You Get
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
๐ Linchpin: Scanning dependencies...
|
|
29
|
+
|
|
30
|
+
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
|
|
31
|
+
โ Package โ Current โ Latest โ Status โ
|
|
32
|
+
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโค
|
|
33
|
+
โ chalk โ ^4.1.2 โ 5.6.2 โ โ MAJOR โ
|
|
34
|
+
โ dotenv โ ^17.2.3 โ 17.2.3 โ OK โ
|
|
35
|
+
โ typescript โ ^5.3.2 โ 5.9.3 โ MINOR โ
|
|
36
|
+
โโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโ
|
|
37
|
+
|
|
38
|
+
๐ Summary: 1 major ยท 1 minor ยท 0 patch
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Scan your project (free - uses npm registry)
|
|
45
|
+
npx linchpin-cli
|
|
46
|
+
|
|
47
|
+
# Deep scan with AI risk analysis (requires API key)
|
|
48
|
+
npx linchpin-cli --deep
|
|
49
|
+
|
|
50
|
+
# Get plain-English explanation of upgrade risks
|
|
51
|
+
npx linchpin-cli explain chalk
|
|
52
|
+
|
|
53
|
+
# Safely upgrade a package (creates backup first)
|
|
54
|
+
npx linchpin-cli align chalk
|
|
55
|
+
|
|
56
|
+
# Batch upgrade all packages interactively
|
|
57
|
+
npx linchpin-cli align --all
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
### Plain English Mode (Default)
|
|
63
|
+
|
|
64
|
+
Instead of jargon like "ESM-only breaking CommonJS", you get:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
๐ฏ Risk Level: Medium
|
|
68
|
+
๐ก Plain English: This update changes how files talk to each other.
|
|
69
|
+
It will break your app unless you spend ~2 hours fixing code.
|
|
70
|
+
โ
Recommendation: Skip for now.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Auto-Backup (Panic Button)
|
|
74
|
+
|
|
75
|
+
Before any upgrade, Linchpin creates a git snapshot:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
๐พ Created restore point. If things break, run: git reset --hard HEAD~1
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Two-Layer Safety
|
|
82
|
+
|
|
83
|
+
1. **SemVer Gate**: Major version jumps are flagged automatically
|
|
84
|
+
2. **AI Gate**: Deep analysis explains the actual risk
|
|
85
|
+
|
|
86
|
+
## Setup (Optional)
|
|
87
|
+
|
|
88
|
+
The basic scan is **free** and uses the npm registry directly.
|
|
89
|
+
|
|
90
|
+
For AI-powered features (`--deep`, `explain`), add a Perplexity API key:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Create .env file in your project
|
|
94
|
+
echo "PERPLEXITY_API_KEY=your-key-here" > .env
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Get a key at: https://www.perplexity.ai/settings/api
|
|
98
|
+
|
|
99
|
+
## For Experienced Devs
|
|
100
|
+
|
|
101
|
+
Add `--technical` for the old-school output:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx linchpin-cli explain chalk --technical
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/bin/vm.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
dotenv_1.default.config(); // Load env vars once at entry point
|
|
9
|
+
const commander_1 = require("commander");
|
|
10
|
+
const check_1 = require("../src/commands/check");
|
|
11
|
+
const explain_1 = require("../src/commands/explain");
|
|
12
|
+
const align_1 = require("../src/commands/align");
|
|
13
|
+
const program = new commander_1.Command();
|
|
14
|
+
program
|
|
15
|
+
.name('linchpin')
|
|
16
|
+
.description("Linchpin: The 'Don't Break My App' Tool")
|
|
17
|
+
.version('0.1.0');
|
|
18
|
+
program
|
|
19
|
+
.command('check', { isDefault: true })
|
|
20
|
+
.description('Scan your project for outdated dependencies')
|
|
21
|
+
.option('-d, --deep', 'Deep scan with AI risk analysis')
|
|
22
|
+
.option('-t, --technical', 'Technical output for experienced devs')
|
|
23
|
+
.action((options) => (0, check_1.checkCommand)(options));
|
|
24
|
+
program
|
|
25
|
+
.command('explain <package>')
|
|
26
|
+
.description('Get a plain-English breakdown of upgrade risks')
|
|
27
|
+
.option('-t, --technical', 'Technical output for experienced devs')
|
|
28
|
+
.action((pkg, options) => (0, explain_1.explainCommand)(pkg, options));
|
|
29
|
+
program
|
|
30
|
+
.command('align [package]')
|
|
31
|
+
.description('Safely update packages with auto-backup')
|
|
32
|
+
.option('-a, --all', 'Update all outdated packages interactively')
|
|
33
|
+
.action((pkgName, options) => (0, align_1.alignCommand)(pkgName, options));
|
|
34
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.alignCommand = alignCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const files_1 = require("../lib/files");
|
|
11
|
+
const ai_1 = require("../lib/ai");
|
|
12
|
+
const npm_1 = require("../lib/npm");
|
|
13
|
+
// Helper: Returns true if the major version changed (e.g. "4.1.2" -> "5.0.0")
|
|
14
|
+
function isMajorUpgrade(current, latest) {
|
|
15
|
+
const cleanCurrent = current.replace(/[^0-9.]/g, '');
|
|
16
|
+
const currentMajor = parseInt(cleanCurrent.split('.')[0]);
|
|
17
|
+
const latestMajor = parseInt(latest.split('.')[0]);
|
|
18
|
+
return latestMajor > currentMajor;
|
|
19
|
+
}
|
|
20
|
+
// Helper: Check if we're in a git repo
|
|
21
|
+
function isGitRepo() {
|
|
22
|
+
try {
|
|
23
|
+
(0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Helper: Create a restore point before upgrades
|
|
31
|
+
function createRestorePoint() {
|
|
32
|
+
if (!isGitRepo()) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// Stage package files
|
|
37
|
+
(0, child_process_1.execSync)('git add package.json package-lock.json 2>nul || git add package.json', { stdio: 'pipe' });
|
|
38
|
+
// Create backup commit
|
|
39
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
40
|
+
(0, child_process_1.execSync)(`git commit -m "linchpin: pre-update snapshot (${timestamp})" --allow-empty`, { stdio: 'pipe' });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function alignCommand(pkgNameOrOptions, options) {
|
|
48
|
+
// Handle both: pin align <package> and pin align --all
|
|
49
|
+
if (typeof pkgNameOrOptions === 'object' || options?.all) {
|
|
50
|
+
await alignAllPackages();
|
|
51
|
+
}
|
|
52
|
+
else if (pkgNameOrOptions) {
|
|
53
|
+
await alignSinglePackage(pkgNameOrOptions);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.log(chalk_1.default.yellow('Usage: pin align <package> or pin align --all'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function alignSinglePackage(pkgName) {
|
|
60
|
+
const useAI = (0, npm_1.hasApiKey)();
|
|
61
|
+
console.log(chalk_1.default.blue.bold(`\n๐ง Linchpin: Aligning ${pkgName}...\n`));
|
|
62
|
+
const pkg = await (0, files_1.getPackageJson)();
|
|
63
|
+
if (!pkg) {
|
|
64
|
+
console.log(chalk_1.default.red('โ No package.json found!'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
68
|
+
const currentVer = allDeps[pkgName];
|
|
69
|
+
if (!currentVer) {
|
|
70
|
+
console.log(chalk_1.default.red(`โ Package "${pkgName}" not found.`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
process.stdout.write(chalk_1.default.yellow(' Finding ideal version...'));
|
|
74
|
+
// Use npm registry (free) or AI depending on API key
|
|
75
|
+
const latestVer = useAI ? await (0, ai_1.getLatestVersion)(pkgName) : (0, npm_1.getRegistryVersion)(pkgName);
|
|
76
|
+
console.log(chalk_1.default.green(` Found ${latestVer}`));
|
|
77
|
+
const cleanCurrent = currentVer.replace(/^[\^~]/, '');
|
|
78
|
+
if (cleanCurrent === latestVer) {
|
|
79
|
+
console.log(chalk_1.default.green(' โ
Already up to date.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(chalk_1.default.gray(`\n Current: ${currentVer}`));
|
|
83
|
+
console.log(chalk_1.default.cyan(` Target: ${latestVer}`));
|
|
84
|
+
// --- SAFETY LOGIC: Check for major version jump ---
|
|
85
|
+
const majorChange = isMajorUpgrade(currentVer, latestVer);
|
|
86
|
+
if (majorChange) {
|
|
87
|
+
console.log(chalk_1.default.bgRed.white.bold(`\n โ ๏ธ MAJOR VERSION JUMP DETECTED (${cleanCurrent} โ ${latestVer})`));
|
|
88
|
+
if (useAI) {
|
|
89
|
+
console.log(chalk_1.default.yellow('\n Checking for breaking changes with AI...'));
|
|
90
|
+
const risk = await (0, ai_1.getUpgradeRisks)(pkgName, currentVer, latestVer);
|
|
91
|
+
console.log(chalk_1.default.gray('\n ------------------------------------------------'));
|
|
92
|
+
console.log(risk);
|
|
93
|
+
console.log(chalk_1.default.gray(' ------------------------------------------------\n'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(chalk_1.default.yellow('\n โน๏ธ Set PERPLEXITY_API_KEY for AI risk analysis.'));
|
|
97
|
+
console.log(chalk_1.default.gray(' Get one at: https://www.perplexity.ai/settings/api\n'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const answers = await inquirer_1.default.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: 'confirm',
|
|
103
|
+
name: 'confirm',
|
|
104
|
+
message: majorChange
|
|
105
|
+
? chalk_1.default.red(`โ ๏ธ This looks risky. Are you SURE you want to upgrade ${pkgName}?`)
|
|
106
|
+
: `Safe to upgrade ${pkgName} to ${latestVer}?`,
|
|
107
|
+
default: !majorChange // Default No if risky, Yes if safe
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
if (!answers.confirm) {
|
|
111
|
+
console.log(chalk_1.default.yellow('\n ๐ก๏ธ Upgrade blocked. Smart move.'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Create restore point before installing
|
|
115
|
+
const hasBackup = createRestorePoint();
|
|
116
|
+
if (hasBackup) {
|
|
117
|
+
console.log(chalk_1.default.gray('\n ๐พ Created restore point. If things break, run: git reset --hard HEAD~1'));
|
|
118
|
+
}
|
|
119
|
+
console.log(chalk_1.default.blue('\n ๐ Installing...'));
|
|
120
|
+
try {
|
|
121
|
+
(0, child_process_1.execSync)(`npm install ${pkgName}@${latestVer}`, { stdio: 'inherit' });
|
|
122
|
+
console.log(chalk_1.default.green(`\n โ
Success! ${pkgName} is now at ${latestVer}`));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.log(chalk_1.default.red('\n โ Update failed. Check the npm errors above.'));
|
|
126
|
+
if (hasBackup) {
|
|
127
|
+
console.log(chalk_1.default.yellow(' ๐ก To undo: git reset --hard HEAD~1'));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function alignAllPackages() {
|
|
132
|
+
const useAI = (0, npm_1.hasApiKey)();
|
|
133
|
+
const source = useAI ? 'AI' : 'npm registry';
|
|
134
|
+
console.log(chalk_1.default.blue.bold('\n๐ง Linchpin: Scanning for updates...\n'));
|
|
135
|
+
const pkg = await (0, files_1.getPackageJson)();
|
|
136
|
+
if (!pkg) {
|
|
137
|
+
console.log(chalk_1.default.red('โ No package.json found!'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
141
|
+
const depEntries = Object.entries(allDeps);
|
|
142
|
+
if (depEntries.length === 0) {
|
|
143
|
+
console.log(chalk_1.default.yellow('โ ๏ธ No dependencies found.'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk_1.default.yellow(` Checking ${depEntries.length} packages via ${source}...\n`));
|
|
147
|
+
const updates = [];
|
|
148
|
+
for (const [name, currentVersion] of depEntries) {
|
|
149
|
+
if (process.stdout.isTTY) {
|
|
150
|
+
process.stdout.write(` Checking ${name}... `);
|
|
151
|
+
}
|
|
152
|
+
// Use npm registry (free) or AI depending on API key
|
|
153
|
+
const latestVersion = useAI ? await (0, ai_1.getLatestVersion)(name) : (0, npm_1.getRegistryVersion)(name);
|
|
154
|
+
if (process.stdout.isTTY && process.stdout.clearLine && process.stdout.cursorTo) {
|
|
155
|
+
process.stdout.clearLine(0);
|
|
156
|
+
process.stdout.cursorTo(0);
|
|
157
|
+
}
|
|
158
|
+
const cleanCurrent = currentVersion.replace(/^[\^~]/, '');
|
|
159
|
+
if (cleanCurrent !== latestVersion && latestVersion !== 'Error' && latestVersion !== 'Unknown') {
|
|
160
|
+
const isRisky = isMajorUpgrade(currentVersion, latestVersion);
|
|
161
|
+
updates.push({
|
|
162
|
+
name,
|
|
163
|
+
current: currentVersion,
|
|
164
|
+
latest: latestVersion,
|
|
165
|
+
risky: isRisky
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (updates.length === 0) {
|
|
170
|
+
console.log(chalk_1.default.green(' โ
All packages are up to date!'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log(chalk_1.default.cyan(`\n Found ${updates.length} packages with updates available:\n`));
|
|
174
|
+
const answers = await inquirer_1.default.prompt([
|
|
175
|
+
{
|
|
176
|
+
type: 'checkbox',
|
|
177
|
+
name: 'selected',
|
|
178
|
+
message: 'Select packages to update (risky ones unchecked by default):',
|
|
179
|
+
pageSize: 15,
|
|
180
|
+
choices: updates.map(u => {
|
|
181
|
+
const label = u.risky
|
|
182
|
+
? chalk_1.default.red(`โ ๏ธ ${u.name} (${u.current} โ ${u.latest}) - MAJOR`)
|
|
183
|
+
: chalk_1.default.green(`โ ${u.name} (${u.current} โ ${u.latest})`);
|
|
184
|
+
return {
|
|
185
|
+
name: label,
|
|
186
|
+
value: { name: u.name, version: u.latest, risky: u.risky },
|
|
187
|
+
checked: !u.risky // Uncheck risky ones by default
|
|
188
|
+
};
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
]);
|
|
192
|
+
const selected = answers.selected;
|
|
193
|
+
if (selected.length === 0) {
|
|
194
|
+
console.log(chalk_1.default.yellow('\n ๐ซ No packages selected. No changes made.'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Warn if any risky packages were selected
|
|
198
|
+
const riskySelected = selected.filter(s => s.risky);
|
|
199
|
+
if (riskySelected.length > 0) {
|
|
200
|
+
console.log(chalk_1.default.bgRed.white.bold(`\n โ ๏ธ You selected ${riskySelected.length} risky package(s)!`));
|
|
201
|
+
const confirmRisky = await inquirer_1.default.prompt([
|
|
202
|
+
{
|
|
203
|
+
type: 'confirm',
|
|
204
|
+
name: 'proceed',
|
|
205
|
+
message: chalk_1.default.red('Are you SURE you want to proceed with major upgrades?'),
|
|
206
|
+
default: false
|
|
207
|
+
}
|
|
208
|
+
]);
|
|
209
|
+
if (!confirmRisky.proceed) {
|
|
210
|
+
console.log(chalk_1.default.yellow('\n ๐ก๏ธ Upgrade cancelled. Smart move.'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Create restore point before installing
|
|
215
|
+
const hasBackup = createRestorePoint();
|
|
216
|
+
if (hasBackup) {
|
|
217
|
+
console.log(chalk_1.default.gray('\n ๐พ Created restore point. If things break, run: git reset --hard HEAD~1'));
|
|
218
|
+
}
|
|
219
|
+
console.log(chalk_1.default.blue(`\n ๐ Installing ${selected.length} package(s)...\n`));
|
|
220
|
+
let successCount = 0;
|
|
221
|
+
let failCount = 0;
|
|
222
|
+
for (const pkg of selected) {
|
|
223
|
+
try {
|
|
224
|
+
console.log(chalk_1.default.gray(` Installing ${pkg.name}@${pkg.version}...`));
|
|
225
|
+
(0, child_process_1.execSync)(`npm install ${pkg.name}@${pkg.version}`, { stdio: 'pipe' });
|
|
226
|
+
successCount++;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.log(chalk_1.default.red(` โ Failed: ${pkg.name}`));
|
|
230
|
+
failCount++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log(chalk_1.default.green(`\n โ
Done! ${successCount} updated, ${failCount} failed.`));
|
|
234
|
+
// Show restore hint if any failures
|
|
235
|
+
if (failCount > 0 && hasBackup) {
|
|
236
|
+
console.log(chalk_1.default.yellow(' ๐ก To undo all changes: git reset --hard HEAD~1'));
|
|
237
|
+
}
|
|
238
|
+
// Mode indicator
|
|
239
|
+
if (!useAI) {
|
|
240
|
+
console.log(chalk_1.default.gray('\n โน๏ธ Free mode (npm registry). Set PERPLEXITY_API_KEY for AI risk analysis.'));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkCommand = checkCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
9
|
+
const files_1 = require("../lib/files");
|
|
10
|
+
const ai_1 = require("../lib/ai");
|
|
11
|
+
const npm_1 = require("../lib/npm");
|
|
12
|
+
// Helper: Determine upgrade type (MAJOR, MINOR, PATCH)
|
|
13
|
+
function getUpgradeType(current, latest) {
|
|
14
|
+
const cleanCurrent = current.replace(/[^0-9.]/g, '');
|
|
15
|
+
const cleanLatest = latest.replace(/[^0-9.]/g, '');
|
|
16
|
+
const [curMajor, curMinor] = cleanCurrent.split('.').map(Number);
|
|
17
|
+
const [latMajor, latMinor] = cleanLatest.split('.').map(Number);
|
|
18
|
+
if (latMajor > curMajor)
|
|
19
|
+
return 'MAJOR';
|
|
20
|
+
if (latMinor > curMinor)
|
|
21
|
+
return 'MINOR';
|
|
22
|
+
if (cleanCurrent !== cleanLatest)
|
|
23
|
+
return 'PATCH';
|
|
24
|
+
return 'OK';
|
|
25
|
+
}
|
|
26
|
+
// Helper: Color status by severity
|
|
27
|
+
function colorStatus(type) {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case 'MAJOR': return chalk_1.default.red.bold('โ MAJOR');
|
|
30
|
+
case 'MINOR': return chalk_1.default.yellow('MINOR');
|
|
31
|
+
case 'PATCH': return chalk_1.default.green('PATCH');
|
|
32
|
+
case 'OK': return chalk_1.default.green('OK');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function checkCommand(options = {}) {
|
|
36
|
+
const isDeep = options.deep || false;
|
|
37
|
+
const isTechnical = options.technical || false;
|
|
38
|
+
const useAI = (0, npm_1.hasApiKey)();
|
|
39
|
+
// Deep scan requires API key
|
|
40
|
+
if (isDeep && !useAI) {
|
|
41
|
+
console.log(chalk_1.default.red('\nโ Deep scan requires an API key.'));
|
|
42
|
+
console.log(chalk_1.default.yellow(' Set PERPLEXITY_API_KEY in your .env file'));
|
|
43
|
+
console.log(chalk_1.default.gray(' Get one at: https://www.perplexity.ai/settings/api\n'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const scanType = isDeep ? 'Deep scanning' : 'Scanning';
|
|
47
|
+
const source = useAI ? 'AI' : 'npm registry';
|
|
48
|
+
console.log(chalk_1.default.blue.bold(`\n๐ Linchpin: ${scanType} dependencies...\n`));
|
|
49
|
+
const pkg = await (0, files_1.getPackageJson)();
|
|
50
|
+
if (!pkg) {
|
|
51
|
+
console.log(chalk_1.default.red('โ No package.json found!'));
|
|
52
|
+
console.log(chalk_1.default.yellow(' Make sure you are in the root of your project.'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
56
|
+
const depEntries = Object.entries(allDeps);
|
|
57
|
+
if (depEntries.length === 0) {
|
|
58
|
+
console.log(chalk_1.default.yellow('โ ๏ธ Found package.json, but no dependencies listed.'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(chalk_1.default.yellow(`โก Checking ${depEntries.length} packages via ${source}...\n`));
|
|
62
|
+
// Collect version info
|
|
63
|
+
const packageData = [];
|
|
64
|
+
for (const [name, currentVersion] of depEntries) {
|
|
65
|
+
if (process.stdout.isTTY) {
|
|
66
|
+
process.stdout.write(` Checking ${name}... `);
|
|
67
|
+
}
|
|
68
|
+
// Use npm registry (free) or AI depending on API key
|
|
69
|
+
const latestVersion = useAI
|
|
70
|
+
? await (0, ai_1.getLatestVersion)(name)
|
|
71
|
+
: (0, npm_1.getRegistryVersion)(name);
|
|
72
|
+
if (process.stdout.isTTY && process.stdout.clearLine && process.stdout.cursorTo) {
|
|
73
|
+
process.stdout.clearLine(0);
|
|
74
|
+
process.stdout.cursorTo(0);
|
|
75
|
+
}
|
|
76
|
+
packageData.push({ name, current: currentVersion, latest: latestVersion });
|
|
77
|
+
}
|
|
78
|
+
// Get risk analysis if deep scan (requires API key)
|
|
79
|
+
let riskMap = new Map();
|
|
80
|
+
if (isDeep && useAI) {
|
|
81
|
+
const modeLabel = isTechnical ? 'technical' : 'plain English';
|
|
82
|
+
console.log(chalk_1.default.yellow(`\n๐ง Analyzing upgrade risks (${modeLabel} mode)...\n`));
|
|
83
|
+
const risks = await (0, ai_1.getBatchRiskAnalysis)(packageData, isTechnical);
|
|
84
|
+
risks.forEach(r => riskMap.set(r.name, r));
|
|
85
|
+
}
|
|
86
|
+
// Build table
|
|
87
|
+
const tableHead = isDeep
|
|
88
|
+
? ['Package', 'Current', 'Latest', 'Status', 'Safe Ver', 'Risk']
|
|
89
|
+
: ['Package', 'Current', 'Latest', 'Status'];
|
|
90
|
+
const colWidths = isDeep
|
|
91
|
+
? [18, 12, 12, 12, 12, 24]
|
|
92
|
+
: [20, 15, 15, 12];
|
|
93
|
+
const table = new cli_table3_1.default({
|
|
94
|
+
head: tableHead,
|
|
95
|
+
colWidths,
|
|
96
|
+
style: {
|
|
97
|
+
head: ['white', 'bold'],
|
|
98
|
+
border: ['gray']
|
|
99
|
+
},
|
|
100
|
+
wordWrap: true
|
|
101
|
+
});
|
|
102
|
+
let majorCount = 0;
|
|
103
|
+
let minorCount = 0;
|
|
104
|
+
let patchCount = 0;
|
|
105
|
+
for (const { name, current, latest } of packageData) {
|
|
106
|
+
const upgradeType = getUpgradeType(current, latest);
|
|
107
|
+
const status = colorStatus(upgradeType);
|
|
108
|
+
// Track counts for summary
|
|
109
|
+
if (upgradeType === 'MAJOR')
|
|
110
|
+
majorCount++;
|
|
111
|
+
if (upgradeType === 'MINOR')
|
|
112
|
+
minorCount++;
|
|
113
|
+
if (upgradeType === 'PATCH')
|
|
114
|
+
patchCount++;
|
|
115
|
+
const displayName = chalk_1.default.bold(name);
|
|
116
|
+
if (isDeep) {
|
|
117
|
+
const risk = riskMap.get(name);
|
|
118
|
+
const safeVer = risk?.safeVersion || (upgradeType !== 'OK' ? 'โ' : latest);
|
|
119
|
+
const riskText = risk?.risk || (upgradeType !== 'OK' ? 'โ' : '');
|
|
120
|
+
const riskColor = risk?.risk === 'Safe' || upgradeType === 'OK'
|
|
121
|
+
? chalk_1.default.green(riskText)
|
|
122
|
+
: chalk_1.default.yellow(riskText);
|
|
123
|
+
table.push([displayName, current, latest, status, safeVer, riskColor]);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
table.push([displayName, current, latest, status]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
console.log(table.toString());
|
|
130
|
+
// Summary footer
|
|
131
|
+
const totalUpdates = majorCount + minorCount + patchCount;
|
|
132
|
+
if (totalUpdates > 0) {
|
|
133
|
+
console.log(chalk_1.default.gray('\n โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
134
|
+
console.log(` ๐ Summary: ` +
|
|
135
|
+
chalk_1.default.red(`${majorCount} major`) + ` ยท ` +
|
|
136
|
+
chalk_1.default.yellow(`${minorCount} minor`) + ` ยท ` +
|
|
137
|
+
chalk_1.default.green(`${patchCount} patch`));
|
|
138
|
+
}
|
|
139
|
+
// Mode indicator
|
|
140
|
+
if (!useAI) {
|
|
141
|
+
console.log(chalk_1.default.gray('\n โน๏ธ Free mode (npm registry). Set PERPLEXITY_API_KEY for AI features.'));
|
|
142
|
+
}
|
|
143
|
+
// Action hints
|
|
144
|
+
console.log(chalk_1.default.gray('\n ๐ก Actions:'));
|
|
145
|
+
if (!isDeep && useAI) {
|
|
146
|
+
console.log(chalk_1.default.gray(' linchpin --deep Full risk analysis'));
|
|
147
|
+
}
|
|
148
|
+
console.log(chalk_1.default.gray(' linchpin explain <pkg> Detailed breakdown'));
|
|
149
|
+
console.log(chalk_1.default.gray(' linchpin align <pkg> Safe upgrade wizard'));
|
|
150
|
+
console.log(chalk_1.default.gray(' linchpin align --all Batch upgrade mode\n'));
|
|
151
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.explainCommand = explainCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const files_1 = require("../lib/files");
|
|
9
|
+
const ai_1 = require("../lib/ai");
|
|
10
|
+
const npm_1 = require("../lib/npm");
|
|
11
|
+
async function explainCommand(pkgName, options = {}) {
|
|
12
|
+
const isTechnical = options.technical || false;
|
|
13
|
+
// Explain requires API key for risk analysis
|
|
14
|
+
if (!(0, npm_1.hasApiKey)()) {
|
|
15
|
+
console.log(chalk_1.default.red('\nโ The explain command requires an API key for AI analysis.'));
|
|
16
|
+
console.log(chalk_1.default.yellow(' Set PERPLEXITY_API_KEY in your .env file'));
|
|
17
|
+
console.log(chalk_1.default.gray(' Get one at: https://www.perplexity.ai/settings/api\n'));
|
|
18
|
+
console.log(chalk_1.default.gray(' Tip: Use "pin check" (free) to see version differences.\n'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(chalk_1.default.blue.bold(`\n๐ต๏ธ Investigating ${pkgName}...\n`));
|
|
22
|
+
// 1. Get current version from local file
|
|
23
|
+
const pkg = await (0, files_1.getPackageJson)();
|
|
24
|
+
if (!pkg) {
|
|
25
|
+
console.log(chalk_1.default.red('โ No package.json found!'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
29
|
+
const currentVer = allDeps[pkgName];
|
|
30
|
+
if (!currentVer) {
|
|
31
|
+
console.log(chalk_1.default.red(`โ Package "${pkgName}" not found in your package.json`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// 2. Get real latest version (use registry for speed, AI not needed here)
|
|
35
|
+
process.stdout.write(chalk_1.default.yellow(' Checking latest version...'));
|
|
36
|
+
const latestVer = (0, npm_1.getRegistryVersion)(pkgName) || await (0, ai_1.getLatestVersion)(pkgName);
|
|
37
|
+
console.log(chalk_1.default.green(` Found ${latestVer}`));
|
|
38
|
+
// Strip ^ or ~ for comparison
|
|
39
|
+
const cleanCurrent = currentVer.replace(/^[\^~]/, '');
|
|
40
|
+
if (cleanCurrent === latestVer) {
|
|
41
|
+
console.log(chalk_1.default.green('\nโ
You are already on the latest version!'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// 3. Ask the AI for the risk analysis
|
|
45
|
+
const modeLabel = isTechnical ? 'technical' : 'plain English';
|
|
46
|
+
console.log(chalk_1.default.yellow(`\n๐ง Analyzing changelogs (${modeLabel} mode)...`));
|
|
47
|
+
const riskAnalysis = await (0, ai_1.getUpgradeRisks)(pkgName, currentVer, latestVer, isTechnical);
|
|
48
|
+
// 4. Print the report
|
|
49
|
+
console.log(chalk_1.default.bold('\nโโโโโโโโโโโโโโโโโโโโ RISK REPORT โโโโโโโโโโโโโโโโโโโโ'));
|
|
50
|
+
console.log(riskAnalysis);
|
|
51
|
+
console.log(chalk_1.default.bold('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
52
|
+
// Action hint
|
|
53
|
+
console.log(chalk_1.default.gray(`\n ๐ก To upgrade: linchpin align ${pkgName}\n`));
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getLatestVersion = getLatestVersion;
|
|
7
|
+
exports.getUpgradeRisks = getUpgradeRisks;
|
|
8
|
+
exports.getBatchRiskAnalysis = getBatchRiskAnalysis;
|
|
9
|
+
const openai_1 = __importDefault(require("openai"));
|
|
10
|
+
let client = null;
|
|
11
|
+
function getClient() {
|
|
12
|
+
if (!client) {
|
|
13
|
+
const apiKey = process.env.PERPLEXITY_API_KEY;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw new Error('Missing PERPLEXITY_API_KEY in .env file');
|
|
16
|
+
}
|
|
17
|
+
client = new openai_1.default({
|
|
18
|
+
apiKey: apiKey,
|
|
19
|
+
baseURL: 'https://api.perplexity.ai'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
24
|
+
async function getLatestVersion(packageName) {
|
|
25
|
+
try {
|
|
26
|
+
const response = await getClient().chat.completions.create({
|
|
27
|
+
model: 'sonar-pro',
|
|
28
|
+
messages: [
|
|
29
|
+
{
|
|
30
|
+
role: 'system',
|
|
31
|
+
content: 'You are a precise JSON-only API. You output ONLY valid JSON.'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
role: 'user',
|
|
35
|
+
content: `Find the absolute latest stable release version for the npm package "${packageName}". Return a JSON object with a single key "version". Example: {"version": "18.2.0"}. Do not include any other text.`
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
const content = response.choices[0].message.content || '{}';
|
|
40
|
+
// Clean up if the AI adds markdown blocks like ```json ... ```
|
|
41
|
+
const cleanJson = content.replace(/```json|```/g, '').trim();
|
|
42
|
+
const data = JSON.parse(cleanJson);
|
|
43
|
+
return data.version || 'Unknown';
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (process.env.DEBUG) {
|
|
47
|
+
console.error('AI Error:', error instanceof Error ? error.message : error);
|
|
48
|
+
}
|
|
49
|
+
return 'Error';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function getUpgradeRisks(pkgName, currentVer, latestVer, technical = false) {
|
|
53
|
+
// Plain English (default) - for solopreneurs/founders
|
|
54
|
+
const founderPrompt = {
|
|
55
|
+
system: `You are a friendly tech advisor explaining things to a solo founder who codes but isn't a DevOps expert.
|
|
56
|
+
No jargon. Simple language. Be reassuring but honest.
|
|
57
|
+
Format your response like this:
|
|
58
|
+
๐ฏ Risk Level: [Low/Medium/High]
|
|
59
|
+
๐ก Plain English: [What this means for your app in 1-2 sentences]
|
|
60
|
+
โฑ๏ธ Effort: [How much work to fix if something breaks]
|
|
61
|
+
โ
Recommendation: [Should they update or skip?]`,
|
|
62
|
+
user: `I'm a solo founder and I want to update "${pkgName}" from ${currentVer} to ${latestVer}.
|
|
63
|
+
Will this break my app? Is it worth the headache? Give me the honest truth in plain English.`
|
|
64
|
+
};
|
|
65
|
+
// Technical mode - for experienced devs
|
|
66
|
+
const technicalPrompt = {
|
|
67
|
+
system: 'You are a senior DevOps engineer. Be concise. Bullet points only.',
|
|
68
|
+
user: `I am upgrading the npm package "${pkgName}" from ${currentVer} to ${latestVer}. What are the major breaking changes or risks? If there are none, say "Safe to upgrade."`
|
|
69
|
+
};
|
|
70
|
+
const prompt = technical ? technicalPrompt : founderPrompt;
|
|
71
|
+
try {
|
|
72
|
+
const response = await getClient().chat.completions.create({
|
|
73
|
+
model: 'sonar-pro',
|
|
74
|
+
messages: [
|
|
75
|
+
{ role: 'system', content: prompt.system },
|
|
76
|
+
{ role: 'user', content: prompt.user }
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
return response.choices[0].message.content || 'No info found.';
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('AI Error:', error instanceof Error ? error.message : error);
|
|
83
|
+
return 'Could not fetch risks.';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function getBatchRiskAnalysis(packages, technical = false) {
|
|
87
|
+
const outdatedPkgs = packages.filter(p => {
|
|
88
|
+
const cleanCurrent = p.current.replace(/^[\^~]/, '');
|
|
89
|
+
return cleanCurrent !== p.latest && p.latest !== 'Error' && p.latest !== 'Unknown';
|
|
90
|
+
});
|
|
91
|
+
if (outdatedPkgs.length === 0) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const pkgList = outdatedPkgs
|
|
95
|
+
.map(p => `- ${p.name}: ${p.current} โ ${p.latest}`)
|
|
96
|
+
.join('\n');
|
|
97
|
+
// Plain English (default) - for solopreneurs
|
|
98
|
+
const founderPrompt = `Analyze these npm package upgrades for a solo founder who isn't a DevOps expert:
|
|
99
|
+
${pkgList}
|
|
100
|
+
|
|
101
|
+
Return a JSON array. For each package, explain the risk in PLAIN ENGLISH (no jargon).
|
|
102
|
+
{
|
|
103
|
+
"name": "package-name",
|
|
104
|
+
"safeVersion": "recommended version",
|
|
105
|
+
"risk": "Plain English risk (e.g., 'Will break your app' or 'Safe to update' or 'Skip - not worth the headache')"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Example: [{"name":"chalk","safeVersion":"4.1.2","risk":"Skip - v5 breaks older setups"},{"name":"dotenv","safeVersion":"17.0.0","risk":"Safe to update"}]`;
|
|
109
|
+
// Technical mode - for experienced devs
|
|
110
|
+
const technicalPrompt = `Analyze these npm package upgrades for breaking changes:
|
|
111
|
+
${pkgList}
|
|
112
|
+
|
|
113
|
+
Return a JSON array with objects for each package:
|
|
114
|
+
{
|
|
115
|
+
"name": "package-name",
|
|
116
|
+
"safeVersion": "recommended safe version (latest if safe, or last stable major)",
|
|
117
|
+
"risk": "brief risk (10 words max) or 'Safe' if no breaking changes"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Example: [{"name":"chalk","safeVersion":"4.1.2","risk":"ESM-only in v5"},{"name":"dotenv","safeVersion":"17.0.0","risk":"Safe"}]`;
|
|
121
|
+
try {
|
|
122
|
+
const response = await getClient().chat.completions.create({
|
|
123
|
+
model: 'sonar-pro',
|
|
124
|
+
messages: [
|
|
125
|
+
{
|
|
126
|
+
role: 'system',
|
|
127
|
+
content: 'You are a precise JSON-only API. Output ONLY valid JSON array, no markdown.'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
role: 'user',
|
|
131
|
+
content: technical ? technicalPrompt : founderPrompt
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
});
|
|
135
|
+
const content = response.choices[0].message.content || '[]';
|
|
136
|
+
const cleanJson = content.replace(/```json|```/g, '').trim();
|
|
137
|
+
return JSON.parse(cleanJson);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (process.env.DEBUG) {
|
|
141
|
+
console.error('AI Error:', error instanceof Error ? error.message : error);
|
|
142
|
+
}
|
|
143
|
+
return outdatedPkgs.map(p => ({
|
|
144
|
+
name: p.name,
|
|
145
|
+
safeVersion: p.latest,
|
|
146
|
+
risk: 'Analysis failed'
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPackageJson = getPackageJson;
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
async function getPackageJson(dir = process.cwd()) {
|
|
7
|
+
const packagePath = (0, path_1.join)(dir, 'package.json');
|
|
8
|
+
try {
|
|
9
|
+
const content = await (0, promises_1.readFile)(packagePath, 'utf-8');
|
|
10
|
+
return JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
console.error('Error reading package.json:', error instanceof Error ? error.message : error);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRegistryVersion = getRegistryVersion;
|
|
4
|
+
exports.getRegistryVersions = getRegistryVersions;
|
|
5
|
+
exports.hasApiKey = hasApiKey;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
/**
|
|
8
|
+
* Get latest version from npm registry (FREE, no API key needed)
|
|
9
|
+
* Falls back gracefully on error
|
|
10
|
+
*/
|
|
11
|
+
function getRegistryVersion(pkgName) {
|
|
12
|
+
try {
|
|
13
|
+
const result = (0, child_process_1.execSync)(`npm view ${pkgName} version`, {
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
timeout: 10000,
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe'] // Suppress stderr
|
|
17
|
+
});
|
|
18
|
+
return result.trim();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return 'Unknown';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get multiple versions in parallel (batch optimization)
|
|
26
|
+
*/
|
|
27
|
+
async function getRegistryVersions(packages) {
|
|
28
|
+
const results = new Map();
|
|
29
|
+
// Run in parallel batches of 5 to avoid overwhelming npm
|
|
30
|
+
const batchSize = 5;
|
|
31
|
+
for (let i = 0; i < packages.length; i += batchSize) {
|
|
32
|
+
const batch = packages.slice(i, i + batchSize);
|
|
33
|
+
const promises = batch.map(async (pkg) => {
|
|
34
|
+
const version = getRegistryVersion(pkg);
|
|
35
|
+
results.set(pkg, version);
|
|
36
|
+
});
|
|
37
|
+
await Promise.all(promises);
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if Perplexity API key is configured
|
|
43
|
+
*/
|
|
44
|
+
function hasApiKey() {
|
|
45
|
+
return !!process.env.PERPLEXITY_API_KEY;
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logger = void 0;
|
|
4
|
+
// ANSI color codes for terminal output
|
|
5
|
+
const colors = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
blue: '\x1b[34m',
|
|
11
|
+
magenta: '\x1b[35m',
|
|
12
|
+
cyan: '\x1b[36m',
|
|
13
|
+
white: '\x1b[37m',
|
|
14
|
+
};
|
|
15
|
+
exports.logger = {
|
|
16
|
+
info: (message) => {
|
|
17
|
+
console.log(`${colors.blue}[INFO]${colors.reset} ${message}`);
|
|
18
|
+
},
|
|
19
|
+
success: (message) => {
|
|
20
|
+
console.log(`${colors.green}[SUCCESS]${colors.reset} ${message}`);
|
|
21
|
+
},
|
|
22
|
+
warn: (message) => {
|
|
23
|
+
console.log(`${colors.yellow}[WARN]${colors.reset} ${message}`);
|
|
24
|
+
},
|
|
25
|
+
error: (message) => {
|
|
26
|
+
console.log(`${colors.red}[ERROR]${colors.reset} ${message}`);
|
|
27
|
+
},
|
|
28
|
+
debug: (message) => {
|
|
29
|
+
if (process.env.DEBUG) {
|
|
30
|
+
console.log(`${colors.magenta}[DEBUG]${colors.reset} ${message}`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "linchpin-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Linchpin: The 'Don't Break My App' Tool - AI-powered dependency management for solo founders",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"linchpin": "./dist/bin/vm.js",
|
|
8
|
+
"pin": "./dist/bin/vm.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "ts-node bin/vm.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"dependency",
|
|
21
|
+
"npm",
|
|
22
|
+
"update",
|
|
23
|
+
"upgrade",
|
|
24
|
+
"ai",
|
|
25
|
+
"cli",
|
|
26
|
+
"devops",
|
|
27
|
+
"package-manager",
|
|
28
|
+
"security"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": ""
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=16.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "^4.1.2",
|
|
41
|
+
"cli-table3": "^0.6.5",
|
|
42
|
+
"commander": "^11.1.0",
|
|
43
|
+
"dotenv": "^17.2.3",
|
|
44
|
+
"inquirer": "^8.2.6",
|
|
45
|
+
"openai": "^4.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/inquirer": "^9.0.9",
|
|
49
|
+
"@types/node": "^20.10.0",
|
|
50
|
+
"ts-node": "^10.9.1",
|
|
51
|
+
"typescript": "^5.3.2"
|
|
52
|
+
}
|
|
53
|
+
}
|