@norrix/cli 0.0.25 → 0.0.27
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/CHANGELOG.md +42 -0
- package/dist/cli.js +98 -7
- package/dist/cli.js.map +1 -1
- package/dist/lib/amplify-config.js +0 -1
- package/dist/lib/cli-settings.d.ts +15 -0
- package/dist/lib/cli-settings.js +50 -0
- package/dist/lib/cli-settings.js.map +1 -0
- package/dist/lib/commands.d.ts +74 -4
- package/dist/lib/commands.js +952 -109
- package/dist/lib/commands.js.map +1 -1
- package/dist/lib/config-helpers.spec.js +8 -0
- package/dist/lib/config.d.ts +50 -0
- package/dist/lib/config.js +41 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/defaults.js +0 -1
- package/dist/lib/dev-defaults.js +0 -1
- package/dist/lib/fingerprinting.js +0 -1
- package/dist/lib/norrix-cli.js +0 -1
- package/dist/lib/prod-defaults.js +0 -1
- package/dist/lib/workspace.d.ts +18 -1
- package/dist/lib/workspace.js +378 -24
- package/dist/lib/workspace.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/commands.js
CHANGED
|
@@ -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() {
|
|
@@ -48,21 +54,196 @@ function ensureInitialized() {
|
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
51
|
-
*
|
|
57
|
+
* Check if an API key is configured for authentication.
|
|
58
|
+
* API key takes precedence over Cognito session for CI/enterprise workflows.
|
|
52
59
|
*/
|
|
53
|
-
|
|
60
|
+
function getApiKey() {
|
|
61
|
+
const key = process.env.NORRIX_API_KEY?.trim();
|
|
62
|
+
if (key && key.startsWith('nrx_')) {
|
|
63
|
+
return key;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Return Authorization headers for API requests.
|
|
69
|
+
*
|
|
70
|
+
* Authentication precedence:
|
|
71
|
+
* 1. NORRIX_API_KEY environment variable (for CI/enterprise workflows)
|
|
72
|
+
* 2. Cognito session (interactive login via `norrix login`)
|
|
73
|
+
*
|
|
74
|
+
* When using an API key, the organization is determined by the key itself,
|
|
75
|
+
* so X-Norrix-Org-Id header is not needed.
|
|
76
|
+
*/
|
|
77
|
+
async function getAuthHeaders(options) {
|
|
54
78
|
ensureInitialized();
|
|
79
|
+
const headers = {};
|
|
80
|
+
// Check for API key first (takes precedence for CI workflows)
|
|
81
|
+
const apiKey = getApiKey();
|
|
82
|
+
if (apiKey) {
|
|
83
|
+
headers['X-Api-Key'] = apiKey;
|
|
84
|
+
// API keys are org-scoped, so we don't need to send org header
|
|
85
|
+
return headers;
|
|
86
|
+
}
|
|
87
|
+
// Fall back to Cognito session
|
|
55
88
|
try {
|
|
56
89
|
const session = await fetchAuthSession();
|
|
57
90
|
const idToken = session.tokens?.idToken?.toString();
|
|
58
91
|
if (idToken) {
|
|
59
|
-
|
|
92
|
+
headers.Authorization = `Bearer ${idToken}`;
|
|
60
93
|
}
|
|
61
94
|
}
|
|
62
95
|
catch (_) {
|
|
63
96
|
/* not signed in */
|
|
64
97
|
}
|
|
65
|
-
|
|
98
|
+
if (options?.includeOrg !== false && CURRENT_ORG_ID) {
|
|
99
|
+
headers['X-Norrix-Org-Id'] = CURRENT_ORG_ID;
|
|
100
|
+
}
|
|
101
|
+
return headers;
|
|
102
|
+
}
|
|
103
|
+
function setCurrentOrgId(orgId) {
|
|
104
|
+
const v = (orgId ?? '').toString().trim();
|
|
105
|
+
CURRENT_ORG_ID = v ? v : undefined;
|
|
106
|
+
}
|
|
107
|
+
async function fetchOrganizations(verbose = false) {
|
|
108
|
+
ensureInitialized();
|
|
109
|
+
try {
|
|
110
|
+
const res = await axios.get(`${API_URL}/orgs`, {
|
|
111
|
+
headers: await getAuthHeaders({ includeOrg: false }),
|
|
112
|
+
});
|
|
113
|
+
const organizations = Array.isArray(res.data?.organizations)
|
|
114
|
+
? res.data.organizations
|
|
115
|
+
: [];
|
|
116
|
+
const selectedOrganizationId = res.data?.selectedOrganizationId
|
|
117
|
+
? String(res.data.selectedOrganizationId)
|
|
118
|
+
: undefined;
|
|
119
|
+
return { organizations, selectedOrganizationId };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (verbose) {
|
|
123
|
+
console.error('--- Verbose error details (orgs fetch) ---');
|
|
124
|
+
console.error(err);
|
|
125
|
+
if (err?.response) {
|
|
126
|
+
console.error('Axios response status:', err.response.status);
|
|
127
|
+
console.error('Axios response data:', err.response.data);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function ensureOrgSelected(params) {
|
|
134
|
+
ensureInitialized();
|
|
135
|
+
const explicit = (params.orgIdArg ?? process.env.NORRIX_ORG_ID ?? '')
|
|
136
|
+
.toString()
|
|
137
|
+
.trim();
|
|
138
|
+
if (explicit) {
|
|
139
|
+
setCurrentOrgId(explicit);
|
|
140
|
+
return explicit;
|
|
141
|
+
}
|
|
142
|
+
const stored = getSelectedOrgId(CURRENT_ENV, API_URL);
|
|
143
|
+
if (!params.requireSelection) {
|
|
144
|
+
if (stored)
|
|
145
|
+
setCurrentOrgId(stored);
|
|
146
|
+
return stored;
|
|
147
|
+
}
|
|
148
|
+
// Validate stored selection (and discover orgs for prompting).
|
|
149
|
+
const { organizations } = await fetchOrganizations(Boolean(params.verbose));
|
|
150
|
+
const normalizedOrgs = organizations.filter((o) => o && o.id);
|
|
151
|
+
if (stored && normalizedOrgs.some((o) => o.id === stored)) {
|
|
152
|
+
setCurrentOrgId(stored);
|
|
153
|
+
return stored;
|
|
154
|
+
}
|
|
155
|
+
if (stored) {
|
|
156
|
+
clearSelectedOrgId(CURRENT_ENV, API_URL);
|
|
157
|
+
}
|
|
158
|
+
if (normalizedOrgs.length === 1) {
|
|
159
|
+
const only = normalizedOrgs[0];
|
|
160
|
+
setSelectedOrgId(CURRENT_ENV, API_URL, only.id);
|
|
161
|
+
setCurrentOrgId(only.id);
|
|
162
|
+
return only.id;
|
|
163
|
+
}
|
|
164
|
+
if (params.nonInteractive) {
|
|
165
|
+
throw new Error('No organization selected. Use --org <orgId> or run `norrix orgs select`.');
|
|
166
|
+
}
|
|
167
|
+
if (!normalizedOrgs.length) {
|
|
168
|
+
throw new Error('No organizations found for this user.');
|
|
169
|
+
}
|
|
170
|
+
const choices = normalizedOrgs.map((o) => {
|
|
171
|
+
const suffix = o.id.length > 8 ? o.id.slice(-8) : o.id;
|
|
172
|
+
return {
|
|
173
|
+
name: `${o.name} (${o.role}) • …${suffix}`,
|
|
174
|
+
value: o.id,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
const { orgId } = await inquirer.prompt([
|
|
178
|
+
{
|
|
179
|
+
type: 'list',
|
|
180
|
+
name: 'orgId',
|
|
181
|
+
message: params.promptMessage || 'Select organization:',
|
|
182
|
+
choices,
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
const selected = String(orgId || '').trim();
|
|
186
|
+
if (!selected) {
|
|
187
|
+
throw new Error('Organization selection cancelled.');
|
|
188
|
+
}
|
|
189
|
+
setSelectedOrgId(CURRENT_ENV, API_URL, selected);
|
|
190
|
+
setCurrentOrgId(selected);
|
|
191
|
+
return selected;
|
|
192
|
+
}
|
|
193
|
+
export async function orgsList(verbose = false) {
|
|
194
|
+
ensureInitialized();
|
|
195
|
+
try {
|
|
196
|
+
const { organizations, selectedOrganizationId } = await fetchOrganizations(verbose);
|
|
197
|
+
if (!organizations.length) {
|
|
198
|
+
console.log('No organizations found.');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log('Organizations:');
|
|
202
|
+
for (const o of organizations) {
|
|
203
|
+
const selectedMark = selectedOrganizationId && o.id === selectedOrganizationId ? ' (selected)' : '';
|
|
204
|
+
console.log(`- ${o.name} [${o.role}] ${o.id}${selectedMark}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
ora().fail(`Failed to list organizations: ${err?.message || err}`);
|
|
209
|
+
if (verbose && err?.response) {
|
|
210
|
+
console.error('Axios response status:', err.response.status);
|
|
211
|
+
console.error('Axios response data:', err.response.data);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function orgsSelect(verbose = false) {
|
|
216
|
+
ensureInitialized();
|
|
217
|
+
try {
|
|
218
|
+
await ensureOrgSelected({
|
|
219
|
+
requireSelection: true,
|
|
220
|
+
nonInteractive: false,
|
|
221
|
+
verbose,
|
|
222
|
+
promptMessage: 'Select default organization for this environment:',
|
|
223
|
+
});
|
|
224
|
+
if (CURRENT_ORG_ID) {
|
|
225
|
+
console.log(`✅ Selected organization: ${CURRENT_ORG_ID}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
ora().fail(`Failed to select organization: ${err?.message || err}`);
|
|
230
|
+
if (verbose && err?.response) {
|
|
231
|
+
console.error('Axios response status:', err.response.status);
|
|
232
|
+
console.error('Axios response data:', err.response.data);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export async function orgsCurrent() {
|
|
237
|
+
ensureInitialized();
|
|
238
|
+
const envOrg = (process.env.NORRIX_ORG_ID ?? '').toString().trim();
|
|
239
|
+
const stored = getSelectedOrgId(CURRENT_ENV, API_URL);
|
|
240
|
+
const current = envOrg || stored;
|
|
241
|
+
if (!current) {
|
|
242
|
+
console.log('No default organization selected for this environment.');
|
|
243
|
+
console.log('Run `norrix orgs select` or pass `--org <orgId>`.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(`Current organization for ${CURRENT_ENV} (${API_URL}): ${current}`);
|
|
66
247
|
}
|
|
67
248
|
/**
|
|
68
249
|
* Norrix CLI Command Implementations
|
|
@@ -80,6 +261,16 @@ async function getAuthHeaders() {
|
|
|
80
261
|
// Get dirname equivalent in ESM
|
|
81
262
|
const __filename = fileURLToPath(import.meta.url);
|
|
82
263
|
const __dirname = path.dirname(__filename);
|
|
264
|
+
/**
|
|
265
|
+
* Safely trim a string value, returning undefined for empty/null values.
|
|
266
|
+
* Handles both CLI args and config file values to ensure no trailing/leading whitespace.
|
|
267
|
+
*/
|
|
268
|
+
function trimString(input) {
|
|
269
|
+
if (input == null)
|
|
270
|
+
return undefined;
|
|
271
|
+
const trimmed = String(input).trim();
|
|
272
|
+
return trimmed || undefined;
|
|
273
|
+
}
|
|
83
274
|
function normalizePath(input) {
|
|
84
275
|
if (!input)
|
|
85
276
|
return undefined;
|
|
@@ -715,8 +906,7 @@ async function resolveWorkspaceContext(projectArg, spinner) {
|
|
|
715
906
|
* For Nx workspaces, this includes the app, dependent libs, and workspace config files.
|
|
716
907
|
* For standalone projects, this zips the current directory.
|
|
717
908
|
*/
|
|
718
|
-
async function zipProject(projectName, isUpdate = false, verbose = false) {
|
|
719
|
-
const workspaceCtx = detectWorkspaceContext();
|
|
909
|
+
async function zipProject(projectName, workspaceCtx, isUpdate = false, verbose = false) {
|
|
720
910
|
if (workspaceCtx.type === 'nx') {
|
|
721
911
|
return zipWorkspaceProject(projectName, workspaceCtx, isUpdate, verbose);
|
|
722
912
|
}
|
|
@@ -813,16 +1003,39 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
|
|
|
813
1003
|
});
|
|
814
1004
|
archive.pipe(output);
|
|
815
1005
|
logWorkspaceContext(workspaceCtx, verbose);
|
|
816
|
-
// Get workspace dependencies using Nx CLI (preferred)
|
|
1006
|
+
// Get workspace dependencies using Nx CLI (preferred) with fallback supplementation
|
|
817
1007
|
let deps;
|
|
818
1008
|
if (workspaceCtx.projectName) {
|
|
819
|
-
deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose
|
|
1009
|
+
deps = getNxProjectDependencies(workspaceCtx.projectName, workspaceCtx.workspaceRoot, verbose, workspaceCtx.appRoot // Pass appRoot for webpack alias detection
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
// Always supplement with fallback detection to catch anything Nx might miss
|
|
1013
|
+
// (e.g., dynamic imports, SCSS dependencies, transitive deps from source scanning)
|
|
1014
|
+
const fallbackDeps = getWorkspaceDependenciesFallback(workspaceCtx, verbose);
|
|
1015
|
+
if (deps) {
|
|
1016
|
+
// Merge fallback libs into Nx-detected libs
|
|
1017
|
+
const mergedLibPaths = new Set(deps.libPaths);
|
|
1018
|
+
for (const libPath of fallbackDeps.libPaths) {
|
|
1019
|
+
if (!mergedLibPaths.has(libPath)) {
|
|
1020
|
+
mergedLibPaths.add(libPath);
|
|
1021
|
+
if (verbose) {
|
|
1022
|
+
console.log(`[workspace] Fallback added additional lib: ${libPath}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
deps.libPaths = Array.from(mergedLibPaths);
|
|
1027
|
+
// Also merge local file deps
|
|
1028
|
+
const mergedLocalFileDeps = new Set(deps.localFileDeps);
|
|
1029
|
+
for (const dep of fallbackDeps.localFileDeps) {
|
|
1030
|
+
mergedLocalFileDeps.add(dep);
|
|
1031
|
+
}
|
|
1032
|
+
deps.localFileDeps = Array.from(mergedLocalFileDeps);
|
|
820
1033
|
}
|
|
821
|
-
|
|
1034
|
+
else {
|
|
822
1035
|
if (verbose) {
|
|
823
|
-
console.log('[workspace] Using fallback dependency detection');
|
|
1036
|
+
console.log('[workspace] Using fallback dependency detection (Nx CLI not available)');
|
|
824
1037
|
}
|
|
825
|
-
deps =
|
|
1038
|
+
deps = fallbackDeps;
|
|
826
1039
|
}
|
|
827
1040
|
// Create manifest for CI
|
|
828
1041
|
const manifest = createWorkspaceManifest(workspaceCtx, deps);
|
|
@@ -909,6 +1122,68 @@ async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false,
|
|
|
909
1122
|
archive.directory(absoluteAssetPath, assetPath);
|
|
910
1123
|
}
|
|
911
1124
|
}
|
|
1125
|
+
// 6. Add local file dependencies (file: protocol paths from package.json)
|
|
1126
|
+
// Skip any that are already covered by libPaths to avoid duplicate entries
|
|
1127
|
+
if (deps.localFileDeps && deps.localFileDeps.length > 0) {
|
|
1128
|
+
const addedDirs = new Set();
|
|
1129
|
+
// Filter out local deps that are subdirectories of already-added libs
|
|
1130
|
+
const filteredLocalDeps = deps.localFileDeps.filter(localDep => {
|
|
1131
|
+
// Check if this local dep is inside any of the lib paths
|
|
1132
|
+
for (const libPath of deps.libPaths) {
|
|
1133
|
+
if (localDep.startsWith(libPath + '/') || localDep === libPath) {
|
|
1134
|
+
if (verbose) {
|
|
1135
|
+
console.log(` - ${localDep} (skipped, covered by ${libPath})`);
|
|
1136
|
+
}
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return true;
|
|
1141
|
+
});
|
|
1142
|
+
if (filteredLocalDeps.length > 0) {
|
|
1143
|
+
console.log(`Adding ${filteredLocalDeps.length} local file dependencies`);
|
|
1144
|
+
}
|
|
1145
|
+
for (const localDep of filteredLocalDeps) {
|
|
1146
|
+
const absoluteLocalPath = path.join(workspaceCtx.workspaceRoot, localDep);
|
|
1147
|
+
if (fs.existsSync(absoluteLocalPath)) {
|
|
1148
|
+
const stat = fs.statSync(absoluteLocalPath);
|
|
1149
|
+
if (stat.isFile()) {
|
|
1150
|
+
// For files, ensure the parent directory structure is maintained
|
|
1151
|
+
if (verbose) {
|
|
1152
|
+
console.log(` - ${localDep} (file)`);
|
|
1153
|
+
}
|
|
1154
|
+
archive.file(absoluteLocalPath, { name: localDep });
|
|
1155
|
+
// Also add the directory if it hasn't been added yet (for other potential files)
|
|
1156
|
+
const parentDir = path.dirname(localDep);
|
|
1157
|
+
if (parentDir && parentDir !== '.' && !addedDirs.has(parentDir)) {
|
|
1158
|
+
// We just add the file, not the whole directory
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
else if (stat.isDirectory()) {
|
|
1162
|
+
// Skip if this directory or a parent is already in libPaths
|
|
1163
|
+
const alreadyCovered = deps.libPaths.some(libPath => localDep.startsWith(libPath + '/') || libPath.startsWith(localDep + '/'));
|
|
1164
|
+
if (alreadyCovered) {
|
|
1165
|
+
if (verbose) {
|
|
1166
|
+
console.log(` - ${localDep} (skipped, overlaps with libPaths)`);
|
|
1167
|
+
}
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (verbose) {
|
|
1171
|
+
console.log(` - ${localDep} (directory)`);
|
|
1172
|
+
}
|
|
1173
|
+
archive.directory(absoluteLocalPath, localDep, (entry) => {
|
|
1174
|
+
if (entry.name.includes('node_modules')) {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
return entry;
|
|
1178
|
+
});
|
|
1179
|
+
addedDirs.add(localDep);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
else if (verbose) {
|
|
1183
|
+
console.log(` - ${localDep} (not found, skipping)`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
912
1187
|
archive.finalize();
|
|
913
1188
|
});
|
|
914
1189
|
}
|
|
@@ -921,6 +1196,23 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
921
1196
|
// Normalize options - support both new object and legacy string projectArg
|
|
922
1197
|
const opts = typeof options === 'string' ? { project: options } : (options || {});
|
|
923
1198
|
ensureInitialized();
|
|
1199
|
+
try {
|
|
1200
|
+
await ensureOrgSelected({
|
|
1201
|
+
orgIdArg: opts.org,
|
|
1202
|
+
nonInteractive: opts.nonInteractive,
|
|
1203
|
+
requireSelection: true,
|
|
1204
|
+
verbose,
|
|
1205
|
+
promptMessage: 'Select organization for this build:',
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
1210
|
+
if (verbose && error?.response) {
|
|
1211
|
+
console.error('Axios response status:', error.response.status);
|
|
1212
|
+
console.error('Axios response data:', error.response.data);
|
|
1213
|
+
}
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
924
1216
|
let spinner;
|
|
925
1217
|
let originalCwd;
|
|
926
1218
|
try {
|
|
@@ -971,6 +1263,31 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
971
1263
|
]);
|
|
972
1264
|
configuration = answer.configuration;
|
|
973
1265
|
}
|
|
1266
|
+
// 2.2 Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
|
|
1267
|
+
let nxConfiguration = opts.nxConfiguration;
|
|
1268
|
+
if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
|
|
1269
|
+
const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
|
|
1270
|
+
if (nxConfigs && nxConfigs.configurations.length > 0) {
|
|
1271
|
+
const choices = nxConfigs.configurations.map((c) => ({
|
|
1272
|
+
name: c === nxConfigs.defaultConfiguration ? `${c} (default)` : c,
|
|
1273
|
+
value: c,
|
|
1274
|
+
}));
|
|
1275
|
+
// Add option to skip/use default
|
|
1276
|
+
choices.unshift({ name: '(none - use defaults)', value: '' });
|
|
1277
|
+
const { chosenNxConfig } = await inquirer.prompt([
|
|
1278
|
+
{
|
|
1279
|
+
type: 'list',
|
|
1280
|
+
name: 'chosenNxConfig',
|
|
1281
|
+
message: 'Nx build configuration (environment):',
|
|
1282
|
+
choices,
|
|
1283
|
+
default: nxConfigs.defaultConfiguration || '',
|
|
1284
|
+
},
|
|
1285
|
+
]);
|
|
1286
|
+
nxConfiguration = chosenNxConfig || undefined;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
// Store resolved nxConfiguration back to opts for later use
|
|
1290
|
+
opts.nxConfiguration = nxConfiguration;
|
|
974
1291
|
const normalizeIosDistribution = (input) => {
|
|
975
1292
|
const v = String(input ?? '')
|
|
976
1293
|
.trim()
|
|
@@ -1098,94 +1415,147 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1098
1415
|
if (configuration === 'release') {
|
|
1099
1416
|
spinner.stop();
|
|
1100
1417
|
if (platform === 'ios') {
|
|
1101
|
-
//
|
|
1418
|
+
// Resolve values from: CLI flags > config file (trimmed to handle whitespace)
|
|
1102
1419
|
const configTeamId = norrixConfig.ios?.teamId;
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1420
|
+
const configP12Path = norrixConfig.ios?.p12Path;
|
|
1421
|
+
const configProfilePath = norrixConfig.ios?.provisioningProfilePath;
|
|
1422
|
+
const configAscKeyId = norrixConfig.ios?.ascApiKeyId;
|
|
1423
|
+
const configAscIssuerId = norrixConfig.ios?.ascIssuerId;
|
|
1424
|
+
const configAscKeyPath = norrixConfig.ios?.ascPrivateKeyPath;
|
|
1425
|
+
const resolvedTeamId = trimString(opts.teamId) || configTeamId;
|
|
1426
|
+
const resolvedP12Path = trimString(opts.p12Path) || configP12Path;
|
|
1427
|
+
const resolvedP12Password = trimString(opts.p12Password);
|
|
1428
|
+
const resolvedProfilePath = trimString(opts.profilePath) || configProfilePath;
|
|
1429
|
+
const resolvedAscKeyId = trimString(opts.ascKeyId) || configAscKeyId;
|
|
1430
|
+
const resolvedAscIssuerId = trimString(opts.ascIssuerId) || configAscIssuerId;
|
|
1431
|
+
const resolvedAscKeyPath = trimString(opts.ascKeyPath) || configAscKeyPath;
|
|
1432
|
+
if (opts.nonInteractive) {
|
|
1433
|
+
// Non-interactive mode: use CLI flags and config values only
|
|
1434
|
+
iosCredentials = {
|
|
1435
|
+
teamId: resolvedTeamId || undefined,
|
|
1436
|
+
p12Base64: readOptionalFileAsBase64(resolvedP12Path),
|
|
1437
|
+
p12Password: resolvedP12Password || undefined,
|
|
1438
|
+
mobileprovisionBase64: readOptionalFileAsBase64(resolvedProfilePath),
|
|
1439
|
+
ascApiKeyId: resolvedAscKeyId || undefined,
|
|
1440
|
+
ascIssuerId: resolvedAscIssuerId || undefined,
|
|
1441
|
+
ascPrivateKey: readOptionalFileAsBase64(resolvedAscKeyPath),
|
|
1442
|
+
};
|
|
1443
|
+
if (resolvedTeamId) {
|
|
1444
|
+
console.log(`Using Apple Team ID: ${resolvedTeamId}`);
|
|
1445
|
+
}
|
|
1446
|
+
if (resolvedAscKeyId) {
|
|
1447
|
+
console.log(`Using ASC API Key: ${resolvedAscKeyId}`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
// Interactive mode: prompt for values with CLI/config as defaults
|
|
1452
|
+
// Flow: Team ID -> ASC Key (recommended) -> If no ASC, then .p12/.mobileprovision
|
|
1453
|
+
const iosAnswers = await inquirer.prompt([
|
|
1454
|
+
{
|
|
1455
|
+
type: 'input',
|
|
1456
|
+
name: 'teamId',
|
|
1457
|
+
message: 'Apple Developer Team ID (required for code signing, e.g. "ABC123XYZ"):',
|
|
1458
|
+
default: resolvedTeamId || '',
|
|
1459
|
+
validate: (input) => {
|
|
1460
|
+
if (!input.trim()) {
|
|
1461
|
+
return true; // Allow empty, workflow will try to proceed without it
|
|
1462
|
+
}
|
|
1463
|
+
if (/^[A-Z0-9]{10}$/.test(input.trim())) {
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
|
|
1467
|
+
},
|
|
1468
|
+
},
|
|
1469
|
+
// ASC API Key is the recommended approach - ask first
|
|
1470
|
+
{
|
|
1471
|
+
type: 'confirm',
|
|
1472
|
+
name: 'useAscKey',
|
|
1473
|
+
message: 'Use App Store Connect API Key for auto-provisioning? (recommended)',
|
|
1474
|
+
default: Boolean(resolvedAscKeyId) || true,
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
type: 'input',
|
|
1478
|
+
name: 'ascApiKeyId',
|
|
1479
|
+
message: 'ASC API Key ID:',
|
|
1480
|
+
default: resolvedAscKeyId || '',
|
|
1481
|
+
when: (a) => a.useAscKey,
|
|
1482
|
+
validate: (input) => {
|
|
1483
|
+
if (!input.trim()) {
|
|
1484
|
+
return 'API Key ID is required when using ASC Key';
|
|
1485
|
+
}
|
|
1121
1486
|
return true;
|
|
1122
|
-
}
|
|
1123
|
-
return 'Team ID should be 10 alphanumeric characters (e.g. "ABC123XYZ"). Leave empty to skip.';
|
|
1487
|
+
},
|
|
1124
1488
|
},
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1489
|
+
{
|
|
1490
|
+
type: 'input',
|
|
1491
|
+
name: 'ascIssuerId',
|
|
1492
|
+
message: 'ASC Issuer ID:',
|
|
1493
|
+
default: resolvedAscIssuerId || '',
|
|
1494
|
+
when: (a) => a.useAscKey,
|
|
1495
|
+
validate: (input) => {
|
|
1496
|
+
if (!input.trim()) {
|
|
1497
|
+
return 'Issuer ID is required when using ASC Key';
|
|
1498
|
+
}
|
|
1499
|
+
return true;
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
type: 'input',
|
|
1504
|
+
name: 'ascPrivateKeyPath',
|
|
1505
|
+
message: 'Path to ASC private key .p8:',
|
|
1506
|
+
default: resolvedAscKeyPath || '',
|
|
1507
|
+
when: (a) => a.useAscKey,
|
|
1508
|
+
validate: (input) => {
|
|
1509
|
+
if (!input.trim()) {
|
|
1510
|
+
return 'Path to .p8 key file is required when using ASC Key';
|
|
1511
|
+
}
|
|
1512
|
+
return true;
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
// Only ask for .p12/.mobileprovision if NOT using ASC Key
|
|
1516
|
+
{
|
|
1517
|
+
type: 'input',
|
|
1518
|
+
name: 'p12Path',
|
|
1519
|
+
message: 'Path to iOS .p12 certificate (optional):',
|
|
1520
|
+
default: resolvedP12Path || '',
|
|
1521
|
+
when: (a) => !a.useAscKey,
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
type: 'password',
|
|
1525
|
+
name: 'p12Password',
|
|
1526
|
+
message: 'Password for .p12 (if any):',
|
|
1527
|
+
mask: '*',
|
|
1528
|
+
default: '',
|
|
1529
|
+
when: (a) => !a.useAscKey && a.p12Path,
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
type: 'input',
|
|
1533
|
+
name: 'mobileprovisionPath',
|
|
1534
|
+
message: 'Path to provisioning profile .mobileprovision (optional):',
|
|
1535
|
+
default: resolvedProfilePath || '',
|
|
1536
|
+
when: (a) => !a.useAscKey,
|
|
1537
|
+
},
|
|
1538
|
+
]);
|
|
1539
|
+
// Use resolved teamId from CLI/config, or from prompt
|
|
1540
|
+
const finalTeamId = trimString(iosAnswers.teamId) || resolvedTeamId;
|
|
1541
|
+
iosCredentials = {
|
|
1542
|
+
teamId: finalTeamId || undefined,
|
|
1543
|
+
p12Base64: readOptionalFileAsBase64(iosAnswers.p12Path),
|
|
1544
|
+
p12Password: trimString(iosAnswers.p12Password),
|
|
1545
|
+
mobileprovisionBase64: readOptionalFileAsBase64(iosAnswers.mobileprovisionPath),
|
|
1546
|
+
ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
|
|
1547
|
+
ascIssuerId: trimString(iosAnswers.ascIssuerId),
|
|
1548
|
+
ascPrivateKey: readOptionalFileAsBase64(iosAnswers.ascPrivateKeyPath),
|
|
1549
|
+
// Track paths for config saving (not sent to API)
|
|
1550
|
+
_p12Path: trimString(iosAnswers.p12Path),
|
|
1551
|
+
_mobileprovisionPath: trimString(iosAnswers.mobileprovisionPath),
|
|
1552
|
+
_ascApiKeyId: trimString(iosAnswers.ascApiKeyId),
|
|
1553
|
+
_ascIssuerId: trimString(iosAnswers.ascIssuerId),
|
|
1554
|
+
_ascPrivateKeyPath: trimString(iosAnswers.ascPrivateKeyPath),
|
|
1555
|
+
};
|
|
1556
|
+
if (finalTeamId) {
|
|
1557
|
+
console.log(`Using Apple Team ID: ${finalTeamId}`);
|
|
1558
|
+
}
|
|
1189
1559
|
}
|
|
1190
1560
|
}
|
|
1191
1561
|
else if (platform === 'android') {
|
|
@@ -1232,14 +1602,14 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1232
1602
|
]);
|
|
1233
1603
|
androidCredentials = {
|
|
1234
1604
|
keystoreBase64: readOptionalFileAsBase64(androidAnswers.keystorePath),
|
|
1235
|
-
keystorePassword: androidAnswers.keystorePassword
|
|
1236
|
-
keyAlias: androidAnswers.keyAlias
|
|
1237
|
-
keyPassword: androidAnswers.keyPassword
|
|
1605
|
+
keystorePassword: trimString(androidAnswers.keystorePassword),
|
|
1606
|
+
keyAlias: trimString(androidAnswers.keyAlias),
|
|
1607
|
+
keyPassword: trimString(androidAnswers.keyPassword),
|
|
1238
1608
|
playServiceAccountJson: readOptionalFileAsBase64(androidAnswers.playJsonPath),
|
|
1239
1609
|
};
|
|
1240
1610
|
// Track Android paths for config saving
|
|
1241
|
-
androidCredentials._keystorePath = androidAnswers.keystorePath
|
|
1242
|
-
androidCredentials._keyAlias = androidAnswers.keyAlias
|
|
1611
|
+
androidCredentials._keystorePath = trimString(androidAnswers.keystorePath);
|
|
1612
|
+
androidCredentials._keyAlias = trimString(androidAnswers.keyAlias);
|
|
1243
1613
|
}
|
|
1244
1614
|
// Offer to save config if no norrix.config.ts exists and we collected useful values
|
|
1245
1615
|
const appRoot = process.cwd();
|
|
@@ -1263,6 +1633,16 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1263
1633
|
if (iosCredentials._mobileprovisionPath) {
|
|
1264
1634
|
saveableOptions.provisioningProfilePath = iosCredentials._mobileprovisionPath;
|
|
1265
1635
|
}
|
|
1636
|
+
// Save ASC API Key details for future builds
|
|
1637
|
+
if (iosCredentials._ascApiKeyId) {
|
|
1638
|
+
saveableOptions.ascApiKeyId = iosCredentials._ascApiKeyId;
|
|
1639
|
+
}
|
|
1640
|
+
if (iosCredentials._ascIssuerId) {
|
|
1641
|
+
saveableOptions.ascIssuerId = iosCredentials._ascIssuerId;
|
|
1642
|
+
}
|
|
1643
|
+
if (iosCredentials._ascPrivateKeyPath) {
|
|
1644
|
+
saveableOptions.ascPrivateKeyPath = iosCredentials._ascPrivateKeyPath;
|
|
1645
|
+
}
|
|
1266
1646
|
}
|
|
1267
1647
|
if (platform === 'android' && androidCredentials) {
|
|
1268
1648
|
if (androidCredentials._keystorePath) {
|
|
@@ -1277,7 +1657,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1277
1657
|
saveableOptions.distributionType ||
|
|
1278
1658
|
saveableOptions.p12Path ||
|
|
1279
1659
|
saveableOptions.provisioningProfilePath ||
|
|
1280
|
-
saveableOptions.keystorePath
|
|
1660
|
+
saveableOptions.keystorePath ||
|
|
1661
|
+
saveableOptions.ascApiKeyId;
|
|
1281
1662
|
if (hasSaveableValues) {
|
|
1282
1663
|
const { shouldSave } = await inquirer.prompt([
|
|
1283
1664
|
{
|
|
@@ -1309,7 +1690,7 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1309
1690
|
writeRuntimeFingerprintFile(projectRoot, fingerprint, platform);
|
|
1310
1691
|
spinner.start('Creating project archive...');
|
|
1311
1692
|
// 3. Zip the project (workspace-aware)
|
|
1312
|
-
const { zipPath, workspaceContext } = await zipProject(projectName, false, verbose);
|
|
1693
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, false, verbose);
|
|
1313
1694
|
spinner.text = 'Project archive created';
|
|
1314
1695
|
// 4. Upload the project zip to S3
|
|
1315
1696
|
spinner.text = 'Working...';
|
|
@@ -1342,6 +1723,11 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1342
1723
|
appPath: workspaceContext.relativeAppPath,
|
|
1343
1724
|
projectName: workspaceContext.projectName,
|
|
1344
1725
|
} : undefined;
|
|
1726
|
+
// For standalone projects, use the project option or infer from app ID/package name
|
|
1727
|
+
// This allows env vars to be scoped to specific standalone projects within an org
|
|
1728
|
+
const standaloneProjectName = workspaceContext.type === 'standalone'
|
|
1729
|
+
? (opts.project || inferredAppId || projectName)
|
|
1730
|
+
: undefined;
|
|
1345
1731
|
const response = await axios.post(`${API_URL}/build`, {
|
|
1346
1732
|
projectName,
|
|
1347
1733
|
appId: inferredAppId,
|
|
@@ -1351,12 +1737,18 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1351
1737
|
version: version || '',
|
|
1352
1738
|
buildNumber: buildNumber || '',
|
|
1353
1739
|
configuration,
|
|
1740
|
+
// Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
|
|
1741
|
+
...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
|
|
1354
1742
|
...(distributionType ? { distributionType } : {}),
|
|
1743
|
+
// Android package type override (apk or aab) - takes precedence over distributionType
|
|
1744
|
+
...(opts.androidPackageType ? { androidPackageType: opts.androidPackageType } : {}),
|
|
1355
1745
|
fingerprint,
|
|
1356
1746
|
// Provide the relative key (without public/) – the workflow prepends public/
|
|
1357
1747
|
s3Key: s3KeyRel,
|
|
1358
1748
|
// Workspace context for Nx monorepos
|
|
1359
1749
|
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
1750
|
+
// For standalone projects, include project name for env var scoping
|
|
1751
|
+
...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
|
|
1360
1752
|
// Only include raw credentials if not encrypted
|
|
1361
1753
|
...(encryptedSecrets ? { encryptedSecrets } : {}),
|
|
1362
1754
|
...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
|
|
@@ -1462,8 +1854,25 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1462
1854
|
* Submit command implementation
|
|
1463
1855
|
* Submits the built app to app stores via the Next.js API gateway
|
|
1464
1856
|
*/
|
|
1465
|
-
export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
|
|
1857
|
+
export async function submit(cliPlatformArg, cliTrackArg, verbose = false, options) {
|
|
1466
1858
|
ensureInitialized();
|
|
1859
|
+
try {
|
|
1860
|
+
await ensureOrgSelected({
|
|
1861
|
+
orgIdArg: options?.org,
|
|
1862
|
+
nonInteractive: Boolean(options?.nonInteractive),
|
|
1863
|
+
requireSelection: true,
|
|
1864
|
+
verbose,
|
|
1865
|
+
promptMessage: 'Select organization for this submission:',
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
catch (error) {
|
|
1869
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
1870
|
+
if (verbose && error?.response) {
|
|
1871
|
+
console.error('Axios response status:', error.response.status);
|
|
1872
|
+
console.error('Axios response data:', error.response.data);
|
|
1873
|
+
}
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1467
1876
|
const spinner = ora('Preparing app for submission...');
|
|
1468
1877
|
try {
|
|
1469
1878
|
spinner.start();
|
|
@@ -1727,6 +2136,23 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1727
2136
|
// Normalize options - support both new object and legacy string projectArg
|
|
1728
2137
|
const opts = typeof options === 'string' ? { project: options } : (options || {});
|
|
1729
2138
|
ensureInitialized();
|
|
2139
|
+
try {
|
|
2140
|
+
await ensureOrgSelected({
|
|
2141
|
+
orgIdArg: opts.org,
|
|
2142
|
+
nonInteractive: opts.nonInteractive,
|
|
2143
|
+
requireSelection: true,
|
|
2144
|
+
verbose,
|
|
2145
|
+
promptMessage: 'Select organization for this update:',
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
catch (error) {
|
|
2149
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2150
|
+
if (verbose && error?.response) {
|
|
2151
|
+
console.error('Axios response status:', error.response.status);
|
|
2152
|
+
console.error('Axios response data:', error.response.data);
|
|
2153
|
+
}
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
1730
2156
|
let spinner;
|
|
1731
2157
|
let originalCwd;
|
|
1732
2158
|
try {
|
|
@@ -1759,6 +2185,33 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1759
2185
|
platform = chosenPlatform;
|
|
1760
2186
|
spinner.start('Preparing over-the-air update...');
|
|
1761
2187
|
}
|
|
2188
|
+
// Determine Nx configuration for workspace builds (CLI arg preferred, then prompt if available)
|
|
2189
|
+
let nxConfiguration = opts.nxConfiguration;
|
|
2190
|
+
if (!nxConfiguration && workspaceCtx.type === 'nx' && !opts.nonInteractive) {
|
|
2191
|
+
spinner.stop();
|
|
2192
|
+
const nxConfigs = detectNxBuildConfigurations(workspaceCtx.appRoot);
|
|
2193
|
+
if (nxConfigs && nxConfigs.configurations.length > 0) {
|
|
2194
|
+
const choices = nxConfigs.configurations.map((c) => ({
|
|
2195
|
+
name: c === nxConfigs.defaultConfiguration ? `${c} (default)` : c,
|
|
2196
|
+
value: c,
|
|
2197
|
+
}));
|
|
2198
|
+
// Add option to skip/use default
|
|
2199
|
+
choices.unshift({ name: '(none - use defaults)', value: '' });
|
|
2200
|
+
const { chosenNxConfig } = await inquirer.prompt([
|
|
2201
|
+
{
|
|
2202
|
+
type: 'list',
|
|
2203
|
+
name: 'chosenNxConfig',
|
|
2204
|
+
message: 'Nx build configuration (environment):',
|
|
2205
|
+
choices,
|
|
2206
|
+
default: nxConfigs.defaultConfiguration || '',
|
|
2207
|
+
},
|
|
2208
|
+
]);
|
|
2209
|
+
nxConfiguration = chosenNxConfig || undefined;
|
|
2210
|
+
}
|
|
2211
|
+
spinner.start('Preparing over-the-air update...');
|
|
2212
|
+
}
|
|
2213
|
+
// Store resolved nxConfiguration back to opts for later use
|
|
2214
|
+
opts.nxConfiguration = nxConfiguration;
|
|
1762
2215
|
// Infer version from native project files (same as build)
|
|
1763
2216
|
const appleVersionInfo = platform === 'ios' || platform === 'visionos'
|
|
1764
2217
|
? getAppleVersionFromInfoPlist(platform)
|
|
@@ -1963,7 +2416,7 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1963
2416
|
spinner.start('Packaging for over-the-air update...');
|
|
1964
2417
|
// Create the update bundle (workspace-aware) - pass true to include node_modules for updates
|
|
1965
2418
|
const projectName = await getProjectName();
|
|
1966
|
-
const { zipPath, workspaceContext } = await zipProject(projectName, true, verbose);
|
|
2419
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, true, verbose);
|
|
1967
2420
|
spinner.text = 'Uploading update to Norrix cloud storage...';
|
|
1968
2421
|
const fileBuffer = fs.readFileSync(zipPath);
|
|
1969
2422
|
const updateFolder = `update-${Date.now()}`;
|
|
@@ -1977,6 +2430,11 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1977
2430
|
appPath: workspaceContext.relativeAppPath,
|
|
1978
2431
|
projectName: workspaceContext.projectName,
|
|
1979
2432
|
} : undefined;
|
|
2433
|
+
// For standalone projects, use the project option or infer from app ID
|
|
2434
|
+
// This allows env vars to be scoped to specific standalone projects within an org
|
|
2435
|
+
const standaloneProjectName = workspaceContext.type === 'standalone'
|
|
2436
|
+
? (opts.project || appId)
|
|
2437
|
+
: undefined;
|
|
1980
2438
|
const response = await axios.post(`${API_URL}/update`, {
|
|
1981
2439
|
appId,
|
|
1982
2440
|
platform,
|
|
@@ -1984,10 +2442,14 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
1984
2442
|
buildNumber: buildNumber || '',
|
|
1985
2443
|
releaseNotes: notes,
|
|
1986
2444
|
fingerprint,
|
|
2445
|
+
// Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
|
|
2446
|
+
...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
|
|
1987
2447
|
// Provide the relative key (without public/). Consumers will prepend public/
|
|
1988
2448
|
s3Key: s3KeyRel,
|
|
1989
2449
|
// Workspace context for Nx monorepos
|
|
1990
2450
|
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
2451
|
+
// For standalone projects, include project name for env var scoping
|
|
2452
|
+
...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
|
|
1991
2453
|
}, {
|
|
1992
2454
|
headers: {
|
|
1993
2455
|
'Content-Type': 'application/json',
|
|
@@ -2079,8 +2541,20 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false, opt
|
|
|
2079
2541
|
* Build Status command implementation
|
|
2080
2542
|
* Checks the status of a build via the Next.js API gateway
|
|
2081
2543
|
*/
|
|
2082
|
-
export async function buildStatus(buildId, verbose = false) {
|
|
2544
|
+
export async function buildStatus(buildId, verbose = false, options) {
|
|
2083
2545
|
ensureInitialized();
|
|
2546
|
+
try {
|
|
2547
|
+
await ensureOrgSelected({
|
|
2548
|
+
orgIdArg: options?.org,
|
|
2549
|
+
nonInteractive: true,
|
|
2550
|
+
requireSelection: false,
|
|
2551
|
+
verbose,
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
catch (error) {
|
|
2555
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2084
2558
|
try {
|
|
2085
2559
|
const spinner = ora(`Checking status of build ${buildId}...`).start();
|
|
2086
2560
|
const response = await axios.get(`${API_URL}/build/${buildId}`, {
|
|
@@ -2117,8 +2591,20 @@ export async function buildStatus(buildId, verbose = false) {
|
|
|
2117
2591
|
* Submit Status command implementation
|
|
2118
2592
|
* Checks the status of a submission via the Next.js API gateway
|
|
2119
2593
|
*/
|
|
2120
|
-
export async function submitStatus(submitId, verbose = false) {
|
|
2594
|
+
export async function submitStatus(submitId, verbose = false, options) {
|
|
2121
2595
|
ensureInitialized();
|
|
2596
|
+
try {
|
|
2597
|
+
await ensureOrgSelected({
|
|
2598
|
+
orgIdArg: options?.org,
|
|
2599
|
+
nonInteractive: true,
|
|
2600
|
+
requireSelection: false,
|
|
2601
|
+
verbose,
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
catch (error) {
|
|
2605
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2122
2608
|
try {
|
|
2123
2609
|
const spinner = ora(`Checking status of submission ${submitId}...`).start();
|
|
2124
2610
|
const response = await axios.get(`${API_URL}/submit/${submitId}`, {
|
|
@@ -2151,8 +2637,20 @@ export async function submitStatus(submitId, verbose = false) {
|
|
|
2151
2637
|
* Update Status command implementation
|
|
2152
2638
|
* Checks the status of an update via the Next.js API gateway
|
|
2153
2639
|
*/
|
|
2154
|
-
export async function updateStatus(updateId, verbose = false) {
|
|
2640
|
+
export async function updateStatus(updateId, verbose = false, options) {
|
|
2155
2641
|
ensureInitialized();
|
|
2642
|
+
try {
|
|
2643
|
+
await ensureOrgSelected({
|
|
2644
|
+
orgIdArg: options?.org,
|
|
2645
|
+
nonInteractive: true,
|
|
2646
|
+
requireSelection: false,
|
|
2647
|
+
verbose,
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
catch (error) {
|
|
2651
|
+
ora().fail(`Organization selection failed: ${error?.message || error}`);
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2156
2654
|
try {
|
|
2157
2655
|
const spinner = ora(`Checking status of update ${updateId}...`).start();
|
|
2158
2656
|
const response = await axios.get(`${API_URL}/update/${updateId}`, {
|
|
@@ -2290,6 +2788,41 @@ export async function uploadFile(filePath, options, verbose = false) {
|
|
|
2290
2788
|
*/
|
|
2291
2789
|
export async function currentUser(verbose = false) {
|
|
2292
2790
|
ensureInitialized();
|
|
2791
|
+
// Check for API key authentication first
|
|
2792
|
+
const apiKey = getApiKey();
|
|
2793
|
+
if (apiKey) {
|
|
2794
|
+
const spinner = ora('Verifying API key...').start();
|
|
2795
|
+
try {
|
|
2796
|
+
// Make a request to verify the API key and get user info
|
|
2797
|
+
const response = await axios.get(`${API_URL}/user`, {
|
|
2798
|
+
headers: await getAuthHeaders(),
|
|
2799
|
+
});
|
|
2800
|
+
spinner.stop();
|
|
2801
|
+
const user = response.data?.user;
|
|
2802
|
+
if (user) {
|
|
2803
|
+
console.log('🔑 Authenticated via API key');
|
|
2804
|
+
console.log(`Email: ${user.email}`);
|
|
2805
|
+
console.log(`Username: ${user.username ?? 'N/A'}`);
|
|
2806
|
+
console.log(`Organization: ${response.data?.organization?.name ?? 'N/A'}`);
|
|
2807
|
+
console.log(`API Key: ${apiKey.substring(0, 12)}...`);
|
|
2808
|
+
}
|
|
2809
|
+
else {
|
|
2810
|
+
console.log('🔑 API key configured but unable to fetch user details.');
|
|
2811
|
+
}
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
catch (error) {
|
|
2815
|
+
spinner.stop();
|
|
2816
|
+
if (error?.response?.status === 401) {
|
|
2817
|
+
console.log('⚠️ API key is invalid or expired.');
|
|
2818
|
+
}
|
|
2819
|
+
else {
|
|
2820
|
+
console.log('⚠️ Failed to verify API key:', error?.message || error);
|
|
2821
|
+
}
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
// Fall back to Cognito session
|
|
2293
2826
|
try {
|
|
2294
2827
|
const spinner = ora('Fetching current user...').start();
|
|
2295
2828
|
let user;
|
|
@@ -2309,6 +2842,10 @@ export async function currentUser(verbose = false) {
|
|
|
2309
2842
|
spinner.stop();
|
|
2310
2843
|
if (!user) {
|
|
2311
2844
|
console.log('⚠️ No user is currently signed in.');
|
|
2845
|
+
console.log('');
|
|
2846
|
+
console.log('To authenticate, either:');
|
|
2847
|
+
console.log(' • Run `norrix sign-in` for interactive login');
|
|
2848
|
+
console.log(' • Set NORRIX_API_KEY environment variable for CI/automation');
|
|
2312
2849
|
return;
|
|
2313
2850
|
}
|
|
2314
2851
|
console.log('Signed-in user info:');
|
|
@@ -2397,4 +2934,310 @@ export async function billingPortal(verbose = false) {
|
|
|
2397
2934
|
}
|
|
2398
2935
|
}
|
|
2399
2936
|
}
|
|
2937
|
+
function normalizeVisibilityType(input) {
|
|
2938
|
+
if (!input)
|
|
2939
|
+
return 'secret';
|
|
2940
|
+
if (input === 'plaintext' || input === 'secret')
|
|
2941
|
+
return input;
|
|
2942
|
+
console.error('Error: --visibility must be either "plaintext" or "secret"');
|
|
2943
|
+
process.exit(1);
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Set an environment variable for build-time injection
|
|
2947
|
+
*/
|
|
2948
|
+
export async function envSet(name, value, verbose = false, options = {}) {
|
|
2949
|
+
ensureInitialized();
|
|
2950
|
+
let spinner;
|
|
2951
|
+
try {
|
|
2952
|
+
const visibilityType = normalizeVisibilityType(options.visibilityType);
|
|
2953
|
+
// Validate secret name (env var format)
|
|
2954
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(name)) {
|
|
2955
|
+
console.error('Error: Name must be a valid environment variable name (alphanumeric and underscores, cannot start with a number)');
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
spinner = ora(`Setting env var "${name}"...`).start();
|
|
2959
|
+
const payload = {
|
|
2960
|
+
type: 'variable',
|
|
2961
|
+
name,
|
|
2962
|
+
visibilityType,
|
|
2963
|
+
project: options.project,
|
|
2964
|
+
};
|
|
2965
|
+
if (visibilityType === 'plaintext') {
|
|
2966
|
+
payload.value = value;
|
|
2967
|
+
}
|
|
2968
|
+
else {
|
|
2969
|
+
Object.assign(payload, encryptSecretValue(value));
|
|
2970
|
+
}
|
|
2971
|
+
const res = await axios.post(`${API_URL}/env`, payload, {
|
|
2972
|
+
headers: await getAuthHeaders(),
|
|
2973
|
+
});
|
|
2974
|
+
spinner.succeed(`Env var "${name}" set successfully`);
|
|
2975
|
+
if (res.data?.message) {
|
|
2976
|
+
console.log(res.data.message);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
catch (error) {
|
|
2980
|
+
const apiMessage = error?.response?.data?.error ||
|
|
2981
|
+
error?.response?.data?.message ||
|
|
2982
|
+
error?.message ||
|
|
2983
|
+
String(error);
|
|
2984
|
+
spinner?.fail(`Failed to set env var: ${apiMessage}`);
|
|
2985
|
+
if (verbose) {
|
|
2986
|
+
console.error('--- Verbose error details (env-set) ---');
|
|
2987
|
+
console.error(error);
|
|
2988
|
+
if (error?.response) {
|
|
2989
|
+
console.error('Axios response status:', error.response.status);
|
|
2990
|
+
console.error('Axios response data:', error.response.data);
|
|
2991
|
+
}
|
|
2992
|
+
if (error?.stack) {
|
|
2993
|
+
console.error(error.stack);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
process.exit(1);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Upload an environment file for build-time injection
|
|
3001
|
+
*/
|
|
3002
|
+
export async function envSetFile(name, filePath, verbose = false, options = {}) {
|
|
3003
|
+
ensureInitialized();
|
|
3004
|
+
let spinner;
|
|
3005
|
+
try {
|
|
3006
|
+
const visibilityType = normalizeVisibilityType(options.visibilityType);
|
|
3007
|
+
const resolvedPath = normalizePath(filePath);
|
|
3008
|
+
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
3009
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
3010
|
+
process.exit(1);
|
|
3011
|
+
}
|
|
3012
|
+
spinner = ora(`Uploading env file "${name}"...`).start();
|
|
3013
|
+
// Read and encode file content
|
|
3014
|
+
const fileContent = fs.readFileSync(resolvedPath);
|
|
3015
|
+
const base64Content = fileContent.toString('base64');
|
|
3016
|
+
const payload = {
|
|
3017
|
+
type: 'file',
|
|
3018
|
+
name,
|
|
3019
|
+
visibilityType,
|
|
3020
|
+
destPath: options.destPath || name,
|
|
3021
|
+
project: options.project,
|
|
3022
|
+
};
|
|
3023
|
+
if (visibilityType === 'plaintext') {
|
|
3024
|
+
payload.value = base64Content;
|
|
3025
|
+
}
|
|
3026
|
+
else {
|
|
3027
|
+
Object.assign(payload, encryptSecretValue(base64Content));
|
|
3028
|
+
}
|
|
3029
|
+
const res = await axios.post(`${API_URL}/env`, payload, {
|
|
3030
|
+
headers: await getAuthHeaders(),
|
|
3031
|
+
});
|
|
3032
|
+
spinner.succeed(`Env file "${name}" uploaded successfully`);
|
|
3033
|
+
if (res.data?.message) {
|
|
3034
|
+
console.log(res.data.message);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
catch (error) {
|
|
3038
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3039
|
+
error?.response?.data?.message ||
|
|
3040
|
+
error?.message ||
|
|
3041
|
+
String(error);
|
|
3042
|
+
spinner?.fail(`Failed to upload env file: ${apiMessage}`);
|
|
3043
|
+
if (verbose) {
|
|
3044
|
+
console.error('--- Verbose error details (env-set-file) ---');
|
|
3045
|
+
console.error(error);
|
|
3046
|
+
if (error?.response) {
|
|
3047
|
+
console.error('Axios response status:', error.response.status);
|
|
3048
|
+
console.error('Axios response data:', error.response.data);
|
|
3049
|
+
}
|
|
3050
|
+
if (error?.stack) {
|
|
3051
|
+
console.error(error.stack);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
process.exit(1);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* List all environment variables/files for a project
|
|
3059
|
+
*/
|
|
3060
|
+
export async function envList(verbose = false, options = {}) {
|
|
3061
|
+
ensureInitialized();
|
|
3062
|
+
let spinner;
|
|
3063
|
+
try {
|
|
3064
|
+
spinner = ora('Fetching env vars...').start();
|
|
3065
|
+
const params = new URLSearchParams();
|
|
3066
|
+
if (options.project) {
|
|
3067
|
+
params.set('project', options.project);
|
|
3068
|
+
}
|
|
3069
|
+
const res = await axios.get(`${API_URL}/env?${params.toString()}`, {
|
|
3070
|
+
headers: await getAuthHeaders(),
|
|
3071
|
+
});
|
|
3072
|
+
spinner.stop();
|
|
3073
|
+
const envs = res.data?.envs || [];
|
|
3074
|
+
const projectName = options.project || res.data?.project || 'default';
|
|
3075
|
+
if (envs.length === 0) {
|
|
3076
|
+
console.log(`\nNo environment variables found for project: ${projectName}`);
|
|
3077
|
+
console.log('\nTo add env vars/files, use:');
|
|
3078
|
+
console.log(' norrix env set <name> <value>');
|
|
3079
|
+
console.log(' norrix env set-file <name> <path>');
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
console.log(`\nEnvironment variables for project: ${projectName}\n`);
|
|
3083
|
+
// Separate variables and files
|
|
3084
|
+
const variables = envs.filter((e) => e.type === 'variable');
|
|
3085
|
+
const files = envs.filter((e) => e.type === 'file');
|
|
3086
|
+
if (variables.length > 0) {
|
|
3087
|
+
console.log('Variables:');
|
|
3088
|
+
for (const variable of variables) {
|
|
3089
|
+
const dateStr = variable.updatedAt
|
|
3090
|
+
? `set ${new Date(variable.updatedAt).toLocaleDateString()}`
|
|
3091
|
+
: '';
|
|
3092
|
+
const visibility = variable.visibilityType || variable.visibility || 'secret';
|
|
3093
|
+
console.log(` • ${variable.name} (${visibility}${dateStr ? `, ${dateStr}` : ''})`);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
if (files.length > 0) {
|
|
3097
|
+
if (variables.length > 0)
|
|
3098
|
+
console.log('');
|
|
3099
|
+
console.log('Files:');
|
|
3100
|
+
for (const file of files) {
|
|
3101
|
+
const dateStr = file.updatedAt
|
|
3102
|
+
? `set ${new Date(file.updatedAt).toLocaleDateString()}`
|
|
3103
|
+
: '';
|
|
3104
|
+
const visibility = file.visibilityType || file.visibility || 'secret';
|
|
3105
|
+
const destPath = file.destPath || file.name;
|
|
3106
|
+
console.log(` • ${file.name} → ${destPath} (${visibility}${dateStr ? `, ${dateStr}` : ''})`);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
console.log('');
|
|
3110
|
+
}
|
|
3111
|
+
catch (error) {
|
|
3112
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3113
|
+
error?.response?.data?.message ||
|
|
3114
|
+
error?.message ||
|
|
3115
|
+
String(error);
|
|
3116
|
+
spinner?.fail(`Failed to list env vars: ${apiMessage}`);
|
|
3117
|
+
if (verbose) {
|
|
3118
|
+
console.error('--- Verbose error details (env-list) ---');
|
|
3119
|
+
console.error(error);
|
|
3120
|
+
if (error?.response) {
|
|
3121
|
+
console.error('Axios response status:', error.response.status);
|
|
3122
|
+
console.error('Axios response data:', error.response.data);
|
|
3123
|
+
}
|
|
3124
|
+
if (error?.stack) {
|
|
3125
|
+
console.error(error.stack);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
process.exit(1);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Delete an environment variable
|
|
3133
|
+
*/
|
|
3134
|
+
export async function envDelete(name, verbose = false, options = {}) {
|
|
3135
|
+
ensureInitialized();
|
|
3136
|
+
let spinner;
|
|
3137
|
+
try {
|
|
3138
|
+
spinner = ora(`Deleting env var "${name}"...`).start();
|
|
3139
|
+
const res = await axios.delete(`${API_URL}/env`, {
|
|
3140
|
+
data: {
|
|
3141
|
+
type: 'variable',
|
|
3142
|
+
name,
|
|
3143
|
+
project: options.project,
|
|
3144
|
+
},
|
|
3145
|
+
headers: await getAuthHeaders(),
|
|
3146
|
+
});
|
|
3147
|
+
spinner.succeed(`Env var "${name}" deleted successfully`);
|
|
3148
|
+
if (res.data?.message) {
|
|
3149
|
+
console.log(res.data.message);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
catch (error) {
|
|
3153
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3154
|
+
error?.response?.data?.message ||
|
|
3155
|
+
error?.message ||
|
|
3156
|
+
String(error);
|
|
3157
|
+
spinner?.fail(`Failed to delete env var: ${apiMessage}`);
|
|
3158
|
+
if (verbose) {
|
|
3159
|
+
console.error('--- Verbose error details (env-delete) ---');
|
|
3160
|
+
console.error(error);
|
|
3161
|
+
if (error?.response) {
|
|
3162
|
+
console.error('Axios response status:', error.response.status);
|
|
3163
|
+
console.error('Axios response data:', error.response.data);
|
|
3164
|
+
}
|
|
3165
|
+
if (error?.stack) {
|
|
3166
|
+
console.error(error.stack);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
process.exit(1);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Delete an environment file
|
|
3174
|
+
*/
|
|
3175
|
+
export async function envDeleteFile(name, verbose = false, options = {}) {
|
|
3176
|
+
ensureInitialized();
|
|
3177
|
+
let spinner;
|
|
3178
|
+
try {
|
|
3179
|
+
spinner = ora(`Deleting env file "${name}"...`).start();
|
|
3180
|
+
const res = await axios.delete(`${API_URL}/env`, {
|
|
3181
|
+
data: {
|
|
3182
|
+
type: 'file',
|
|
3183
|
+
name,
|
|
3184
|
+
project: options.project,
|
|
3185
|
+
},
|
|
3186
|
+
headers: await getAuthHeaders(),
|
|
3187
|
+
});
|
|
3188
|
+
spinner.succeed(`Env file "${name}" deleted successfully`);
|
|
3189
|
+
if (res.data?.message) {
|
|
3190
|
+
console.log(res.data.message);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
catch (error) {
|
|
3194
|
+
const apiMessage = error?.response?.data?.error ||
|
|
3195
|
+
error?.response?.data?.message ||
|
|
3196
|
+
error?.message ||
|
|
3197
|
+
String(error);
|
|
3198
|
+
spinner?.fail(`Failed to delete env file: ${apiMessage}`);
|
|
3199
|
+
if (verbose) {
|
|
3200
|
+
console.error('--- Verbose error details (env-delete-file) ---');
|
|
3201
|
+
console.error(error);
|
|
3202
|
+
if (error?.response) {
|
|
3203
|
+
console.error('Axios response status:', error.response.status);
|
|
3204
|
+
console.error('Axios response data:', error.response.data);
|
|
3205
|
+
}
|
|
3206
|
+
if (error?.stack) {
|
|
3207
|
+
console.error(error.stack);
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
process.exit(1);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Encrypt a secret value using the Norrix public key (envelope encryption)
|
|
3215
|
+
*/
|
|
3216
|
+
function encryptSecretValue(value) {
|
|
3217
|
+
const publicKeyPem = process.env.NORRIX_BUILD_PUBLIC_KEY;
|
|
3218
|
+
if (!publicKeyPem) {
|
|
3219
|
+
// If no public key is configured, send value in a format the server can handle
|
|
3220
|
+
// The server will encrypt at rest using its own key
|
|
3221
|
+
return {
|
|
3222
|
+
encryptedValue: Buffer.from(value, 'utf8').toString('base64'),
|
|
3223
|
+
encryptedKey: '',
|
|
3224
|
+
iv: '',
|
|
3225
|
+
tag: '',
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
const nodeCrypto = crypto;
|
|
3229
|
+
const aesKey = nodeCrypto.randomBytes(32); // AES-256
|
|
3230
|
+
const iv = nodeCrypto.randomBytes(12); // GCM nonce
|
|
3231
|
+
const cipher = nodeCrypto.createCipheriv('aes-256-gcm', aesKey, iv);
|
|
3232
|
+
const plaintext = Buffer.from(value, 'utf8');
|
|
3233
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
3234
|
+
const tag = cipher.getAuthTag();
|
|
3235
|
+
const encryptedKey = nodeCrypto.publicEncrypt({ key: publicKeyPem, oaepHash: 'sha256' }, aesKey);
|
|
3236
|
+
return {
|
|
3237
|
+
encryptedValue: ciphertext.toString('base64'),
|
|
3238
|
+
encryptedKey: encryptedKey.toString('base64'),
|
|
3239
|
+
iv: iv.toString('base64'),
|
|
3240
|
+
tag: tag.toString('base64'),
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
2400
3243
|
//# sourceMappingURL=commands.js.map
|