linchpin-cli 0.2.1 → 0.2.3
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 +37 -1
- package/dist/src/__tests__/align.integration.test.js +209 -0
- package/dist/src/__tests__/check.integration.test.js +155 -0
- package/dist/src/__tests__/files.test.js +64 -0
- package/dist/src/commands/check.js +30 -1
- package/dist/src/commands/login.js +6 -0
- package/dist/src/commands/plan.js +152 -0
- package/dist/src/lib/ai.js +2 -1
- package/dist/src/lib/files.js +26 -0
- package/dist/src/lib/sentry.js +116 -0
- package/dist/src/utils/logger.js +9 -1
- package/package.json +12 -3
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(
|
|
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,155 @@
|
|
|
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
|
+
detectProjectContext: jest.fn().mockReturnValue({
|
|
8
|
+
isExpo: false,
|
|
9
|
+
expoVersion: undefined,
|
|
10
|
+
isMonorepo: false,
|
|
11
|
+
monorepoTool: undefined,
|
|
12
|
+
enginesNode: undefined,
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
jest.mock('../lib/npm', () => ({
|
|
16
|
+
getRegistryVersion: jest.fn(),
|
|
17
|
+
hasApiKey: jest.fn(),
|
|
18
|
+
isUsingCloudApi: jest.fn(),
|
|
19
|
+
apiRequest: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock('../lib/ai', () => ({
|
|
22
|
+
getLatestVersion: jest.fn(),
|
|
23
|
+
getBatchRiskAnalysis: 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 mockGetPackageJson = files_1.getPackageJson;
|
|
30
|
+
const mockGetRegistryVersion = npm_1.getRegistryVersion;
|
|
31
|
+
const mockHasApiKey = npm_1.hasApiKey;
|
|
32
|
+
const mockIsUsingCloudApi = npm_1.isUsingCloudApi;
|
|
33
|
+
describe('checkCommand integration', () => {
|
|
34
|
+
let consoleSpy;
|
|
35
|
+
let stdoutSpy;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
39
|
+
stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation();
|
|
40
|
+
// Default mocks
|
|
41
|
+
mockHasApiKey.mockReturnValue(false);
|
|
42
|
+
mockIsUsingCloudApi.mockReturnValue(false);
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
consoleSpy.mockRestore();
|
|
46
|
+
stdoutSpy.mockRestore();
|
|
47
|
+
});
|
|
48
|
+
describe('when package.json is missing', () => {
|
|
49
|
+
it('shows error message', async () => {
|
|
50
|
+
mockGetPackageJson.mockResolvedValue(null);
|
|
51
|
+
await (0, check_1.checkCommand)({});
|
|
52
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No package.json found'));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('when package.json has no dependencies', () => {
|
|
56
|
+
it('shows warning message', async () => {
|
|
57
|
+
mockGetPackageJson.mockResolvedValue({
|
|
58
|
+
name: 'test-project',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
dependencies: {},
|
|
61
|
+
devDependencies: {},
|
|
62
|
+
});
|
|
63
|
+
await (0, check_1.checkCommand)({});
|
|
64
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('no dependencies listed'));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('when scanning dependencies (free mode)', () => {
|
|
68
|
+
it('checks all dependencies and displays results', async () => {
|
|
69
|
+
mockGetPackageJson.mockResolvedValue({
|
|
70
|
+
name: 'test-project',
|
|
71
|
+
version: '1.0.0',
|
|
72
|
+
dependencies: {
|
|
73
|
+
'express': '^4.18.0',
|
|
74
|
+
'lodash': '^4.17.21',
|
|
75
|
+
},
|
|
76
|
+
devDependencies: {
|
|
77
|
+
'jest': '^29.0.0',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
mockGetRegistryVersion
|
|
81
|
+
.mockReturnValueOnce('4.21.0') // express - MINOR
|
|
82
|
+
.mockReturnValueOnce('4.17.21') // lodash - OK
|
|
83
|
+
.mockReturnValueOnce('30.0.0'); // jest - MAJOR
|
|
84
|
+
await (0, check_1.checkCommand)({});
|
|
85
|
+
// Should check each package
|
|
86
|
+
expect(mockGetRegistryVersion).toHaveBeenCalledTimes(3);
|
|
87
|
+
expect(mockGetRegistryVersion).toHaveBeenCalledWith('express');
|
|
88
|
+
expect(mockGetRegistryVersion).toHaveBeenCalledWith('lodash');
|
|
89
|
+
expect(mockGetRegistryVersion).toHaveBeenCalledWith('jest');
|
|
90
|
+
// Should display scanning message
|
|
91
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Checking 3 packages'));
|
|
92
|
+
// Should show free mode hint
|
|
93
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Free mode'));
|
|
94
|
+
});
|
|
95
|
+
it('counts upgrade types correctly', async () => {
|
|
96
|
+
mockGetPackageJson.mockResolvedValue({
|
|
97
|
+
name: 'test-project',
|
|
98
|
+
version: '1.0.0',
|
|
99
|
+
dependencies: {
|
|
100
|
+
'pkg-major': '1.0.0',
|
|
101
|
+
'pkg-minor': '1.0.0',
|
|
102
|
+
'pkg-patch': '1.0.0',
|
|
103
|
+
'pkg-ok': '1.0.0',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
mockGetRegistryVersion
|
|
107
|
+
.mockReturnValueOnce('2.0.0') // MAJOR
|
|
108
|
+
.mockReturnValueOnce('1.1.0') // MINOR
|
|
109
|
+
.mockReturnValueOnce('1.0.1') // PATCH
|
|
110
|
+
.mockReturnValueOnce('1.0.0'); // OK
|
|
111
|
+
await (0, check_1.checkCommand)({});
|
|
112
|
+
// Should display summary with correct counts
|
|
113
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 major'));
|
|
114
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 minor'));
|
|
115
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1 patch'));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('deep scan mode', () => {
|
|
119
|
+
it('requires API key for deep scan', async () => {
|
|
120
|
+
mockHasApiKey.mockReturnValue(false);
|
|
121
|
+
await (0, check_1.checkCommand)({ deep: true });
|
|
122
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Deep scan requires an API key'));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('error handling', () => {
|
|
126
|
+
it('handles npm registry errors gracefully', async () => {
|
|
127
|
+
mockGetPackageJson.mockResolvedValue({
|
|
128
|
+
name: 'test-project',
|
|
129
|
+
version: '1.0.0',
|
|
130
|
+
dependencies: {
|
|
131
|
+
'nonexistent-package': '1.0.0',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
mockGetRegistryVersion.mockReturnValue('Unknown');
|
|
135
|
+
await (0, check_1.checkCommand)({});
|
|
136
|
+
// Should not throw, should display result
|
|
137
|
+
expect(mockGetRegistryVersion).toHaveBeenCalledWith('nonexistent-package');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('getUpgradeType edge cases', () => {
|
|
142
|
+
it('handles zero versions', () => {
|
|
143
|
+
expect((0, check_1.getUpgradeType)('0.1.0', '1.0.0')).toBe('MAJOR');
|
|
144
|
+
expect((0, check_1.getUpgradeType)('0.0.1', '0.1.0')).toBe('MINOR');
|
|
145
|
+
expect((0, check_1.getUpgradeType)('0.0.1', '0.0.2')).toBe('PATCH');
|
|
146
|
+
});
|
|
147
|
+
it('handles prerelease versions', () => {
|
|
148
|
+
// Prerelease tags are stripped, major/minor detection still works
|
|
149
|
+
expect((0, check_1.getUpgradeType)('1.0.0-alpha', '2.0.0')).toBe('MAJOR');
|
|
150
|
+
expect((0, check_1.getUpgradeType)('1.0.0-beta', '1.1.0')).toBe('MINOR');
|
|
151
|
+
});
|
|
152
|
+
it('handles versions with v prefix', () => {
|
|
153
|
+
expect((0, check_1.getUpgradeType)('v1.0.0', '2.0.0')).toBe('MAJOR');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -29,3 +29,67 @@ describe('getPackageJson', () => {
|
|
|
29
29
|
expect(typeof pkg?.devDependencies).toBe('object');
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
+
describe('detectProjectContext', () => {
|
|
33
|
+
it('detects Expo projects', () => {
|
|
34
|
+
const pkg = {
|
|
35
|
+
name: 'my-expo-app',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
dependencies: {
|
|
38
|
+
'expo': '^52.0.0',
|
|
39
|
+
'react': '^18.2.0',
|
|
40
|
+
'react-native': '0.76.0',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const context = (0, files_1.detectProjectContext)(pkg);
|
|
44
|
+
expect(context.isExpo).toBe(true);
|
|
45
|
+
expect(context.expoVersion).toBe('52.0.0');
|
|
46
|
+
});
|
|
47
|
+
it('detects non-Expo projects', () => {
|
|
48
|
+
const pkg = {
|
|
49
|
+
name: 'my-web-app',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
dependencies: {
|
|
52
|
+
'react': '^18.2.0',
|
|
53
|
+
'next': '^14.0.0',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const context = (0, files_1.detectProjectContext)(pkg);
|
|
57
|
+
expect(context.isExpo).toBe(false);
|
|
58
|
+
expect(context.expoVersion).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
it('detects Turborepo monorepo', () => {
|
|
61
|
+
const pkg = {
|
|
62
|
+
name: 'my-monorepo',
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
devDependencies: {
|
|
65
|
+
'turbo': '^2.0.0',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const context = (0, files_1.detectProjectContext)(pkg);
|
|
69
|
+
expect(context.isMonorepo).toBe(true);
|
|
70
|
+
expect(context.monorepoTool).toBe('turborepo');
|
|
71
|
+
});
|
|
72
|
+
it('detects Nx monorepo', () => {
|
|
73
|
+
const pkg = {
|
|
74
|
+
name: 'my-nx-workspace',
|
|
75
|
+
version: '1.0.0',
|
|
76
|
+
devDependencies: {
|
|
77
|
+
'nx': '^18.0.0',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const context = (0, files_1.detectProjectContext)(pkg);
|
|
81
|
+
expect(context.isMonorepo).toBe(true);
|
|
82
|
+
expect(context.monorepoTool).toBe('nx');
|
|
83
|
+
});
|
|
84
|
+
it('detects Node.js engine requirement', () => {
|
|
85
|
+
const pkg = {
|
|
86
|
+
name: 'my-app',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
engines: {
|
|
89
|
+
node: '>=20.0.0',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const context = (0, files_1.detectProjectContext)(pkg);
|
|
93
|
+
expect(context.enginesNode).toBe('>=20.0.0');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -59,6 +59,17 @@ async function checkCommand(options = {}) {
|
|
|
59
59
|
console.log(chalk_1.default.yellow('⚠️ Found package.json, but no dependencies listed.'));
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
+
// Detect project context
|
|
63
|
+
const projectContext = (0, files_1.detectProjectContext)(pkg);
|
|
64
|
+
// Show Expo detection
|
|
65
|
+
if (projectContext.isExpo) {
|
|
66
|
+
console.log(chalk_1.default.magenta.bold(`📱 Expo project detected`) + chalk_1.default.magenta(` (SDK ${projectContext.expoVersion || 'unknown'})`));
|
|
67
|
+
console.log(chalk_1.default.gray(' Expo-specific compatibility rules will be applied.\n'));
|
|
68
|
+
}
|
|
69
|
+
// Show monorepo detection
|
|
70
|
+
if (projectContext.isMonorepo) {
|
|
71
|
+
console.log(chalk_1.default.cyan(`📦 Monorepo detected (${projectContext.monorepoTool || 'workspaces'})\n`));
|
|
72
|
+
}
|
|
62
73
|
console.log(chalk_1.default.yellow(`⚡ Checking ${depEntries.length} packages via ${source}...\n`));
|
|
63
74
|
// Collect version info
|
|
64
75
|
const packageData = [];
|
|
@@ -81,7 +92,7 @@ async function checkCommand(options = {}) {
|
|
|
81
92
|
if (isDeep && useAI) {
|
|
82
93
|
const modeLabel = isTechnical ? 'technical' : 'plain English';
|
|
83
94
|
console.log(chalk_1.default.yellow(`\n🧠 Analyzing upgrade risks (${modeLabel} mode)...\n`));
|
|
84
|
-
const risks = await (0, ai_1.getBatchRiskAnalysis)(packageData, isTechnical);
|
|
95
|
+
const risks = await (0, ai_1.getBatchRiskAnalysis)(packageData, isTechnical, projectContext);
|
|
85
96
|
risks.forEach(r => riskMap.set(r.name, r));
|
|
86
97
|
}
|
|
87
98
|
// Build table
|
|
@@ -137,6 +148,16 @@ async function checkCommand(options = {}) {
|
|
|
137
148
|
chalk_1.default.yellow(`${minorCount} minor`) + ` · ` +
|
|
138
149
|
chalk_1.default.green(`${patchCount} patch`));
|
|
139
150
|
}
|
|
151
|
+
// Ambient marketing - show when value delivered (critical issues found), ~30% of runs
|
|
152
|
+
if (majorCount > 0 && Math.random() < 0.3) {
|
|
153
|
+
const messages = [
|
|
154
|
+
`🛡️ Caught ${majorCount} breaking change${majorCount > 1 ? 's' : ''} before production. Share: linchpin.dev`,
|
|
155
|
+
`💡 Found this useful? Tell a friend: npx linchpin-cli`,
|
|
156
|
+
`🚀 Avoiding broken deploys? Share Linchpin: linchpin.dev`,
|
|
157
|
+
];
|
|
158
|
+
const msg = messages[Math.floor(Math.random() * messages.length)];
|
|
159
|
+
console.log(chalk_1.default.cyan(`\n ${msg}`));
|
|
160
|
+
}
|
|
140
161
|
// Mode indicator
|
|
141
162
|
if (!useAI) {
|
|
142
163
|
console.log(chalk_1.default.gray('\n ℹ️ Free mode (npm registry). Run `linchpin login` for AI features.'));
|
|
@@ -173,6 +194,14 @@ async function checkCommand(options = {}) {
|
|
|
173
194
|
warningCount: minorCount,
|
|
174
195
|
scanResults,
|
|
175
196
|
deepAnalysis: isDeep,
|
|
197
|
+
// Include project context for Expo-aware analysis
|
|
198
|
+
projectContext: {
|
|
199
|
+
isExpo: projectContext.isExpo,
|
|
200
|
+
expoVersion: projectContext.expoVersion,
|
|
201
|
+
isMonorepo: projectContext.isMonorepo,
|
|
202
|
+
monorepoTool: projectContext.monorepoTool,
|
|
203
|
+
enginesNode: projectContext.enginesNode,
|
|
204
|
+
},
|
|
176
205
|
}).then(result => {
|
|
177
206
|
if (result.error && process.env.DEBUG) {
|
|
178
207
|
console.error('Failed to sync scan:', result.error);
|
|
@@ -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
|
+
}
|
package/dist/src/lib/ai.js
CHANGED
|
@@ -99,7 +99,7 @@ Will this break my app? Is it worth the headache? Give me the honest truth in pl
|
|
|
99
99
|
return 'Could not fetch risks.';
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
async function getBatchRiskAnalysis(packages, technical = false) {
|
|
102
|
+
async function getBatchRiskAnalysis(packages, technical = false, projectContext) {
|
|
103
103
|
const outdatedPkgs = packages.filter(p => {
|
|
104
104
|
const cleanCurrent = p.current.replace(/^[\^~]/, '');
|
|
105
105
|
return cleanCurrent !== p.latest && p.latest !== 'Error' && p.latest !== 'Unknown';
|
|
@@ -113,6 +113,7 @@ async function getBatchRiskAnalysis(packages, technical = false) {
|
|
|
113
113
|
action: 'batch_risk',
|
|
114
114
|
packages: outdatedPkgs,
|
|
115
115
|
technical,
|
|
116
|
+
projectContext, // Include Expo/monorepo context
|
|
116
117
|
});
|
|
117
118
|
if (result.error) {
|
|
118
119
|
console.error('Cloud API error:', result.error);
|
package/dist/src/lib/files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getPackageJson = getPackageJson;
|
|
4
|
+
exports.detectProjectContext = detectProjectContext;
|
|
4
5
|
const promises_1 = require("fs/promises");
|
|
5
6
|
const path_1 = require("path");
|
|
6
7
|
async function getPackageJson(dir = process.cwd()) {
|
|
@@ -14,3 +15,28 @@ async function getPackageJson(dir = process.cwd()) {
|
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Detect project context (Expo, monorepo, Node version)
|
|
20
|
+
*/
|
|
21
|
+
function detectProjectContext(pkg) {
|
|
22
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
23
|
+
// Detect Expo
|
|
24
|
+
const expoVersion = allDeps['expo'];
|
|
25
|
+
const isExpo = !!expoVersion;
|
|
26
|
+
// Detect monorepo (basic detection)
|
|
27
|
+
const hasWorkspaces = !!pkg.workspaces;
|
|
28
|
+
const hasTurbo = !!allDeps['turbo'];
|
|
29
|
+
const hasNx = !!allDeps['nx'] || !!allDeps['@nx/workspace'];
|
|
30
|
+
const hasLerna = !!allDeps['lerna'];
|
|
31
|
+
const isMonorepo = hasWorkspaces || hasTurbo || hasNx || hasLerna;
|
|
32
|
+
const monorepoTool = hasTurbo ? 'turborepo' : hasNx ? 'nx' : hasLerna ? 'lerna' : hasWorkspaces ? 'workspaces' : undefined;
|
|
33
|
+
// Get Node.js engine requirement
|
|
34
|
+
const enginesNode = pkg.engines?.node;
|
|
35
|
+
return {
|
|
36
|
+
isExpo,
|
|
37
|
+
expoVersion: expoVersion?.replace(/^[\^~]/, ''),
|
|
38
|
+
isMonorepo,
|
|
39
|
+
monorepoTool,
|
|
40
|
+
enginesNode,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/src/utils/logger.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
@@ -49,12 +50,20 @@
|
|
|
49
50
|
"resend": "^6.7.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
53
|
+
"@babel/core": "^7.28.5",
|
|
52
54
|
"@types/inquirer": "^9.0.9",
|
|
53
55
|
"@types/jest": "^30.0.0",
|
|
54
|
-
"@types/node": "^
|
|
56
|
+
"@types/node": "^25.0.6",
|
|
57
|
+
"@types/react": "^19.2.8",
|
|
58
|
+
"@types/react-dom": "^19.2.3",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
60
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
61
|
+
"eslint": "^9.39.2",
|
|
62
|
+
"eslint-config-expo": "^10.0.0",
|
|
63
|
+
"eslint-config-prettier": "^10.1.8",
|
|
55
64
|
"jest": "^30.2.0",
|
|
56
65
|
"ts-jest": "^29.4.6",
|
|
57
66
|
"ts-node": "^10.9.1",
|
|
58
|
-
"typescript": "^5.3
|
|
67
|
+
"typescript": "^5.9.3"
|
|
59
68
|
}
|
|
60
69
|
}
|