@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,205 @@
1
+ /**
2
+ * Configuration Manager
3
+ * Handles storing and retrieving CLI configuration and session data
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ class ConfigManager {
11
+ constructor() {
12
+ this.configDir = path.join(os.homedir(), '.l4yercak3');
13
+ this.configFile = path.join(this.configDir, 'config.json');
14
+ }
15
+
16
+ /**
17
+ * Ensure config directory exists
18
+ */
19
+ ensureConfigDir() {
20
+ if (!fs.existsSync(this.configDir)) {
21
+ fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Get full config object
27
+ */
28
+ getConfig() {
29
+ this.ensureConfigDir();
30
+
31
+ if (!fs.existsSync(this.configFile)) {
32
+ return {
33
+ session: null,
34
+ organizations: [],
35
+ settings: {
36
+ backendUrl: process.env.L4YERCAK3_BACKEND_URL || 'https://backend.l4yercak3.com',
37
+ },
38
+ };
39
+ }
40
+
41
+ try {
42
+ const data = fs.readFileSync(this.configFile, 'utf8');
43
+ return JSON.parse(data);
44
+ } catch (error) {
45
+ console.error('Error reading config file:', error.message);
46
+ return {
47
+ session: null,
48
+ organizations: [],
49
+ settings: {},
50
+ };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Save config object
56
+ */
57
+ saveConfig(config) {
58
+ this.ensureConfigDir();
59
+
60
+ try {
61
+ fs.writeFileSync(
62
+ this.configFile,
63
+ JSON.stringify(config, null, 2),
64
+ { mode: 0o600 } // Read/write for owner only
65
+ );
66
+ return true;
67
+ } catch (error) {
68
+ console.error('Error saving config file:', error.message);
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get current session
75
+ */
76
+ getSession() {
77
+ const config = this.getConfig();
78
+ return config.session;
79
+ }
80
+
81
+ /**
82
+ * Save session
83
+ */
84
+ saveSession(session) {
85
+ const config = this.getConfig();
86
+ config.session = session;
87
+ return this.saveConfig(config);
88
+ }
89
+
90
+ /**
91
+ * Clear session (logout)
92
+ */
93
+ clearSession() {
94
+ const config = this.getConfig();
95
+ config.session = null;
96
+ return this.saveConfig(config);
97
+ }
98
+
99
+ /**
100
+ * Check if user is logged in
101
+ */
102
+ isLoggedIn() {
103
+ const session = this.getSession();
104
+ if (!session || !session.token) {
105
+ return false;
106
+ }
107
+
108
+ // Check expiration
109
+ if (session.expiresAt && session.expiresAt < Date.now()) {
110
+ return false;
111
+ }
112
+
113
+ return true;
114
+ }
115
+
116
+ /**
117
+ * Get backend URL from config or env
118
+ */
119
+ getBackendUrl() {
120
+ const config = this.getConfig();
121
+ return config.settings?.backendUrl || process.env.L4YERCAK3_BACKEND_URL || 'https://backend.l4yercak3.com';
122
+ }
123
+
124
+ /**
125
+ * Set backend URL
126
+ */
127
+ setBackendUrl(url) {
128
+ const config = this.getConfig();
129
+ if (!config.settings) {
130
+ config.settings = {};
131
+ }
132
+ config.settings.backendUrl = url;
133
+ return this.saveConfig(config);
134
+ }
135
+
136
+ /**
137
+ * Add organization to config
138
+ */
139
+ addOrganization(org) {
140
+ const config = this.getConfig();
141
+ if (!config.organizations) {
142
+ config.organizations = [];
143
+ }
144
+
145
+ // Remove if exists
146
+ config.organizations = config.organizations.filter(o => o.id !== org.id);
147
+
148
+ // Add new
149
+ config.organizations.push(org);
150
+
151
+ return this.saveConfig(config);
152
+ }
153
+
154
+ /**
155
+ * Get organizations
156
+ */
157
+ getOrganizations() {
158
+ const config = this.getConfig();
159
+ return config.organizations || [];
160
+ }
161
+
162
+ /**
163
+ * Save project configuration
164
+ * Stores configuration for a specific project (by project path)
165
+ */
166
+ saveProjectConfig(projectPath, projectConfig) {
167
+ const config = this.getConfig();
168
+ if (!config.projects) {
169
+ config.projects = {};
170
+ }
171
+
172
+ // Normalize project path (use absolute path)
173
+ const normalizedPath = path.resolve(projectPath);
174
+ config.projects[normalizedPath] = {
175
+ ...projectConfig,
176
+ updatedAt: Date.now(),
177
+ };
178
+
179
+ return this.saveConfig(config);
180
+ }
181
+
182
+ /**
183
+ * Get project configuration
184
+ */
185
+ getProjectConfig(projectPath) {
186
+ const config = this.getConfig();
187
+ if (!config.projects) {
188
+ return null;
189
+ }
190
+
191
+ const normalizedPath = path.resolve(projectPath);
192
+ return config.projects[normalizedPath] || null;
193
+ }
194
+
195
+ /**
196
+ * Get all project configurations
197
+ */
198
+ getAllProjectConfigs() {
199
+ const config = this.getConfig();
200
+ return config.projects || {};
201
+ }
202
+ }
203
+
204
+ module.exports = new ConfigManager();
205
+
@@ -0,0 +1,85 @@
1
+ /**
2
+ * API Client Pattern Detector
3
+ * Detects existing API client implementations
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ class ApiClientDetector {
10
+ /**
11
+ * Detect existing API client patterns
12
+ */
13
+ detect(projectPath = process.cwd()) {
14
+ const results = {
15
+ hasApiClient: false,
16
+ clientPath: null,
17
+ clientType: null, // 'fetch', 'axios', 'custom'
18
+ hasEnvFile: false,
19
+ envFilePath: null,
20
+ };
21
+
22
+ // Common API client locations
23
+ const clientPaths = [
24
+ 'lib/api-client.ts',
25
+ 'lib/api-client.js',
26
+ 'lib/api.ts',
27
+ 'lib/api.js',
28
+ 'src/lib/api-client.ts',
29
+ 'src/lib/api-client.js',
30
+ 'src/lib/api.ts',
31
+ 'src/lib/api.js',
32
+ 'utils/api-client.ts',
33
+ 'utils/api-client.js',
34
+ 'utils/api.ts',
35
+ 'utils/api.js',
36
+ 'src/utils/api-client.ts',
37
+ 'src/utils/api-client.js',
38
+ ];
39
+
40
+ // Check for existing API client
41
+ for (const clientPath of clientPaths) {
42
+ const fullPath = path.join(projectPath, clientPath);
43
+ if (fs.existsSync(fullPath)) {
44
+ results.hasApiClient = true;
45
+ results.clientPath = clientPath;
46
+
47
+ // Try to detect client type
48
+ try {
49
+ const content = fs.readFileSync(fullPath, 'utf8');
50
+ if (content.includes('axios')) {
51
+ results.clientType = 'axios';
52
+ } else if (content.includes('fetch')) {
53
+ results.clientType = 'fetch';
54
+ } else {
55
+ results.clientType = 'custom';
56
+ }
57
+ } catch (error) {
58
+ results.clientType = 'unknown';
59
+ }
60
+ break;
61
+ }
62
+ }
63
+
64
+ // Check for environment files
65
+ const envFiles = [
66
+ '.env.local',
67
+ '.env',
68
+ '.env.development',
69
+ '.env.production',
70
+ ];
71
+
72
+ for (const envFile of envFiles) {
73
+ const fullPath = path.join(projectPath, envFile);
74
+ if (fs.existsSync(fullPath)) {
75
+ results.hasEnvFile = true;
76
+ results.envFilePath = envFile;
77
+ break;
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }
83
+ }
84
+
85
+ module.exports = new ApiClientDetector();
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Base Detector Class
3
+ *
4
+ * All project type detectors should extend this class or implement its interface.
5
+ * This provides a consistent API for detection across all project types.
6
+ */
7
+
8
+ class BaseDetector {
9
+ /**
10
+ * Unique identifier for this project type
11
+ * @returns {string} Detector name (e.g., 'nextjs', 'react', 'vue')
12
+ */
13
+ get name() {
14
+ throw new Error('Detector must implement name getter');
15
+ }
16
+
17
+ /**
18
+ * Detection priority (higher = checked first)
19
+ * Range: 0-100
20
+ *
21
+ * Priority guidelines:
22
+ * - 100: Very specific frameworks (Next.js, Nuxt, SvelteKit)
23
+ * - 75: Framework with build tool (Vite + React, Webpack + Vue)
24
+ * - 50: Pure frameworks (React, Vue, Svelte)
25
+ * - 25: Generic JavaScript/TypeScript projects
26
+ *
27
+ * @returns {number} Priority value
28
+ */
29
+ get priority() {
30
+ return 50; // Default priority
31
+ }
32
+
33
+ /**
34
+ * Detect if project matches this type
35
+ *
36
+ * @param {string} projectPath - Path to project directory (defaults to process.cwd())
37
+ * @returns {object} Detection result with:
38
+ * - detected: {boolean} Is this the project type?
39
+ * - confidence: {number} 0-1, how sure are we?
40
+ * - metadata: {object} Type-specific information
41
+ */
42
+ detect(_projectPath = process.cwd()) {
43
+ throw new Error('Detector must implement detect() method');
44
+ }
45
+
46
+ /**
47
+ * Get supported features for this project type
48
+ *
49
+ * @returns {object} Feature support matrix:
50
+ * - oauth: {boolean|'manual'} true = full support, 'manual' = guide only, false = not supported
51
+ * - stripe: {boolean} Stripe integration support
52
+ * - crm: {boolean} CRM features support
53
+ * - projects: {boolean} Projects feature support
54
+ * - invoices: {boolean} Invoices feature support
55
+ */
56
+ getSupportedFeatures() {
57
+ return {
58
+ oauth: false,
59
+ stripe: false,
60
+ crm: false,
61
+ projects: false,
62
+ invoices: false,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Get available generators for this project type
68
+ *
69
+ * @returns {string[]} List of generator names that work with this project type
70
+ */
71
+ getAvailableGenerators() {
72
+ return ['api-client', 'env']; // Default: shared generators only
73
+ }
74
+ }
75
+
76
+ module.exports = BaseDetector;
77
+
@@ -0,0 +1,74 @@
1
+ /**
2
+ * GitHub Repository Detector
3
+ * Detects GitHub repository information from git remotes
4
+ */
5
+
6
+ const { execSync } = require('child_process');
7
+ const path = require('path');
8
+
9
+ class GitHubDetector {
10
+ /**
11
+ * Detect GitHub repository information
12
+ */
13
+ detect(projectPath = process.cwd()) {
14
+ const results = {
15
+ hasGit: false,
16
+ isGitHub: false,
17
+ owner: null,
18
+ repo: null,
19
+ url: null,
20
+ branch: null,
21
+ };
22
+
23
+ // Check if .git directory exists
24
+ const gitDir = path.join(projectPath, '.git');
25
+ if (!require('fs').existsSync(gitDir)) {
26
+ return results;
27
+ }
28
+
29
+ results.hasGit = true;
30
+
31
+ try {
32
+ // Get git remote URL
33
+ const remoteUrl = execSync('git config --get remote.origin.url', {
34
+ cwd: projectPath,
35
+ encoding: 'utf8',
36
+ stdio: ['pipe', 'pipe', 'ignore'],
37
+ }).trim();
38
+
39
+ if (!remoteUrl) {
40
+ return results;
41
+ }
42
+
43
+ // Parse GitHub URL (supports both HTTPS and SSH formats)
44
+ // https://github.com/owner/repo.git
45
+ // git@github.com:owner/repo.git
46
+ const githubMatch = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
47
+
48
+ if (githubMatch) {
49
+ results.isGitHub = true;
50
+ results.owner = githubMatch[1];
51
+ results.repo = githubMatch[2].replace(/\.git$/, '');
52
+ results.url = `https://github.com/${results.owner}/${results.repo}`;
53
+ }
54
+
55
+ // Get current branch
56
+ try {
57
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
58
+ cwd: projectPath,
59
+ encoding: 'utf8',
60
+ stdio: ['pipe', 'pipe', 'ignore'],
61
+ }).trim();
62
+ results.branch = branch;
63
+ } catch (error) {
64
+ // Couldn't get branch
65
+ }
66
+ } catch (error) {
67
+ // Error reading git config
68
+ }
69
+
70
+ return results;
71
+ }
72
+ }
73
+
74
+ module.exports = new GitHubDetector();
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Project Detectors
3
+ * Main entry point for all project detection
4
+ *
5
+ * This module orchestrates both framework detection (Next.js, React, etc.)
6
+ * and project metadata detection (GitHub, API clients, etc.)
7
+ */
8
+
9
+ const registry = require('./registry');
10
+ const githubDetector = require('./github-detector');
11
+ const apiClientDetector = require('./api-client-detector');
12
+ const oauthDetector = require('./oauth-detector');
13
+
14
+ class ProjectDetector {
15
+ /**
16
+ * Run all detectors and return combined results
17
+ *
18
+ * @param {string} projectPath - Path to project directory
19
+ * @returns {object} Combined detection results
20
+ */
21
+ detect(projectPath = process.cwd()) {
22
+ // Detect framework/project type
23
+ const frameworkDetection = registry.detectProjectType(projectPath);
24
+
25
+ // Detect project metadata (framework-agnostic)
26
+ const githubInfo = githubDetector.detect(projectPath);
27
+ const apiClientInfo = apiClientDetector.detect(projectPath);
28
+ const oauthInfo = oauthDetector.detect(projectPath);
29
+
30
+ // Get detector instance if we have a match
31
+ const detector = frameworkDetection.detected
32
+ ? registry.getDetector(frameworkDetection.detected)
33
+ : null;
34
+
35
+ return {
36
+ // Framework detection
37
+ framework: {
38
+ type: frameworkDetection.detected,
39
+ confidence: frameworkDetection.confidence,
40
+ metadata: frameworkDetection.metadata,
41
+ supportedFeatures: detector?.getSupportedFeatures() || {},
42
+ availableGenerators: detector?.getAvailableGenerators() || [],
43
+ },
44
+
45
+ // Project metadata (framework-agnostic)
46
+ github: githubInfo,
47
+ apiClient: apiClientInfo,
48
+ oauth: oauthInfo,
49
+
50
+ // Raw detection results (for debugging)
51
+ _raw: {
52
+ frameworkResults: frameworkDetection.allResults,
53
+ },
54
+
55
+ // Project path
56
+ projectPath,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Get detector for a specific project type
62
+ *
63
+ * @param {string} type - Project type name (e.g., 'nextjs')
64
+ * @returns {object|null} Detector instance or null
65
+ */
66
+ getDetector(type) {
67
+ return registry.getDetector(type);
68
+ }
69
+
70
+ /**
71
+ * Get all available project types
72
+ *
73
+ * @returns {array} Array of detector names
74
+ */
75
+ getAvailableTypes() {
76
+ return registry.getAllDetectors().map(d => d.name);
77
+ }
78
+ }
79
+
80
+ module.exports = new ProjectDetector();
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Next.js Project Detector
3
+ * Detects Next.js projects and their configuration
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const BaseDetector = require('./base-detector');
9
+
10
+ class NextJsDetector extends BaseDetector {
11
+ get name() {
12
+ return 'nextjs';
13
+ }
14
+
15
+ get priority() {
16
+ return 100; // High priority - very specific framework
17
+ }
18
+
19
+ /**
20
+ * Detect if current directory is a Next.js project
21
+ */
22
+ detect(projectPath = process.cwd()) {
23
+ const results = {
24
+ isNextJs: false,
25
+ version: null,
26
+ routerType: null, // 'app' or 'pages'
27
+ hasTypeScript: false,
28
+ config: null,
29
+ };
30
+
31
+ // Check for package.json
32
+ const packageJsonPath = path.join(projectPath, 'package.json');
33
+ if (!fs.existsSync(packageJsonPath)) {
34
+ return results;
35
+ }
36
+
37
+ try {
38
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
39
+ const dependencies = {
40
+ ...packageJson.dependencies,
41
+ ...packageJson.devDependencies,
42
+ };
43
+
44
+ // Check if Next.js is installed
45
+ if (dependencies.next) {
46
+ results.isNextJs = true;
47
+ results.version = dependencies.next;
48
+ }
49
+
50
+ // Check for TypeScript
51
+ if (dependencies.typescript || fs.existsSync(path.join(projectPath, 'tsconfig.json'))) {
52
+ results.hasTypeScript = true;
53
+ }
54
+
55
+ // Detect router type (App Router vs Pages Router)
56
+ const appDir = path.join(projectPath, 'app');
57
+ const pagesDir = path.join(projectPath, 'pages');
58
+ const srcAppDir = path.join(projectPath, 'src', 'app');
59
+ const srcPagesDir = path.join(projectPath, 'src', 'pages');
60
+
61
+ if (fs.existsSync(appDir) || fs.existsSync(srcAppDir)) {
62
+ results.routerType = 'app';
63
+ } else if (fs.existsSync(pagesDir) || fs.existsSync(srcPagesDir)) {
64
+ results.routerType = 'pages';
65
+ }
66
+
67
+ // Read Next.js config if exists
68
+ const configFiles = [
69
+ 'next.config.js',
70
+ 'next.config.mjs',
71
+ 'next.config.ts',
72
+ 'next.config.cjs',
73
+ ];
74
+
75
+ for (const configFile of configFiles) {
76
+ const configPath = path.join(projectPath, configFile);
77
+ if (fs.existsSync(configPath)) {
78
+ try {
79
+ // For .js/.mjs/.cjs files, we'd need to require them
80
+ // For now, just note that config exists
81
+ results.config = configFile;
82
+ break;
83
+ } catch (error) {
84
+ // Config file exists but couldn't be read
85
+ }
86
+ }
87
+ }
88
+ } catch (error) {
89
+ // Error reading package.json
90
+ }
91
+
92
+ // Return in BaseDetector format
93
+ if (!results.isNextJs) {
94
+ return {
95
+ detected: false,
96
+ confidence: 0,
97
+ metadata: {},
98
+ };
99
+ }
100
+
101
+ return {
102
+ detected: true,
103
+ confidence: 0.95, // High confidence for Next.js detection
104
+ metadata: {
105
+ version: results.version,
106
+ routerType: results.routerType,
107
+ hasTypeScript: results.hasTypeScript,
108
+ config: results.config,
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Get supported features for Next.js
115
+ */
116
+ getSupportedFeatures() {
117
+ return {
118
+ oauth: true, // Full NextAuth.js support
119
+ stripe: true, // Full Stripe support
120
+ crm: true, // Full CRM support
121
+ projects: true, // Full Projects support
122
+ invoices: true, // Full Invoices support
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Get available generators for Next.js
128
+ */
129
+ getAvailableGenerators() {
130
+ return [
131
+ 'api-client',
132
+ 'env',
133
+ 'nextauth', // Next.js-specific
134
+ 'oauth-guide',
135
+ ];
136
+ }
137
+ }
138
+
139
+ module.exports = new NextJsDetector();