@friggframework/devtools 2.0.0--canary.545.88cf983.0 → 2.0.0--canary.545.c870571.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.
@@ -1,7 +1,15 @@
1
1
  const { spawnSync } = require('child_process');
2
2
  const path = require('path');
3
+ const fs = require('fs');
3
4
 
4
5
  async function buildCommand(options) {
6
+ // Check if the app uses a non-AWS provider
7
+ const providerResult = loadProviderIfConfigured();
8
+ if (providerResult) {
9
+ return buildWithProvider(providerResult, options);
10
+ }
11
+
12
+ // Default: AWS build via serverless framework
5
13
  console.log('Building the serverless application...');
6
14
 
7
15
  // Suppress AWS SDK warning message about maintenance mode
@@ -10,13 +18,13 @@ async function buildCommand(options) {
10
18
  // Skip AWS discovery for local builds (unless --production flag is set)
11
19
  if (!options.production) {
12
20
  process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
13
- console.log('🏠 Building in local mode (use --production flag for production builds with AWS discovery)');
21
+ console.log('Building in local mode (use --production flag for production builds with AWS discovery)');
14
22
  } else {
15
- console.log('🚀 Building in production mode with AWS discovery enabled');
23
+ console.log('Building in production mode with AWS discovery enabled');
16
24
  }
17
25
 
18
26
  // AWS discovery is now handled directly in serverless-template.js
19
- console.log('📦 Packaging serverless application...');
27
+ console.log('Packaging serverless application...');
20
28
  const backendPath = path.resolve(process.cwd());
21
29
  const infrastructurePath = 'infrastructure.js';
22
30
  const command = 'osls'; // OSS-Serverless (drop-in replacement for serverless v3)
@@ -51,16 +59,102 @@ async function buildCommand(options) {
51
59
  console.error(`Serverless build failed with code ${result.status}`);
52
60
  process.exit(1);
53
61
  }
62
+ }
63
+
64
+ /**
65
+ * Check if the appDefinition specifies a non-AWS provider and resolve it.
66
+ */
67
+ function loadProviderIfConfigured() {
68
+ try {
69
+ const { loadProviderForCli } = require('../utils/provider-helper');
70
+ const result = loadProviderForCli();
71
+ if (result && result.provider) {
72
+ return result;
73
+ }
74
+ } catch {
75
+ // Provider helper not available or appDefinition not found — fall through
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Build using a provider plugin.
82
+ * Provider build steps:
83
+ * 1. generateConfig() — generate platform-specific config (netlify.toml, etc.)
84
+ * 2. getFunctionEntryPoints() — copy function files to the project
85
+ */
86
+ async function buildWithProvider({ provider, appDefinition, providerName }, options) {
87
+ console.log(`Building for ${providerName} provider...`);
88
+ const projectDir = path.resolve(process.cwd());
89
+
90
+ // 1. Validate
91
+ if (typeof provider.validate === 'function') {
92
+ const validation = provider.validate(appDefinition);
93
+ if (validation.errors?.length > 0) {
94
+ console.error(`\nValidation errors for ${providerName}:`);
95
+ for (const error of validation.errors) {
96
+ console.error(` - ${error}`);
97
+ }
98
+ process.exit(1);
99
+ }
100
+ if (validation.warnings?.length > 0) {
101
+ for (const warning of validation.warnings) {
102
+ console.warn(` Warning: ${warning}`);
103
+ }
104
+ }
105
+ }
106
+
107
+ // 2. Generate platform config (e.g., netlify.toml)
108
+ if (typeof provider.generateConfig === 'function') {
109
+ console.log(`Generating ${providerName} configuration...`);
110
+ const config = provider.generateConfig(appDefinition);
54
111
 
55
- // childProcess.on('error', (error) => {
56
- // console.error(`Error executing command: ${error.message}`);
57
- // });
112
+ const configFileNames = {
113
+ netlify: 'netlify.toml',
114
+ };
115
+ const configFileName = configFileNames[providerName] || `${providerName}.config`;
116
+ const configPath = path.join(projectDir, configFileName);
117
+
118
+ fs.writeFileSync(configPath, config, 'utf-8');
119
+ console.log(` Written ${configFileName}`);
120
+ }
121
+
122
+ // 3. Copy function entry points
123
+ if (typeof provider.getFunctionEntryPoints === 'function') {
124
+ console.log('Generating function entry points...');
125
+ const entryPoints = provider.getFunctionEntryPoints(appDefinition);
126
+ const functionsDir = path.join(projectDir, 'netlify', 'functions');
127
+
128
+ fs.mkdirSync(functionsDir, { recursive: true });
129
+
130
+ for (const [filename, content] of Object.entries(entryPoints)) {
131
+ const filePath = path.join(functionsDir, filename);
132
+ fs.writeFileSync(filePath, content, 'utf-8');
133
+ if (options.verbose) {
134
+ console.log(` Written ${path.relative(projectDir, filePath)}`);
135
+ }
136
+ }
137
+
138
+ console.log(` Generated ${Object.keys(entryPoints).length} function entry points`);
139
+ }
140
+
141
+ // 4. Generate env template (informational)
142
+ if (typeof provider.generateEnvTemplate === 'function') {
143
+ const envTemplate = provider.generateEnvTemplate(appDefinition);
144
+ const missingEnvVars = Object.entries(envTemplate)
145
+ .filter(([key]) => !process.env[key])
146
+ .map(([key, desc]) => `${key}: ${desc}`);
147
+
148
+ if (missingEnvVars.length > 0) {
149
+ console.log(`\n Required environment variables not set locally:`);
150
+ for (const entry of missingEnvVars) {
151
+ console.log(` - ${entry}`);
152
+ }
153
+ console.log(` Configure these in your ${providerName} dashboard.`);
154
+ }
155
+ }
58
156
 
59
- // childProcess.on('close', (code) => {
60
- // if (code !== 0) {
61
- // console.log(`Child process exited with code ${code}`);
62
- // }
63
- // });
157
+ console.log(`\nBuild complete for ${providerName}.`);
64
158
  }
65
159
 
66
160
  module.exports = { buildCommand };
@@ -268,9 +268,17 @@ async function runPostDeploymentHealthCheck(stackName, options) {
268
268
  }
269
269
 
270
270
  async function deployCommand(options) {
271
+ const appDefinition = loadAppDefinition();
272
+
273
+ // Check if the app uses a non-AWS provider
274
+ const providerResult = loadProviderIfConfigured(appDefinition);
275
+ if (providerResult) {
276
+ return deployWithProvider(providerResult, options);
277
+ }
278
+
279
+ // Default: AWS deployment via serverless framework
271
280
  console.log('Deploying the serverless application...');
272
281
 
273
- const appDefinition = loadAppDefinition();
274
282
  const environment = validateAndBuildEnvironment(appDefinition, options);
275
283
 
276
284
  // Execute deployment
@@ -302,4 +310,78 @@ async function deployCommand(options) {
302
310
  }
303
311
  }
304
312
 
313
+ /**
314
+ * Check if the appDefinition specifies a non-AWS provider and resolve it.
315
+ * Returns null for AWS (default) so the caller falls through to existing behavior.
316
+ *
317
+ * @param {Object|null} appDefinition
318
+ * @returns {{ provider: Object, appDefinition: Object, providerName: string } | null}
319
+ */
320
+ function loadProviderIfConfigured(appDefinition) {
321
+ const providerName = appDefinition?.provider;
322
+ if (!providerName || providerName === 'aws') {
323
+ return null;
324
+ }
325
+
326
+ try {
327
+ const { resolveProvider } = require('@friggframework/core/providers/resolve-provider');
328
+ const provider = resolveProvider(appDefinition);
329
+ return { provider, appDefinition, providerName };
330
+ } catch (error) {
331
+ console.error(`Failed to load provider '${providerName}': ${error.message}`);
332
+ process.exit(1);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Deploy using a provider plugin (Netlify, etc.).
338
+ * Delegates entirely to the provider's deploy lifecycle:
339
+ * 1. validate() — check appDefinition for provider-specific issues
340
+ * 2. preflightCheck() — verify prerequisites (CLI tools, credentials)
341
+ * 3. deploy() — execute the deployment
342
+ */
343
+ async function deployWithProvider({ provider, appDefinition, providerName }, options) {
344
+ console.log(`Deploying with ${providerName} provider...`);
345
+
346
+ // 1. Validate appDefinition for this provider
347
+ if (typeof provider.validate === 'function') {
348
+ const validation = provider.validate(appDefinition);
349
+ if (validation.errors?.length > 0) {
350
+ console.error(`\nValidation errors for ${providerName}:`);
351
+ for (const error of validation.errors) {
352
+ console.error(` - ${error}`);
353
+ }
354
+ process.exit(1);
355
+ }
356
+ if (validation.warnings?.length > 0) {
357
+ for (const warning of validation.warnings) {
358
+ console.warn(` Warning: ${warning}`);
359
+ }
360
+ }
361
+ }
362
+
363
+ // 2. Deploy via provider
364
+ try {
365
+ const result = await provider.deploy(appDefinition, {
366
+ stage: options.stage,
367
+ prod: options.stage === 'production' || options.stage === 'prod',
368
+ dryRun: options.dryRun || false,
369
+ });
370
+
371
+ console.log(`\n✓ Deployment completed successfully!`);
372
+ if (result.url) {
373
+ console.log(` URL: ${result.url}`);
374
+ }
375
+ } catch (error) {
376
+ console.error(`\n✗ Deployment failed: ${error.message}`);
377
+ if (error.missing) {
378
+ console.error(' Missing prerequisites:');
379
+ for (const item of error.missing) {
380
+ console.error(` - ${item}`);
381
+ }
382
+ }
383
+ process.exit(1);
384
+ }
385
+ }
386
+
305
387
  module.exports = { deployCommand };
@@ -247,6 +247,13 @@ async function promptForStackSelection(region) {
247
247
  */
248
248
  async function doctorCommand(stackName, options = {}) {
249
249
  try {
250
+ // Guard: doctor only works with AWS (CloudFormation stacks)
251
+ if (isNonAwsProvider()) {
252
+ output.error('The doctor command is only available for AWS deployments.');
253
+ output.log('Your appDefinition uses a non-AWS provider.');
254
+ process.exit(1);
255
+ }
256
+
250
257
  // Extract options with defaults
251
258
  const region = options.region || process.env.AWS_REGION || 'us-east-1';
252
259
  const format = options.format || 'console';
@@ -333,4 +340,17 @@ async function doctorCommand(stackName, options = {}) {
333
340
  }
334
341
  }
335
342
 
343
+ /**
344
+ * Check if the current appDefinition uses a non-AWS provider.
345
+ */
346
+ function isNonAwsProvider() {
347
+ try {
348
+ const { loadProviderForCli } = require('../utils/provider-helper');
349
+ const result = loadProviderForCli();
350
+ return result && result.providerName !== 'aws';
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+
336
356
  module.exports = { doctorCommand };
@@ -9,7 +9,14 @@ const { generateIAMCloudFormation, getFeatureSummary } = require('../infrastruct
9
9
  */
10
10
  async function generateIamCommand(options = {}) {
11
11
  try {
12
- console.log('🔍 Finding Frigg application...');
12
+ // Guard: generate-iam only works with AWS (IAM / CloudFormation)
13
+ if (isNonAwsProvider()) {
14
+ console.error('The generate-iam command is only available for AWS deployments.');
15
+ console.log('Your appDefinition uses a non-AWS provider.');
16
+ process.exit(1);
17
+ }
18
+
19
+ console.log('Finding Frigg application...');
13
20
 
14
21
  // Find the backend package.json
15
22
  const backendPath = findNearestBackendPackageJson();
@@ -115,4 +122,17 @@ async function generateIamCommand(options = {}) {
115
122
  }
116
123
  }
117
124
 
125
+ /**
126
+ * Check if the current appDefinition uses a non-AWS provider.
127
+ */
128
+ function isNonAwsProvider() {
129
+ try {
130
+ const { loadProviderForCli } = require('./utils/provider-helper');
131
+ const result = loadProviderForCli();
132
+ return result && result.providerName !== 'aws';
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
118
138
  module.exports = { generateIamCommand };
@@ -426,6 +426,13 @@ async function handleReconcileRepair(stackIdentifier, report, options) {
426
426
  */
427
427
  async function repairCommand(stackName, options = {}) {
428
428
  try {
429
+ // Guard: repair only works with AWS (CloudFormation stacks)
430
+ if (isNonAwsProvider()) {
431
+ output.error('The repair command is only available for AWS deployments.');
432
+ output.log('Your appDefinition uses a non-AWS provider.');
433
+ process.exit(1);
434
+ }
435
+
429
436
  // Validate required parameter
430
437
  if (!stackName) {
431
438
  output.error('Error: Stack name is required');
@@ -534,4 +541,17 @@ async function repairCommand(stackName, options = {}) {
534
541
  }
535
542
  }
536
543
 
544
+ /**
545
+ * Check if the current appDefinition uses a non-AWS provider.
546
+ */
547
+ function isNonAwsProvider() {
548
+ try {
549
+ const { loadProviderForCli } = require('../utils/provider-helper');
550
+ const result = loadProviderForCli();
551
+ return result && result.providerName !== 'aws';
552
+ } catch {
553
+ return false;
554
+ }
555
+ }
556
+
537
557
  module.exports = { repairCommand };
@@ -63,6 +63,13 @@ async function startCommand(options) {
63
63
  console.log(chalk.green('✓ Pre-flight checks passed\n'));
64
64
  console.log('Starting backend and optional frontend...');
65
65
 
66
+ // Check if the app uses a non-AWS provider
67
+ const providerResult = loadProviderIfConfigured();
68
+ if (providerResult) {
69
+ return startWithProvider(providerResult, options);
70
+ }
71
+
72
+ // Default: AWS local development via serverless-offline
66
73
  // Suppress AWS SDK warning message about maintenance mode
67
74
  process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = '1';
68
75
  // Skip AWS discovery for local development
@@ -109,6 +116,76 @@ async function startCommand(options) {
109
116
  });
110
117
  }
111
118
 
119
+ /**
120
+ * Check if the appDefinition specifies a non-AWS provider and resolve it.
121
+ * Returns null for AWS (default) so the caller falls through to existing behavior.
122
+ */
123
+ function loadProviderIfConfigured() {
124
+ try {
125
+ const { loadProviderForCli } = require('../utils/provider-helper');
126
+ const result = loadProviderForCli();
127
+ if (result && result.provider) {
128
+ return result;
129
+ }
130
+ } catch {
131
+ // Provider helper not available or appDefinition not found — fall through
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Start local dev server using the provider's recommended approach.
138
+ * Each provider has a different local dev story:
139
+ * - Netlify: `netlify dev` (reads netlify.toml, serves functions locally)
140
+ * - AWS: `osls offline` (serverless-offline, handled by default path above)
141
+ */
142
+ function startWithProvider({ provider, providerName }, options) {
143
+ const backendPath = path.resolve(process.cwd());
144
+
145
+ // Provider-specific dev server commands
146
+ const DEV_COMMANDS = {
147
+ netlify: { command: 'netlify', args: ['dev'] },
148
+ };
149
+
150
+ const devCmd = DEV_COMMANDS[providerName];
151
+ if (!devCmd) {
152
+ console.error(chalk.red(
153
+ `Provider '${providerName}' does not have a local dev server configured.\n` +
154
+ ` Supported providers for local dev: ${Object.keys(DEV_COMMANDS).join(', ')}, aws`
155
+ ));
156
+ process.exit(1);
157
+ }
158
+
159
+ console.log(chalk.blue(`Starting local dev server (${providerName})...`));
160
+
161
+ const args = [...devCmd.args];
162
+ if (options.verbose) {
163
+ console.log(`Executing command: ${devCmd.command} ${args.join(' ')}`);
164
+ console.log(`Working directory: ${backendPath}`);
165
+ }
166
+
167
+ const childProcess = spawn(devCmd.command, args, {
168
+ cwd: backendPath,
169
+ stdio: 'inherit',
170
+ env: { ...process.env },
171
+ });
172
+
173
+ childProcess.on('error', (error) => {
174
+ if (error.code === 'ENOENT') {
175
+ console.error(chalk.red(
176
+ `'${devCmd.command}' not found. Install it with: npm install -g ${devCmd.command}-cli`
177
+ ));
178
+ } else {
179
+ console.error(`Error executing command: ${error.message}`);
180
+ }
181
+ });
182
+
183
+ childProcess.on('close', (code) => {
184
+ if (code !== 0) {
185
+ console.log(`Child process exited with code ${code}`);
186
+ }
187
+ });
188
+
112
189
  /**
113
190
  * Run interactive pre-flight checks with resolution prompts
114
191
  * @param {string} projectPath - Path to the Frigg project
@@ -0,0 +1,55 @@
1
+ const path = require('path');
2
+ const { loadProviderForCli, loadCliAppDefinition } = require('../provider-helper');
3
+
4
+ describe('provider-helper', () => {
5
+ describe('loadCliAppDefinition', () => {
6
+ it('returns null when no index.js exists', () => {
7
+ const result = loadCliAppDefinition('/nonexistent/path');
8
+ expect(result).toBeNull();
9
+ });
10
+
11
+ it('returns null when index.js has no Definition export', () => {
12
+ // Use a directory that has an index.js but no Definition
13
+ const result = loadCliAppDefinition(
14
+ path.join(__dirname, '..', '..')
15
+ );
16
+ // frigg-cli/index.js doesn't export Definition
17
+ expect(result).toBeNull();
18
+ });
19
+ });
20
+
21
+ describe('loadProviderForCli', () => {
22
+ it('returns null when no appDefinition found', () => {
23
+ // Run from a directory with no Frigg app
24
+ const originalCwd = process.cwd();
25
+ try {
26
+ process.chdir('/tmp');
27
+ const result = loadProviderForCli();
28
+ expect(result).toBeNull();
29
+ } finally {
30
+ process.chdir(originalCwd);
31
+ }
32
+ });
33
+
34
+ it('returns null provider for aws (default)', () => {
35
+ // Mock: loadCliAppDefinition returns an appDef with provider: 'aws'
36
+ jest.mock('../provider-helper', () => {
37
+ const original = jest.requireActual('../provider-helper');
38
+ return {
39
+ ...original,
40
+ loadProviderForCli: (options) => {
41
+ // Simulate AWS provider (no provider field defaults to aws)
42
+ return { appDefinition: { provider: 'aws' }, provider: null, providerName: 'aws' };
43
+ },
44
+ };
45
+ });
46
+
47
+ const { loadProviderForCli: mocked } = require('../provider-helper');
48
+ const result = mocked();
49
+ expect(result.providerName).toBe('aws');
50
+ expect(result.provider).toBeNull();
51
+
52
+ jest.restoreAllMocks();
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CLI Provider Helper
3
+ *
4
+ * Loads the appDefinition from the user's project and resolves the
5
+ * corresponding provider plugin package. Used by CLI commands to
6
+ * delegate to the correct provider (AWS, Netlify, etc.).
7
+ */
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ /**
12
+ * Load the appDefinition and resolve the provider plugin for CLI commands.
13
+ *
14
+ * This is a lightweight loader for the CLI context — it reads the backend
15
+ * index.js directly (no need for the full core app-definition-loader which
16
+ * searches for the nearest backend package.json at runtime).
17
+ *
18
+ * @param {Object} [options]
19
+ * @param {string} [options.cwd] - Working directory (default: process.cwd())
20
+ * @returns {{ appDefinition: Object, provider: Object, providerName: string } | null}
21
+ * Returns null if no appDefinition is found (caller should fall back to default behavior).
22
+ */
23
+ function loadProviderForCli(options = {}) {
24
+ const cwd = options.cwd || process.cwd();
25
+ const appDefinition = loadCliAppDefinition(cwd);
26
+
27
+ if (!appDefinition) {
28
+ return null;
29
+ }
30
+
31
+ const providerName = appDefinition.provider || 'aws';
32
+
33
+ // For 'aws', return null provider — CLI commands fall back to existing behavior
34
+ if (providerName === 'aws') {
35
+ return { appDefinition, provider: null, providerName: 'aws' };
36
+ }
37
+
38
+ // For other providers, resolve the package
39
+ const {
40
+ resolveProvider,
41
+ } = require('@friggframework/core/providers/resolve-provider');
42
+
43
+ const provider = resolveProvider(appDefinition);
44
+ return { appDefinition, provider, providerName };
45
+ }
46
+
47
+ /**
48
+ * Load the appDefinition from the backend directory.
49
+ * Tries backend/index.js then index.js in the current directory.
50
+ *
51
+ * @param {string} cwd
52
+ * @returns {Object|null}
53
+ */
54
+ function loadCliAppDefinition(cwd) {
55
+ // Try backend/index.js first (standard Frigg app structure)
56
+ const backendIndexPath = path.join(cwd, 'backend', 'index.js');
57
+ const rootIndexPath = path.join(cwd, 'index.js');
58
+
59
+ for (const indexPath of [backendIndexPath, rootIndexPath]) {
60
+ if (fs.existsSync(indexPath)) {
61
+ try {
62
+ const exported = require(indexPath);
63
+ if (exported.Definition) {
64
+ return exported.Definition;
65
+ }
66
+ } catch {
67
+ // Failed to load, try next
68
+ }
69
+ }
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ module.exports = { loadProviderForCli, loadCliAppDefinition };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.545.88cf983.0",
4
+ "version": "2.0.0--canary.545.c870571.0",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -25,9 +25,9 @@
25
25
  "@babel/eslint-parser": "^7.18.9",
26
26
  "@babel/parser": "^7.25.3",
27
27
  "@babel/traverse": "^7.25.3",
28
- "@friggframework/core": "2.0.0--canary.545.88cf983.0",
29
- "@friggframework/schemas": "2.0.0--canary.545.88cf983.0",
30
- "@friggframework/test": "2.0.0--canary.545.88cf983.0",
28
+ "@friggframework/core": "2.0.0--canary.545.c870571.0",
29
+ "@friggframework/schemas": "2.0.0--canary.545.c870571.0",
30
+ "@friggframework/test": "2.0.0--canary.545.c870571.0",
31
31
  "@hapi/boom": "^10.0.1",
32
32
  "@inquirer/prompts": "^5.3.8",
33
33
  "axios": "^1.7.2",
@@ -55,8 +55,8 @@
55
55
  "validate-npm-package-name": "^5.0.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@friggframework/eslint-config": "2.0.0--canary.545.88cf983.0",
59
- "@friggframework/prettier-config": "2.0.0--canary.545.88cf983.0",
58
+ "@friggframework/eslint-config": "2.0.0--canary.545.c870571.0",
59
+ "@friggframework/prettier-config": "2.0.0--canary.545.c870571.0",
60
60
  "aws-sdk-client-mock": "^4.1.0",
61
61
  "aws-sdk-client-mock-jest": "^4.1.0",
62
62
  "exit-x": "^0.2.2",
@@ -89,5 +89,5 @@
89
89
  "publishConfig": {
90
90
  "access": "public"
91
91
  },
92
- "gitHead": "88cf983e3f46da0d15dc926fcc0b2a18344bd5f6"
92
+ "gitHead": "c8705715da2a28da19d7543735088d87660d0655"
93
93
  }