linchpin-cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/vm.js CHANGED
@@ -6,16 +6,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const dotenv_1 = __importDefault(require("dotenv"));
8
8
  dotenv_1.default.config(); // Load env vars once at entry point
9
+ // Read version from package.json
10
+ // Path: from bin/vm.ts -> ../package.json (source)
11
+ // Path: from dist/bin/vm.js -> ../../package.json (compiled)
12
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
13
+ const pkg = require('../../package.json');
14
+ const CLI_VERSION = pkg.version;
15
+ const sentry_1 = require("../src/lib/sentry");
16
+ const auth_1 = require("../src/lib/auth");
17
+ // Initialize Sentry early to catch all errors
18
+ (0, sentry_1.initSentry)(CLI_VERSION);
19
+ // Set user if already logged in (for error tracking)
20
+ const existingToken = (0, auth_1.loadCredentials)();
21
+ if (existingToken) {
22
+ (0, sentry_1.setUser)({ id: existingToken.substring(0, 16) });
23
+ }
9
24
  const commander_1 = require("commander");
10
25
  const check_1 = require("../src/commands/check");
11
26
  const explain_1 = require("../src/commands/explain");
12
27
  const align_1 = require("../src/commands/align");
13
28
  const login_1 = require("../src/commands/login");
29
+ const plan_1 = require("../src/commands/plan");
14
30
  const program = new commander_1.Command();
15
31
  program
16
32
  .name('linchpin')
17
33
  .description("Linchpin: The 'Don't Break My App' Tool")
18
- .version('0.1.0');
34
+ .version(CLI_VERSION);
19
35
  program
20
36
  .command('check', { isDefault: true })
21
37
  .description('Scan your project for outdated dependencies')
@@ -44,4 +60,24 @@ program
44
60
  .command('whoami')
45
61
  .description('Show current login status')
46
62
  .action(() => (0, login_1.whoamiCommand)());
63
+ program
64
+ .command('plan <file>')
65
+ .description('Execute an upgrade plan from a JSON file')
66
+ .option('-y, --yes', 'Skip confirmation prompts')
67
+ .option('--dry-run', 'Show what would be done without making changes')
68
+ .action((file, options) => (0, plan_1.planCommand)(file, options));
69
+ // Global error handler to capture unhandled errors
70
+ process.on('uncaughtException', async (error) => {
71
+ (0, sentry_1.captureException)(error, { type: 'uncaughtException' });
72
+ console.error('An unexpected error occurred:', error.message);
73
+ await (0, sentry_1.flush)();
74
+ process.exit(1);
75
+ });
76
+ process.on('unhandledRejection', async (reason) => {
77
+ const error = reason instanceof Error ? reason : new Error(String(reason));
78
+ (0, sentry_1.captureException)(error, { type: 'unhandledRejection' });
79
+ console.error('An unexpected error occurred:', error.message);
80
+ await (0, sentry_1.flush)();
81
+ process.exit(1);
82
+ });
47
83
  program.parse(process.argv);
