linchpin-cli 0.2.2 → 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.
@@ -4,6 +4,13 @@ const check_1 = require("../commands/check");
4
4
  // Mock dependencies
5
5
  jest.mock('../lib/files', () => ({
6
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
+ }),
7
14
  }));
8
15
  jest.mock('../lib/npm', () => ({
9
16
  getRegistryVersion: jest.fn(),
@@ -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);
@@ -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);
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linchpin-cli",
3
- "version": "0.2.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": {
@@ -50,12 +50,20 @@
50
50
  "resend": "^6.7.0"
51
51
  },
52
52
  "devDependencies": {
53
+ "@babel/core": "^7.28.5",
53
54
  "@types/inquirer": "^9.0.9",
54
55
  "@types/jest": "^30.0.0",
55
- "@types/node": "^20.10.0",
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",
56
64
  "jest": "^30.2.0",
57
65
  "ts-jest": "^29.4.6",
58
66
  "ts-node": "^10.9.1",
59
- "typescript": "^5.3.2"
67
+ "typescript": "^5.9.3"
60
68
  }
61
69
  }