@simplens/onboard 1.0.0 → 1.0.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.
Files changed (82) hide show
  1. package/README.md +331 -214
  2. package/dist/__tests__/env-config.test.d.ts +2 -0
  3. package/dist/__tests__/env-config.test.d.ts.map +1 -0
  4. package/dist/__tests__/env-config.test.js +23 -0
  5. package/dist/__tests__/env-config.test.js.map +1 -0
  6. package/dist/__tests__/infra-prompts.test.d.ts +2 -0
  7. package/dist/__tests__/infra-prompts.test.d.ts.map +1 -0
  8. package/dist/__tests__/infra-prompts.test.js +43 -0
  9. package/dist/__tests__/infra-prompts.test.js.map +1 -0
  10. package/dist/__tests__/infra.test.d.ts +2 -0
  11. package/dist/__tests__/infra.test.d.ts.map +1 -0
  12. package/dist/__tests__/infra.test.js +14 -0
  13. package/dist/__tests__/infra.test.js.map +1 -0
  14. package/dist/__tests__/nginx.test.d.ts +2 -0
  15. package/dist/__tests__/nginx.test.d.ts.map +1 -0
  16. package/dist/__tests__/nginx.test.js +16 -0
  17. package/dist/__tests__/nginx.test.js.map +1 -0
  18. package/dist/env-config.d.ts +27 -12
  19. package/dist/env-config.d.ts.map +1 -1
  20. package/dist/env-config.js +258 -141
  21. package/dist/env-config.js.map +1 -1
  22. package/dist/index.js +341 -71
  23. package/dist/index.js.map +1 -1
  24. package/dist/infra.d.ts +17 -14
  25. package/dist/infra.d.ts.map +1 -1
  26. package/dist/infra.js +265 -176
  27. package/dist/infra.js.map +1 -1
  28. package/dist/plugins.d.ts +5 -10
  29. package/dist/plugins.d.ts.map +1 -1
  30. package/dist/plugins.js +75 -44
  31. package/dist/plugins.js.map +1 -1
  32. package/dist/services.d.ts +1 -23
  33. package/dist/services.d.ts.map +1 -1
  34. package/dist/services.js +47 -62
  35. package/dist/services.js.map +1 -1
  36. package/dist/templates.d.ts +3 -2
  37. package/dist/templates.d.ts.map +1 -1
  38. package/dist/templates.js +203 -198
  39. package/dist/templates.js.map +1 -1
  40. package/dist/types/domain.d.ts +2 -0
  41. package/dist/types/domain.d.ts.map +1 -1
  42. package/dist/ui.d.ts +45 -0
  43. package/dist/ui.d.ts.map +1 -0
  44. package/dist/ui.js +93 -0
  45. package/dist/ui.js.map +1 -0
  46. package/dist/utils/logger.d.ts +1 -0
  47. package/dist/utils/logger.d.ts.map +1 -1
  48. package/dist/utils/logger.js +32 -7
  49. package/dist/utils/logger.js.map +1 -1
  50. package/dist/utils.d.ts +8 -0
  51. package/dist/utils.d.ts.map +1 -1
  52. package/dist/utils.js +66 -2
  53. package/dist/utils.js.map +1 -1
  54. package/dist/validators.d.ts +1 -52
  55. package/dist/validators.d.ts.map +1 -1
  56. package/dist/validators.js +10 -57
  57. package/dist/validators.js.map +1 -1
  58. package/package.json +3 -5
  59. package/src/__tests__/env-config.test.ts +28 -0
  60. package/src/__tests__/errors.test.ts +187 -187
  61. package/src/__tests__/infra-prompts.test.ts +54 -0
  62. package/src/__tests__/infra.test.ts +15 -0
  63. package/src/__tests__/utils.test.ts +142 -142
  64. package/src/__tests__/validators.test.ts +195 -195
  65. package/src/config/constants.ts +86 -86
  66. package/src/config/index.ts +1 -1
  67. package/src/env-config.ts +455 -320
  68. package/src/index.ts +534 -203
  69. package/src/infra.ts +404 -300
  70. package/src/plugins.ts +221 -190
  71. package/src/services.ts +175 -190
  72. package/src/templates.ts +209 -203
  73. package/src/types/domain.ts +129 -127
  74. package/src/types/errors.ts +173 -173
  75. package/src/types/index.ts +2 -2
  76. package/src/ui.ts +91 -0
  77. package/src/utils/index.ts +1 -1
  78. package/src/utils/logger.ts +144 -118
  79. package/src/utils.ts +183 -105
  80. package/src/validators.ts +145 -192
  81. package/tsconfig.json +18 -18
  82. package/vitest.config.ts +22 -20
