@norrix/cli 0.0.25 → 0.0.26

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.
@@ -10,15 +10,17 @@ import archiver from 'archiver';
10
10
  import { configureAmplify, loadCliEnvFiles } from './amplify-config.js';
11
11
  import { computeFingerprint, writeRuntimeFingerprintFile } from './fingerprinting.js';
12
12
  import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig } from './config.js';
13
- import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, } from './workspace.js';
13
+ import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, detectNxBuildConfigurations, } from './workspace.js';
14
14
  import { signIn as amplifySignIn, signOut as amplifySignOut, getCurrentUser, fetchAuthSession, } from 'aws-amplify/auth';
15
15
  import crypto from 'crypto';
16
16
  import { Amplify } from 'aws-amplify';
17
17
  import { PROD_DEFAULTS } from './prod-defaults.js';
18
18
  import { DEV_DEFAULTS } from './dev-defaults.js';
19
+ import { clearSelectedOrgId, getSelectedOrgId, setSelectedOrgId } from './cli-settings.js';
19
20
  let CURRENT_ENV = 'prod';
20
21
  let CURRENT_DEFAULTS = PROD_DEFAULTS;
21
22
  let API_URL = PROD_DEFAULTS.apiUrl;
23
+ let CURRENT_ORG_ID;
22
24
  let IS_INITIALIZED = false;
