@norrix/cli 0.0.24 → 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.
- package/CHANGELOG.md +42 -0
- package/dist/cli.js +118 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.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 +104 -6
- package/dist/lib/commands.js +1339 -155
- package/dist/lib/commands.js.map +1 -1
- package/dist/lib/config-helpers.spec.js +8 -0
- package/dist/lib/config.d.ts +197 -0
- package/dist/lib/config.js +246 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/defaults.js +0 -1
- package/dist/lib/dev-defaults.js +0 -1
- package/dist/lib/fingerprinting.d.ts +8 -0
- package/dist/lib/fingerprinting.js +4 -2
- package/dist/lib/fingerprinting.js.map +1 -1
- package/dist/lib/norrix-cli.js +0 -1
- package/dist/lib/prod-defaults.js +0 -1
- package/dist/lib/workspace.d.ts +138 -0
- package/dist/lib/workspace.js +826 -0
- package/dist/lib/workspace.js.map +1 -0
- package/package.json +1 -1
package/dist/lib/commands.js
CHANGED
|
@@ -9,14 +9,18 @@ import archiver from 'archiver';
|
|
|
9
9
|
// import FormData from 'form-data';
|
|
10
10
|
import { configureAmplify, loadCliEnvFiles } from './amplify-config.js';
|
|
11
11
|
import { computeFingerprint, writeRuntimeFingerprintFile } from './fingerprinting.js';
|
|
12
|
+
import { loadNorrixConfig, hasNorrixConfig, saveNorrixConfig } from './config.js';
|
|
13
|
+
import { detectWorkspaceContext, getNxProjectDependencies, getWorkspaceDependenciesFallback, createWorkspaceManifest, logWorkspaceContext, isAtWorkspaceRoot, discoverNativeScriptApps, getWorkspaceContextForApp, detectNxBuildConfigurations, } from './workspace.js';
|
|
12
14
|
import { signIn as amplifySignIn, signOut as amplifySignOut, getCurrentUser, fetchAuthSession, } from 'aws-amplify/auth';
|
|
13
15
|
import crypto from 'crypto';
|
|
14
16
|
import { Amplify } from 'aws-amplify';
|
|
15
17
|
import { PROD_DEFAULTS } from './prod-defaults.js';
|
|
16
18
|
import { DEV_DEFAULTS } from './dev-defaults.js';
|
|
19
|
+
import { clearSelectedOrgId, getSelectedOrgId, setSelectedOrgId } from './cli-settings.js';
|
|
17
20
|
let CURRENT_ENV = 'prod';
|
|
18
21
|
let CURRENT_DEFAULTS = PROD_DEFAULTS;
|
|
19
22
|
let API_URL = PROD_DEFAULTS.apiUrl;
|
|
23
|
+
let CURRENT_ORG_ID;
|
|
20
24
|
let IS_INITIALIZED = false;
|
|
21
25
|
function defaultsForEnv(env) {
|
|
22
26
|
return env === 'dev' ? DEV_DEFAULTS : PROD_DEFAULTS;
|
|
@@ -38,6 +42,10 @@ export function initNorrixCli(env = 'prod') {
|
|
|
38
42
|
CURRENT_DEFAULTS = defaultsForEnv(env);
|
|
39
43
|
configureAmplify(env);
|
|
40
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);
|
|
41
49
|
IS_INITIALIZED = true;
|
|
42
50
|
}
|
|
43
51
|
function ensureInitialized() {
|
|
@@ -48,19 +56,168 @@ function ensureInitialized() {
|
|
|
48
56
|
/**
|
|
49
57
|
* Return Authorization header containing the current Cognito ID token (if signed in).
|
|
50
58
|
*/
|
|
51
|
-
async function getAuthHeaders() {
|
|
59
|
+
async function getAuthHeaders(options) {
|
|
52
60
|
ensureInitialized();
|
|
61
|
+
const headers = {};
|
|
53
62
|
try {
|
|
54
63
|
const session = await fetchAuthSession();
|
|
55
64
|
const idToken = session.tokens?.idToken?.toString();
|
|
56
65
|
if (idToken) {
|
|
57
|
-
|
|
66
|
+
headers.Authorization = `Bearer ${idToken}`;
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
catch (_) {
|
|
61
70
|
/* not signed in */
|
|
62
71
|
}
|
|
63
|
-
|
|
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}`);
|
|
64
221
|
}
|
|
65
222
|
/**
|
|
66
223
|
* Norrix CLI Command Implementations
|
|
@@ -78,6 +235,16 @@ async function getAuthHeaders() {
|
|
|
78
235
|
// Get dirname equivalent in ESM
|
|
79
236
|
const __filename = fileURLToPath(import.meta.url);
|
|
80
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
|
+
}
|
|
81
248
|
function normalizePath(input) {
|
|
82
249
|
if (!input)
|
|
83
250
|
return undefined;
|
|
@@ -635,20 +802,105 @@ function getAndroidVersionFromAppGradle() {
|
|
|
635
802
|
}
|
|
636
803
|
}
|
|
637
804
|
/**
|
|
638
|
-
*
|
|
805
|
+
* Resolve workspace context for a command.
|
|
806
|
+
*
|
|
807
|
+
* When running from workspace root:
|
|
808
|
+
* - If --project is provided, use that project
|
|
809
|
+
* - Otherwise, discover NativeScript apps and prompt for selection
|
|
810
|
+
*
|
|
811
|
+
* When running from within an app directory:
|
|
812
|
+
* - Use the current directory's context
|
|
813
|
+
*
|
|
814
|
+
* @param projectArg - Optional project name from --project flag
|
|
815
|
+
* @param spinner - Optional spinner to stop before prompting
|
|
639
816
|
*/
|
|
640
|
-
async function
|
|
817
|
+
async function resolveWorkspaceContext(projectArg, spinner) {
|
|
818
|
+
const originalCwd = process.cwd();
|
|
819
|
+
// If we're at workspace root (has nx.json but no nativescript.config)
|
|
820
|
+
if (isAtWorkspaceRoot()) {
|
|
821
|
+
// If --project was provided, use it
|
|
822
|
+
if (projectArg) {
|
|
823
|
+
const ctx = getWorkspaceContextForApp(projectArg);
|
|
824
|
+
if (!ctx) {
|
|
825
|
+
const apps = discoverNativeScriptApps();
|
|
826
|
+
const appNames = apps.map((a) => a.name).join(', ');
|
|
827
|
+
throw new Error(`Project '${projectArg}' not found in workspace. Available NativeScript apps: ${appNames || 'none'}`);
|
|
828
|
+
}
|
|
829
|
+
// Change to the app directory for subsequent operations
|
|
830
|
+
process.chdir(ctx.appRoot);
|
|
831
|
+
return { workspaceContext: ctx, originalCwd };
|
|
832
|
+
}
|
|
833
|
+
// Discover NativeScript apps and prompt for selection
|
|
834
|
+
const apps = discoverNativeScriptApps();
|
|
835
|
+
if (apps.length === 0) {
|
|
836
|
+
throw new Error('No NativeScript apps found in this workspace. ' +
|
|
837
|
+
'Run this command from within a NativeScript app directory, or ensure your apps have nativescript.config.ts files.');
|
|
838
|
+
}
|
|
839
|
+
if (apps.length === 1) {
|
|
840
|
+
// Only one app, use it automatically
|
|
841
|
+
if (spinner)
|
|
842
|
+
spinner.stop();
|
|
843
|
+
console.log(`Found NativeScript app: ${apps[0].name} (${apps[0].path})`);
|
|
844
|
+
const ctx = getWorkspaceContextForApp(apps[0].name);
|
|
845
|
+
if (ctx) {
|
|
846
|
+
process.chdir(ctx.appRoot);
|
|
847
|
+
return { workspaceContext: ctx, originalCwd };
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Multiple apps - stop spinner before prompting
|
|
851
|
+
if (spinner)
|
|
852
|
+
spinner.stop();
|
|
853
|
+
// Multiple apps - prompt for selection
|
|
854
|
+
const { selectedApp } = await inquirer.prompt([
|
|
855
|
+
{
|
|
856
|
+
type: 'list',
|
|
857
|
+
name: 'selectedApp',
|
|
858
|
+
message: 'Select a NativeScript app:',
|
|
859
|
+
choices: apps.map((app) => ({
|
|
860
|
+
name: `${app.name} (${app.path})`,
|
|
861
|
+
value: app.name,
|
|
862
|
+
})),
|
|
863
|
+
pageSize: 15, // Show more items to avoid scroll issues
|
|
864
|
+
loop: false, // Don't loop back to start when at end
|
|
865
|
+
},
|
|
866
|
+
]);
|
|
867
|
+
const ctx = getWorkspaceContextForApp(selectedApp);
|
|
868
|
+
if (!ctx) {
|
|
869
|
+
throw new Error(`Failed to get context for selected app: ${selectedApp}`);
|
|
870
|
+
}
|
|
871
|
+
process.chdir(ctx.appRoot);
|
|
872
|
+
return { workspaceContext: ctx, originalCwd };
|
|
873
|
+
}
|
|
874
|
+
// Not at workspace root - use current directory
|
|
875
|
+
const ctx = detectWorkspaceContext();
|
|
876
|
+
return { workspaceContext: ctx, originalCwd };
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Creates a zip file of the current directory (NativeScript project).
|
|
880
|
+
* For Nx workspaces, this includes the app, dependent libs, and workspace config files.
|
|
881
|
+
* For standalone projects, this zips the current directory.
|
|
882
|
+
*/
|
|
883
|
+
async function zipProject(projectName, workspaceCtx, isUpdate = false, verbose = false) {
|
|
884
|
+
if (workspaceCtx.type === 'nx') {
|
|
885
|
+
return zipWorkspaceProject(projectName, workspaceCtx, isUpdate, verbose);
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
return zipStandaloneProject(projectName, workspaceCtx, isUpdate);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Zip a standalone NativeScript project (original behavior)
|
|
893
|
+
*/
|
|
894
|
+
async function zipStandaloneProject(projectName, workspaceCtx, isUpdate = false) {
|
|
641
895
|
return new Promise((resolve, reject) => {
|
|
642
896
|
const outputPath = path.join(process.cwd(), `${projectName}.zip`);
|
|
643
897
|
const output = fs.createWriteStream(outputPath);
|
|
644
898
|
const archive = archiver('zip', {
|
|
645
|
-
zlib: { level: 9 },
|
|
899
|
+
zlib: { level: 9 },
|
|
646
900
|
});
|
|
647
|
-
// Listen for all archive data to be written
|
|
648
901
|
output.on('close', () => {
|
|
649
|
-
resolve(outputPath);
|
|
902
|
+
resolve({ zipPath: outputPath, workspaceContext: workspaceCtx });
|
|
650
903
|
});
|
|
651
|
-
// Listen for warnings and errors
|
|
652
904
|
archive.on('warning', (err) => {
|
|
653
905
|
if (err.code === 'ENOENT') {
|
|
654
906
|
console.warn('Archive warning:', err);
|
|
@@ -660,9 +912,8 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
660
912
|
archive.on('error', (err) => {
|
|
661
913
|
reject(err);
|
|
662
914
|
});
|
|
663
|
-
// Pipe archive data to the file
|
|
664
915
|
archive.pipe(output);
|
|
665
|
-
// Determine the primary app directory
|
|
916
|
+
// Determine the primary app directory
|
|
666
917
|
const nsAppPath = getNativeScriptAppPath();
|
|
667
918
|
const nsAppDir = nsAppPath
|
|
668
919
|
? path.join(process.cwd(), nsAppPath)
|
|
@@ -682,18 +933,12 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
682
933
|
}
|
|
683
934
|
else {
|
|
684
935
|
console.warn('Warning: app directory not found in the project root');
|
|
685
|
-
const checked = [nsAppDir, srcDir, appDir].filter(Boolean).join(', ');
|
|
686
|
-
console.warn(`Checked locations: ${checked}`);
|
|
687
|
-
console.log('Creating an empty app directory in the zip');
|
|
688
936
|
}
|
|
689
|
-
// For both builds/updates, include App_Resources and exclude node_modules.
|
|
690
|
-
// For OTA updates specifically, we also exclude the runtime fingerprint
|
|
691
|
-
// file, since it reflects the native store binary, not the OTA payload.
|
|
692
937
|
const ignorePatterns = [
|
|
693
|
-
'node_modules/**',
|
|
694
|
-
'*.zip',
|
|
695
|
-
'platforms/**',
|
|
696
|
-
'hooks/**',
|
|
938
|
+
'node_modules/**',
|
|
939
|
+
'*.zip',
|
|
940
|
+
'platforms/**',
|
|
941
|
+
'hooks/**',
|
|
697
942
|
];
|
|
698
943
|
if (isUpdate) {
|
|
699
944
|
ignorePatterns.push('**/assets/norrix.fingerprint.json');
|
|
@@ -702,7 +947,217 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
702
947
|
cwd: process.cwd(),
|
|
703
948
|
ignore: ignorePatterns,
|
|
704
949
|
});
|
|
705
|
-
|
|
950
|
+
archive.finalize();
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Zip an Nx workspace project including the app and its dependencies
|
|
955
|
+
*/
|
|
956
|
+
async function zipWorkspaceProject(projectName, workspaceCtx, isUpdate = false, verbose = false) {
|
|
957
|
+
return new Promise((resolve, reject) => {
|
|
958
|
+
const outputPath = path.join(workspaceCtx.appRoot, `${projectName}.zip`);
|
|
959
|
+
const output = fs.createWriteStream(outputPath);
|
|
960
|
+
const archive = archiver('zip', {
|
|
961
|
+
zlib: { level: 9 },
|
|
962
|
+
});
|
|
963
|
+
output.on('close', () => {
|
|
964
|
+
resolve({ zipPath: outputPath, workspaceContext: workspaceCtx });
|
|
965
|
+
});
|
|
966
|
+
archive.on('warning', (err) => {
|
|
967
|
+
if (err.code === 'ENOENT') {
|
|
968
|
+
if (verbose)
|
|
969
|
+
console.warn('Archive warning:', err);
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
reject(err);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
archive.on('error', (err) => {
|
|
976
|
+
reject(err);
|
|
977
|
+
});
|
|
978
|
+
archive.pipe(output);
|
|
979
|
+
logWorkspaceContext(workspaceCtx, verbose);
|
|
980
|
+
// Get workspace dependencies using Nx CLI (preferred) with fallback supplementation
|
|
981
|
+
let deps;
|
|
982
|
+
if (workspaceCtx.projectName) {
|
|
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);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
if (verbose) {
|
|
1010
|
+
console.log('[workspace] Using fallback dependency detection (Nx CLI not available)');
|
|
1011
|
+
}
|
|
1012
|
+
deps = fallbackDeps;
|
|
1013
|
+
}
|
|
1014
|
+
// Create manifest for CI
|
|
1015
|
+
const manifest = createWorkspaceManifest(workspaceCtx, deps);
|
|
1016
|
+
archive.append(JSON.stringify(manifest, null, 2), {
|
|
1017
|
+
name: '.norrix/manifest.json',
|
|
1018
|
+
});
|
|
1019
|
+
// Base ignore patterns for the entire workspace
|
|
1020
|
+
const ignorePatterns = [
|
|
1021
|
+
'**/node_modules/**',
|
|
1022
|
+
'**/*.zip',
|
|
1023
|
+
'**/platforms/**',
|
|
1024
|
+
'**/dist/**',
|
|
1025
|
+
'**/.git/**',
|
|
1026
|
+
'**/hooks/**',
|
|
1027
|
+
// Exclude other apps (not the current one)
|
|
1028
|
+
'apps/**',
|
|
1029
|
+
];
|
|
1030
|
+
if (isUpdate) {
|
|
1031
|
+
ignorePatterns.push('**/assets/norrix.fingerprint.json');
|
|
1032
|
+
}
|
|
1033
|
+
// 1. Add the app itself at its relative workspace path
|
|
1034
|
+
console.log(`Adding app: ${workspaceCtx.relativeAppPath}`);
|
|
1035
|
+
archive.directory(workspaceCtx.appRoot, workspaceCtx.relativeAppPath, (entry) => {
|
|
1036
|
+
// Filter out node_modules, platforms, etc.
|
|
1037
|
+
if (entry.name.includes('node_modules') ||
|
|
1038
|
+
entry.name.includes('platforms') ||
|
|
1039
|
+
entry.name.endsWith('.zip')) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
if (isUpdate && entry.name.includes('norrix.fingerprint.json')) {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
return entry;
|
|
1046
|
+
});
|
|
1047
|
+
// 2. Add dependent libs
|
|
1048
|
+
if (deps.libPaths.length > 0) {
|
|
1049
|
+
console.log(`Adding ${deps.libPaths.length} library dependencies`);
|
|
1050
|
+
for (const libPath of deps.libPaths) {
|
|
1051
|
+
const absoluteLibPath = path.join(workspaceCtx.workspaceRoot, libPath);
|
|
1052
|
+
if (fs.existsSync(absoluteLibPath)) {
|
|
1053
|
+
if (verbose) {
|
|
1054
|
+
console.log(` - ${libPath}`);
|
|
1055
|
+
}
|
|
1056
|
+
archive.directory(absoluteLibPath, libPath, (entry) => {
|
|
1057
|
+
if (entry.name.includes('node_modules')) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
return entry;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
// 3. Add root config files
|
|
1066
|
+
console.log('Adding workspace root configuration files');
|
|
1067
|
+
for (const configFile of deps.rootConfigs) {
|
|
1068
|
+
const configPath = path.join(workspaceCtx.workspaceRoot, configFile);
|
|
1069
|
+
if (fs.existsSync(configPath)) {
|
|
1070
|
+
if (verbose) {
|
|
1071
|
+
console.log(` - ${configFile}`);
|
|
1072
|
+
}
|
|
1073
|
+
archive.file(configPath, { name: configFile });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// 4. Add tools directory if it exists and is referenced
|
|
1077
|
+
for (const toolPath of deps.toolPaths) {
|
|
1078
|
+
const absoluteToolPath = path.join(workspaceCtx.workspaceRoot, toolPath);
|
|
1079
|
+
if (fs.existsSync(absoluteToolPath) && fs.statSync(absoluteToolPath).isDirectory()) {
|
|
1080
|
+
console.log(`Adding tools: ${toolPath}`);
|
|
1081
|
+
archive.directory(absoluteToolPath, toolPath, (entry) => {
|
|
1082
|
+
if (entry.name.includes('node_modules')) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
return entry;
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// 5. Add asset paths if they exist
|
|
1090
|
+
for (const assetPath of deps.assetPaths) {
|
|
1091
|
+
const absoluteAssetPath = path.join(workspaceCtx.workspaceRoot, assetPath);
|
|
1092
|
+
if (fs.existsSync(absoluteAssetPath) && fs.statSync(absoluteAssetPath).isDirectory()) {
|
|
1093
|
+
if (verbose) {
|
|
1094
|
+
console.log(`Adding assets: ${assetPath}`);
|
|
1095
|
+
}
|
|
1096
|
+
archive.directory(absoluteAssetPath, assetPath);
|
|
1097
|
+
}
|
|
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
|
+
}
|
|
706
1161
|
archive.finalize();
|
|
707
1162
|
});
|
|
708
1163
|
}
|
|
@@ -710,16 +1165,50 @@ async function zipProject(projectName, isUpdate = false) {
|
|
|
710
1165
|
* Build command implementation
|
|
711
1166
|
* Uploads project to S3 and triggers build via the Next.js API gateway -> WarpBuild
|
|
712
1167
|
*/
|
|
713
|
-
export async function build(cliPlatformArg, cliConfigurationArg, cliDistributionArg, verbose = false
|
|
1168
|
+
export async function build(cliPlatformArg, cliConfigurationArg, cliDistributionArg, verbose = false, options // string for backwards compatibility with old projectArg
|
|
1169
|
+
) {
|
|
1170
|
+
// Normalize options - support both new object and legacy string projectArg
|
|
1171
|
+
const opts = typeof options === 'string' ? { project: options } : (options || {});
|
|
714
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
|
+
}
|
|
715
1190
|
let spinner;
|
|
1191
|
+
let originalCwd;
|
|
716
1192
|
try {
|
|
717
1193
|
spinner = ora('Preparing app for building...');
|
|
718
1194
|
spinner.start();
|
|
1195
|
+
// 0. Resolve workspace context (handles --project and prompting)
|
|
1196
|
+
const resolved = await resolveWorkspaceContext(opts.project, spinner);
|
|
1197
|
+
originalCwd = resolved.originalCwd;
|
|
1198
|
+
const workspaceCtx = resolved.workspaceContext;
|
|
1199
|
+
// Restart spinner after potential prompts
|
|
1200
|
+
if (!spinner.isSpinning) {
|
|
1201
|
+
spinner.start('Preparing app for building...');
|
|
1202
|
+
}
|
|
1203
|
+
// Load Norrix config file if present
|
|
1204
|
+
const norrixConfig = await loadNorrixConfig(process.cwd());
|
|
1205
|
+
if (workspaceCtx.type === 'nx' && verbose) {
|
|
1206
|
+
logWorkspaceContext(workspaceCtx, verbose);
|
|
1207
|
+
}
|
|
719
1208
|
// 1. Get project info
|
|
720
1209
|
const projectName = await getProjectName();
|
|
721
|
-
// 2. Determine platform (CLI arg preferred, otherwise prompt)
|
|
722
|
-
let platform = (cliPlatformArg || '').toLowerCase();
|
|
1210
|
+
// 2. Determine platform (CLI arg preferred, then config, otherwise prompt)
|
|
1211
|
+
let platform = (cliPlatformArg || norrixConfig.defaultPlatform || '').toLowerCase();
|
|
723
1212
|
const validPlatforms = ['android', 'ios', 'visionos'];
|
|
724
1213
|
spinner.stop();
|
|
725
1214
|
if (!validPlatforms.includes(platform)) {
|
|
@@ -748,6 +1237,31 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
748
1237
|
]);
|
|
749
1238
|
configuration = answer.configuration;
|
|
750
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;
|
|
751
1265
|
const normalizeIosDistribution = (input) => {
|
|
752
1266
|
const v = String(input ?? '')
|
|
753
1267
|
.trim()
|
|
@@ -772,14 +1286,20 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
772
1286
|
}
|
|
773
1287
|
return undefined;
|
|
774
1288
|
};
|
|
775
|
-
// 2.2 iOS distribution type (release only)
|
|
1289
|
+
// 2.2 iOS distribution type (release only): CLI arg > config file > prompt
|
|
776
1290
|
let distributionType;
|
|
777
1291
|
if (platform === 'ios' && configuration === 'release') {
|
|
1292
|
+
// Try CLI arg first
|
|
778
1293
|
distributionType = normalizeIosDistribution(cliDistributionArg);
|
|
779
1294
|
if (!distributionType && cliDistributionArg) {
|
|
780
1295
|
throw new Error(`Invalid iOS distribution type '${cliDistributionArg}'. Use 'appstore', 'adhoc', or 'enterprise'.`);
|
|
781
1296
|
}
|
|
782
|
-
|
|
1297
|
+
// Fall back to config file
|
|
1298
|
+
if (!distributionType && norrixConfig.ios?.distributionType) {
|
|
1299
|
+
distributionType = norrixConfig.ios.distributionType;
|
|
1300
|
+
}
|
|
1301
|
+
// Prompt if still not set (unless non-interactive mode)
|
|
1302
|
+
if (!distributionType && !opts.nonInteractive) {
|
|
783
1303
|
const { distribution } = await inquirer.prompt([
|
|
784
1304
|
{
|
|
785
1305
|
type: 'list',
|
|
@@ -795,6 +1315,10 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
795
1315
|
]);
|
|
796
1316
|
distributionType = distribution;
|
|
797
1317
|
}
|
|
1318
|
+
// Default to appstore in non-interactive mode if nothing else provided
|
|
1319
|
+
if (!distributionType) {
|
|
1320
|
+
distributionType = 'appstore';
|
|
1321
|
+
}
|
|
798
1322
|
}
|
|
799
1323
|
const appleVersionInfo = platform === 'ios' || platform === 'visionos'
|
|
800
1324
|
? getAppleVersionFromInfoPlist(platform)
|
|
@@ -865,70 +1389,148 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
865
1389
|
if (configuration === 'release') {
|
|
866
1390
|
spinner.stop();
|
|
867
1391
|
if (platform === 'ios') {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1392
|
+
// Resolve values from: CLI flags > config file (trimmed to handle whitespace)
|
|
1393
|
+
const configTeamId = norrixConfig.ios?.teamId;
|
|
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
|
+
}
|
|
1460
|
+
return true;
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
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
|
+
}
|
|
1533
|
+
}
|
|
932
1534
|
}
|
|
933
1535
|
else if (platform === 'android') {
|
|
934
1536
|
const androidAnswers = await inquirer.prompt([
|
|
@@ -974,11 +1576,82 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
974
1576
|
]);
|
|
975
1577
|
androidCredentials = {
|
|
976
1578
|
keystoreBase64: readOptionalFileAsBase64(androidAnswers.keystorePath),
|
|
977
|
-
keystorePassword: androidAnswers.keystorePassword
|
|
978
|
-
keyAlias: androidAnswers.keyAlias
|
|
979
|
-
keyPassword: androidAnswers.keyPassword
|
|
1579
|
+
keystorePassword: trimString(androidAnswers.keystorePassword),
|
|
1580
|
+
keyAlias: trimString(androidAnswers.keyAlias),
|
|
1581
|
+
keyPassword: trimString(androidAnswers.keyPassword),
|
|
980
1582
|
playServiceAccountJson: readOptionalFileAsBase64(androidAnswers.playJsonPath),
|
|
981
1583
|
};
|
|
1584
|
+
// Track Android paths for config saving
|
|
1585
|
+
androidCredentials._keystorePath = trimString(androidAnswers.keystorePath);
|
|
1586
|
+
androidCredentials._keyAlias = trimString(androidAnswers.keyAlias);
|
|
1587
|
+
}
|
|
1588
|
+
// Offer to save config if no norrix.config.ts exists and we collected useful values
|
|
1589
|
+
const appRoot = process.cwd();
|
|
1590
|
+
if (!hasNorrixConfig(appRoot) && !opts.nonInteractive) {
|
|
1591
|
+
// Collect saveable values
|
|
1592
|
+
const saveableOptions = {
|
|
1593
|
+
platform: platform,
|
|
1594
|
+
};
|
|
1595
|
+
if (platform === 'ios' && iosCredentials) {
|
|
1596
|
+
if (iosCredentials.teamId) {
|
|
1597
|
+
saveableOptions.teamId = iosCredentials.teamId;
|
|
1598
|
+
}
|
|
1599
|
+
if (distributionType) {
|
|
1600
|
+
saveableOptions.distributionType = distributionType;
|
|
1601
|
+
}
|
|
1602
|
+
// Don't save actual credential file paths since they may contain secrets
|
|
1603
|
+
// But save paths that users can re-use
|
|
1604
|
+
if (iosCredentials._p12Path) {
|
|
1605
|
+
saveableOptions.p12Path = iosCredentials._p12Path;
|
|
1606
|
+
}
|
|
1607
|
+
if (iosCredentials._mobileprovisionPath) {
|
|
1608
|
+
saveableOptions.provisioningProfilePath = iosCredentials._mobileprovisionPath;
|
|
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
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (platform === 'android' && androidCredentials) {
|
|
1622
|
+
if (androidCredentials._keystorePath) {
|
|
1623
|
+
saveableOptions.keystorePath = androidCredentials._keystorePath;
|
|
1624
|
+
}
|
|
1625
|
+
if (androidCredentials._keyAlias) {
|
|
1626
|
+
saveableOptions.keyAlias = androidCredentials._keyAlias;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
// Only offer to save if we have something useful
|
|
1630
|
+
const hasSaveableValues = saveableOptions.teamId ||
|
|
1631
|
+
saveableOptions.distributionType ||
|
|
1632
|
+
saveableOptions.p12Path ||
|
|
1633
|
+
saveableOptions.provisioningProfilePath ||
|
|
1634
|
+
saveableOptions.keystorePath ||
|
|
1635
|
+
saveableOptions.ascApiKeyId;
|
|
1636
|
+
if (hasSaveableValues) {
|
|
1637
|
+
const { shouldSave } = await inquirer.prompt([
|
|
1638
|
+
{
|
|
1639
|
+
type: 'confirm',
|
|
1640
|
+
name: 'shouldSave',
|
|
1641
|
+
message: 'Save these settings to norrix.config.ts for future builds?',
|
|
1642
|
+
default: true,
|
|
1643
|
+
},
|
|
1644
|
+
]);
|
|
1645
|
+
if (shouldSave) {
|
|
1646
|
+
try {
|
|
1647
|
+
const savedPath = saveNorrixConfig(appRoot, saveableOptions);
|
|
1648
|
+
console.log(`✓ Configuration saved to ${path.basename(savedPath)}`);
|
|
1649
|
+
}
|
|
1650
|
+
catch (saveError) {
|
|
1651
|
+
console.warn(`Warning: Could not save config file: ${saveError.message}`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
982
1655
|
}
|
|
983
1656
|
spinner.start('Creating project archive...');
|
|
984
1657
|
}
|
|
@@ -990,8 +1663,8 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
990
1663
|
});
|
|
991
1664
|
writeRuntimeFingerprintFile(projectRoot, fingerprint, platform);
|
|
992
1665
|
spinner.start('Creating project archive...');
|
|
993
|
-
// 3. Zip the project
|
|
994
|
-
const zipPath = await zipProject(projectName, false);
|
|
1666
|
+
// 3. Zip the project (workspace-aware)
|
|
1667
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, false, verbose);
|
|
995
1668
|
spinner.text = 'Project archive created';
|
|
996
1669
|
// 4. Upload the project zip to S3
|
|
997
1670
|
spinner.text = 'Working...';
|
|
@@ -1018,6 +1691,17 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1018
1691
|
catch {
|
|
1019
1692
|
inferredAppId = undefined;
|
|
1020
1693
|
}
|
|
1694
|
+
// Include workspace info for CI to properly navigate the project structure
|
|
1695
|
+
const workspaceInfo = workspaceContext.type === 'nx' ? {
|
|
1696
|
+
workspaceType: workspaceContext.type,
|
|
1697
|
+
appPath: workspaceContext.relativeAppPath,
|
|
1698
|
+
projectName: workspaceContext.projectName,
|
|
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;
|
|
1021
1705
|
const response = await axios.post(`${API_URL}/build`, {
|
|
1022
1706
|
projectName,
|
|
1023
1707
|
appId: inferredAppId,
|
|
@@ -1027,10 +1711,18 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1027
1711
|
version: version || '',
|
|
1028
1712
|
buildNumber: buildNumber || '',
|
|
1029
1713
|
configuration,
|
|
1714
|
+
// Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
|
|
1715
|
+
...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
|
|
1030
1716
|
...(distributionType ? { distributionType } : {}),
|
|
1717
|
+
// Android package type override (apk or aab) - takes precedence over distributionType
|
|
1718
|
+
...(opts.androidPackageType ? { androidPackageType: opts.androidPackageType } : {}),
|
|
1031
1719
|
fingerprint,
|
|
1032
1720
|
// Provide the relative key (without public/) – the workflow prepends public/
|
|
1033
1721
|
s3Key: s3KeyRel,
|
|
1722
|
+
// Workspace context for Nx monorepos
|
|
1723
|
+
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
1724
|
+
// For standalone projects, include project name for env var scoping
|
|
1725
|
+
...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
|
|
1034
1726
|
// Only include raw credentials if not encrypted
|
|
1035
1727
|
...(encryptedSecrets ? { encryptedSecrets } : {}),
|
|
1036
1728
|
...(!encryptedSecrets && iosCredentials ? { iosCredentials } : {}),
|
|
@@ -1091,8 +1783,21 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1091
1783
|
: 'App Store'}`);
|
|
1092
1784
|
}
|
|
1093
1785
|
console.log(` You can check the status with: norrix build-status ${buildId}`);
|
|
1786
|
+
// Restore original cwd if we changed it
|
|
1787
|
+
if (originalCwd && process.cwd() !== originalCwd) {
|
|
1788
|
+
process.chdir(originalCwd);
|
|
1789
|
+
}
|
|
1094
1790
|
}
|
|
1095
1791
|
catch (error) {
|
|
1792
|
+
// Restore original cwd if we changed it
|
|
1793
|
+
if (originalCwd && process.cwd() !== originalCwd) {
|
|
1794
|
+
try {
|
|
1795
|
+
process.chdir(originalCwd);
|
|
1796
|
+
}
|
|
1797
|
+
catch {
|
|
1798
|
+
// Ignore chdir errors during error handling
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1096
1801
|
const apiMessage = (error?.response?.data &&
|
|
1097
1802
|
(error.response.data.error || error.response.data.message)) ||
|
|
1098
1803
|
undefined;
|
|
@@ -1123,8 +1828,25 @@ export async function build(cliPlatformArg, cliConfigurationArg, cliDistribution
|
|
|
1123
1828
|
* Submit command implementation
|
|
1124
1829
|
* Submits the built app to app stores via the Next.js API gateway
|
|
1125
1830
|
*/
|
|
1126
|
-
export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
|
|
1831
|
+
export async function submit(cliPlatformArg, cliTrackArg, verbose = false, options) {
|
|
1127
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
|
+
}
|
|
1128
1850
|
const spinner = ora('Preparing app for submission...');
|
|
1129
1851
|
try {
|
|
1130
1852
|
spinner.start();
|
|
@@ -1383,12 +2105,44 @@ export async function submit(cliPlatformArg, cliTrackArg, verbose = false) {
|
|
|
1383
2105
|
* Update command implementation
|
|
1384
2106
|
* Publishes over-the-air updates to deployed apps via the Next.js API gateway
|
|
1385
2107
|
*/
|
|
1386
|
-
export async function update(cliPlatformArg, cliVersionArg, verbose = false
|
|
2108
|
+
export async function update(cliPlatformArg, cliVersionArg, verbose = false, options // string for backwards compatibility with old projectArg
|
|
2109
|
+
) {
|
|
2110
|
+
// Normalize options - support both new object and legacy string projectArg
|
|
2111
|
+
const opts = typeof options === 'string' ? { project: options } : (options || {});
|
|
1387
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
|
+
}
|
|
1388
2130
|
let spinner;
|
|
2131
|
+
let originalCwd;
|
|
1389
2132
|
try {
|
|
1390
2133
|
spinner = ora('Preparing over-the-air update...');
|
|
1391
2134
|
spinner.start();
|
|
2135
|
+
// 0. Resolve workspace context (handles --project and prompting)
|
|
2136
|
+
const resolved = await resolveWorkspaceContext(opts.project, spinner);
|
|
2137
|
+
originalCwd = resolved.originalCwd;
|
|
2138
|
+
const workspaceCtx = resolved.workspaceContext;
|
|
2139
|
+
// Restart spinner after potential prompts
|
|
2140
|
+
if (!spinner.isSpinning) {
|
|
2141
|
+
spinner.start('Preparing over-the-air update...');
|
|
2142
|
+
}
|
|
2143
|
+
if (workspaceCtx.type === 'nx' && verbose) {
|
|
2144
|
+
logWorkspaceContext(workspaceCtx, verbose);
|
|
2145
|
+
}
|
|
1392
2146
|
// Normalize and/or ask for platform first (CLI arg takes precedence if valid)
|
|
1393
2147
|
let platform = (cliPlatformArg || '').toLowerCase();
|
|
1394
2148
|
const validPlatforms = ['android', 'ios', 'visionos'];
|
|
@@ -1405,6 +2159,33 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1405
2159
|
platform = chosenPlatform;
|
|
1406
2160
|
spinner.start('Preparing over-the-air update...');
|
|
1407
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;
|
|
1408
2189
|
// Infer version from native project files (same as build)
|
|
1409
2190
|
const appleVersionInfo = platform === 'ios' || platform === 'visionos'
|
|
1410
2191
|
? getAppleVersionFromInfoPlist(platform)
|
|
@@ -1424,27 +2205,45 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1424
2205
|
// Ask for app ID and version first
|
|
1425
2206
|
spinner.stop();
|
|
1426
2207
|
const cliVersion = (cliVersionArg || '').trim();
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
2208
|
+
// Resolve appId: CLI flag → inferred → prompt
|
|
2209
|
+
let appId;
|
|
2210
|
+
if (opts.appId) {
|
|
2211
|
+
appId = opts.appId;
|
|
2212
|
+
}
|
|
2213
|
+
else {
|
|
2214
|
+
const { appId: promptedAppId } = await inquirer.prompt([
|
|
2215
|
+
{
|
|
2216
|
+
type: 'input',
|
|
2217
|
+
name: 'appId',
|
|
2218
|
+
message: 'Enter the App ID to update:',
|
|
2219
|
+
default: inferredAppId || '',
|
|
2220
|
+
validate: (input) => input.length > 0 || 'App ID is required',
|
|
2221
|
+
},
|
|
2222
|
+
]);
|
|
2223
|
+
appId = promptedAppId;
|
|
2224
|
+
}
|
|
2225
|
+
// Resolve version: CLI arg → inferred → prompt
|
|
2226
|
+
let version;
|
|
2227
|
+
if (cliVersion) {
|
|
2228
|
+
version = cliVersion;
|
|
2229
|
+
}
|
|
2230
|
+
else if (inferredVersion && opts.nonInteractive) {
|
|
2231
|
+
version = inferredVersion;
|
|
2232
|
+
}
|
|
2233
|
+
else {
|
|
2234
|
+
const { version: promptedVersion } = await inquirer.prompt([
|
|
2235
|
+
{
|
|
2236
|
+
type: 'input',
|
|
2237
|
+
name: 'version',
|
|
2238
|
+
message: inferredVersion
|
|
2239
|
+
? `Update version (${inferredVersion}, enter to accept):`
|
|
2240
|
+
: 'Update version:',
|
|
2241
|
+
default: inferredVersion,
|
|
2242
|
+
validate: (input) => input.length > 0 || 'Version is required',
|
|
2243
|
+
},
|
|
2244
|
+
]);
|
|
2245
|
+
version = promptedVersion;
|
|
2246
|
+
}
|
|
1448
2247
|
// Ask the server what the next buildNumber would be for this app so we
|
|
1449
2248
|
// can present a sensible default in the prompt, matching what the API
|
|
1450
2249
|
// will auto-increment to if left blank.
|
|
@@ -1468,38 +2267,51 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1468
2267
|
: platform === 'android'
|
|
1469
2268
|
? androidVersionInfo.buildNumber
|
|
1470
2269
|
: undefined;
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2270
|
+
// Resolve buildNumber: CLI flag → server suggestion → local inferred → prompt
|
|
2271
|
+
let buildNumber;
|
|
2272
|
+
let notes = '';
|
|
2273
|
+
if (opts.buildNumber) {
|
|
2274
|
+
buildNumber = opts.buildNumber;
|
|
2275
|
+
}
|
|
2276
|
+
else if (opts.nonInteractive) {
|
|
2277
|
+
// In non-interactive mode, use server suggestion or leave undefined for auto-increment
|
|
2278
|
+
buildNumber = serverSuggestedBuildNumber || localInferredBuildNumber || undefined;
|
|
2279
|
+
}
|
|
2280
|
+
else {
|
|
2281
|
+
const buildPromptAnswers = await inquirer.prompt([
|
|
2282
|
+
{
|
|
2283
|
+
type: 'input',
|
|
2284
|
+
name: 'buildNumber',
|
|
2285
|
+
message: (() => {
|
|
2286
|
+
if (serverSuggestedBuildNumber) {
|
|
2287
|
+
return `Update build number (${serverSuggestedBuildNumber}, enter to accept or override; blank to auto increment from server):`;
|
|
2288
|
+
}
|
|
2289
|
+
if (localInferredBuildNumber) {
|
|
2290
|
+
return `Update build number (${localInferredBuildNumber}, enter to auto increment from server):`;
|
|
2291
|
+
}
|
|
2292
|
+
return 'Update build number (leave blank to auto increment from server):';
|
|
2293
|
+
})(),
|
|
2294
|
+
default: serverSuggestedBuildNumber || localInferredBuildNumber || '',
|
|
2295
|
+
validate: (input) => {
|
|
2296
|
+
const val = String(input).trim();
|
|
2297
|
+
if (!val)
|
|
2298
|
+
return true; // allow blank -> server will auto-increment
|
|
2299
|
+
if (!/^\d+$/.test(val)) {
|
|
2300
|
+
return 'Build number must be a positive integer or blank to auto-increment';
|
|
2301
|
+
}
|
|
2302
|
+
return true;
|
|
2303
|
+
},
|
|
1493
2304
|
},
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
2305
|
+
{
|
|
2306
|
+
type: 'input',
|
|
2307
|
+
name: 'notes',
|
|
2308
|
+
message: 'Release notes (optional):',
|
|
2309
|
+
default: '',
|
|
2310
|
+
},
|
|
2311
|
+
]);
|
|
2312
|
+
buildNumber = String(buildPromptAnswers.buildNumber || '').trim() || undefined;
|
|
2313
|
+
notes = buildPromptAnswers.notes || '';
|
|
2314
|
+
}
|
|
1503
2315
|
// Check the app directory structure before packaging
|
|
1504
2316
|
const srcAppDir = path.join(process.cwd(), 'src', 'app');
|
|
1505
2317
|
const appDir = path.join(process.cwd(), 'app');
|
|
@@ -1576,9 +2388,9 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1576
2388
|
// fingerprint JSON under the app source tree is the
|
|
1577
2389
|
// single source of truth for OTA compatibility.
|
|
1578
2390
|
spinner.start('Packaging for over-the-air update...');
|
|
1579
|
-
// Create the update bundle - pass true to include node_modules for updates
|
|
2391
|
+
// Create the update bundle (workspace-aware) - pass true to include node_modules for updates
|
|
1580
2392
|
const projectName = await getProjectName();
|
|
1581
|
-
const zipPath = await zipProject(projectName, true);
|
|
2393
|
+
const { zipPath, workspaceContext } = await zipProject(projectName, workspaceCtx, true, verbose);
|
|
1582
2394
|
spinner.text = 'Uploading update to Norrix cloud storage...';
|
|
1583
2395
|
const fileBuffer = fs.readFileSync(zipPath);
|
|
1584
2396
|
const updateFolder = `update-${Date.now()}`;
|
|
@@ -1586,6 +2398,17 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1586
2398
|
const s3KeyRel = `updates/${updateFolder}/${appId}-${safeVersion}.zip`;
|
|
1587
2399
|
await putObjectToStorage(`public/${s3KeyRel}`, fileBuffer);
|
|
1588
2400
|
spinner.text = 'Upload complete. Starting update...';
|
|
2401
|
+
// Include workspace info for CI to properly navigate the project structure
|
|
2402
|
+
const workspaceInfo = workspaceContext.type === 'nx' ? {
|
|
2403
|
+
workspaceType: workspaceContext.type,
|
|
2404
|
+
appPath: workspaceContext.relativeAppPath,
|
|
2405
|
+
projectName: workspaceContext.projectName,
|
|
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;
|
|
1589
2412
|
const response = await axios.post(`${API_URL}/update`, {
|
|
1590
2413
|
appId,
|
|
1591
2414
|
platform,
|
|
@@ -1593,8 +2416,14 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1593
2416
|
buildNumber: buildNumber || '',
|
|
1594
2417
|
releaseNotes: notes,
|
|
1595
2418
|
fingerprint,
|
|
2419
|
+
// Nx configuration (e.g., 'prod', 'stg', 'dev') for monorepo builds
|
|
2420
|
+
...(opts.nxConfiguration ? { nxConfiguration: opts.nxConfiguration } : {}),
|
|
1596
2421
|
// Provide the relative key (without public/). Consumers will prepend public/
|
|
1597
2422
|
s3Key: s3KeyRel,
|
|
2423
|
+
// Workspace context for Nx monorepos
|
|
2424
|
+
...(workspaceInfo ? { workspace: workspaceInfo } : {}),
|
|
2425
|
+
// For standalone projects, include project name for env var scoping
|
|
2426
|
+
...(standaloneProjectName ? { projectName: standaloneProjectName } : {}),
|
|
1598
2427
|
}, {
|
|
1599
2428
|
headers: {
|
|
1600
2429
|
'Content-Type': 'application/json',
|
|
@@ -1644,8 +2473,21 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1644
2473
|
}
|
|
1645
2474
|
console.log(formatVersionBuildLine(recordedUpdateVersion, recordedUpdateBuildNumber));
|
|
1646
2475
|
console.log(` You can check the status with: norrix update-status ${updateId}`);
|
|
2476
|
+
// Restore original cwd if we changed it
|
|
2477
|
+
if (originalCwd && process.cwd() !== originalCwd) {
|
|
2478
|
+
process.chdir(originalCwd);
|
|
2479
|
+
}
|
|
1647
2480
|
}
|
|
1648
2481
|
catch (error) {
|
|
2482
|
+
// Restore original cwd if we changed it
|
|
2483
|
+
if (originalCwd && process.cwd() !== originalCwd) {
|
|
2484
|
+
try {
|
|
2485
|
+
process.chdir(originalCwd);
|
|
2486
|
+
}
|
|
2487
|
+
catch {
|
|
2488
|
+
// Ignore chdir errors during error handling
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
1649
2491
|
const apiMessage = (error?.response?.data &&
|
|
1650
2492
|
(error.response.data.error || error.response.data.message)) ||
|
|
1651
2493
|
undefined;
|
|
@@ -1673,8 +2515,20 @@ export async function update(cliPlatformArg, cliVersionArg, verbose = false) {
|
|
|
1673
2515
|
* Build Status command implementation
|
|
1674
2516
|
* Checks the status of a build via the Next.js API gateway
|
|
1675
2517
|
*/
|
|
1676
|
-
export async function buildStatus(buildId, verbose = false) {
|
|
2518
|
+
export async function buildStatus(buildId, verbose = false, options) {
|
|
1677
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
|
+
}
|
|
1678
2532
|
try {
|
|
1679
2533
|
const spinner = ora(`Checking status of build ${buildId}...`).start();
|
|
1680
2534
|
const response = await axios.get(`${API_URL}/build/${buildId}`, {
|
|
@@ -1711,8 +2565,20 @@ export async function buildStatus(buildId, verbose = false) {
|
|
|
1711
2565
|
* Submit Status command implementation
|
|
1712
2566
|
* Checks the status of a submission via the Next.js API gateway
|
|
1713
2567
|
*/
|
|
1714
|
-
export async function submitStatus(submitId, verbose = false) {
|
|
2568
|
+
export async function submitStatus(submitId, verbose = false, options) {
|
|
1715
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
|
+
}
|
|
1716
2582
|
try {
|
|
1717
2583
|
const spinner = ora(`Checking status of submission ${submitId}...`).start();
|
|
1718
2584
|
const response = await axios.get(`${API_URL}/submit/${submitId}`, {
|
|
@@ -1745,8 +2611,20 @@ export async function submitStatus(submitId, verbose = false) {
|
|
|
1745
2611
|
* Update Status command implementation
|
|
1746
2612
|
* Checks the status of an update via the Next.js API gateway
|
|
1747
2613
|
*/
|
|
1748
|
-
export async function updateStatus(updateId, verbose = false) {
|
|
2614
|
+
export async function updateStatus(updateId, verbose = false, options) {
|
|
1749
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
|
+
}
|
|
1750
2628
|
try {
|
|
1751
2629
|
const spinner = ora(`Checking status of update ${updateId}...`).start();
|
|
1752
2630
|
const response = await axios.get(`${API_URL}/update/${updateId}`, {
|
|
@@ -1991,4 +2869,310 @@ export async function billingPortal(verbose = false) {
|
|
|
1991
2869
|
}
|
|
1992
2870
|
}
|
|
1993
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
|
+
}
|
|
1994
3178
|
//# sourceMappingURL=commands.js.map
|