@@ -0,0 +1,209 @@
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
+ const align_1 = require("../commands/align");
7
+ // Mock dependencies
8
+ jest.mock('../lib/files', () => ({
9
+ getPackageJson: jest.fn(),
10
+ }));
11
+ jest.mock('../lib/npm', () => ({
12
+ getRegistryVersion: jest.fn(),
13
+ hasApiKey: jest.fn(),
14
+ }));
15
+ jest.mock('../lib/ai', () => ({
16
+ getLatestVersion: jest.fn(),
17
+ getUpgradeRisks: jest.fn(),
18
+ }));
19
+ jest.mock('child_process', () => ({
20
+ execSync: jest.fn(),
21
+ }));
22
+ jest.mock('inquirer', () => ({
23
+ prompt: jest.fn(),
24
+ }));
25
+ // Mock chalk (uses __mocks__/chalk.js)
26
+ jest.mock('chalk');
27
+ const files_1 = require("../lib/files");
28
+ const npm_1 = require("../lib/npm");
29
+ const child_process_1 = require("child_process");
30
+ const inquirer_1 = __importDefault(require("inquirer"));
31
+ const mockGetPackageJson = files_1.getPackageJson;
32
+ const mockGetRegistryVersion = npm_1.getRegistryVersion;
33
+ const mockHasApiKey = npm_1.hasApiKey;
34
+ const mockExecSync = child_process_1.execSync;
35
+ const mockPrompt = inquirer_1.default.prompt;
36
+ describe('alignCommand integration', () => {
37
+ let consoleSpy;
38
+ let stdoutSpy;
39
+ beforeEach(() => {
40
+ jest.clearAllMocks();
41
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
42
+ stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation();
43
+ // Default mocks
44
+ mockHasApiKey.mockReturnValue(false);
45
+ });
46
+ afterEach(() => {
47
+ consoleSpy.mockRestore();
48
+ stdoutSpy.mockRestore();
49
+ });
50
+ describe('single package alignment', () => {
51
+ it('shows error when package.json is missing', async () => {
52
+ mockGetPackageJson.mockResolvedValue(null);
53
+ await (0, align_1.alignCommand)('express');
54
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No package.json found'));
55
+ });
56
+ it('shows error when package is not found', async () => {
57
+ mockGetPackageJson.mockResolvedValue({
58
+ name: 'test-project',
59
+ version: '1.0.0',
60
+ dependencies: { 'lodash': '4.17.0' },
61
+ });
62
+ await (0, align_1.alignCommand)('nonexistent-pkg');
63
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('not found'));
64
+ });
65
+ it('shows up-to-date message when no update needed', async () => {
66
+ mockGetPackageJson.mockResolvedValue({
67
+ name: 'test-project',
68
+ version: '1.0.0',
69
+ dependencies: { 'express': '4.18.2' },
70
+ });
71
+ mockGetRegistryVersion.mockReturnValue('4.18.2');
72
+ await (0, align_1.alignCommand)('express');
73
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Already up to date'));
74
+ });
75
+ it('prompts for confirmation on safe upgrade', async () => {
76
+ mockGetPackageJson.mockResolvedValue({
77
+ name: 'test-project',
78
+ version: '1.0.0',
79
+ dependencies: { 'express': '^4.17.0' },
80
+ });
81
+ mockGetRegistryVersion.mockReturnValue('4.18.2');
82
+ mockPrompt.mockResolvedValue({ confirm: false });
83
+ // Mock git check
84
+ mockExecSync.mockImplementation((cmd) => {
85
+ if (cmd.includes('rev-parse'))
86
+ throw new Error('not a git repo');
87
+ return Buffer.from('');
88
+ });
89
+ await (0, align_1.alignCommand)('express');
90
+ // Should show current and target versions
91
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Current'));
92
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Target'));
93
+ // Should prompt for confirmation
94
+ expect(mockPrompt).toHaveBeenCalled();
95
+ // User declined, should show blocked message
96
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('blocked'));
97
+ });
98
+ it('installs package when user confirms', async () => {
99
+ mockGetPackageJson.mockResolvedValue({
100
+ name: 'test-project',
101
+ version: '1.0.0',
102
+ dependencies: { 'express': '^4.17.0' },
103
+ });
104
+ mockGetRegistryVersion.mockReturnValue('4.18.2');
105
+ mockPrompt.mockResolvedValue({ confirm: true });
106
+ // Mock git and npm commands
107
+ mockExecSync.mockImplementation((cmd) => {
108
+ if (cmd.includes('rev-parse'))
109
+ throw new Error('not a git repo');
110
+ return Buffer.from('');
111
+ });
112
+ await (0, align_1.alignCommand)('express');
113
+ // Should run npm install
114
+ expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining('npm install express@4.18.2'), expect.anything());
115
+ // Should show success
116
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Success'));
117
+ });
118
+ it('warns about major version upgrades', async () => {
119
+ mockGetPackageJson.mockResolvedValue({
120
+ name: 'test-project',
121
+ version: '1.0.0',
122
+ dependencies: { 'express': '^3.0.0' },
123
+ });
124
+ mockGetRegistryVersion.mockReturnValue('4.18.2');
125
+ mockPrompt.mockResolvedValue({ confirm: false });
126
+ mockExecSync.mockImplementation((cmd) => {
127
+ if (cmd.includes('rev-parse'))
128
+ throw new Error('not a git repo');
129
+ return Buffer.from('');
130
+ });
131
+ await (0, align_1.alignCommand)('express');
132
+ // Should show major version warning
133
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('MAJOR VERSION JUMP'));
134
+ });
135
+ it('creates git restore point when in git repo', async () => {
136
+ mockGetPackageJson.mockResolvedValue({
137
+ name: 'test-project',
138
+ version: '1.0.0',
139
+ dependencies: { 'express': '^4.17.0' },
140
+ });
141
+ mockGetRegistryVersion.mockReturnValue('4.18.2');
142
+ mockPrompt.mockResolvedValue({ confirm: true });
143
+ // Mock as git repo
144
+ mockExecSync.mockImplementation(() => Buffer.from(''));
145
+ await (0, align_1.alignCommand)('express');
146
+ // Should create backup commit
147
+ expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining('git commit'), expect.anything());
148
+ // Should show restore hint
149
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('restore point'));
150
+ });
151
+ });
152
+ describe('align --all mode', () => {
153
+ it('shows up-to-date when no updates available', async () => {
154
+ mockGetPackageJson.mockResolvedValue({
155
+ name: 'test-project',
156
+ version: '1.0.0',
157
+ dependencies: {
158
+ 'express': '4.18.2',
159
+ 'lodash': '4.17.21',
160
+ },
161
+ });
162
+ mockGetRegistryVersion
163
+ .mockReturnValueOnce('4.18.2')
164
+ .mockReturnValueOnce('4.17.21');
165
+ await (0, align_1.alignCommand)({ all: true });
166
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('All packages are up to date'));
167
+ });
168
+ it('allows user to select packages for update', async () => {
169
+ mockGetPackageJson.mockResolvedValue({
170
+ name: 'test-project',
171
+ version: '1.0.0',
172
+ dependencies: {
173
+ 'express': '4.17.0',
174
+ 'lodash': '4.17.0',
175
+ },
176
+ });
177
+ mockGetRegistryVersion
178
+ .mockReturnValueOnce('4.18.2') // express - minor
179
+ .mockReturnValueOnce('4.17.21'); // lodash - patch
180
+ mockPrompt.mockResolvedValue({ selected: [] });
181
+ mockExecSync.mockImplementation((cmd) => {
182
+ if (cmd.includes('rev-parse'))
183
+ throw new Error('not a git repo');
184
+ return Buffer.from('');
185
+ });
186
+ await (0, align_1.alignCommand)({ all: true });
187
+ // Should show found updates
188
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Found 2 packages with updates'));
189
+ // User selected none
190
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No packages selected'));
191
+ });
192
+ });
193
+ });
194
+ describe('isMajorUpgrade', () => {
195
+ it('detects major version changes', () => {
196
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '2.0.0')).toBe(true);
197
+ expect((0, align_1.isMajorUpgrade)('4.17.21', '5.0.0')).toBe(true);
198
+ expect((0, align_1.isMajorUpgrade)('^3.0.0', '4.0.0')).toBe(true);
199
+ });
200
+ it('returns false for minor/patch changes', () => {
201
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '1.1.0')).toBe(false);
202
+ expect((0, align_1.isMajorUpgrade)('1.0.0', '1.0.1')).toBe(false);
203
+ expect((0, align_1.isMajorUpgrade)('^4.17.0', '4.18.2')).toBe(false);
204
+ });
205
+ it('handles edge cases', () => {
206
+ expect((0, align_1.isMajorUpgrade)('0.9.0', '1.0.0')).toBe(true);
207
+ expect((0, align_1.isMajorUpgrade)('~1.0.0', '2.0.0')).toBe(true);
208
+ });
209
+ });
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const check_1 = require("../commands/check");
4
+ // Mock dependencies
5
+ jest.mock('../lib/files', () => ({
6
+ getPackageJson: jest.fn(),
7
+ }));
8
+ jest.mock('../lib/npm', () => ({
9
+ getRegistryVersion: jest.fn(),
10
+ hasApiKey: jest.fn(),
11
+ isUsingCloudApi: jest.fn(),
12
+ apiRequest: jest.fn(),
13
+ }));
14
+ jest.mock('../lib/ai', () => ({
15
+ getLatestVersion: jest.fn(),
16
+ getBatchRiskAnalysis: jest.fn(),
17
+ }));
18
+ // Mock chalk (uses __mocks__/chalk.js)
19
+ jest.mock('chalk');
20
+ const files_1 = require("../lib/files");
21
+ const npm_1 = require("../lib/npm");
22
+ const mockGetPackageJson = files_1.getPackageJson;
23
+ const mockGetRegistryVersion = npm_1.getRegistryVersion;
24
+ const mockHasApiKey = npm_1.hasApiKey;
25
+ const mockIsUsingCloudApi = npm_1.isUsingCloudApi;
26
+ describe('checkCommand integration', () => {
27
+ let consoleSpy;
28
+ let stdoutSpy;
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
32
+ stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation();
33
+ // Default mocks
34
+ mockHasApiKey.mockReturnValue(false);
35
+ mockIsUsingCloudApi.mockReturnValue(false);
36
+ });
37
+ afterEach(() => {
38
+ consoleSpy.mockRestore();
39
+ stdoutSpy.mockRestore();
40
+ });
41
+ describe('when package.json is missing', () => {
42
+ it('shows error message', async () => {
43
+ mockGetPackageJson.mockResolvedValue(null);
44
+ await (0, check_1.checkCommand)({});
45
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No package.json found'));
46
+ });
47
+ });
48
+ describe('when package.json has no dependencies', () => {
49
+ it('shows warning message', async () => {
50
+ mockGetPackageJson.mockResolvedValue({
51
+ name: 'test-project',
52
+ version: '1.0.0',
53
+ dependencies: {},
54
+ devDependencies: {},
55
+ });
56
+ await (0, check_1.checkCommand)({});
57
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('no dependencies listed'));
58
+ });
59
+ });
60
+ describe('when scanning dependencies (free mode)', () => {
61
+ it('checks all dependencies and displays results', async () => {
62
+ mockGetPackageJson.mockResolvedValue({
63
+ name: 'test-project',
64
+ version: '1.0.0',
65
+ dependencies: {
66
+ 'express': '^4.18.0',
67
+ 'lodash': '^4.17.21',
68
+ },
69
+ devDependencies: {
70
+ 'jest': '^29.0.0',
71
+ },
72
+ });
73
+ mockGetRegistryVersion
74
+ .mockReturnValueOnce('4.21.0') // express - MINOR
75
+ .mockReturnValueOnce('4.17.21') // lodash - OK
76
+ .mockReturnValueOnce('30.0.0'); // jest - MAJOR
77
+ await (0, check_1.checkCommand)({});
78
+ // Should check each package
79
+ expect(mockGetRegistryVersion).toHaveBeenCalledTimes(3);
80
+ expect(mockGetRegistryVersion).toHaveBeenCalledWith('express');
81
+ expect(mockGetRegistryVersion).toHaveBeenCalledWith('lodash');
82
+ expect(mockGetRegistryVersion).toHaveBeenCalledWith('jest');
83
+ // Should display scanning message
84
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Checking 3 packages'));
85
+ // Should show free mode hint
86
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Free mode'));
87
+ });
88
+ it('counts upgrade types correctly', async () => {
89
+ mockGetPackageJson.mockResolvedValue({
90
+ name: 'test-project',
91
+ version: '1.0.0',
92
+ dependencies: {
93
+ 'pkg-major': '1.0.0',
94
+ 'pkg-minor': '1.0.0',
95
+ 'pkg-patch': '1.0.0',
96
+ 'pkg-ok': '1.0.0',
97
+ },
98
+ });
99
+ mockGetRegistryVersion
100
+ .mockReturnValueOnce('2.0.0') // MAJOR
101
+ .mockReturnValueOnce('1.1.0') // MINOR
102
+ .mockReturnValueOnce('1.0.1') // PATCH
103
+ .mockReturnValueOnce('1.0.0'); // OK
104
+ await (0, check_1.checkCommand)({});
105
+ // Should display summary with correct counts
106
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 major'));
107
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 minor'));
108
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 patch'));
109
+ });
110
+ });
111
+ describe('deep scan mode', () => {
112
+ it('requires API key for deep scan', async () => {
113
+ mockHasApiKey.mockReturnValue(false);
114
+ await (0, check_1.checkCommand)({ deep: true });
115
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Deep scan requires an API key'));
116
+ });
117
+ });
118
+ describe('error handling', () => {
119
+ it('handles npm registry errors gracefully', async () => {
120
+ mockGetPackageJson.mockResolvedValue({
121
+ name: 'test-project',
122
+ version: '1.0.0',
123
+ dependencies: {
124
+ 'nonexistent-package': '1.0.0',
125
+ },
126
+ });
127
+ mockGetRegistryVersion.mockReturnValue('Unknown');
128
+ await (0, check_1.checkCommand)({});
129
+ // Should not throw, should display result
130
+ expect(mockGetRegistryVersion).toHaveBeenCalledWith('nonexistent-package');
131
+ });
132
+ });
133
+ });
134
+ describe('getUpgradeType edge cases', () => {
135
+ it('handles zero versions', () => {
136
+ expect((0, check_1.getUpgradeType)('0.1.0', '1.0.0')).toBe('MAJOR');
137
+ expect((0, check_1.getUpgradeType)('0.0.1', '0.1.0')).toBe('MINOR');
138
+ expect((0, check_1.getUpgradeType)('0.0.1', '0.0.2')).toBe('PATCH');
139
+ });
140
+ it('handles prerelease versions', () => {
141
+ // Prerelease tags are stripped, major/minor detection still works
142
+ expect((0, check_1.getUpgradeType)('1.0.0-alpha', '2.0.0')).toBe('MAJOR');
143
+ expect((0, check_1.getUpgradeType)('1.0.0-beta', '1.1.0')).toBe('MINOR');
144
+ });
145
+ it('handles versions with v prefix', () => {
146
+ expect((0, check_1.getUpgradeType)('v1.0.0', '2.0.0')).toBe('MAJOR');
147
+ });
148
+ });
@@ -144,6 +144,41 @@ async function checkCommand(options = {}) {
144
144
  else if ((0, npm_1.isUsingCloudApi)()) {
145
145
  console.log(chalk_1.default.gray('\n ☁️ Connected to Linchpin cloud.'));
146
146
  }
147
+ // Sync scan results to cloud if logged in
148
+ if ((0, npm_1.isUsingCloudApi)()) {
149
+ const scanResults = packageData.map(({ name, current, latest }) => {
150
+ const upgradeType = getUpgradeType(current, latest);
151
+ const riskLevel = upgradeType === 'MAJOR' ? 'critical' : upgradeType === 'MINOR' ? 'warning' : 'safe';
152
+ const risk = riskMap.get(name);
153
+ return {
154
+ name,
155
+ current,
156
+ latest,
157
+ riskLevel,
158
+ reason: risk?.risk || '',
159
+ aiRisk: risk?.risk,
160
+ safeVersion: risk?.safeVersion,
161
+ };
162
+ });
163
+ const total = packageData.length;
164
+ const safeCount = total - majorCount - minorCount - patchCount;
165
+ const healthScore = total > 0
166
+ ? Math.round(((safeCount * 100) + (patchCount * 80) + (minorCount * 50) + (majorCount * 0)) / total)
167
+ : 100;
168
+ // Fire and forget - don't block CLI output
169
+ (0, npm_1.apiRequest)('/api/cli/scan', {
170
+ healthScore,
171
+ packagesCount: total,
172
+ criticalCount: majorCount,
173
+ warningCount: minorCount,
174
+ scanResults,
175
+ deepAnalysis: isDeep,
176
+ }).then(result => {
177
+ if (result.error && process.env.DEBUG) {
178
+ console.error('Failed to sync scan:', result.error);
179
+ }
180
+ });
181
+ }
147
182
  // Action hints
148
183
  console.log(chalk_1.default.gray('\n 💡 Actions:'));
149
184
  if (!isDeep && useAI) {
@@ -41,6 +41,7 @@ exports.logoutCommand = logoutCommand;
41
41
  exports.whoamiCommand = whoamiCommand;
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
43
  const auth_1 = require("../lib/auth");
44
+ const sentry_1 = require("../lib/sentry");
44
45
  const API_BASE = process.env.LINCHPIN_API_URL || 'https://getlinchpin.dev';
45
46
  // Open URL in browser (cross-platform)
46
47
  function openBrowser(url) {
@@ -144,6 +145,9 @@ async function loginCommand() {
144
145
  // Step 4: Save credentials
145
146
  try {
146
147
  (0, auth_1.saveCredentials)(token);
148
+ // Set user in Sentry for error tracking (using token hash for anonymity)
149
+ const userHash = token.substring(0, 16);
150
+ (0, sentry_1.setUser)({ id: userHash });
147
151
  console.log(chalk_1.default.green.bold(' ✓ Successfully logged in!\n'));
148
152
  console.log(chalk_1.default.gray(` Credentials saved to: ${(0, auth_1.getCredentialsPath)()}`));
149
153
  console.log(chalk_1.default.gray(' You can now use AI-powered features.\n'));
@@ -157,6 +161,8 @@ async function logoutCommand() {
157
161
  const { clearCredentials } = await Promise.resolve().then(() => __importStar(require('../lib/auth')));
158
162
  const hadCredentials = (0, auth_1.loadCredentials)() !== null;
159
163
  clearCredentials();
164
+ // Clear user from Sentry
165
+ (0, sentry_1.clearUser)();
160
166
  if (hadCredentials) {
161
167
  console.log(chalk_1.default.green('\n ✓ Successfully logged out.\n'));
162
168
  }
@@ -0,0 +1,152 @@
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.planCommand = planCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const readline_1 = __importDefault(require("readline"));
11
+ const logger_1 = require("../utils/logger");
12
+ // Console helpers
13
+ const log = (msg) => console.log(msg);
14
+ const highlight = (msg) => console.log(`\x1b[36m${msg}\x1b[0m`);
15
+ const { info, success, error } = logger_1.logger;
16
+ const warning = logger_1.logger.warn;
17
+ function askQuestion(query) {
18
+ const rl = readline_1.default.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+ return new Promise((resolve) => {
23
+ rl.question(query, (answer) => {
24
+ rl.close();
25
+ resolve(answer.trim().toLowerCase());
26
+ });
27
+ });
28
+ }
29
+ async function planCommand(filePath, options) {
30
+ // Resolve file path
31
+ const resolvedPath = path_1.default.resolve(process.cwd(), filePath);
32
+ // Check if file exists
33
+ if (!fs_1.default.existsSync(resolvedPath)) {
34
+ error(`Plan file not found: ${resolvedPath}`);
35
+ process.exit(1);
36
+ }
37
+ // Read and parse plan
38
+ let plan;
39
+ try {
40
+ const content = fs_1.default.readFileSync(resolvedPath, 'utf-8');
41
+ plan = JSON.parse(content);
42
+ }
43
+ catch (err) {
44
+ error(`Failed to parse plan file: ${err}`);
45
+ process.exit(1);
46
+ }
47
+ // Validate plan structure
48
+ if (!plan.steps || !Array.isArray(plan.steps) || plan.steps.length === 0) {
49
+ error('Invalid plan file: no steps found');
50
+ process.exit(1);
51
+ }
52
+ log('');
53
+ highlight('='.repeat(50));
54
+ highlight(' LINCHPIN UPGRADE PLAN');
55
+ highlight('='.repeat(50));
56
+ log('');
57
+ if (plan.created) {
58
+ info(`Plan created: ${new Date(plan.created).toLocaleString()}`);
59
+ }
60
+ info(`Total steps: ${plan.steps.length}`);
61
+ log('');
62
+ // Show warnings
63
+ if (plan.warnings && plan.warnings.length > 0) {
64
+ warning('Warnings:');
65
+ plan.warnings.forEach((w) => {
66
+ log(` - ${w}`);
67
+ });
68
+ log('');
69
+ }
70
+ // Show steps
71
+ log('Steps:');
72
+ plan.steps.forEach((step) => {
73
+ log(` ${step.order}. ${step.group}`);
74
+ log(` Packages: ${step.packages.join(', ')}`);
75
+ log(` Note: ${step.note}`);
76
+ log(` Command: ${step.command}`);
77
+ log('');
78
+ });
79
+ if (options.dryRun) {
80
+ info('Dry run mode - no changes will be made');
81
+ return;
82
+ }
83
+ // Confirm unless --yes flag
84
+ if (!options.yes) {
85
+ const answer = await askQuestion('Execute this plan? (y/n): ');
86
+ if (answer !== 'y' && answer !== 'yes') {
87
+ info('Plan execution cancelled');
88
+ return;
89
+ }
90
+ }
91
+ log('');
92
+ highlight('Executing plan...');
93
+ log('');
94
+ // Create backup of package.json and package-lock.json
95
+ const backupDir = path_1.default.join(process.cwd(), '.linchpin-backup');
96
+ if (!fs_1.default.existsSync(backupDir)) {
97
+ fs_1.default.mkdirSync(backupDir);
98
+ }
99
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
100
+ const pkgJsonPath = path_1.default.join(process.cwd(), 'package.json');
101
+ const lockPath = path_1.default.join(process.cwd(), 'package-lock.json');
102
+ if (fs_1.default.existsSync(pkgJsonPath)) {
103
+ fs_1.default.copyFileSync(pkgJsonPath, path_1.default.join(backupDir, `package.json.${timestamp}`));
104
+ }
105
+ if (fs_1.default.existsSync(lockPath)) {
106
+ fs_1.default.copyFileSync(lockPath, path_1.default.join(backupDir, `package-lock.json.${timestamp}`));
107
+ }
108
+ success('Backup created in .linchpin-backup/');
109
+ log('');
110
+ // Execute each step
111
+ let completed = 0;
112
+ let failed = 0;
113
+ for (const step of plan.steps) {
114
+ log(`Step ${step.order}: ${step.group}`);
115
+ info(`Running: ${step.command}`);
116
+ try {
117
+ (0, child_process_1.execSync)(step.command, {
118
+ cwd: process.cwd(),
119
+ stdio: 'inherit',
120
+ });
121
+ success(`Step ${step.order} completed`);
122
+ completed++;
123
+ }
124
+ catch (err) {
125
+ error(`Step ${step.order} failed: ${err}`);
126
+ failed++;
127
+ // Ask if user wants to continue
128
+ if (!options.yes) {
129
+ const answer = await askQuestion('Continue with remaining steps? (y/n): ');
130
+ if (answer !== 'y' && answer !== 'yes') {
131
+ info('Plan execution stopped');
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ log('');
137
+ }
138
+ // Summary
139
+ log('');
140
+ highlight('='.repeat(50));
141
+ highlight(' PLAN EXECUTION COMPLETE');
142
+ highlight('='.repeat(50));
143
+ log('');
144
+ success(`Completed: ${completed}/${plan.steps.length} steps`);
145
+ if (failed > 0) {
146
+ warning(`Failed: ${failed} step(s)`);
147
+ }
148
+ log('');
149
+ info('Backup files are in .linchpin-backup/');
150
+ info('Run "npm test" to verify your app still works!');
151
+ log('');
152
+ }
@@ -0,0 +1,116 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.initSentry = initSentry;
37
+ exports.captureException = captureException;
38
+ exports.captureMessage = captureMessage;
39
+ exports.setUser = setUser;
40
+ exports.clearUser = clearUser;
41
+ exports.flush = flush;
42
+ const Sentry = __importStar(require("@sentry/node"));
43
+ const SENTRY_DSN = process.env.SENTRY_DSN || 'https://9cf669968b5daf7a525d9e8a4dd66ee7@o4510643901956096.ingest.us.sentry.io/4510687506333696';
44
+ // Version is passed in to avoid path issues after compilation
45
+ let isInitialized = false;
46
+ let cliVersion = '0.0.0';
47
+ function initSentry(version) {
48
+ // Skip if already initialized
49
+ if (isInitialized) {
50
+ return;
51
+ }
52
+ if (version) {
53
+ cliVersion = version;
54
+ }
55
+ Sentry.init({
56
+ dsn: SENTRY_DSN,
57
+ environment: process.env.NODE_ENV || 'production',
58
+ release: `linchpin-cli@${cliVersion}`,
59
+ // Only send errors, not transactions (keeps it lightweight)
60
+ tracesSampleRate: 0,
61
+ // Filter out sensitive data
62
+ beforeSend(event) {
63
+ // Remove any file paths that might contain user info
64
+ if (event.exception?.values) {
65
+ event.exception.values.forEach((exception) => {
66
+ if (exception.stacktrace?.frames) {
67
+ exception.stacktrace.frames.forEach((frame) => {
68
+ // Normalize paths to hide user directories
69
+ if (frame.filename) {
70
+ frame.filename = frame.filename.replace(/^.*[\\\/]node_modules/, 'node_modules');
71
+ }
72
+ });
73
+ }
74
+ });
75
+ }
76
+ return event;
77
+ },
78
+ });
79
+ isInitialized = true;
80
+ }
81
+ function captureException(error, context) {
82
+ if (!isInitialized) {
83
+ return;
84
+ }
85
+ Sentry.withScope((scope) => {
86
+ if (context) {
87
+ scope.setExtras(context);
88
+ }
89
+ Sentry.captureException(error);
90
+ });
91
+ }
92
+ function captureMessage(message, level = 'info') {
93
+ if (!isInitialized) {
94
+ return;
95
+ }
96
+ Sentry.captureMessage(message, level);
97
+ }
98
+ function setUser(user) {
99
+ if (!isInitialized) {
100
+ return;
101
+ }
102
+ Sentry.setUser(user);
103
+ }
104
+ function clearUser() {
105
+ if (!isInitialized) {
106
+ return;
107
+ }
108
+ Sentry.setUser(null);
109
+ }
110
+ // Flush events before process exits
111
+ async function flush(timeout = 2000) {
112
+ if (!isInitialized) {
113
+ return true;
114
+ }
115
+ return Sentry.flush(timeout);
116
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logger = void 0;
4
+ const sentry_1 = require("../lib/sentry");
4
5
  // ANSI color codes for terminal output
5
6
  const colors = {
6
7
  reset: '\x1b[0m',
@@ -21,9 +22,16 @@ exports.logger = {
21
22
  },
22
23
  warn: (message) => {
23
24
  console.log(`${colors.yellow}[WARN]${colors.reset} ${message}`);
25
+ (0, sentry_1.captureMessage)(message, 'warning');
24
26
  },
25
- error: (message) => {
27
+ error: (message, error) => {
26
28
  console.log(`${colors.red}[ERROR]${colors.reset} ${message}`);
29
+ if (error) {
30
+ (0, sentry_1.captureException)(error, { message });
31
+ }
32
+ else {
33
+ (0, sentry_1.captureMessage)(message, 'error');
34
+ }
27
35
  },
28
36
  debug: (message) => {
29
37
  if (process.env.DEBUG) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linchpin-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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": {
@@ -39,6 +39,7 @@
39
39
  "node": ">=16.0.0"
40
40
  },
41
41
  "dependencies": {
42
+ "@sentry/node": "^10.32.1",
42
43
  "chalk": "^4.1.2",
43
44
  "cli-table3": "^0.6.5",
44
45
  "commander": "^11.1.0",