linchpin-cli 0.1.0 → 0.2.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/dist/bin/vm.js CHANGED
@@ -10,6 +10,7 @@ const commander_1 = require("commander");
10
10
  const check_1 = require("../src/commands/check");
11
11
  const explain_1 = require("../src/commands/explain");
12
12
  const align_1 = require("../src/commands/align");
13
+ const login_1 = require("../src/commands/login");
13
14
  const program = new commander_1.Command();
14
15
  program
15
16
  .name('linchpin')
@@ -31,4 +32,16 @@ program
31
32
  .description('Safely update packages with auto-backup')
32
33
  .option('-a, --all', 'Update all outdated packages interactively')
33
34
  .action((pkgName, options) => (0, align_1.alignCommand)(pkgName, options));
35
+ program
36
+ .command('login')
37
+ .description('Log in to your Linchpin account for AI features')
38
+ .action(() => (0, login_1.loginCommand)());
39
+ program
40
+ .command('logout')
41
+ .description('Log out of your Linchpin account')
42
+ .action(() => (0, login_1.logoutCommand)());
43
+ program
44
+ .command('whoami')
45
+ .description('Show current login status')
46
+ .action(() => (0, login_1.whoamiCommand)());
34
47
  program.parse(process.argv);
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const align_1 = require("../commands/align");
4
+ describe('isMajorUpgrade', () => {
5
+ describe('returns true for major upgrades', () => {
6
+ it('detects simple major bump', () => {
7
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '2.0.0')).toBe(true);
8
+ });
9
+ it('detects major bump from higher minor/patch', () => {
10
+ expect((0, align_1.isMajorUpgrade)('1.5.3', '2.0.0')).toBe(true);
11
+ });
12
+ it('handles caret prefix in current version', () => {
13
+ expect((0, align_1.isMajorUpgrade)('^1.0.0', '2.0.0')).toBe(true);
14
+ });
15
+ it('handles tilde prefix in current version', () => {
16
+ expect((0, align_1.isMajorUpgrade)('~1.0.0', '2.0.0')).toBe(true);
17
+ });
18
+ it('detects multi-version major jumps', () => {
19
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '5.0.0')).toBe(true);
20
+ });
21
+ it('handles version 0 to 1 upgrade', () => {
22
+ expect((0, align_1.isMajorUpgrade)('0.9.0', '1.0.0')).toBe(true);
23
+ });
24
+ });
25
+ describe('returns false for non-major upgrades', () => {
26
+ it('minor version bump is not major', () => {
27
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '1.1.0')).toBe(false);
28
+ });
29
+ it('patch version bump is not major', () => {
30
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '1.0.1')).toBe(false);
31
+ });
32
+ it('same version is not major', () => {
33
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '1.0.0')).toBe(false);
34
+ });
35
+ it('handles caret prefix for same major', () => {
36
+ expect((0, align_1.isMajorUpgrade)('^1.0.0', '1.5.0')).toBe(false);
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const check_1 = require("../commands/check");
4
+ describe('getUpgradeType', () => {
5
+ describe('MAJOR upgrades', () => {
6
+ it('detects major version bump', () => {
7
+ expect((0, check_1.getUpgradeType)('1.0.0', '2.0.0')).toBe('MAJOR');
8
+ });
9
+ it('detects major bump regardless of minor/patch', () => {
10
+ expect((0, check_1.getUpgradeType)('1.5.3', '2.0.0')).toBe('MAJOR');
11
+ });
12
+ it('handles versions with caret prefix', () => {
13
+ expect((0, check_1.getUpgradeType)('^1.0.0', '2.0.0')).toBe('MAJOR');
14
+ });
15
+ it('handles versions with tilde prefix', () => {
16
+ expect((0, check_1.getUpgradeType)('~1.0.0', '2.0.0')).toBe('MAJOR');
17
+ });
18
+ it('handles large major version jumps', () => {
19
+ expect((0, check_1.getUpgradeType)('1.0.0', '10.0.0')).toBe('MAJOR');
20
+ });
21
+ });
22
+ describe('MINOR upgrades', () => {
23
+ it('detects minor version bump', () => {
24
+ expect((0, check_1.getUpgradeType)('1.0.0', '1.1.0')).toBe('MINOR');
25
+ });
26
+ it('detects minor bump with patch changes', () => {
27
+ expect((0, check_1.getUpgradeType)('1.0.5', '1.2.0')).toBe('MINOR');
28
+ });
29
+ it('handles versions with caret prefix', () => {
30
+ expect((0, check_1.getUpgradeType)('^1.0.0', '1.5.0')).toBe('MINOR');
31
+ });
32
+ });
33
+ describe('PATCH upgrades', () => {
34
+ it('detects patch version bump', () => {
35
+ expect((0, check_1.getUpgradeType)('1.0.0', '1.0.1')).toBe('PATCH');
36
+ });
37
+ it('detects larger patch bumps', () => {
38
+ expect((0, check_1.getUpgradeType)('1.0.0', '1.0.10')).toBe('PATCH');
39
+ });
40
+ it('handles versions with caret prefix', () => {
41
+ expect((0, check_1.getUpgradeType)('^1.0.0', '1.0.5')).toBe('PATCH');
42
+ });
43
+ });
44
+ describe('OK (no upgrade needed)', () => {
45
+ it('returns OK when versions match', () => {
46
+ expect((0, check_1.getUpgradeType)('1.0.0', '1.0.0')).toBe('OK');
47
+ });
48
+ it('returns OK when versions match ignoring prefix', () => {
49
+ expect((0, check_1.getUpgradeType)('^1.0.0', '1.0.0')).toBe('OK');
50
+ });
51
+ it('returns OK when versions match with tilde', () => {
52
+ expect((0, check_1.getUpgradeType)('~1.0.0', '1.0.0')).toBe('OK');
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const files_1 = require("../lib/files");
4
+ describe('getPackageJson', () => {
5
+ it('reads package.json from current directory by default', async () => {
6
+ const pkg = await (0, files_1.getPackageJson)();
7
+ expect(pkg).not.toBeNull();
8
+ expect(pkg?.name).toBe('linchpin-cli');
9
+ });
10
+ it('reads package.json from specified directory', async () => {
11
+ const pkg = await (0, files_1.getPackageJson)(process.cwd());
12
+ expect(pkg).not.toBeNull();
13
+ expect(pkg?.name).toBe('linchpin-cli');
14
+ });
15
+ it('returns null for non-existent directory', async () => {
16
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
17
+ const pkg = await (0, files_1.getPackageJson)('/non/existent/path');
18
+ expect(pkg).toBeNull();
19
+ consoleSpy.mockRestore();
20
+ });
21
+ it('parses dependencies correctly', async () => {
22
+ const pkg = await (0, files_1.getPackageJson)();
23
+ expect(pkg?.dependencies).toBeDefined();
24
+ expect(typeof pkg?.dependencies).toBe('object');
25
+ });
26
+ it('parses devDependencies correctly', async () => {
27
+ const pkg = await (0, files_1.getPackageJson)();
28
+ expect(pkg?.devDependencies).toBeDefined();
29
+ expect(typeof pkg?.devDependencies).toBe('object');
30
+ });
31
+ });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const npm_1 = require("../lib/npm");
4
+ describe('npm utilities', () => {
5
+ const originalEnv = process.env;
6
+ beforeEach(() => {
7
+ jest.resetModules();
8
+ process.env = { ...originalEnv };
9
+ });
10
+ afterAll(() => {
11
+ process.env = originalEnv;
12
+ });
13
+ describe('hasApiKey', () => {
14
+ it('returns true when PERPLEXITY_API_KEY is set', () => {
15
+ process.env.PERPLEXITY_API_KEY = 'test-key-123';
16
+ expect((0, npm_1.hasApiKey)()).toBe(true);
17
+ });
18
+ it('returns false when PERPLEXITY_API_KEY is not set', () => {
19
+ delete process.env.PERPLEXITY_API_KEY;
20
+ expect((0, npm_1.hasApiKey)()).toBe(false);
21
+ });
22
+ it('returns false when PERPLEXITY_API_KEY is empty string', () => {
23
+ process.env.PERPLEXITY_API_KEY = '';
24
+ expect((0, npm_1.hasApiKey)()).toBe(false);
25
+ });
26
+ });
27
+ });
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isMajorUpgrade = isMajorUpgrade;
6
7
  exports.alignCommand = alignCommand;
7
8
  const chalk_1 = __importDefault(require("chalk"));
8
9
  const inquirer_1 = __importDefault(require("inquirer"));
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getUpgradeType = getUpgradeType;
6
7
  exports.checkCommand = checkCommand;
7
8
  const chalk_1 = __importDefault(require("chalk"));
8
9
  const cli_table3_1 = __importDefault(require("cli-table3"));
@@ -138,7 +139,10 @@ async function checkCommand(options = {}) {
138
139
  }
139
140
  // Mode indicator
140
141
  if (!useAI) {
141
- console.log(chalk_1.default.gray('\n ℹ️ Free mode (npm registry). Set PERPLEXITY_API_KEY for AI features.'));
142
+ console.log(chalk_1.default.gray('\n ℹ️ Free mode (npm registry). Run `linchpin login` for AI features.'));
143
+ }
144
+ else if ((0, npm_1.isUsingCloudApi)()) {
145
+ console.log(chalk_1.default.gray('\n ☁️ Connected to Linchpin cloud.'));
142
146
  }
143
147
  // Action hints
144
148
  console.log(chalk_1.default.gray('\n 💡 Actions:'));
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.loginCommand = loginCommand;
40
+ exports.logoutCommand = logoutCommand;
41
+ exports.whoamiCommand = whoamiCommand;
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const auth_1 = require("../lib/auth");
44
+ const API_BASE = process.env.LINCHPIN_API_URL || 'https://getlinchpin.dev';
45
+ // Open URL in browser (cross-platform)
46
+ function openBrowser(url) {
47
+ const { exec } = require('child_process');
48
+ const platform = process.platform;
49
+ let command;
50
+ if (platform === 'win32') {
51
+ command = `start "" "${url}"`;
52
+ }
53
+ else if (platform === 'darwin') {
54
+ command = `open "${url}"`;
55
+ }
56
+ else {
57
+ command = `xdg-open "${url}"`;
58
+ }
59
+ exec(command, (err) => {
60
+ if (err) {
61
+ console.log(chalk_1.default.yellow(`\n Could not open browser automatically.`));
62
+ console.log(chalk_1.default.yellow(` Please open this URL manually:\n`));
63
+ console.log(chalk_1.default.cyan(` ${url}\n`));
64
+ }
65
+ });
66
+ }
67
+ // Poll for authorization
68
+ async function pollForToken(code, maxAttempts = 60) {
69
+ for (let i = 0; i < maxAttempts; i++) {
70
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Poll every 2 seconds
71
+ try {
72
+ const response = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
73
+ if (response.status === 410) {
74
+ // Code expired
75
+ return null;
76
+ }
77
+ if (!response.ok) {
78
+ continue;
79
+ }
80
+ const data = await response.json();
81
+ if (data.status === 'authorized' && data.token) {
82
+ return data.token;
83
+ }
84
+ // Still pending, continue polling
85
+ if (data.status === 'pending') {
86
+ continue;
87
+ }
88
+ }
89
+ catch {
90
+ // Network error, continue polling
91
+ continue;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ async function loginCommand() {
97
+ // Check if already logged in
98
+ const existingToken = (0, auth_1.loadCredentials)();
99
+ if (existingToken) {
100
+ console.log(chalk_1.default.yellow('\n You are already logged in.'));
101
+ console.log(chalk_1.default.gray(` Credentials stored at: ${(0, auth_1.getCredentialsPath)()}`));
102
+ console.log(chalk_1.default.gray(' Run `linchpin logout` to log out.\n'));
103
+ return;
104
+ }
105
+ console.log(chalk_1.default.blue.bold('\n🔐 Linchpin CLI Login\n'));
106
+ // Step 1: Get auth code from API
107
+ console.log(chalk_1.default.gray(' Contacting server...'));
108
+ let authData;
109
+ try {
110
+ const response = await fetch(`${API_BASE}/api/cli/auth`, {
111
+ method: 'POST',
112
+ });
113
+ if (!response.ok) {
114
+ throw new Error('Failed to start authentication');
115
+ }
116
+ authData = await response.json();
117
+ }
118
+ catch (err) {
119
+ console.log(chalk_1.default.red('\n ❌ Could not connect to Linchpin servers.'));
120
+ console.log(chalk_1.default.gray(' Check your internet connection and try again.\n'));
121
+ return;
122
+ }
123
+ // Step 2: Open browser for user to authorize
124
+ console.log(chalk_1.default.green(' Opening browser for authentication...\n'));
125
+ openBrowser(authData.verification_url);
126
+ console.log(chalk_1.default.white(' Waiting for authorization...'));
127
+ console.log(chalk_1.default.gray(' (Press Ctrl+C to cancel)\n'));
128
+ // Show a simple spinner
129
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
130
+ let spinnerIdx = 0;
131
+ const spinnerInterval = setInterval(() => {
132
+ process.stdout.write(`\r ${chalk_1.default.blue(spinner[spinnerIdx])} Waiting for browser authorization...`);
133
+ spinnerIdx = (spinnerIdx + 1) % spinner.length;
134
+ }, 100);
135
+ // Step 3: Poll for token
136
+ const token = await pollForToken(authData.code);
137
+ clearInterval(spinnerInterval);
138
+ process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear spinner line
139
+ if (!token) {
140
+ console.log(chalk_1.default.red(' ❌ Authorization timed out or was denied.'));
141
+ console.log(chalk_1.default.gray(' Please try again.\n'));
142
+ return;
143
+ }
144
+ // Step 4: Save credentials
145
+ try {
146
+ (0, auth_1.saveCredentials)(token);
147
+ console.log(chalk_1.default.green.bold(' ✓ Successfully logged in!\n'));
148
+ console.log(chalk_1.default.gray(` Credentials saved to: ${(0, auth_1.getCredentialsPath)()}`));
149
+ console.log(chalk_1.default.gray(' You can now use AI-powered features.\n'));
150
+ }
151
+ catch (err) {
152
+ console.log(chalk_1.default.red(' ❌ Failed to save credentials.'));
153
+ console.log(chalk_1.default.gray(` ${err}\n`));
154
+ }
155
+ }
156
+ async function logoutCommand() {
157
+ const { clearCredentials } = await Promise.resolve().then(() => __importStar(require('../lib/auth')));
158
+ const hadCredentials = (0, auth_1.loadCredentials)() !== null;
159
+ clearCredentials();
160
+ if (hadCredentials) {
161
+ console.log(chalk_1.default.green('\n ✓ Successfully logged out.\n'));
162
+ }
163
+ else {
164
+ console.log(chalk_1.default.yellow('\n You were not logged in.\n'));
165
+ }
166
+ }
167
+ async function whoamiCommand() {
168
+ const token = (0, auth_1.loadCredentials)();
169
+ if (!token) {
170
+ console.log(chalk_1.default.yellow('\n Not logged in.'));
171
+ console.log(chalk_1.default.gray(' Run `linchpin login` to authenticate.\n'));
172
+ return;
173
+ }
174
+ console.log(chalk_1.default.blue('\n🔐 Linchpin CLI Status\n'));
175
+ console.log(chalk_1.default.green(' ✓ Logged in'));
176
+ console.log(chalk_1.default.gray(` Token: ${token.substring(0, 10)}...`));
177
+ console.log(chalk_1.default.gray(` Credentials: ${(0, auth_1.getCredentialsPath)()}\n`));
178
+ }
@@ -7,6 +7,7 @@ exports.getLatestVersion = getLatestVersion;
7
7
  exports.getUpgradeRisks = getUpgradeRisks;
8
8
  exports.getBatchRiskAnalysis = getBatchRiskAnalysis;
9
9
  const openai_1 = __importDefault(require("openai"));
10
+ const npm_1 = require("./npm");
10
11
  let client = null;
11
12
  function getClient() {
12
13
  if (!client) {
@@ -50,6 +51,21 @@ async function getLatestVersion(packageName) {
50
51
  }
51
52
  }
52
53
  async function getUpgradeRisks(pkgName, currentVer, latestVer, technical = false) {
54
+ // Use cloud API if logged in (no local Perplexity key)
55
+ if ((0, npm_1.isUsingCloudApi)()) {
56
+ const result = await (0, npm_1.apiRequest)('/api/cli/analyze', {
57
+ action: 'explain',
58
+ packageName: pkgName,
59
+ currentVersion: currentVer,
60
+ latestVersion: latestVer,
61
+ technical,
62
+ });
63
+ if (result.error) {
64
+ return result.error;
65
+ }
66
+ return result.data?.explanation || 'No info found.';
67
+ }
68
+ // Local Perplexity API
53
69
  // Plain English (default) - for solopreneurs/founders
54
70
  const founderPrompt = {
55
71
  system: `You are a friendly tech advisor explaining things to a solo founder who codes but isn't a DevOps expert.
@@ -91,6 +107,24 @@ async function getBatchRiskAnalysis(packages, technical = false) {
91
107
  if (outdatedPkgs.length === 0) {
92
108
  return [];
93
109
  }
110
+ // Use cloud API if logged in (no local Perplexity key)
111
+ if ((0, npm_1.isUsingCloudApi)()) {
112
+ const result = await (0, npm_1.apiRequest)('/api/cli/analyze', {
113
+ action: 'batch_risk',
114
+ packages: outdatedPkgs,
115
+ technical,
116
+ });
117
+ if (result.error) {
118
+ console.error('Cloud API error:', result.error);
119
+ return outdatedPkgs.map(p => ({
120
+ name: p.name,
121
+ safeVersion: p.latest,
122
+ risk: result.error || 'Analysis failed'
123
+ }));
124
+ }
125
+ return result.data?.risks || [];
126
+ }
127
+ // Local Perplexity API
94
128
  const pkgList = outdatedPkgs
95
129
  .map(p => `- ${p.name}: ${p.current} → ${p.latest}`)
96
130
  .join('\n');
@@ -0,0 +1,61 @@
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.getCredentialsPath = getCredentialsPath;
7
+ exports.ensureConfigDir = ensureConfigDir;
8
+ exports.saveCredentials = saveCredentials;
9
+ exports.loadCredentials = loadCredentials;
10
+ exports.clearCredentials = clearCredentials;
11
+ exports.hasStoredCredentials = hasStoredCredentials;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.linchpin');
16
+ const CREDENTIALS_FILE = path_1.default.join(CONFIG_DIR, 'credentials.json');
17
+ function getCredentialsPath() {
18
+ return CREDENTIALS_FILE;
19
+ }
20
+ function ensureConfigDir() {
21
+ if (!fs_1.default.existsSync(CONFIG_DIR)) {
22
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
23
+ }
24
+ }
25
+ function saveCredentials(token) {
26
+ ensureConfigDir();
27
+ const credentials = {
28
+ token,
29
+ created_at: new Date().toISOString(),
30
+ };
31
+ fs_1.default.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
32
+ mode: 0o600, // Read/write for owner only
33
+ });
34
+ }
35
+ function loadCredentials() {
36
+ try {
37
+ if (!fs_1.default.existsSync(CREDENTIALS_FILE)) {
38
+ return null;
39
+ }
40
+ const content = fs_1.default.readFileSync(CREDENTIALS_FILE, 'utf-8');
41
+ const credentials = JSON.parse(content);
42
+ return credentials.token || null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function clearCredentials() {
49
+ try {
50
+ if (fs_1.default.existsSync(CREDENTIALS_FILE)) {
51
+ fs_1.default.unlinkSync(CREDENTIALS_FILE);
52
+ }
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function hasStoredCredentials() {
60
+ return loadCredentials() !== null;
61
+ }
@@ -3,7 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getRegistryVersion = getRegistryVersion;
4
4
  exports.getRegistryVersions = getRegistryVersions;
5
5
  exports.hasApiKey = hasApiKey;
6
+ exports.isUsingCloudApi = isUsingCloudApi;
7
+ exports.getApiToken = getApiToken;
8
+ exports.apiRequest = apiRequest;
6
9
  const child_process_1 = require("child_process");
10
+ const auth_1 = require("./auth");
11
+ const API_BASE = process.env.LINCHPIN_API_URL || 'https://getlinchpin.dev';
7
12
  /**
8
13
  * Get latest version from npm registry (FREE, no API key needed)
9
14
  * Falls back gracefully on error
@@ -39,8 +44,58 @@ async function getRegistryVersions(packages) {
39
44
  return results;
40
45
  }
41
46
  /**
42
- * Check if Perplexity API key is configured
47
+ * Check if user has access to AI features
48
+ * Either via local PERPLEXITY_API_KEY or stored Linchpin credentials
43
49
  */
44
50
  function hasApiKey() {
45
- return !!process.env.PERPLEXITY_API_KEY;
51
+ // Check for local Perplexity key first (legacy/dev mode)
52
+ if (process.env.PERPLEXITY_API_KEY) {
53
+ return true;
54
+ }
55
+ // Check for stored Linchpin credentials
56
+ const token = (0, auth_1.loadCredentials)();
57
+ return token !== null;
58
+ }
59
+ /**
60
+ * Check if using cloud API (stored credentials) vs local API key
61
+ */
62
+ function isUsingCloudApi() {
63
+ // If local key is set, prefer that
64
+ if (process.env.PERPLEXITY_API_KEY) {
65
+ return false;
66
+ }
67
+ return (0, auth_1.loadCredentials)() !== null;
68
+ }
69
+ /**
70
+ * Get the stored API token (for cloud API calls)
71
+ */
72
+ function getApiToken() {
73
+ return (0, auth_1.loadCredentials)();
74
+ }
75
+ /**
76
+ * Make an authenticated request to the Linchpin API
77
+ */
78
+ async function apiRequest(endpoint, body) {
79
+ const token = (0, auth_1.loadCredentials)();
80
+ if (!token) {
81
+ return { error: 'Not logged in. Run `linchpin login` first.' };
82
+ }
83
+ try {
84
+ const response = await fetch(`${API_BASE}${endpoint}`, {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ 'Authorization': `Bearer ${token}`,
89
+ },
90
+ body: JSON.stringify(body),
91
+ });
92
+ const data = await response.json();
93
+ if (!response.ok) {
94
+ return { error: data.error || 'API request failed' };
95
+ }
96
+ return { data };
97
+ }
98
+ catch (err) {
99
+ return { error: 'Failed to connect to Linchpin servers' };
100
+ }
46
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linchpin-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Linchpin: The 'Don't Break My App' Tool - AI-powered dependency management for solo founders",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -14,6 +14,8 @@
14
14
  "scripts": {
15
15
  "build": "tsc",
16
16
  "start": "ts-node bin/vm.ts",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch",
17
19
  "prepublishOnly": "npm run build"
18
20
  },
19
21
  "keywords": [
@@ -42,11 +44,16 @@
42
44
  "commander": "^11.1.0",
43
45
  "dotenv": "^17.2.3",
44
46
  "inquirer": "^8.2.6",
45
- "openai": "^4.0.0"
47
+ "lucide-react": "^0.562.0",
48
+ "openai": "^4.0.0",
49
+ "resend": "^6.7.0"
46
50
  },
47
51
  "devDependencies": {
48
52
  "@types/inquirer": "^9.0.9",
53
+ "@types/jest": "^30.0.0",
49
54
  "@types/node": "^20.10.0",
55
+ "jest": "^30.2.0",
56
+ "ts-jest": "^29.4.6",
50
57
  "ts-node": "^10.9.1",
51
58
  "typescript": "^5.3.2"
52
59
  }