@aifabrix/builder 2.2.0 → 2.3.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.
package/lib/cli.js CHANGED
@@ -21,6 +21,7 @@ const chalk = require('chalk');
21
21
  const logger = require('./utils/logger');
22
22
  const { validateCommand, handleCommandError } = require('./utils/cli-utils');
23
23
  const { handleLogin } = require('./commands/login');
24
+ const { handleSecure } = require('./commands/secure');
24
25
 
25
26
  /**
26
27
  * Sets up all CLI commands on the Commander program instance
@@ -335,18 +336,19 @@ function setupCommands(program) {
335
336
  // Commander.js converts --set-id to setId in options object
336
337
  const setIdValue = options.setId || options['set-id'];
337
338
  if (setIdValue) {
338
- const id = parseInt(setIdValue, 10);
339
- if (isNaN(id) || id < 0) {
340
- throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
339
+ const digitsOnly = /^[0-9]+$/.test(setIdValue);
340
+ if (!digitsOnly) {
341
+ throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
341
342
  }
342
- await config.setDeveloperId(id);
343
- process.env.AIFABRIX_DEVELOPERID = id.toString();
344
- logger.log(chalk.green(`✓ Developer ID set to ${id}`));
343
+ // Convert to number for setDeveloperId and getDevPorts (they expect numbers)
344
+ const devIdNum = parseInt(setIdValue, 10);
345
+ await config.setDeveloperId(devIdNum);
346
+ process.env.AIFABRIX_DEVELOPERID = setIdValue;
347
+ logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
345
348
  // Use the ID we just set instead of reading from file to avoid race conditions
346
- const devId = id;
347
- const ports = devConfig.getDevPorts(devId);
349
+ const ports = devConfig.getDevPorts(devIdNum);
348
350
  logger.log('\n🔧 Developer Configuration\n');
349
- logger.log(`Developer ID: ${devId}`);
351
+ logger.log(`Developer ID: ${setIdValue}`);
350
352
  logger.log('\nPorts:');
351
353
  logger.log(` App: ${ports.app}`);
352
354
  logger.log(` Postgres: ${ports.postgres}`);
@@ -358,7 +360,9 @@ function setupCommands(program) {
358
360
  }
359
361
 
360
362
  const devId = await config.getDeveloperId();
361
- const ports = devConfig.getDevPorts(devId);
363
+ // Convert string developer ID to number for getDevPorts
364
+ const devIdNum = parseInt(devId, 10);
365
+ const ports = devConfig.getDevPorts(devIdNum);
362
366
  logger.log('\n🔧 Developer Configuration\n');
363
367
  logger.log(`Developer ID: ${devId}`);
364
368
  logger.log('\nPorts:');
@@ -373,6 +377,19 @@ function setupCommands(program) {
373
377
  process.exit(1);
374
378
  }
375
379
  });
380
+
381
+ // Security command
382
+ program.command('secure')
383
+ .description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
384
+ .option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
385
+ .action(async(options) => {
386
+ try {
387
+ await handleSecure(options);
388
+ } catch (error) {
389
+ handleCommandError(error, 'secure');
390
+ process.exit(1);
391
+ }
392
+ });
376
393
  }
377
394
 
378
395
  module.exports = {
@@ -0,0 +1,242 @@
1
+ /**
2
+ * AI Fabrix Builder - Secure Command
3
+ *
4
+ * Handles encryption of secrets in secrets.local.yaml files
5
+ * Sets encryption key in config.yaml and encrypts all secret values
6
+ *
7
+ * @fileoverview Secure command implementation for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const yaml = require('js-yaml');
16
+ const inquirer = require('inquirer');
17
+ const chalk = require('chalk');
18
+ const logger = require('../utils/logger');
19
+ const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
20
+ const { validateEncryptionKey } = require('../utils/secrets-encryption');
21
+ const { encryptYamlValues } = require('../utils/yaml-preserve');
22
+
23
+ /**
24
+ * Finds all secrets.local.yaml files to encrypt
25
+ * Includes user secrets file and build secrets from all apps
26
+ *
27
+ * @async
28
+ * @function findSecretsFiles
29
+ * @returns {Promise<Array<{path: string, type: string}>>} Array of secrets file paths
30
+ */
31
+ async function findSecretsFiles() {
32
+ const files = [];
33
+
34
+ // User's secrets file
35
+ const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
36
+ if (fs.existsSync(userSecretsPath)) {
37
+ files.push({ path: userSecretsPath, type: 'user' });
38
+ }
39
+
40
+ // Find all apps and check for build.secrets
41
+ // Scan builder directory for apps
42
+ try {
43
+ const builderDir = path.join(process.cwd(), 'builder');
44
+ if (fs.existsSync(builderDir)) {
45
+ const entries = fs.readdirSync(builderDir, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ if (entry.isDirectory()) {
48
+ const appName = entry.name;
49
+ const variablesPath = path.join(builderDir, appName, 'variables.yaml');
50
+ if (fs.existsSync(variablesPath)) {
51
+ try {
52
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
53
+ const variables = yaml.load(variablesContent);
54
+
55
+ if (variables?.build?.secrets) {
56
+ const buildSecretsPath = path.resolve(
57
+ path.dirname(variablesPath),
58
+ variables.build.secrets
59
+ );
60
+ if (fs.existsSync(buildSecretsPath)) {
61
+ files.push({ path: buildSecretsPath, type: `app:${appName}` });
62
+ }
63
+ }
64
+ } catch (error) {
65
+ // Ignore errors, continue
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ } catch (error) {
72
+ // Ignore errors, continue
73
+ }
74
+
75
+ // Check config.yaml for general secrets-path
76
+ try {
77
+ const { getSecretsPath } = require('../config');
78
+ const generalSecretsPath = await getSecretsPath();
79
+ if (generalSecretsPath) {
80
+ const resolvedPath = path.isAbsolute(generalSecretsPath)
81
+ ? generalSecretsPath
82
+ : path.resolve(process.cwd(), generalSecretsPath);
83
+ if (fs.existsSync(resolvedPath) && !files.some(f => f.path === resolvedPath)) {
84
+ files.push({ path: resolvedPath, type: 'general' });
85
+ }
86
+ }
87
+ } catch (error) {
88
+ // Ignore errors, continue
89
+ }
90
+
91
+ return files;
92
+ }
93
+
94
+ /**
95
+ * Encrypts all non-encrypted values in a secrets file
96
+ * Preserves YAML structure, comments, and formatting
97
+ * Skips URLs (http:// and https://) as they are not secrets
98
+ *
99
+ * @async
100
+ * @function encryptSecretsFile
101
+ * @param {string} filePath - Path to secrets file
102
+ * @param {string} encryptionKey - Encryption key
103
+ * @returns {Promise<{encrypted: number, total: number}>} Count of encrypted values
104
+ */
105
+ async function encryptSecretsFile(filePath, encryptionKey) {
106
+ const content = fs.readFileSync(filePath, 'utf8');
107
+
108
+ // Validate that file contains valid YAML structure (optional check)
109
+ try {
110
+ const secrets = yaml.load(content);
111
+ if (!secrets || typeof secrets !== 'object') {
112
+ throw new Error(`Invalid secrets file format: ${filePath}`);
113
+ }
114
+ } catch (error) {
115
+ // If YAML parsing fails, still try to encrypt (might have syntax issues but could be fixable)
116
+ // The line-by-line parser will handle it gracefully
117
+ }
118
+
119
+ // Use line-by-line encryption to preserve comments and formatting
120
+ const result = encryptYamlValues(content, encryptionKey);
121
+
122
+ // Write back to file preserving all formatting
123
+ fs.writeFileSync(filePath, result.content, { mode: 0o600 });
124
+
125
+ return { encrypted: result.encrypted, total: result.total };
126
+ }
127
+
128
+ /**
129
+ * Prompt for encryption key if not provided
130
+ *
131
+ * @async
132
+ * @function promptForEncryptionKey
133
+ * @returns {Promise<string>} Encryption key
134
+ */
135
+ async function promptForEncryptionKey() {
136
+ const answer = await inquirer.prompt([{
137
+ type: 'password',
138
+ name: 'key',
139
+ message: 'Enter encryption key (32 bytes, hex or base64):',
140
+ mask: '*',
141
+ validate: (input) => {
142
+ if (!input || input.trim().length === 0) {
143
+ return 'Encryption key is required';
144
+ }
145
+ try {
146
+ validateEncryptionKey(input.trim());
147
+ return true;
148
+ } catch (error) {
149
+ return error.message;
150
+ }
151
+ }
152
+ }]);
153
+
154
+ return answer.key.trim();
155
+ }
156
+
157
+ /**
158
+ * Handle secure command action
159
+ * Sets encryption key and encrypts all secrets files
160
+ *
161
+ * @async
162
+ * @function handleSecure
163
+ * @param {Object} options - Command options
164
+ * @param {string} [options.secretsEncryption] - Encryption key (optional, will prompt if not provided)
165
+ * @returns {Promise<void>} Resolves when encryption completes
166
+ * @throws {Error} If encryption fails
167
+ */
168
+ async function handleSecure(options) {
169
+ logger.log(chalk.blue('\n🔐 Securing secrets files...\n'));
170
+
171
+ // Get or prompt for encryption key
172
+ let encryptionKey = options.secretsEncryption || options['secrets-encryption'];
173
+ if (!encryptionKey) {
174
+ // Check if key already exists in config
175
+ const existingKey = await getSecretsEncryptionKey();
176
+ if (existingKey) {
177
+ logger.log(chalk.yellow('⚠️ Encryption key already configured in config.yaml'));
178
+ const useExisting = await inquirer.prompt([{
179
+ type: 'confirm',
180
+ name: 'use',
181
+ message: 'Use existing encryption key?',
182
+ default: true
183
+ }]);
184
+ if (useExisting.use) {
185
+ encryptionKey = existingKey;
186
+ } else {
187
+ encryptionKey = await promptForEncryptionKey();
188
+ await setSecretsEncryptionKey(encryptionKey);
189
+ logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
190
+ }
191
+ } else {
192
+ encryptionKey = await promptForEncryptionKey();
193
+ await setSecretsEncryptionKey(encryptionKey);
194
+ logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
195
+ }
196
+ } else {
197
+ // Validate and save the provided key
198
+ validateEncryptionKey(encryptionKey);
199
+ await setSecretsEncryptionKey(encryptionKey);
200
+ logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
201
+ }
202
+
203
+ // Find all secrets files
204
+ const secretsFiles = await findSecretsFiles();
205
+
206
+ if (secretsFiles.length === 0) {
207
+ logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
208
+ logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml'));
209
+ return;
210
+ }
211
+
212
+ logger.log(chalk.gray(`Found ${secretsFiles.length} secrets file(s) to process:\n`));
213
+
214
+ // Encrypt each file
215
+ let totalEncrypted = 0;
216
+ let totalValues = 0;
217
+
218
+ for (const file of secretsFiles) {
219
+ try {
220
+ logger.log(chalk.gray(`Processing: ${file.path} (${file.type})`));
221
+ const result = await encryptSecretsFile(file.path, encryptionKey);
222
+ totalEncrypted += result.encrypted;
223
+ totalValues += result.total;
224
+
225
+ if (result.encrypted > 0) {
226
+ logger.log(chalk.green(` ✓ Encrypted ${result.encrypted} of ${result.total} values`));
227
+ } else {
228
+ logger.log(chalk.gray(` - All values already encrypted (${result.total} total)`));
229
+ }
230
+ } catch (error) {
231
+ logger.log(chalk.red(` ✗ Error: ${error.message}`));
232
+ }
233
+ }
234
+
235
+ logger.log(chalk.green('\n✅ Encryption complete!'));
236
+ logger.log(chalk.gray(` Files processed: ${secretsFiles.length}`));
237
+ logger.log(chalk.gray(` Values encrypted: ${totalEncrypted} of ${totalValues} total`));
238
+ logger.log(chalk.gray(' Encryption key stored in: ~/.aifabrix/config.yaml\n'));
239
+ }
240
+
241
+ module.exports = { handleSecure };
242
+
package/lib/config.js CHANGED
@@ -35,10 +35,24 @@ async function getConfig() {
35
35
  config = {};
36
36
  }
37
37
 
38
- // Ensure developerId defaults to 0 if not set
39
- if (typeof config['developer-id'] === 'undefined') {
40
- config['developer-id'] = 0;
38
+ // Ensure developer-id exists as a digit-only string (default "0") and validate
39
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
40
+ if (typeof config['developer-id'] === 'undefined' || config['developer-id'] === null) {
41
+ config['developer-id'] = '0';
42
+ } else if (typeof config['developer-id'] === 'number') {
43
+ // Convert numeric to string to preserve type consistency
44
+ if (config['developer-id'] < 0 || !Number.isFinite(config['developer-id'])) {
45
+ throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must be a non-negative digit string.`);
46
+ }
47
+ config['developer-id'] = String(config['developer-id']);
48
+ } else if (typeof config['developer-id'] === 'string') {
49
+ if (!DEV_ID_DIGITS_REGEX.test(config['developer-id'])) {
50
+ throw new Error(`Invalid developer-id value: "${config['developer-id']}". Must contain only digits 0-9.`);
51
+ }
52
+ } else {
53
+ throw new Error(`Invalid developer-id value type: ${typeof config['developer-id']}. Must be a non-negative digit string.`);
41
54
  }
55
+
42
56
  // Ensure environment defaults to 'dev' if not set
43
57
  if (typeof config.environment === 'undefined') {
44
58
  config.environment = 'dev';
@@ -51,15 +65,15 @@ async function getConfig() {
51
65
  if (typeof config.device !== 'object' || config.device === null) {
52
66
  config.device = {};
53
67
  }
54
- // Cache developer ID as property for easy access (use nullish coalescing to allow 0)
55
- cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : 0;
68
+ // Cache developer ID as property for easy access (string, default "0")
69
+ cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : '0';
56
70
  return config;
57
71
  } catch (error) {
58
72
  if (error.code === 'ENOENT') {
59
- // Default developer ID is 0, default environment is 'dev'
60
- cachedDeveloperId = 0;
73
+ // Default developer ID is "0", default environment is 'dev'
74
+ cachedDeveloperId = '0';
61
75
  return {
62
- 'developer-id': 0,
76
+ 'developer-id': '0',
63
77
  environment: 'dev',
64
78
  environments: {},
65
79
  device: {}
@@ -80,7 +94,8 @@ async function saveConfig(data) {
80
94
  await fs.mkdir(CONFIG_DIR, { recursive: true });
81
95
 
82
96
  // Set secure permissions
83
- const configContent = yaml.dump(data);
97
+ // Force quotes to ensure numeric-like strings (e.g., "01") remain strings in YAML
98
+ const configContent = yaml.dump(data, { forceQuotes: true });
84
99
  // Write file first
85
100
  await fs.writeFile(CONFIG_FILE, configContent, {
86
101
  mode: 0o600,
@@ -117,7 +132,7 @@ async function clearConfig() {
117
132
  * Get developer ID from configuration
118
133
  * Loads config if not already cached, then returns cached developer ID
119
134
  * Developer ID: 0 = default infra, > 0 = developer-specific
120
- * @returns {Promise<number>} Developer ID (defaults to 0)
135
+ * @returns {Promise<string>} Developer ID as string (defaults to "0")
121
136
  */
122
137
  async function getDeveloperId() {
123
138
  // Always reload from file to ensure we have the latest value
@@ -130,21 +145,33 @@ async function getDeveloperId() {
130
145
 
131
146
  /**
132
147
  * Set developer ID in configuration
133
- * @param {number} developerId - Developer ID to set (0 = default infra, > 0 = developer-specific)
148
+ * @param {number|string} developerId - Developer ID to set (digit-only string preserved, or number). "0" = default infra, > "0" = developer-specific
134
149
  * @returns {Promise<void>}
135
150
  */
136
151
  async function setDeveloperId(developerId) {
137
- if (typeof developerId !== 'number' || developerId < 0) {
138
- throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
152
+ const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
153
+ let devIdString;
154
+ if (typeof developerId === 'number') {
155
+ if (!Number.isFinite(developerId) || developerId < 0) {
156
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
157
+ }
158
+ devIdString = String(developerId);
159
+ } else if (typeof developerId === 'string') {
160
+ if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
161
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
162
+ }
163
+ devIdString = developerId;
164
+ } else {
165
+ throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
139
166
  }
140
167
  // Clear cache first to ensure we get fresh data from file
141
168
  cachedDeveloperId = null;
142
169
  // Read file directly to avoid any caching issues
143
170
  const config = await getConfig();
144
171
  // Update developer ID
145
- config['developer-id'] = developerId;
172
+ config['developer-id'] = devIdString;
146
173
  // Update cache before saving
147
- cachedDeveloperId = developerId;
174
+ cachedDeveloperId = devIdString;
148
175
  // Save the entire config object to ensure all fields are preserved
149
176
  await saveConfig(config);
150
177
  // Verify the file was saved correctly by reading it back
@@ -154,8 +181,10 @@ async function setDeveloperId(developerId) {
154
181
  // Read file again with fresh file handle to avoid OS caching
155
182
  const savedContent = await fs.readFile(CONFIG_FILE, 'utf8');
156
183
  const savedConfig = yaml.load(savedContent);
157
- if (savedConfig['developer-id'] !== developerId) {
158
- throw new Error(`Failed to save developer ID: expected ${developerId}, got ${savedConfig['developer-id']}. File content: ${savedContent.substring(0, 200)}`);
184
+ // YAML may parse numbers as numbers, so convert to string for comparison
185
+ const savedDevIdString = String(savedConfig['developer-id']);
186
+ if (savedDevIdString !== devIdString) {
187
+ throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
159
188
  }
160
189
  // Clear the cache to force reload from file on next getDeveloperId() call
161
190
  // This ensures we get the value that was actually saved to disk
@@ -288,7 +317,7 @@ async function saveClientToken(environment, appName, controllerUrl, token, expir
288
317
  /**
289
318
  * Initialize and load developer ID
290
319
  * Call this to ensure developerId is loaded and cached
291
- * @returns {Promise<number>} Developer ID
320
+ * @returns {Promise<string>} Developer ID (string)
292
321
  */
293
322
  async function loadDeveloperId() {
294
323
  if (cachedDeveloperId === null) {
@@ -297,6 +326,60 @@ async function loadDeveloperId() {
297
326
  return cachedDeveloperId;
298
327
  }
299
328
 
329
+ /**
330
+ * Get secrets encryption key from configuration
331
+ * @returns {Promise<string|null>} Encryption key or null if not set
332
+ */
333
+ async function getSecretsEncryptionKey() {
334
+ const config = await getConfig();
335
+ return config['secrets-encryption'] || null;
336
+ }
337
+
338
+ /**
339
+ * Set secrets encryption key in configuration
340
+ * @param {string} key - Encryption key (32 bytes, hex or base64)
341
+ * @returns {Promise<void>}
342
+ * @throws {Error} If key format is invalid
343
+ */
344
+ async function setSecretsEncryptionKey(key) {
345
+ if (!key || typeof key !== 'string') {
346
+ throw new Error('Encryption key is required and must be a string');
347
+ }
348
+
349
+ // Validate key format using encryption utilities
350
+ const { validateEncryptionKey } = require('./utils/secrets-encryption');
351
+ validateEncryptionKey(key);
352
+
353
+ const config = await getConfig();
354
+ config['secrets-encryption'] = key;
355
+ await saveConfig(config);
356
+ }
357
+
358
+ /**
359
+ * Get general secrets path from configuration
360
+ * Used as fallback when build.secrets is not set in variables.yaml
361
+ * @returns {Promise<string|null>} Secrets path or null if not set
362
+ */
363
+ async function getSecretsPath() {
364
+ const config = await getConfig();
365
+ return config['secrets-path'] || null;
366
+ }
367
+
368
+ /**
369
+ * Set general secrets path in configuration
370
+ * @param {string} secretsPath - Path to general secrets file
371
+ * @returns {Promise<void>}
372
+ */
373
+ async function setSecretsPath(secretsPath) {
374
+ if (!secretsPath || typeof secretsPath !== 'string') {
375
+ throw new Error('Secrets path is required and must be a string');
376
+ }
377
+
378
+ const config = await getConfig();
379
+ config['secrets-path'] = secretsPath;
380
+ await saveConfig(config);
381
+ }
382
+
300
383
  // Create exports object
301
384
  const exportsObj = {
302
385
  getConfig,
@@ -312,6 +395,10 @@ const exportsObj = {
312
395
  getClientToken,
313
396
  saveDeviceToken,
314
397
  saveClientToken,
398
+ getSecretsEncryptionKey,
399
+ setSecretsEncryptionKey,
400
+ getSecretsPath,
401
+ setSecretsPath,
315
402
  CONFIG_DIR,
316
403
  CONFIG_FILE
317
404
  };
@@ -321,7 +408,7 @@ const exportsObj = {
321
408
  // Developer ID: 0 = default infra, > 0 = developer-specific
322
409
  Object.defineProperty(exportsObj, 'developerId', {
323
410
  get() {
324
- return cachedDeveloperId !== null ? cachedDeveloperId : 0; // Default to 0 if not loaded yet
411
+ return cachedDeveloperId !== null ? cachedDeveloperId : '0'; // Default to "0" if not loaded yet
325
412
  },
326
413
  enumerable: true,
327
414
  configurable: true
package/lib/infra.js CHANGED
@@ -28,21 +28,23 @@ const execAsync = promisify(exec);
28
28
  /**
29
29
  * Gets infrastructure directory name based on developer ID
30
30
  * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
31
- * @param {number} devId - Developer ID
31
+ * @param {number|string} devId - Developer ID
32
32
  * @returns {string} Infrastructure directory name
33
33
  */
34
34
  function getInfraDirName(devId) {
35
- return devId === 0 ? 'infra' : `infra-dev${devId}`;
35
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
36
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
36
37
  }
37
38
 
38
39
  /**
39
40
  * Gets Docker Compose project name based on developer ID
40
41
  * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
41
- * @param {number} devId - Developer ID
42
+ * @param {number|string} devId - Developer ID
42
43
  * @returns {string} Docker Compose project name
43
44
  */
44
45
  function getInfraProjectName(devId) {
45
- return devId === 0 ? 'infra' : `infra-dev${devId}`;
46
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
47
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
46
48
  }
47
49
 
48
50
  // Wrapper to support cwd option
@@ -96,7 +98,10 @@ async function startInfra(developerId = null) {
96
98
 
97
99
  // Get developer ID from parameter or config
98
100
  const devId = developerId || await config.getDeveloperId();
99
- const ports = devConfig.getDevPorts(devId);
101
+ // Convert to number for getDevPorts (it expects numbers)
102
+ const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
103
+ const ports = devConfig.getDevPorts(devIdNum);
104
+ const idNum = devIdNum;
100
105
 
101
106
  // Load compose template (Handlebars)
102
107
  const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
@@ -116,7 +121,7 @@ async function startInfra(developerId = null) {
116
121
  const templateContent = fs.readFileSync(templatePath, 'utf8');
117
122
  const template = handlebars.compile(templateContent);
118
123
  // Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
119
- const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
124
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
120
125
  const composeContent = template({
121
126
  devId: devId,
122
127
  postgresPort: ports.postgres,
@@ -261,7 +266,9 @@ async function checkInfraHealth(devId = null) {
261
266
  */
262
267
  async function getInfraStatus() {
263
268
  const devId = await config.getDeveloperId();
264
- const ports = devConfig.getDevPorts(devId);
269
+ // Convert string developer ID to number for getDevPorts
270
+ const devIdNum = parseInt(devId, 10);
271
+ const ports = devConfig.getDevPorts(devIdNum);
265
272
  const services = {
266
273
  postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
267
274
  redis: { port: ports.redis, url: `localhost:${ports.redis}` },
package/lib/push.js CHANGED
@@ -21,12 +21,35 @@ const execAsync = promisify(exec);
21
21
  * @returns {Promise<boolean>} True if Azure CLI is available
22
22
  */
23
23
  async function checkAzureCLIInstalled() {
24
- try {
25
- await execAsync('az --version');
26
- return true;
27
- } catch (error) {
28
- return false;
24
+ // On Windows, use shell option to ensure proper command resolution
25
+ const options = process.platform === 'win32' ? { shell: true } : {};
26
+
27
+ // Try multiple methods to detect Azure CLI (commands that don't require authentication)
28
+ const commands = process.platform === 'win32'
29
+ ? ['az --version', 'az.cmd --version']
30
+ : ['az --version'];
31
+
32
+ for (const command of commands) {
33
+ try {
34
+ // Use a timeout to avoid hanging if command doesn't exist
35
+ await execAsync(command, { ...options, timeout: 5000 });
36
+ return true;
37
+ } catch (error) {
38
+ // Log the error for debugging (only in development)
39
+ if (process.env.DEBUG) {
40
+ logger.log(chalk.gray(`[DEBUG] Command '${command}' failed: ${error.message}`));
41
+ }
42
+ // Continue to next command if this one fails
43
+ continue;
44
+ }
45
+ }
46
+
47
+ // If all commands failed, Azure CLI is not available
48
+ // Log for debugging if enabled
49
+ if (process.env.DEBUG) {
50
+ logger.log(chalk.gray('[DEBUG] All Azure CLI detection methods failed'));
29
51
  }
52
+ return false;
30
53
  }
31
54
 
32
55
  /**
@@ -103,7 +126,9 @@ function validateRegistryURL(registryUrl) {
103
126
  async function checkACRAuthentication(registry) {
104
127
  try {
105
128
  const registryName = extractRegistryName(registry);
106
- await execAsync(`az acr show --name ${registryName}`);
129
+ // On Windows, use shell option to ensure proper command resolution
130
+ const options = process.platform === 'win32' ? { shell: true } : {};
131
+ await execAsync(`az acr show --name ${registryName}`, options);
107
132
  return true;
108
133
  } catch (error) {
109
134
  return false;
@@ -119,7 +144,9 @@ async function authenticateACR(registry) {
119
144
  try {
120
145
  const registryName = extractRegistryName(registry);
121
146
  logger.log(chalk.blue(`Authenticating with ${registry}...`));
122
- await execAsync(`az acr login --name ${registryName}`);
147
+ // On Windows, use shell option to ensure proper command resolution
148
+ const options = process.platform === 'win32' ? { shell: true } : {};
149
+ await execAsync(`az acr login --name ${registryName}`, options);
123
150
  logger.log(chalk.green(`✓ Authenticated with ${registry}`));
124
151
  } catch (error) {
125
152
  throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);