23
25
  function defaultsForEnv(env) {
24
26
  return env === 'dev' ? DEV_DEFAULTS : PROD_DEFAULTS;
@@ -40,6 +42,10 @@ export function initNorrixCli(env = 'prod') {
40
42
  CURRENT_DEFAULTS = defaultsForEnv(env);
41
43
  configureAmplify(env);
42
44
  API_URL = process.env.NORRIX_API_URL || CURRENT_DEFAULTS.apiUrl;
45
+ // Load persisted org selection for this env + API URL profile (if any).
46
+ // Allow per-invocation override via NORRIX_ORG_ID env var.
47
+ const envOrg = (process.env.NORRIX_ORG_ID ?? '').toString().trim();
48
+ CURRENT_ORG_ID = envOrg || getSelectedOrgId(env, API_URL);
43
49
  IS_INITIALIZED = true;
44
50
  }
45
51
  function ensureInitialized() {
@@ -50,19 +56,168 @@ function ensureInitialized() {
50
56
  /**
51
57
  * Return Authorization header containing the current Cognito ID token (if signed in).
52
58
  */
53
- async function getAuthHeaders() {
59
+ async function getAuthHeaders(options) {
54
60
  ensureInitialized();
61
+ const headers = {};
55
62
  try {
56
63
  const session = await fetchAuthSession();
57
64
  const idToken = session.tokens?.idToken?.toString();
58
65
  if (idToken) {
59
- return { Authorization: `Bearer ${idToken}` };
66
+ headers.Authorization = `Bearer ${idToken}`;
60
67
  }
61
68
  }
62
69
  catch (_) {
63
70
  /* not signed in */
64
71
  }
65
- return {};
72
+ if (options?.includeOrg !== false && CURRENT_ORG_ID) {
73
+ headers['X-Norrix-Org-Id'] = CURRENT_ORG_ID;
74
+ }
75
+ return headers;
76
+ }
77
+ function setCurrentOrgId(orgId) {
78
+ const v = (orgId ?? '').toString().trim();
79
+ CURRENT_ORG_ID = v ? v : undefined;
80
+ }
81
+ async function fetchOrganizations(verbose = false) {
82
+ ensureInitialized();
83
+ try {
84
+ const res = await axios.get(`${API_URL}/orgs`, {
85
+ headers: await getAuthHeaders({ includeOrg: false }),
86
+ });
87
+ const organizations = Array.isArray(res.data?.organizations)
88
+ ? res.data.organizations
89
+ : [];
90
+ const selectedOrganizationId = res.data?.selectedOrganizationId
91
+ ? String(res.data.selectedOrganizationId)
92
+ : undefined;
93
+ return { organizations, selectedOrganizationId };
94
+ }
95
+ catch (err) {
96
+ if (verbose) {
97
+ console.error('--- Verbose error details (orgs fetch) ---');
98
+ console.error(err);
99
+ if (err?.response) {
100
+ console.error('Axios response status:', err.response.status);
101
+ console.error('Axios response data:', err.response.data);
102
+ }
103
+ }
104
+ throw err;
105
+ }
106
+ }
107
+ async function ensureOrgSelected(params) {
108
+ ensureInitialized();
109
+ const explicit = (params.orgIdArg ?? process.env.NORRIX_ORG_ID ?? '')
110
+ .toString()
111
+ .trim();
112
+ if (explicit) {
113
+ setCurrentOrgId(explicit);
114
+ return explicit;
115
+ }
116
+ const stored = getSelectedOrgId(CURRENT_ENV, API_URL);
117
+ if (!params.requireSelection) {
118
+ if (stored)
119
+ setCurrentOrgId(stored);
120
+ return stored;
121
+ }
122
+ // Validate stored selection (and discover orgs for prompting).
123
+ const { organizations } = await fetchOrganizations(Boolean(params.verbose));
124
+ const normalizedOrgs = organizations.filter((o) => o && o.id);
125
+ if (stored && normalizedOrgs.some((o) => o.id === stored)) {
126
+ setCurrentOrgId(stored);
127
+ return stored;
128
+ }
129
+ if (stored) {
130
+ clearSelectedOrgId(CURRENT_ENV, API_URL);
131
+ }
132
+ if (normalizedOrgs.length === 1) {
133
+ const only = normalizedOrgs[0];
134
+ setSelectedOrgId(CURRENT_ENV, API_URL, only.id);
135
+ setCurrentOrgId(only.id);
136
+ return only.id;
137
+ }
138
+ if (params.nonInteractive) {
139
+ throw new Error('No organization selected. Use --org <orgId> or run `norrix orgs select`.');
140
+ }
141
+ if (!normalizedOrgs.length) {
142
+ throw new Error('No organizations found for this user.');
143
+ }
144
+ const choices = normalizedOrgs.map((o) => {
145
+ const suffix = o.id.length > 8 ? o.id.slice(-8) : o.id;
146
+ return {
147
+ name: `${o.name} (${o.role}) • …${suffix}`,
148
+ value: o.id,
149
+ };
150
+ });
151
+ const { orgId } = await inquirer.prompt([
152
+ {
153
+ type: 'list',
154
+ name: 'orgId',
155
+ message: params.promptMessage || 'Select organization:',
156
+ choices,
157
+ },
158
+ ]);
159
+ const selected = String(orgId || '').trim();
160
+ if (!selected) {
161
+ throw new Error('Organization selection cancelled.');
162
+ }
163
+ setSelectedOrgId(CURRENT_ENV, API_URL, selected);
164
+ setCurrentOrgId(selected);
165
+ return selected;
166
+ }
167
+ export async function orgsList(verbose = false) {
168
+ ensureInitialized();
169
+ try {
170
+ const { organizations, selectedOrganizationId } = await fetchOrganizations(verbose);
171
+ if (!organizations.length) {
172
+ console.log('No organizations found.');
173
+ return;
174
+ }
175
+ console.log('Organizations:');
176
+ for (const o of organizations) {
177
+ const selectedMark = selectedOrganizationId && o.id === selectedOrganizationId ? ' (selected)' : '';
178
+ console.log(`- ${o.name} [${o.role}] ${o.id}${selectedMark}`);
179
+ }
180
+ }
181
+ catch (err) {
182
+ ora().fail(`Failed to list organizations: ${err?.message || err}`);
183
+ if (verbose && err?.response) {
184
+ console.error('Axios response status:', err.response.status);
185
+ console.error('Axios response data:', err.response.data);
186
+ }
187
+ }
188
+ }
189
+ export async function orgsSelect(verbose = false) {
190
+ ensureInitialized();
191
+ try {
192
+ await ensureOrgSelected({
193
+ requireSelection: true,
194
+ nonInteractive: false,
195
+ verbose,
196
+ promptMessage: 'Select default organization for this environment:',
197
+ });
198
+ if (CURRENT_ORG_ID) {
199
+ console.log(`✅ Selected organization: ${CURRENT_ORG_ID}`);
200
+ }
201
+ }
202
+ catch (err) {
203
+ ora().fail(`Failed to select organization: ${err?.message || err}`);
204
+ if (verbose && err?.response) {
205
+ console.error('Axios response status:', err.response.status);
206
+ console.error('Axios response data:', err.response.data);
207
+ }
208
+ }
209
+ }
210
+ export async function orgsCurrent() {
211
+ ensureInitialized();
212
+ const envOrg = (process.env.NORRIX_ORG_ID ?? '').toString().trim();
213
+ const stored = getSelectedOrgId(CURRENT_ENV, API_URL);
214
+ const current = envOrg || stored;
215
+ if (!current) {
216
+ console.log('No default organization selected for this environment.');
217
+ console.log('Run `norrix orgs select` or pass `--org <orgId>`.');
218
+ return;
219
+ }
220
+ console.log(`Current organization for ${CURRENT_ENV} (${API_URL}): ${current}`);
66
221
  }
67
222
  /**
68
223
  * Norrix CLI Command Implementations
@@ -80,6 +235,16 @@ async function getAuthHeaders() {
80
235
  // Get dirname equivalent in ESM
81
236
  const __filename = fileURLToPath(import.meta.url);
82
237
  const __dirname = path.dirname(__filename);
238
+ /**
239
+ * Safely trim a string value, returning undefined for empty/null values.
240
+ * Handles both CLI args and config file values to ensure no trailing/leading whitespace.
241
+ */
242
+ function trimString(input) {
243
+ if (input == null)
244
+ return undefined;
245
+ const trimmed = String(input).trim();
246
+ return trimmed || undefined;
247
+ }
83
248
  function normalizePath(input) {
84
249
  if (!input)
85
250
  return undefined;
@@ -715,8 +880,7 @@ async function resolveWorkspaceContext(projectArg, spinner) {
715
880
  * For Nx workspaces, this includes the app, dependent libs, and workspace config files.
716
881
  * For standalone projects, this zips the current directory.
717
882
  */
718
- async function zipProject(projectName, isUpdate = false, verbose = false) {
719
- const workspaceCtx = detectWorkspaceContext();
883
+ async function zipProject(projectName, workspaceCtx, isUpdate = false, verbose = false) {
720
884
  if (workspaceCtx.type === 'nx') {
721
885
  return zipWorkspaceProject(projectName, workspaceCtx, isUpdate, verbose);
722
886
  }
@@ -813,16 +977,39 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
813
977
  });
814
978
  archive.pipe(output);
815
979
  logWorkspaceContext(workspaceCtx, verbose);
816
- // Get workspace dependencies using Nx CLI (preferred) or fallback
980
+ // Get workspace dependencies using Nx CLI (preferred) with fallback supplementation
817
981
  let deps;
818
982
  if (workspaceCtx.projectName) {
819
- deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose);
983
+ deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose, workspaceCtx.appRoot // Pass appRoot for webpack alias detection
984
+ );
985
+ }
986
+ // Always supplement with fallback detection to catch anything Nx might miss
987
+ // (e.g., dynamic imports, SCSS dependencies, transitive deps from source scanning)
988
+ const fallbackDeps = getWorkspaceDependenciesFallback(workspaceCtx, verbose);
989
+ if (deps) {
990
+ // Merge fallback libs into Nx-detected libs
991
+ const mergedLibPaths = new Set(deps.libPaths);
992
+ for (const libPath of fallbackDeps.libPaths) {
993
+ if (!mergedLibPaths.has(libPath)) {
994
+ mergedLibPaths.add(libPath);
995
+ if (verbose) {
996
+ console.log(`[workspace] Fallback added additional lib: ${libPath}`);
997
+ }
998
+ }
999
+ }
1000
+ deps.libPaths = Array.from(mergedLibPaths);
1001
+ // Also merge local file deps
1002
+ const mergedLocalFileDeps = new Set(deps.localFileDeps);
1003
+ for (const dep of fallbackDeps.localFileDeps) {
1004
+ mergedLocalFileDeps.add(dep);
1005
+ }
1006
+ deps.localFileDeps = Array.from(mergedLocalFileDeps);
820
1007
  }
821
- if (!deps) {
1008
+ else {
822
1009
  if (verbose) {
823
- console.log('[workspace] Using fallback dependency detection');
1010
+ console.log('[workspace] Using fallback dependency detection (Nx CLI not available)');
824
1011
  }
825
- deps = getWorkspaceDependenciesFallback(workspaceCtx, verbose);
1012
+ deps = fallbackDeps;
826
1013
  }
827
1014
  // Create manifest for CI
828
1015
  const manifest = createWorkspaceManifest(workspaceCtx, deps);
@@ -909,6 +1096,68 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
909
1096
  archive.directory(absoluteAssetPath, assetPath);
910
1097
  }
911
1098
  }
1099
+ // 6. Add local file dependencies (file: protocol paths from package.json)
1100
+ // Skip any that are already covered by libPaths to avoid duplicate entries
1101
+ if (deps.localFileDeps && deps.localFileDeps.length > 0) {
1102
+ const addedDirs = new Set();
1103
+ // Filter out local deps that are subdirectories of already-added libs
1104
+ const filteredLocalDeps = deps.localFileDeps.filter(localDep => {
1105
+ // Check if this local dep is inside any of the lib paths
1106
+ for (const libPath of deps.libPaths) {
1107
+ if (localDep.startsWith(libPath + '/') || localDep === libPath) {
1108
+ if (verbose) {
1109
+ console.log(` - ${localDep} (skipped, covered by ${libPath})`);
1110
+ }
1111
+ return false;
1112
+ }
1113
+ }
1114
+ return true;
1115
+ });
1116
+ if (filteredLocalDeps.length > 0) {
1117
+ console.log(`Adding ${filteredLocalDeps.length} local file dependencies`);
1118
+ }
1119
+ for (const localDep of filteredLocalDeps) {
1120
+ const absoluteLocalPath = path.join(workspaceCtx.workspaceRoot, localDep);
1121
+ if (fs.existsSync(absoluteLocalPath)) {
1122
+ const stat = fs.statSync(absoluteLocalPath);
1123
+ if (stat.isFile()) {
1124
+ // For files, ensure the parent directory structure is maintained
1125
+ if (verbose) {
1126
+ console.log(` - ${localDep} (file)`);
1127
+ }
1128
+ archive.file(absoluteLocalPath, { name: localDep });
1129
+ // Also add the directory if it hasn't been added yet (for other potential files)
1130
+ const parentDir = path.dirname(localDep);
1131
+ if (parentDir && parentDir !== '.' && !addedDirs.has(parentDir)) {
1132
+ // We just add the file, not the whole directory
1133
+ }
1134
+ }
1135
+ else if (stat.isDirectory()) {
1136
+ // Skip if this directory or a parent is already in libPaths
1137
+ const alreadyCovered = deps.libPaths.some(libPath => localDep.startsWith(libPath + '/') || libPath.startsWith(localDep + '/'));
1138
+ if (alreadyCovered) {
1139
+ if (verbose) {
1140
+ console.log(` - ${localDep} (skipped, overlaps with libPaths)`);
1141
+ }
1142
+ continue;
1143
+ }
1144
+ if (verbose) {
1145
+ console.log(` - ${localDep} (directory)`);
1146
+ }
1147
+ archive.directory(absoluteLocalPath, localDep, (entry) => {
1148
+ if (entry.name.includes('node_modules')) {
1149
+ return false;
1150
+ }
1151
+ return entry;
1152
+ });
1153
+ addedDirs.add(localDep);
1154
+ }
1155
+ }
1156
+ else if (verbose) {
1157
+ console.log(` - ${localDep} (not found, skipping)`);
1158
+ }
1159
+ }
1160
+ }
912
1161
  archive.finalize();
913
1162
  });
914
1163
  }
