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 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
+ }