package/src/plugins.ts CHANGED
@@ -1,190 +1,221 @@
1
- import inquirer from 'inquirer';
2
- import { execa } from 'execa';
3
- import yaml from 'js-yaml';
4
- import { readFile, logInfo, logSuccess, logError, logWarning } from './utils.js';
5
- import path from 'path';
6
- import type { PluginInfo, SimplensConfig } from './types/domain.js';
7
-
8
- /**
9
- * Fetches available SimpleNS plugins using the config-gen CLI tool.
10
- * Falls back to default plugins if fetching fails.
11
- *
12
- * @returns Array of available plugin information
13
- *
14
- * @remarks
15
- * Uses `npx @simplens/config-gen list --official` to fetch plugins.
16
- * Default fallback plugins: mock, nodemailer-gmail, resend
17
- *
18
- * @example
19
- * ```ts
20
- * const plugins = await fetchAvailablePlugins();
21
- * // Returns: [{ package: '@simplens/mock', name: 'Mock Provider', ... }, ...]
22
- * ```
23
- */
24
- export async function fetchAvailablePlugins(): Promise<PluginInfo[]> {
25
- logInfo('Fetching available plugins...');
26
-
27
- try {
28
- // Execute config-gen list command
29
- const { stdout } = await execa('npx', ['@simplens/config-gen', 'list', '--official'], {
30
- stdio: 'pipe',
31
- });
32
-
33
- // Parse output to extract plugins
34
- // Expected format: " @simplens/package-name Plugin Name"
35
- const plugins: PluginInfo[] = [];
36
- const lines = stdout.split('\n');
37
-
38
- for (const line of lines) {
39
- // Match plugin lines (starts with @simplens/)
40
- const match = line.match(/^\s+(@simplens\/[\w-]+)\s+(.+)$/);
41
- if (match) {
42
- const [, packageName, rest] = match;
43
- // Extract name and description
44
- const parts = rest.split(/\s{2,}/); // Split by multiple spaces
45
- plugins.push({
46
- package: packageName.trim(),
47
- name: parts[0]?.trim() || packageName,
48
- description: parts[1]?.trim() || '',
49
- });
50
- }
51
- }
52
-
53
- logSuccess(`Found ${plugins.length} available plugins`);
54
- return plugins;
55
- } catch (error: any) {
56
- logWarning('Could not fetch plugins list. Using defaults.');
57
- // Return default plugins as fallback
58
- return [
59
- { package: '@simplens/mock', name: 'Mock Provider', description: 'Mock notification provider for testing' },
60
- { package: '@simplens/nodemailer-gmail', name: 'Gmail', description: 'Send emails via Gmail' },
61
- { package: '@simplens/resend', name: 'Resend', description: 'Send emails via Resend' },
62
- ];
63
- }
64
- }
65
-
66
- /**
67
- * Prompt user to select plugins
68
- */
69
- export async function promptPluginSelection(availablePlugins: PluginInfo[]): Promise<string[]> {
70
- if (availablePlugins.length === 0) {
71
- logWarning('No plugins available to select.');
72
- return [];
73
- }
74
-
75
- const answer = await inquirer.prompt<{ plugins: string[] }>([
76
- {
77
- type: 'checkbox',
78
- name: 'plugins',
79
- message: 'Select plugins to install:',
80
- choices: availablePlugins.map(p => ({
81
- name: `${p.name} (${p.package}) - ${p.description}`,
82
- value: p.package,
83
- checked: p.package === '@simplens/mock', // Mock checked by default
84
- })),
85
- },
86
- ]);
87
-
88
- return answer.plugins;
89
- }
90
-
91
- /**
92
- * Generate plugin configuration using config-gen
93
- */
94
- export async function generatePluginConfig(
95
- targetDir: string,
96
- selectedPlugins: string[]
97
- ): Promise<void> {
98
- if (selectedPlugins.length === 0) {
99
- logInfo('No plugins selected, skipping config generation.');
100
- return;
101
- }
102
-
103
- logInfo(`Generating configuration for ${selectedPlugins.length} plugin(s)...`);
104
-
105
- try {
106
- const configPath = path.join(targetDir, 'simplens.config.yaml');
107
-
108
- // Execute config-gen for all selected plugins
109
- // Use relative path to avoid WSL path issues when npx runs Windows binaries
110
- await execa(
111
- 'npx',
112
- ['@simplens/config-gen', 'gen', ...selectedPlugins, '-o', 'simplens.config.yaml'],
113
- { cwd: targetDir, stdio: 'inherit' }
114
- );
115
-
116
- logSuccess('Generated simplens.config.yaml');
117
- } catch (error: any) {
118
- logError('Failed to generate plugin configuration');
119
- throw error;
120
- }
121
- }
122
-
123
- /**
124
- * Parse simplens.config.yaml to extract credential keys
125
- */
126
- export async function parseConfigCredentials(configPath: string): Promise<string[]> {
127
- try {
128
- const content = await readFile(configPath);
129
- const config: any = yaml.load(content);
130
-
131
- const credentialKeys = new Set<string>();
132
-
133
- // Extract credential keys from providers (ONLY from credentials, not optionalConfig)
134
- if (config.providers && Array.isArray(config.providers)) {
135
- for (const provider of config.providers) {
136
- if (provider.credentials && typeof provider.credentials === 'object') {
137
- for (const [key, value] of Object.entries(provider.credentials)) {
138
- // Extract env var name from ${ENV_VAR} format
139
- if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
140
- const envVar = value.slice(2, -1);
141
- credentialKeys.add(envVar);
142
- }
143
- }
144
- }
145
- // NOTE: We intentionally skip optionalConfig - those are optional!
146
- }
147
- }
148
-
149
- return Array.from(credentialKeys);
150
- } catch (error) {
151
- logWarning('Could not parse config file for credentials');
152
- return [];
153
- }
154
- }
155
-
156
- /**
157
- * Prompt for plugin-specific credentials
158
- */
159
- export async function promptPluginCredentials(credentialKeys: string[]): Promise<Map<string, string>> {
160
- if (credentialKeys.length === 0) {
161
- logInfo('No plugin credentials required.');
162
- return new Map();
163
- }
164
-
165
- logInfo('Configuring plugin credentials...');
166
-
167
- const result = new Map<string, string>();
168
-
169
- for (const key of credentialKeys) {
170
- const answer = await inquirer.prompt<{ value: string }>([
171
- {
172
- type: key.toLowerCase().includes('password') || key.toLowerCase().includes('key')
173
- ? 'password'
174
- : 'input',
175
- name: 'value',
176
- message: `${key}:`,
177
- validate: (input: string) => {
178
- if (!input || input.trim().length === 0) {
179
- return `${key} is required`;
180
- }
181
- return true;
182
- },
183
- },
184
- ]);
185
- result.set(key, answer.value);
186
- }
187
-
188
- logSuccess('Plugin credentials configured');
189
- return result;
190
- }
1
+ import { execa } from 'execa';
2
+ import yaml from 'js-yaml';
3
+ import crypto from 'crypto';
4
+ import { readFile, logInfo, logSuccess, logError, logWarning } from './utils.js';
5
+ import { multiselect, text, password } from '@clack/prompts';
6
+ import { handleCancel, spinner } from './ui.js';
7
+ import path from 'path';
8
+ import type { PluginInfo, SimplensConfig } from './types/domain.js';
9
+
10
+ /**
11
+ * Fetches available SimpleNS plugins using the config-gen CLI tool.
12
+ * Falls back to default plugins if fetching fails.
13
+ *
14
+ * @returns Array of available plugin information
15
+ */
16
+ export async function fetchAvailablePlugins(): Promise<PluginInfo[]> {
17
+ const s = spinner();
18
+ s.start('Fetching available plugins...');
19
+
20
+ try {
21
+ // Execute config-gen list command
22
+ const { stdout } = await execa('npx', ['@simplens/config-gen', 'list', '--official'], {
23
+ stdio: 'pipe',
24
+ });
25
+
26
+ // Parse output to extract plugins
27
+ // Expected format: " @simplens/package-name Plugin Name"
28
+ const plugins: PluginInfo[] = [];
29
+ const lines = stdout.split('\n');
30
+
31
+ for (const line of lines) {
32
+ // Match plugin lines (starts with @simplens/)
33
+ const match = line.match(/^\s+(@simplens\/[\w-]+)\s+(.+)$/);
34
+ if (match) {
35
+ const [, packageName, rest] = match;
36
+ // Extract name and description
37
+ const parts = rest.split(/\s{2,}/); // Split by multiple spaces
38
+ plugins.push({
39
+ package: packageName.trim(),
40
+ name: parts[0]?.trim() || packageName,
41
+ description: parts[1]?.trim() || '',
42
+ });
43
+ }
44
+ }
45
+
46
+ s.stop(`Found ${plugins.length} available plugins`);
47
+ return plugins;
48
+ } catch (error: unknown) {
49
+ s.stop('Could not fetch plugins list. Using defaults.');
50
+ logWarning('Falling back to default plugin list.');
51
+ // Return default plugins as fallback
52
+ return [
53
+ { package: '@simplens/mock', name: 'Mock Provider', description: 'Mock notification provider for testing' },
54
+ { package: '@simplens/nodemailer-gmail', name: 'Gmail', description: 'Send emails via Gmail' },
55
+ { package: '@simplens/resend', name: 'Resend', description: 'Send emails via Resend' },
56
+ ];
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Prompt user to select plugins
62
+ */
63
+ export async function promptPluginSelection(availablePlugins: PluginInfo[]): Promise<string[]> {
64
+ if (availablePlugins.length === 0) {
65
+ logWarning('No plugins available to select.');
66
+ return [];
67
+ }
68
+
69
+ const selected = await multiselect({
70
+ message: 'Select plugins to install (Space to select, Enter to confirm):',
71
+ options: availablePlugins.map(p => ({
72
+ value: p.package,
73
+ label: `${p.name} (${p.package})`,
74
+ hint: p.description,
75
+ })),
76
+ initialValues: availablePlugins
77
+ .filter(p => p.package === '@simplens/mock')
78
+ .map(p => p.package),
79
+ withGuide: true,
80
+ });
81
+
82
+ handleCancel(selected);
83
+ return selected as string[];
84
+ }
85
+
86
+ /**
87
+ * Generate plugin configuration using config-gen
88
+ */
89
+ export async function generatePluginConfig(
90
+ targetDir: string,
91
+ selectedPlugins: string[]
92
+ ): Promise<void> {
93
+ if (selectedPlugins.length === 0) {
94
+ logInfo('No plugins selected, skipping config generation.');
95
+ return;
96
+ }
97
+
98
+ const s = spinner();
99
+ s.start(`Generating configuration for ${selectedPlugins.length} plugin(s)...`);
100
+
101
+ try {
102
+ // Execute config-gen for all selected plugins
103
+ // Use relative path to avoid WSL path issues when npx runs Windows binaries
104
+ await execa(
105
+ 'npx',
106
+ ['@simplens/config-gen', 'gen', ...selectedPlugins, '-o', 'simplens.config.yaml'],
107
+ { cwd: targetDir, stdio: 'pipe' }
108
+ );
109
+
110
+ s.stop('Generated simplens.config.yaml');
111
+ } catch (error: unknown) {
112
+ s.error('Failed to generate plugin configuration');
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Parse simplens.config.yaml to extract credential keys
119
+ */
120
+ export async function parseConfigCredentials(configPath: string): Promise<string[]> {
121
+ try {
122
+ const content = await readFile(configPath);
123
+ const config: any = yaml.load(content);
124
+
125
+ const credentialKeys = new Set<string>();
126
+
127
+ // Extract credential keys from providers (ONLY from credentials, not optionalConfig)
128
+ if (config.providers && Array.isArray(config.providers)) {
129
+ for (const provider of config.providers) {
130
+ if (provider.credentials && typeof provider.credentials === 'object') {
131
+ for (const [key, value] of Object.entries(provider.credentials)) {
132
+ // Extract env var name from ${ENV_VAR} format
133
+ if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
134
+ const envVar = value.slice(2, -1);
135
+ credentialKeys.add(envVar);
136
+ }
137
+ }
138
+ }
139
+ // NOTE: We intentionally skip optionalConfig - those are optional!
140
+ }
141
+ }
142
+
143
+ return Array.from(credentialKeys);
144
+ } catch (error) {
145
+ logWarning('Could not parse config file for credentials');
146
+ return [];
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Generate default placeholder values for plugin credentials
152
+ * Used in --full mode for non-interactive setup
153
+ */
154
+ export function generateDefaultPluginCredentials(credentialKeys: string[]): Map<string, string> {
155
+ const result = new Map<string, string>();
156
+
157
+ for (const key of credentialKeys) {
158
+ // Generate placeholder values based on key name patterns
159
+ if (key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
160
+ result.set(key, crypto.randomBytes(16).toString('base64'));
161
+ } else if (key.toLowerCase().includes('apikey') || key.toLowerCase().includes('api_key')) {
162
+ result.set(key, `sk_${crypto.randomBytes(24).toString('base64').slice(0, 32)}`);
163
+ } else if (key.toLowerCase().includes('token')) {
164
+ result.set(key, crypto.randomBytes(32).toString('hex'));
165
+ } else if (key.toLowerCase().includes('email') || key.toLowerCase().includes('user')) {
166
+ result.set(key, 'CHANGE_ME@example.com');
167
+ } else {
168
+ // Generic placeholder
169
+ result.set(key, 'CHANGE_ME');
170
+ }
171
+ }
172
+
173
+ return result;
174
+ }
175
+
176
+ /**
177
+ * Prompt for plugin-specific credentials
178
+ */
179
+ export async function promptPluginCredentials(credentialKeys: string[]): Promise<Map<string, string>> {
180
+ if (credentialKeys.length === 0) {
181
+ logInfo('No plugin credentials required.');
182
+ return new Map();
183
+ }
184
+
185
+ logInfo('Configuring plugin credentials...');
186
+
187
+ const result = new Map<string, string>();
188
+
189
+ for (const key of credentialKeys) {
190
+ const isSecret = key.toLowerCase().includes('password') || key.toLowerCase().includes('key');
191
+
192
+ let answer: string | symbol;
193
+ if (isSecret) {
194
+ answer = await password({
195
+ message: `${key}:`,
196
+ validate: (input: string | undefined) => {
197
+ if (!input || input.trim().length === 0) {
198
+ return `${key} is required`;
199
+ }
200
+ return undefined;
201
+ },
202
+ });
203
+ } else {
204
+ answer = await text({
205
+ message: `${key}:`,
206
+ validate: (input: string | undefined) => {
207
+ if (!input || input.trim().length === 0) {
208
+ return `${key} is required`;
209
+ }
210
+ return undefined;
211
+ },
212
+ });
213
+ }
214
+
215
+ handleCancel(answer);
216
+ result.set(key, answer as string);
217
+ }
218
+
219
+ logSuccess('Plugin credentials configured');
220
+ return result;
221
+ }