@l4yercak3/cli 1.0.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.
Files changed (61) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.cursor/rules.md +203 -0
  3. package/.eslintrc.js +31 -0
  4. package/README.md +227 -0
  5. package/bin/cli.js +61 -0
  6. package/docs/ADDING_NEW_PROJECT_TYPE.md +156 -0
  7. package/docs/ARCHITECTURE_RELATIONSHIPS.md +411 -0
  8. package/docs/CLI_AUTHENTICATION.md +214 -0
  9. package/docs/DETECTOR_ARCHITECTURE.md +326 -0
  10. package/docs/DEVELOPMENT.md +194 -0
  11. package/docs/IMPLEMENTATION_PHASES.md +468 -0
  12. package/docs/OAUTH_CLARIFICATION.md +258 -0
  13. package/docs/OAUTH_SETUP_GUIDE_TEMPLATE.md +211 -0
  14. package/docs/PHASE_0_PROGRESS.md +120 -0
  15. package/docs/PHASE_1_COMPLETE.md +366 -0
  16. package/docs/PHASE_SUMMARY.md +149 -0
  17. package/docs/PLAN.md +511 -0
  18. package/docs/README.md +56 -0
  19. package/docs/STRIPE_INTEGRATION.md +447 -0
  20. package/docs/SUMMARY.md +230 -0
  21. package/docs/UPDATED_PLAN.md +447 -0
  22. package/package.json +53 -0
  23. package/src/api/backend-client.js +148 -0
  24. package/src/commands/login.js +146 -0
  25. package/src/commands/logout.js +24 -0
  26. package/src/commands/spread.js +364 -0
  27. package/src/commands/status.js +62 -0
  28. package/src/config/config-manager.js +205 -0
  29. package/src/detectors/api-client-detector.js +85 -0
  30. package/src/detectors/base-detector.js +77 -0
  31. package/src/detectors/github-detector.js +74 -0
  32. package/src/detectors/index.js +80 -0
  33. package/src/detectors/nextjs-detector.js +139 -0
  34. package/src/detectors/oauth-detector.js +122 -0
  35. package/src/detectors/registry.js +97 -0
  36. package/src/generators/api-client-generator.js +197 -0
  37. package/src/generators/env-generator.js +162 -0
  38. package/src/generators/gitignore-generator.js +92 -0
  39. package/src/generators/index.js +50 -0
  40. package/src/generators/nextauth-generator.js +242 -0
  41. package/src/generators/oauth-guide-generator.js +277 -0
  42. package/src/logo.js +116 -0
  43. package/tests/api-client-detector.test.js +214 -0
  44. package/tests/api-client-generator.test.js +169 -0
  45. package/tests/backend-client.test.js +361 -0
  46. package/tests/base-detector.test.js +101 -0
  47. package/tests/commands/login.test.js +98 -0
  48. package/tests/commands/logout.test.js +70 -0
  49. package/tests/commands/status.test.js +167 -0
  50. package/tests/config-manager.test.js +313 -0
  51. package/tests/detector-index.test.js +209 -0
  52. package/tests/detector-registry.test.js +93 -0
  53. package/tests/env-generator.test.js +278 -0
  54. package/tests/generators-index.test.js +215 -0
  55. package/tests/github-detector.test.js +145 -0
  56. package/tests/gitignore-generator.test.js +109 -0
  57. package/tests/logo.test.js +96 -0
  58. package/tests/nextauth-generator.test.js +231 -0
  59. package/tests/nextjs-detector.test.js +235 -0
  60. package/tests/oauth-detector.test.js +264 -0
  61. package/tests/oauth-guide-generator.test.js +273 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Tests for API Client Detector
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ jest.mock('fs');
9
+
10
+ const ApiClientDetector = require('../src/detectors/api-client-detector');
11
+
12
+ describe('ApiClientDetector', () => {
13
+ const mockProjectPath = '/test/project';
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ fs.existsSync.mockReturnValue(false);
18
+ });
19
+
20
+ describe('detect', () => {
21
+ it('returns hasApiClient false when no API client exists', () => {
22
+ fs.existsSync.mockReturnValue(false);
23
+
24
+ const result = ApiClientDetector.detect(mockProjectPath);
25
+
26
+ expect(result.hasApiClient).toBe(false);
27
+ expect(result.clientPath).toBeNull();
28
+ expect(result.clientType).toBeNull();
29
+ });
30
+
31
+ describe('API client path detection', () => {
32
+ const testPaths = [
33
+ 'lib/api-client.ts',
34
+ 'lib/api-client.js',
35
+ 'lib/api.ts',
36
+ 'lib/api.js',
37
+ 'src/lib/api-client.ts',
38
+ 'src/lib/api-client.js',
39
+ 'utils/api-client.ts',
40
+ 'utils/api.js',
41
+ 'src/utils/api-client.ts',
42
+ ];
43
+
44
+ testPaths.forEach((clientPath) => {
45
+ it(`detects API client at ${clientPath}`, () => {
46
+ fs.existsSync.mockImplementation((p) =>
47
+ p === path.join(mockProjectPath, clientPath)
48
+ );
49
+ fs.readFileSync.mockReturnValue('fetch()');
50
+
51
+ const result = ApiClientDetector.detect(mockProjectPath);
52
+
53
+ expect(result.hasApiClient).toBe(true);
54
+ expect(result.clientPath).toBe(clientPath);
55
+ });
56
+ });
57
+ });
58
+
59
+ describe('client type detection', () => {
60
+ it('detects axios client', () => {
61
+ fs.existsSync.mockImplementation((p) =>
62
+ p === path.join(mockProjectPath, 'lib/api-client.ts')
63
+ );
64
+ fs.readFileSync.mockReturnValue(`
65
+ import axios from 'axios';
66
+ export const api = axios.create({});
67
+ `);
68
+
69
+ const result = ApiClientDetector.detect(mockProjectPath);
70
+
71
+ expect(result.clientType).toBe('axios');
72
+ });
73
+
74
+ it('detects fetch client', () => {
75
+ fs.existsSync.mockImplementation((p) =>
76
+ p === path.join(mockProjectPath, 'lib/api-client.ts')
77
+ );
78
+ fs.readFileSync.mockReturnValue(`
79
+ export async function request(url) {
80
+ return fetch(url);
81
+ }
82
+ `);
83
+
84
+ const result = ApiClientDetector.detect(mockProjectPath);
85
+
86
+ expect(result.clientType).toBe('fetch');
87
+ });
88
+
89
+ it('detects custom client (no axios or fetch)', () => {
90
+ fs.existsSync.mockImplementation((p) =>
91
+ p === path.join(mockProjectPath, 'lib/api-client.ts')
92
+ );
93
+ fs.readFileSync.mockReturnValue(`
94
+ import { request } from 'custom-http';
95
+ export const api = request;
96
+ `);
97
+
98
+ const result = ApiClientDetector.detect(mockProjectPath);
99
+
100
+ expect(result.clientType).toBe('custom');
101
+ });
102
+
103
+ it('prefers axios over fetch when both present', () => {
104
+ fs.existsSync.mockImplementation((p) =>
105
+ p === path.join(mockProjectPath, 'lib/api-client.ts')
106
+ );
107
+ fs.readFileSync.mockReturnValue(`
108
+ import axios from 'axios';
109
+ // also uses fetch as fallback
110
+ const fallback = fetch;
111
+ `);
112
+
113
+ const result = ApiClientDetector.detect(mockProjectPath);
114
+
115
+ expect(result.clientType).toBe('axios');
116
+ });
117
+
118
+ it('returns unknown on read error', () => {
119
+ fs.existsSync.mockImplementation((p) =>
120
+ p === path.join(mockProjectPath, 'lib/api-client.ts')
121
+ );
122
+ fs.readFileSync.mockImplementation(() => {
123
+ throw new Error('Read failed');
124
+ });
125
+
126
+ const result = ApiClientDetector.detect(mockProjectPath);
127
+
128
+ expect(result.hasApiClient).toBe(true);
129
+ expect(result.clientType).toBe('unknown');
130
+ });
131
+ });
132
+
133
+ describe('environment file detection', () => {
134
+ it('detects .env.local', () => {
135
+ fs.existsSync.mockImplementation((p) =>
136
+ p === path.join(mockProjectPath, '.env.local')
137
+ );
138
+
139
+ const result = ApiClientDetector.detect(mockProjectPath);
140
+
141
+ expect(result.hasEnvFile).toBe(true);
142
+ expect(result.envFilePath).toBe('.env.local');
143
+ });
144
+
145
+ it('detects .env', () => {
146
+ fs.existsSync.mockImplementation((p) =>
147
+ p === path.join(mockProjectPath, '.env')
148
+ );
149
+
150
+ const result = ApiClientDetector.detect(mockProjectPath);
151
+
152
+ expect(result.hasEnvFile).toBe(true);
153
+ expect(result.envFilePath).toBe('.env');
154
+ });
155
+
156
+ it('detects .env.development', () => {
157
+ fs.existsSync.mockImplementation((p) =>
158
+ p === path.join(mockProjectPath, '.env.development')
159
+ );
160
+
161
+ const result = ApiClientDetector.detect(mockProjectPath);
162
+
163
+ expect(result.hasEnvFile).toBe(true);
164
+ expect(result.envFilePath).toBe('.env.development');
165
+ });
166
+
167
+ it('detects .env.production', () => {
168
+ fs.existsSync.mockImplementation((p) =>
169
+ p === path.join(mockProjectPath, '.env.production')
170
+ );
171
+
172
+ const result = ApiClientDetector.detect(mockProjectPath);
173
+
174
+ expect(result.hasEnvFile).toBe(true);
175
+ expect(result.envFilePath).toBe('.env.production');
176
+ });
177
+
178
+ it('prefers .env.local over other env files', () => {
179
+ fs.existsSync.mockImplementation((p) =>
180
+ p === path.join(mockProjectPath, '.env.local') ||
181
+ p === path.join(mockProjectPath, '.env')
182
+ );
183
+
184
+ const result = ApiClientDetector.detect(mockProjectPath);
185
+
186
+ expect(result.envFilePath).toBe('.env.local');
187
+ });
188
+
189
+ it('returns hasEnvFile false when no env file exists', () => {
190
+ fs.existsSync.mockReturnValue(false);
191
+
192
+ const result = ApiClientDetector.detect(mockProjectPath);
193
+
194
+ expect(result.hasEnvFile).toBe(false);
195
+ expect(result.envFilePath).toBeNull();
196
+ });
197
+ });
198
+
199
+ it('stops at first found API client', () => {
200
+ let callCount = 0;
201
+ fs.existsSync.mockImplementation((p) => {
202
+ callCount++;
203
+ return p === path.join(mockProjectPath, 'lib/api-client.ts');
204
+ });
205
+ fs.readFileSync.mockReturnValue('fetch');
206
+
207
+ ApiClientDetector.detect(mockProjectPath);
208
+
209
+ // Should stop checking after finding first client
210
+ // (only checks up to lib/api-client.ts which is index 0)
211
+ expect(callCount).toBeLessThan(20);
212
+ });
213
+ });
214
+ });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Tests for API Client Generator
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ jest.mock('fs');
9
+
10
+ const ApiClientGenerator = require('../src/generators/api-client-generator');
11
+
12
+ describe('ApiClientGenerator', () => {
13
+ const mockProjectPath = '/test/project';
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ fs.existsSync.mockReturnValue(false);
18
+ fs.mkdirSync.mockReturnValue(undefined);
19
+ fs.writeFileSync.mockReturnValue(undefined);
20
+ });
21
+
22
+ describe('generate', () => {
23
+ it('creates api-client.js in lib folder when no src exists', () => {
24
+ fs.existsSync.mockReturnValue(false);
25
+
26
+ const options = {
27
+ projectPath: mockProjectPath,
28
+ apiKey: 'test-api-key',
29
+ backendUrl: 'https://backend.test.com',
30
+ organizationId: 'org-123',
31
+ isTypeScript: false,
32
+ };
33
+
34
+ const result = ApiClientGenerator.generate(options);
35
+
36
+ expect(result).toBe(path.join(mockProjectPath, 'lib', 'api-client.js'));
37
+ expect(fs.mkdirSync).toHaveBeenCalledWith(
38
+ path.join(mockProjectPath, 'lib'),
39
+ { recursive: true }
40
+ );
41
+ expect(fs.writeFileSync).toHaveBeenCalled();
42
+ });
43
+
44
+ it('creates api-client.ts in src/lib folder when src exists', () => {
45
+ fs.existsSync.mockImplementation((p) =>
46
+ p === path.join(mockProjectPath, 'src')
47
+ );
48
+
49
+ const options = {
50
+ projectPath: mockProjectPath,
51
+ apiKey: 'test-api-key',
52
+ backendUrl: 'https://backend.test.com',
53
+ organizationId: 'org-123',
54
+ isTypeScript: true,
55
+ };
56
+
57
+ const result = ApiClientGenerator.generate(options);
58
+
59
+ expect(result).toBe(path.join(mockProjectPath, 'src', 'lib', 'api-client.ts'));
60
+ });
61
+
62
+ it('does not create lib dir if it exists', () => {
63
+ fs.existsSync.mockReturnValue(true);
64
+
65
+ const options = {
66
+ projectPath: mockProjectPath,
67
+ apiKey: 'test-api-key',
68
+ backendUrl: 'https://backend.test.com',
69
+ organizationId: 'org-123',
70
+ isTypeScript: false,
71
+ };
72
+
73
+ ApiClientGenerator.generate(options);
74
+
75
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
76
+ });
77
+ });
78
+
79
+ describe('generateCode', () => {
80
+ it('generates JavaScript code without type annotations', () => {
81
+ const code = ApiClientGenerator.generateCode({
82
+ apiKey: 'my-api-key',
83
+ backendUrl: 'https://api.example.com',
84
+ organizationId: 'org-456',
85
+ isTypeScript: false,
86
+ });
87
+
88
+ expect(code).toContain('L4YERCAK3 API Client');
89
+ expect(code).toContain("apiKey = 'my-api-key'");
90
+ expect(code).toContain("baseUrl = 'https://api.example.com'");
91
+ expect(code).toContain("this.organizationId = 'org-456'");
92
+ expect(code).toContain('module.exports = L4YERCAK3Client;');
93
+ expect(code).not.toContain(': string');
94
+ expect(code).not.toContain(': Promise<any>');
95
+ });
96
+
97
+ it('generates TypeScript code with type annotations', () => {
98
+ const code = ApiClientGenerator.generateCode({
99
+ apiKey: 'my-api-key',
100
+ backendUrl: 'https://api.example.com',
101
+ organizationId: 'org-456',
102
+ isTypeScript: true,
103
+ });
104
+
105
+ expect(code).toContain(': string');
106
+ expect(code).toContain(': Promise<any>');
107
+ expect(code).toContain(': RequestInit');
108
+ expect(code).toContain('export default L4YERCAK3Client;');
109
+ });
110
+
111
+ it('includes CRM methods', () => {
112
+ const code = ApiClientGenerator.generateCode({
113
+ apiKey: 'key',
114
+ backendUrl: 'url',
115
+ organizationId: 'org',
116
+ isTypeScript: false,
117
+ });
118
+
119
+ expect(code).toContain('getContacts()');
120
+ expect(code).toContain('getContact(contactId');
121
+ expect(code).toContain('createContact(data');
122
+ expect(code).toContain('updateContact(contactId');
123
+ expect(code).toContain('deleteContact(contactId');
124
+ });
125
+
126
+ it('includes Projects methods', () => {
127
+ const code = ApiClientGenerator.generateCode({
128
+ apiKey: 'key',
129
+ backendUrl: 'url',
130
+ organizationId: 'org',
131
+ isTypeScript: false,
132
+ });
133
+
134
+ expect(code).toContain('getProjects()');
135
+ expect(code).toContain('getProject(projectId');
136
+ expect(code).toContain('createProject(data');
137
+ expect(code).toContain('updateProject(projectId');
138
+ expect(code).toContain('deleteProject(projectId');
139
+ });
140
+
141
+ it('includes Invoices methods', () => {
142
+ const code = ApiClientGenerator.generateCode({
143
+ apiKey: 'key',
144
+ backendUrl: 'url',
145
+ organizationId: 'org',
146
+ isTypeScript: false,
147
+ });
148
+
149
+ expect(code).toContain('getInvoices()');
150
+ expect(code).toContain('getInvoice(invoiceId');
151
+ expect(code).toContain('createInvoice(data');
152
+ expect(code).toContain('updateInvoice(invoiceId');
153
+ expect(code).toContain('deleteInvoice(invoiceId');
154
+ });
155
+
156
+ it('includes request method with proper headers', () => {
157
+ const code = ApiClientGenerator.generateCode({
158
+ apiKey: 'key',
159
+ backendUrl: 'url',
160
+ organizationId: 'org',
161
+ isTypeScript: false,
162
+ });
163
+
164
+ expect(code).toContain("'Content-Type': 'application/json'");
165
+ expect(code).toContain('Authorization');
166
+ expect(code).toContain('X-Organization-Id');
167
+ });
168
+ });
169
+ });