@@ -921,6 +1170,23 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
921
1170
  // Normalize options - support both new object and legacy string projectArg
922
1171
  const opts = typeof options === 'string' ? { project: options } : (options || {});
923
1172
  ensureInitialized();
1173
+ try {
1174
+ await ensureOrgSelected({
1175
+ orgIdArg: opts.org,
1176
+ nonInteractive: opts.nonInteractive,
1177
+ requireSelection: true,
1178
+ verbose,
1179
+ promptMessage: 'Select organization for this build:',
1180
+ });
1181
+ }
1182
+ catch (error) {
1183
+ ora().fail(`Organization selection failed: ${error?.message || error}`);
1184
+ if (verbose && error?.response) {
1185
+ console.error('Axios response status:', error.response.status);
1186
+ console.error('Axios response data:', error.response.data);
1187
+ }
1188
+ return;
1189
+ }
924
1190
  let spinner;
925
1191
  let originalCwd;
926
1192
  try {
@@ -971,6 +1237,31 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
971
1237
  ]);
972
1238
  configuration = answer.configuration;
973
1239
  }
1240
+ // 2.2 Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
1241
+ let nxConfiguration = opts.nxConfiguration;
1242
+ if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
1243
+ const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
1244
+ if (nxConfigs && nxConfigs.configurations.length > 0) {
1245
+ const choices = nxConfigs.configurations.map((c) => ({
1246
+ name: c === nxConfigs.defaultConfiguration ? `${c} (default)` : c,
1247
+ value: c,
1248
+ }));
1249
+ // Add option to skip/use default
1250
+ choices.unshift({ name: '(none - use defaults)', value: '' });
1251
+ const { chosenNxConfig } = await inquirer.prompt([
1252
+ {
1253
+ type: 'list',
1254
+ name: 'chosenNxConfig',
1255
+ message: 'Nx build configuration (environment):',
1256
+ choices,
1257
+ default: nxConfigs.defaultConfiguration || '',
1258
+ },
1259
+ ]);
1260
+ nxConfiguration = chosenNxConfig || undefined;
1261
+ }
1262
+ }
1263
+ // Store resolved nxConfiguration back to opts for later use
1264
+ opts.nxConfiguration = nxConfiguration;
974
1265
  const normalizeIosDistribution = (input) => {
975
1266
  const v = String(input ?? '')
976
1267
  .trim()
@@ -1098,94 +1389,147 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1098
1389
  if (configuration === 'release') {
1099
1390
  spinner.stop();
1100
1391
  if (platform === 'ios') {
1101
- // Determine Team ID from: CLI flag > config file > prompt
1392
+ // Resolve values from: CLI flags > config file (trimmed to handle whitespace)
1102
1393
  const configTeamId = norrixConfig.ios?.teamId;
1103
- const resolvedTeamId = opts.teamId || configTeamId;
1104
- // If we have teamId from CLI or config, and non-interactive mode, skip prompts
1105
- const shouldPromptForTeamId = !resolvedTeamId && !opts.nonInteractive;
1106
- const iosAnswers = await inquirer.prompt([
1107
- {
1108
- type: 'input',
1109
- name: 'teamId',
1110
- message: 'Apple Developer Team ID (required for code signing, e.g. "ABC123XYZ"):',
1111
- default: resolvedTeamId || '',
1112
- when: () => shouldPromptForTeamId || !resolvedTeamId,
1113
- validate: (input) => {
1114
- // Team ID is optional if user provides their own p12/profile
1115
- // but we'll strongly recommend it
1116
- if (!input.trim()) {
1117
- return true; // Allow empty, workflow will try to proceed without it
1118
- }
1119
- // Basic validation: Apple Team IDs are typically 10 alphanumeric chars
1120
- if (/^[A-Z0-9]{10}$/.test(input.trim())) {
1394
+ const configP12Path = norrixConfig.ios?.p12Path;
1395
+ const configProfilePath = norrixConfig.ios?.provisioningProfilePath;
1396
+ const configAscKeyId = norrixConfig.ios?.ascApiKeyId;
1397
+ const configAscIssuerId = norrixConfig.ios?.ascIssuerId;
1398
+ const configAscKeyPath = norrixConfig.ios?.ascPrivateKeyPath;
1399
+ const resolvedTeamId = trimString(opts.teamId) || configTeamId;
1400
+ const resolvedP12Path = trimString(opts.p12Path) || configP12Path;
1401
+ const resolvedP12Password = trimString(opts.p12Password);
1402
+ const resolvedProfilePath = trimString(opts.profilePath) || configProfilePath;
1403
+ const resolvedAscKeyId = trimString(opts.ascKeyId) || configAscKeyId;
1404
+ const resolvedAscIssuerId = trimString(opts.ascIssuerId) || configAscIssuerId;
1405
+ const resolvedAscKeyPath = trimString(opts.ascKeyPath) || configAscKeyPath;
1406
+ if (opts.nonInteractive) {
1407
+ // Non-interactive mode: use CLI flags and config values only
1408
+ iosCredentials = {
1409
+ teamId: resolvedTeamId || undefined,
1410
+ p12Base64: readOptionalFileAsBase64(resolvedP12Path),
1411
+ p12Password: resolvedP12Password || undefined,
1412
+ mobileprovisionBase64: readOptionalFileAsBase64(resolvedProfilePath),
1413
+ ascApiKeyId: resolvedAscKeyId || undefined,
1414
+ ascIssuerId: resolvedAscIssuerId || undefined,
1415
+ ascPrivateKey: readOptionalFileAsBase64(resolvedAscKeyPath),
1416
+ };
1417
+ if (resolvedTeamId) {
1418
+ console.log(`Using Apple Team ID: ${resolvedTeamId}`);
1419
+ }
1420
+ if (resolvedAscKeyId) {
1421
+ console.log(`Using ASC API Key: ${resolvedAscKeyId}`);
1422
+ }
1423
+ }
1424
+ else {
1425
+ // Interactive mode: prompt for values with CLI/config as defaults
1426
+ // Flow: Team ID -> ASC Key (recommended) -> If no ASC, then .p12/.mobileprovision
1427
+ const iosAnswers = await inquirer.prompt([
1428
+ {
1429
+ type: 'input',
1430
+ name: 'teamId',
1431
+ message: 'Apple Developer Team ID (required for code signing, e.g. "ABC123XYZ"):',
1432
+ default: resolvedTeamId || '',
1433
+ validate: (input) => {
1434
+ if (!input.trim()) {
1435
+ return true; // Allow empty, workflow will try to proceed without it
1436
+ }
1437
+ if (/^[A-Z0-9]{10}$/.test(input.trim())) {
1438
+ return true;
1439
+ }
1440
+ return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
1441
+ },
1442
+ },
1443
+ // ASC API Key is the recommended approach - ask first
1444
+ {
1445
+ type: 'confirm',
1446
+ name: 'useAscKey',
1447
+ message: 'Use App Store Connect API Key for auto-provisioning? (recommended)',
1448
+ default: Boolean(resolvedAscKeyId) || true,
1449
+ },
1450
+ {
1451
+ type: 'input',
1452
+ name: 'ascApiKeyId',
1453
+ message: 'ASC API Key ID:',
1454
+ default: resolvedAscKeyId || '',
1455
+ when: (a) => a.useAscKey,
1456
+ validate: (input) => {
1457
+ if (!input.trim()) {
1458
+ return 'API Key ID is required when using ASC Key';
1459
+ }
1121
1460
  return true;
1122
- }
1123
- return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
1461
+ },
1124
1462
  },
1125
- },
1126
- {
1127
- type: 'input',
1128
- name: 'p12Path',
1129
- message: 'Path to iOS .p12 certificate (optional):',
1130
- default: '',
1131
- },
1132
- {
1133
- type: 'password',
1134
- name: 'p12Password',
1135
- message: 'Password for .p12 (if any):',
1136
- mask: '*',
1137
- default: '',
1138
- },
1139
- {
1140
- type: 'input',
1141
- name: 'mobileprovisionPath',
1142
- message: 'Path to provisioning profile .mobileprovision (optional):',
1143
- default: '',
1144
- },
1145
- {
1146
- type: 'confirm',
1147
- name: 'hasAscKey',
1148
- message: 'Provide App Store Connect API Key? (optional, for auto-provisioning)',
1149
- default: false,
1150
- },
1151
- {
1152
- type: 'input',
1153
- name: 'ascApiKeyId',
1154
- message: 'ASC API Key ID (optional):',
1155
- default: '',
1156
- when: (a) => a.hasAscKey,
1157
- },
1158
- {
1159
- type: 'input',
1160
- name: 'ascIssuerId',
1161
- message: 'ASC Issuer ID (optional):',
1162
- default: '',
1163
- when: (a) => a.hasAscKey,
1164
- },
1165
- {
1166
- type: 'input',
1167
- name: 'ascPrivateKeyPath',
1168
- message: 'Path to ASC private key .p8 (optional):',
1169
- default: '',
1170
- when: (a) => a.hasAscKey,
1171
- },
1172
- ]);
1173
- // Use resolved teamId from CLI/config, or from prompt
1174
- const finalTeamId = resolvedTeamId || iosAnswers.teamId?.trim();
1175
- iosCredentials = {
1176
- teamId: finalTeamId || undefined,
1177
- p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
1178
- p12Password: iosAnswers.p12Password || undefined,
1179
- mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
1180
- ascApiKeyId: iosAnswers.ascApiKeyId || undefined,
1181
- ascIssuerId: iosAnswers.ascIssuerId || undefined,
1182
- ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
1183
- // Track paths for config saving (not sent to API)
1184
- _p12Path: iosAnswers.p12Path || undefined,
1185
- _mobileprovisionPath: iosAnswers.mobileprovisionPath || undefined,
1186
- };
1187
- if (finalTeamId) {
1188
- console.log(`Using Apple Team ID: ${finalTeamId}`);
1463
+ {
1464
+ type: 'input',
1465
+ name: 'ascIssuerId',
1466
+ message: 'ASC Issuer ID:',
1467
+ default: resolvedAscIssuerId || '',
1468
+ when: (a) => a.useAscKey,
1469
+ validate: (input) => {
1470
+ if (!input.trim()) {
1471
+ return 'Issuer ID is required when using ASC Key';
1472
+ }
1473
+ return true;
1474
+ },
1475
+ },
1476
+ {
1477
+ type: 'input',
1478
+ name: 'ascPrivateKeyPath',
1479
+ message: 'Path to ASC private key .p8:',
1480
+ default: resolvedAscKeyPath || '',
1481
+ when: (a) => a.useAscKey,
1482
+ validate: (input) => {
1483
+ if (!input.trim()) {
1484
+ return 'Path to .p8 key file is required when using ASC Key';
1485
+ }
1486
+ return true;
1487
+ },
1488
+ },
1489
+ // Only ask for .p12/.mobileprovision if NOT using ASC Key
1490
+ {
1491
+ type: 'input',
1492
+ name: 'p12Path',
1493
+ message: 'Path to iOS .p12 certificate (optional):',
1494
+ default: resolvedP12Path || '',
1495
+ when: (a) => !a.useAscKey,
1496
+ },
1497
+ {
1498
+ type: 'password',
1499
+ name: 'p12Password',
1500
+ message: 'Password for .p12 (if any):',
1501
+ mask: '*',
1502
+ default: '',
1503
+ when: (a) => !a.useAscKey && a.p12Path,
1504
+ },
1505
+ {
1506
+ type: 'input',
1507
+ name: 'mobileprovisionPath',
1508
+ message: 'Path to provisioning profile .mobileprovision (optional):',
1509
+ default: resolvedProfilePath || '',
1510
+ when: (a) => !a.useAscKey,
1511
+ },
1512
+ ]);
1513
+ // Use resolved teamId from CLI/config, or from prompt
1514
+ const finalTeamId = trimString(iosAnswers.teamId) || resolvedTeamId;
1515
+ iosCredentials = {
1516
+ teamId: finalTeamId || undefined,
1517
+ p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
1518
+ p12Password: trimString(iosAnswers.p12Password),
1519
+ mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
1520
+ ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
1521
+ ascIssuerId: trimString(iosAnswers.ascIssuerId),
1522
+ ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
1523
+ // Track paths for config saving (not sent to API)
1524
+ _p12Path: trimString(iosAnswers.p12Path),
1525
+ _mobileprovisionPath: trimString(iosAnswers.mobileprovisionPath),
1526
+ _ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
1527
+ _ascIssuerId: trimString(iosAnswers.ascIssuerId),
1528
+ _ascPrivateKeyPath: trimString(iosAnswers.ascPrivateKeyPath),
1529
+ };
1530
+ if (finalTeamId) {
1531
+ console.log(`Using Apple Team ID: ${finalTeamId}`);
1532
+ }
1189
1533
  }
1190
1534
  }
1191
1535
  else if (platform === 'android') {
@@ -1232,14 +1576,14 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1232
1576
  ]);
1233
1577
  androidCredentials = {
1234
1578
  keystoreBase64: readOptionalFileAsBase64(androidAnswers.keystorePath),
1235
- keystorePassword: androidAnswers.keystorePassword || undefined,
1236
- keyAlias: androidAnswers.keyAlias || undefined,
1237
- keyPassword: androidAnswers.keyPassword || undefined,
1579
+ keystorePassword: trimString(androidAnswers.keystorePassword),
1580
+ keyAlias: trimString(androidAnswers.keyAlias),
1581
+ keyPassword: trimString(androidAnswers.keyPassword),
1238
1582
  playServiceAccountJson: readOptionalFileAsBase64(androidAnswers.playJsonPath),
1239
1583
  };
1240
1584
  // Track Android paths for config saving
1241
- androidCredentials._keystorePath = androidAnswers.keystorePath || undefined;
1242
- androidCredentials._keyAlias = androidAnswers.keyAlias || undefined;
1585
+ androidCredentials._keystorePath = trimString(androidAnswers.keystorePath);
1586
+ androidCredentials._keyAlias = trimString(androidAnswers.keyAlias);
1243
1587
  }
1244
1588
  // Offer to save config if no norrix.config.ts exists and we collected useful values
1245
1589
  const appRoot = process.cwd();
@@ -1263,6 +1607,16 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1263
1607
  if (iosCredentials._mobileprovisionPath) {
1264
1608
  saveableOptions.provisioningProfilePath = iosCredentials._mobileprovisionPath;
1265
1609
  }
1610
+ // Save ASC API Key details for future builds
1611
+ if (iosCredentials._ascApiKeyId) {
1612
+ saveableOptions.ascApiKeyId = iosCredentials._ascApiKeyId;
1613
+ }
1614
+ if (iosCredentials._ascIssuerId) {
1615
+ saveableOptions.ascIssuerId = iosCredentials._ascIssuerId;
1616
+ }
1617
+ if (iosCredentials._ascPrivateKeyPath) {
1618
+ saveableOptions.ascPrivateKeyPath = iosCredentials._ascPrivateKeyPath;
1619
+ }
1266
1620
  }
1267
1621
  if (platform === 'android' && androidCredentials) {
1268
1622
  if (androidCredentials._keystorePath) {
@@ -1277,7 +1631,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1277
1631
  saveableOptions.distributionType ||
1278
1632
  saveableOptions.p12Path ||
1279
1633
  saveableOptions.provisioningProfilePath ||
1280
- saveableOptions.keystorePath;
1634
+ saveableOptions.keystorePath ||
1635
+ saveableOptions.ascApiKeyId;
1281
1636
  if (hasSaveableValues) {
1282
1637
  const { shouldSave } = await inquirer.prompt([
1283
1638
  {
@@ -1309,7 +1664,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1309
1664
  writeRuntimeFingerprintFile(projectRoot, fingerprint, platform);
1310
1665
  spinner.start('Creating project archive...');
1311
1666
  // 3. Zip the project (workspace-aware)
1312
- const { zipPath, workspaceContext } = await zipProject(projectName, false, verbose);
1667
+ const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, false, verbose);
1313
1668
  spinner.text = 'Project archive created';
1314
1669
  // 4. Upload the project zip to S3
1315
1670
  spinner.text = 'Working...';
@@ -1342,6 +1697,11 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1342
1697
  appPath: workspaceContext.relativeAppPath,
1343
1698
  projectName: workspaceContext.projectName,
1344
1699
  } : undefined;
1700
+ // For standalone projects, use the project option or infer from app ID/package name
1701
+ // This allows env vars to be scoped to specific standalone projects within an org
1702
+ const standaloneProjectName = workspaceContext.type === 'standalone'
1703
+ ? (opts.project || inferredAppId || projectName)
1704
+ : undefined;
1345
1705
  const response = await axios.post(`${API_URL}/build`, {
1346
1706
  projectName,
1347
1707
  appId: inferredAppId,
@@ -1351,12 +1711,18 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1351
1711
  version: version || '',
1352
1712
  buildNumber: buildNumber || '',
1353
1713
  configuration,
1714
+ // Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
1715
+ ...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
1354
1716
  ...(distributionType ? { distributionType } : {}),
1717
+ // Android package type override (apk or aab) - takes precedence over distributionType
1718
+ ...(opts.androidPackageType ? { androidPackageType: opts.androidPackageType } : {}),
1355
1719
  fingerprint,
1356
1720
  // Provide the relative key (without public/) – the workflow prepends public/
1357
1721
  s3Key: s3KeyRel,
1358
1722
  // Workspace context for Nx monorepos
1359
1723
  ...(workspaceInfo ? { workspace: workspaceInfo } : {}),
1724
+ // For standalone projects, include project name for env var scoping
1725
+ ...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
1360
1726
  // Only include raw credentials if not encrypted
1361
1727
  ...(encryptedSecrets ? { encryptedSecrets } : {}),
1362
1728
  ...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
@@ -1462,8 +1828,25 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
1462
1828
  * Submit command implementation
1463
1829
  * Submits the built app to app stores via the Next.js API gateway
1464
1830
  */
1465
- export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
1831
+ export async function submit(cliPlatformArg, cliTrackArg, verbose = false, options) {
1466
1832
  ensureInitialized();
1833
+ try {
1834
+ await ensureOrgSelected({
1835
+ orgIdArg: options?.org,
1836
+ nonInteractive: Boolean(options?.nonInteractive),
1837
+ requireSelection: true,
1838
+ verbose,
1839
+ promptMessage: 'Select organization for this submission:',
1840
+ });
1841
+ }
1842
+ catch (error) {
1843
+ ora().fail(`Organization selection failed: ${error?.message || error}`);
1844
+ if (verbose && error?.response) {
1845
+ console.error('Axios response status:', error.response.status);
1846
+ console.error('Axios response data:', error.response.data);
1847
+ }
1848
+ return;
1849
+ }
1467
1850
  const spinner = ora('Preparing app for submission...');
1468
1851
  try {
1469
1852
  spinner.start();
@@ -1727,6 +2110,23 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
1727
2110
  // Normalize options - support both new object and legacy string projectArg
1728
2111
  const opts = typeof options === 'string' ? { project: options } : (options || {});
1729
2112
  ensureInitialized();
2113
+ try {
2114
+ await ensureOrgSelected({
2115
+ orgIdArg: opts.org,
2116
+ nonInteractive: opts.nonInteractive,
2117
+ requireSelection: true,
2118
+ verbose,
2119
+ promptMessage: 'Select organization for this update:',
2120
+ });
2121
+ }
2122
+ catch (error) {
2123
+ ora().fail(`Organization selection failed: ${error?.message || error}`);
2124
+ if (verbose && error?.response) {
2125
+ console.error('Axios response status:', error.response.status);
2126
+ console.error('Axios response data:', error.response.data);
2127
+ }
2128
+ return;
2129
+ }
1730
2130
  let spinner;
1731
2131
  let originalCwd;
1732
2132
  try {
@@ -1759,6 +2159,33 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
1759
2159
  platform = chosenPlatform;
1760
2160
  spinner.start('Preparing over-the-air update...');
1761
2161
  }
2162
+ // Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
2163
+ let nxConfiguration = opts.nxConfiguration;
2164
+ if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
2165
+ spinner.stop();
2166
+ const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
2167
+ if (nxConfigs && nxConfigs.configurations.length > 0) {
2168
+ const choices = nxConfigs.configurations.map((c) => ({
2169
+ name: c === nxConfigs.defaultConfiguration ? `${c} (default)` : c,
2170
+ value: c,
2171
+ }));
2172
+ // Add option to skip/use default
2173
+ choices.unshift({ name: '(none - use defaults)', value: '' });
2174
+ const { chosenNxConfig } = await inquirer.prompt([
2175
+ {
2176
+ type: 'list',
2177
+ name: 'chosenNxConfig',
2178
+ message: 'Nx build configuration (environment):',
2179
+ choices,
2180
+ default: nxConfigs.defaultConfiguration || '',
2181
+ },
2182
+ ]);
2183
+ nxConfiguration = chosenNxConfig || undefined;
2184
+ }
2185
+ spinner.start('Preparing over-the-air update...');
2186
+ }
2187
+ // Store resolved nxConfiguration back to opts for later use
2188
+ opts.nxConfiguration = nxConfiguration;
1762
2189
  // Infer version from native project files (same as build)
1763
2190
  const appleVersionInfo = platform === 'ios' || platform === 'visionos'
1764
2191
  ? getAppleVersionFromInfoPlist(platform)
@@ -1963,7 +2390,7 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
1963
2390
  spinner.start('Packaging for over-the-air update...');
1964
2391
  // Create the update bundle (workspace-aware) - pass true to include node_modules for updates
1965
2392
  const projectName = await getProjectName();
1966
- const { zipPath, workspaceContext } = await zipProject(projectName, true, verbose);
2393
+ const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, true, verbose);
1967
2394
  spinner.text = 'Uploading update to Norrix cloud storage...';
1968
2395
  const fileBuffer = fs.readFileSync(zipPath);
1969
2396
  const updateFolder = `update-${Date.now()}`;
@@ -1977,6 +2404,11 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
1977
2404
  appPath: workspaceContext.relativeAppPath,
1978
2405
  projectName: workspaceContext.projectName,
1979
2406
  } : undefined;
2407
+ // For standalone projects, use the project option or infer from app ID
2408
+ // This allows env vars to be scoped to specific standalone projects within an org
2409
+ const standaloneProjectName = workspaceContext.type === 'standalone'
2410
+ ? (opts.project || appId)
2411
+ : undefined;
1980
2412
  const response = await axios.post(`${API_URL}/update`, {
1981
2413
  appId,
1982
2414
  platform,
@@ -1984,10 +2416,14 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
1984
2416
  buildNumber: buildNumber || '',
1985
2417
  releaseNotes: notes,
1986
2418
  fingerprint,
2419
+ // Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
2420
+ ...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
1987
2421
  // Provide the relative key (without public/). Consumers will prepend public/
1988
2422
  s3Key: s3KeyRel,
1989
2423
  // Workspace context for Nx monorepos
1990
2424
  ...(workspaceInfo ? { workspace: workspaceInfo } : {}),
2425
+ // For standalone projects, include project name for env var scoping
2426
+ ...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
1991
2427
  }, {
1992
2428
  headers: {
1993
2429
  'Content-Type': 'application/json',
@@ -2079,8 +2515,20 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
2079
2515
  * Build Status command implementation
2080
2516
  * Checks the status of a build via the Next.js API gateway
2081
2517
  */
2082
- export async function buildStatus(buildId, verbose = false) {
2518
+ export async function buildStatus(buildId, verbose = false, options) {
2083
2519
  ensureInitialized();
2520
+ try {
2521
+ await ensureOrgSelected({
2522
+ orgIdArg: options?.org,
2523
+ nonInteractive: true,
2524
+ requireSelection: false,
2525
+ verbose,
2526
+ });
2527
+ }
2528
+ catch (error) {
2529
+ ora().fail(`Organization selection failed: ${error?.message || error}`);
2530
+ return;
2531
+ }
2084
2532
  try {
2085
2533
  const spinner = ora(`Checking status of build ${buildId}...`).start();
2086
2534
  const response = await axios.get(`${API_URL}/build/${buildId}`, {
@@ -2117,8 +2565,20 @@ export async function buildStatus(buildId, verbose = false) {
2117
2565
  * Submit Status command implementation
2118
2566
  * Checks the status of a submission via the Next.js API gateway
2119
2567
  */
2120
- export async function submitStatus(submitId, verbose = false) {
2568
+ export async function submitStatus(submitId, verbose = false, options) {
2121
2569
  ensureInitialized();
2570
+ try {
2571
+ await ensureOrgSelected({
2572
+ orgIdArg: options?.org,
2573
+ nonInteractive: true,
2574
+ requireSelection: false,
2575
+ verbose,
2576
+ });
2577
+ }
2578
+ catch (error) {
2579
+ ora().fail(`Organization selection failed: ${error?.message || error}`);
2580
+ return;
2581
+ }
2122
2582
  try {
2123
2583
  const spinner = ora(`Checking status of submission ${submitId}...`).start();
2124
2584
  const response = await axios.get(`${API_URL}/submit/${submitId}`, {
@@ -2151,8 +2611,20 @@ export async function submitStatus(submitId, verbose = false) {
2151
2611
  * Update Status command implementation
2152
2612
  * Checks the status of an update via the Next.js API gateway
2153
2613
  */
2154
- export async function updateStatus(updateId, verbose = false) {
2614
+ export async function updateStatus(updateId, verbose = false, options) {
2155
2615
  ensureInitialized();
2616
+ try {
2617
+ await ensureOrgSelected({
2618
+ orgIdArg: options?.org,
2619
+ nonInteractive: true,
2620
+ requireSelection: false,
2621
+ verbose,
2622
+ });
2623
+ }
2624
+ catch (error) {
2625
+ ora().fail(`Organization selection failed: ${error?.message || error}`);
2626
+ return;
2627
+ }
2156
2628
  try {
2157
2629
  const spinner = ora(`Checking status of update ${updateId}...`).start();
2158
2630
  const response = await axios.get(`${API_URL}/update/${updateId}`, {
@@ -2397,4 +2869,310 @@ export async function billingPortal(verbose = false) {
2397
2869
  }
2398
2870
  }
2399
2871
  }
2872
+ function normalizeVisibilityType(input) {
2873
+ if (!input)
2874
+ return 'secret';
2875
+ if (input === 'plaintext' || input === 'secret')
2876
+ return input;
2877
+ console.error('Error: --visibility must be either "plaintext" or "secret"');
2878
+ process.exit(1);
2879
+ }
2880
+ /**
2881
+ * Set an environment variable for build-time injection
2882
+ */
2883
+ export async function envSet(name, value, verbose = false, options = {}) {
2884
+ ensureInitialized();
2885
+ let spinner;
2886
+ try {
2887
+ const visibilityType = normalizeVisibilityType(options.visibilityType);
2888
+ // Validate secret name (env var format)
2889
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(name)) {
2890
+ console.error('Error: Name must be a valid environment variable name (alphanumeric and underscores, cannot start with a number)');
2891
+ process.exit(1);
2892
+ }
2893
+ spinner = ora(`Setting env var "${name}"...`).start();
2894
+ const payload = {
2895
+ type: 'variable',
2896
+ name,
2897
+ visibilityType,
2898
+ project: options.project,
2899
+ };
2900
+ if (visibilityType === 'plaintext') {
2901
+ payload.value = value;
2902
+ }
2903
+ else {
2904
+ Object.assign(payload, encryptSecretValue(value));
2905
+ }
2906
+ const res = await axios.post(`${API_URL}/env`, payload, {
2907
+ headers: await getAuthHeaders(),
2908
+ });
2909
+ spinner.succeed(`Env var "${name}" set successfully`);
2910
+ if (res.data?.message) {
2911
+ console.log(res.data.message);
2912
+ }
2913
+ }
2914
+ catch (error) {
2915
+ const apiMessage = error?.response?.data?.error ||
2916
+ error?.response?.data?.message ||
2917
+ error?.message ||
2918
+ String(error);
2919
+ spinner?.fail(`Failed to set env var: ${apiMessage}`);
2920
+ if (verbose) {
2921
+ console.error('--- Verbose error details (env-set) ---');
2922
+ console.error(error);
2923
+ if (error?.response) {
2924
+ console.error('Axios response status:', error.response.status);
2925
+ console.error('Axios response data:', error.response.data);
2926
+ }
2927
+ if (error?.stack) {
2928
+ console.error(error.stack);
2929
+ }
2930
+ }
2931
+ process.exit(1);
2932
+ }
2933
+ }
2934
+ /**
2935
+ * Upload an environment file for build-time injection
2936
+ */
2937
+ export async function envSetFile(name, filePath, verbose = false, options = {}) {
2938
+ ensureInitialized();
2939
+ let spinner;
2940
+ try {
2941
+ const visibilityType = normalizeVisibilityType(options.visibilityType);
2942
+ const resolvedPath = normalizePath(filePath);
2943
+ if (!resolvedPath || !fs.existsSync(resolvedPath)) {
2944
+ console.error(`Error: File not found: ${filePath}`);
2945
+ process.exit(1);
2946
+ }
2947
+ spinner = ora(`Uploading env file "${name}"...`).start();
2948
+ // Read and encode file content
2949
+ const fileContent = fs.readFileSync(resolvedPath);
2950
+ const base64Content = fileContent.toString('base64');
2951
+ const payload = {
2952
+ type: 'file',
2953
+ name,
2954
+ visibilityType,
2955
+ destPath: options.destPath || name,
2956
+ project: options.project,
2957
+ };
2958
+ if (visibilityType === 'plaintext') {
2959
+ payload.value = base64Content;
2960
+ }
2961
+ else {
2962
+ Object.assign(payload, encryptSecretValue(base64Content));
2963
+ }
2964
+ const res = await axios.post(`${API_URL}/env`, payload, {
2965
+ headers: await getAuthHeaders(),
2966
+ });
2967
+ spinner.succeed(`Env file "${name}" uploaded successfully`);
2968
+ if (res.data?.message) {
2969
+ console.log(res.data.message);
2970
+ }
2971
+ }
2972
+ catch (error) {
2973
+ const apiMessage = error?.response?.data?.error ||
2974
+ error?.response?.data?.message ||
2975
+ error?.message ||
2976
+ String(error);
2977
+ spinner?.fail(`Failed to upload env file: ${apiMessage}`);
2978
+ if (verbose) {
2979
+ console.error('--- Verbose error details (env-set-file) ---');
2980
+ console.error(error);
2981
+ if (error?.response) {
2982
+ console.error('Axios response status:', error.response.status);
2983
+ console.error('Axios response data:', error.response.data);
2984
+ }
2985
+ if (error?.stack) {
2986
+ console.error(error.stack);
2987
+ }
2988
+ }
2989
+ process.exit(1);
2990
+ }
2991
+ }
2992
+ /**
2993
+ * List all environment variables/files for a project
2994
+ */
2995
+ export async function envList(verbose = false, options = {}) {
2996
+ ensureInitialized();
2997
+ let spinner;
2998
+ try {
2999
+ spinner = ora('Fetching env vars...').start();
3000
+ const params = new URLSearchParams();
3001
+ if (options.project) {
3002
+ params.set('project', options.project);
3003
+ }
3004
+ const res = await axios.get(`${API_URL}/env?${params.toString()}`, {
3005
+ headers: await getAuthHeaders(),
3006
+ });
3007
+ spinner.stop();
3008
+ const envs = res.data?.envs || [];
3009
+ const projectName = options.project || res.data?.project || 'default';
3010
+ if (envs.length === 0) {
3011
+ console.log(`\nNo environment variables found for project: ${projectName}`);
3012
+ console.log('\nTo add env vars/files, use:');
3013
+ console.log(' norrix env set <name> <value>');
3014
+ console.log(' norrix env set-file <name> <path>');
3015
+ return;
3016
+ }
3017
+ console.log(`\nEnvironment variables for project: ${projectName}\n`);
3018
+ // Separate variables and files
3019
+ const variables = envs.filter((e) => e.type === 'variable');
3020
+ const files = envs.filter((e) => e.type === 'file');
3021
+ if (variables.length > 0) {
3022
+ console.log('Variables:');
3023
+ for (const variable of variables) {
3024
+ const dateStr = variable.updatedAt
3025
+ ? `set ${new Date(variable.updatedAt).toLocaleDateString()}`
3026
+ : '';
3027
+ const visibility = variable.visibilityType || variable.visibility || 'secret';
3028
+ console.log(` • ${variable.name} (${visibility}${dateStr ? `, ${dateStr}` : ''})`);
3029
+ }
3030
+ }
3031
+ if (files.length > 0) {
3032
+ if (variables.length > 0)
3033
+ console.log('');
3034
+ console.log('Files:');
3035
+ for (const file of files) {
3036
+ const dateStr = file.updatedAt
3037
+ ? `set ${new Date(file.updatedAt).toLocaleDateString()}`
3038
+ : '';
3039
+ const visibility = file.visibilityType || file.visibility || 'secret';
3040
+ const destPath = file.destPath || file.name;
3041
+ console.log(` • ${file.name} → ${destPath} (${visibility}${dateStr ? `, ${dateStr}` : ''})`);
3042
+ }
3043
+ }
3044
+ console.log('');
3045
+ }
3046
+ catch (error) {
3047
+ const apiMessage = error?.response?.data?.error ||
3048
+ error?.response?.data?.message ||
3049
+ error?.message ||
3050
+ String(error);
3051
+ spinner?.fail(`Failed to list env vars: ${apiMessage}`);
3052
+ if (verbose) {
3053
+ console.error('--- Verbose error details (env-list) ---');
3054
+ console.error(error);
3055
+ if (error?.response) {
3056
+ console.error('Axios response status:', error.response.status);
3057
+ console.error('Axios response data:', error.response.data);
3058
+ }
3059
+ if (error?.stack) {
3060
+ console.error(error.stack);
3061
+ }
3062
+ }
3063
+ process.exit(1);
3064
+ }
3065
+ }
3066
+ /**
3067
+ * Delete an environment variable
3068
+ */
3069
+ export async function envDelete(name, verbose = false, options = {}) {
3070
+ ensureInitialized();
3071
+ let spinner;
3072
+ try {
3073
+ spinner = ora(`Deleting env var "${name}"...`).start();
3074
+ const res = await axios.delete(`${API_URL}/env`, {
3075
+ data: {
3076
+ type: 'variable',
3077
+ name,
3078
+ project: options.project,
3079
+ },
3080
+ headers: await getAuthHeaders(),
3081
+ });
3082
+ spinner.succeed(`Env var "${name}" deleted successfully`);
3083
+ if (res.data?.message) {
3084
+ console.log(res.data.message);
3085
+ }
3086
+ }
3087
+ catch (error) {
3088
+ const apiMessage = error?.response?.data?.error ||
3089
+ error?.response?.data?.message ||
3090
+ error?.message ||
3091
+ String(error);
3092
+ spinner?.fail(`Failed to delete env var: ${apiMessage}`);
3093
+ if (verbose) {
3094
+ console.error('--- Verbose error details (env-delete) ---');
3095
+ console.error(error);
3096
+ if (error?.response) {
3097
+ console.error('Axios response status:', error.response.status);
3098
+ console.error('Axios response data:', error.response.data);
3099
+ }
3100
+ if (error?.stack) {
3101
+ console.error(error.stack);
3102
+ }
3103
+ }
3104
+ process.exit(1);
3105
+ }
3106
+ }
3107
+ /**
3108
+ * Delete an environment file
3109
+ */
3110
+ export async function envDeleteFile(name, verbose = false, options = {}) {
3111
+ ensureInitialized();
3112
+ let spinner;
3113
+ try {
3114
+ spinner = ora(`Deleting env file "${name}"...`).start();
3115
+ const res = await axios.delete(`${API_URL}/env`, {
3116
+ data: {
3117
+ type: 'file',
3118
+ name,
3119
+ project: options.project,
3120
+ },
3121
+ headers: await getAuthHeaders(),
3122
+ });
3123
+ spinner.succeed(`Env file "${name}" deleted successfully`);
3124
+ if (res.data?.message) {
3125
+ console.log(res.data.message);
3126
+ }
3127
+ }
3128
+ catch (error) {
3129
+ const apiMessage = error?.response?.data?.error ||
3130
+ error?.response?.data?.message ||
3131
+ error?.message ||
3132
+ String(error);
3133
+ spinner?.fail(`Failed to delete env file: ${apiMessage}`);
3134
+ if (verbose) {
3135
+ console.error('--- Verbose error details (env-delete-file) ---');
3136
+ console.error(error);
3137
+ if (error?.response) {
3138
+ console.error('Axios response status:', error.response.status);
3139
+ console.error('Axios response data:', error.response.data);
3140
+ }
3141
+ if (error?.stack) {
3142
+ console.error(error.stack);
3143
+ }
3144
+ }
3145
+ process.exit(1);
3146
+ }
3147
+ }
3148
+ /**
3149
+ * Encrypt a secret value using the Norrix public key (envelope encryption)
3150
+ */
3151
+ function encryptSecretValue(value) {
3152
+ const publicKeyPem = process.env.NORRIX_BUILD_PUBLIC_KEY;
3153
+ if (!publicKeyPem) {
3154
+ // If no public key is configured, send value in a format the server can handle
3155
+ // The server will encrypt at rest using its own key
3156
+ return {
3157
+ encryptedValue: Buffer.from(value, 'utf8').toString('base64'),
3158
+ encryptedKey: '',
3159
+ iv: '',
3160
+ tag: '',
3161
+ };
3162
+ }
3163
+ const nodeCrypto = crypto;
3164
+ const aesKey = nodeCrypto.randomBytes(32); // AES-256
3165
+ const iv = nodeCrypto.randomBytes(12); // GCM nonce
3166
+ const cipher = nodeCrypto.createCipheriv('aes-256-gcm', aesKey, iv);
3167
+ const plaintext = Buffer.from(value, 'utf8');
3168
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
3169
+ const tag = cipher.getAuthTag();
3170
+ const encryptedKey = nodeCrypto.publicEncrypt({ key: publicKeyPem, oaepHash: 'sha256' }, aesKey);
3171
+ return {
3172
+ encryptedValue: ciphertext.toString('base64'),
3173
+ encryptedKey: encryptedKey.toString('base64'),
3174
+ iv: iv.toString('base64'),
3175
+ tag: tag.toString('base64'),
3176
+ };
3177
+ }
2400
3178
  //# sourceMappingURL=commands.js.map