@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +40 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +68 -60
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +389 -288
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +122 -127
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +513 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +250 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +32 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +162 -173
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +78 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +122 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +62 -67
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +34 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +58 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +12 -10
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +13 -14
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +41 -33
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +23 -25
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +30 -37
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +18 -11
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +47 -43
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +2 -2
  42. package/dist/gateways/api-gateway.d.ts +43 -12
  43. package/dist/gateways/api-gateway.js +240 -100
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +57 -0
  46. package/dist/gateways/supabase-gateway.d.ts +11 -11
  47. package/dist/gateways/supabase-gateway.js +15 -39
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +93 -2
  50. package/dist/methods.d.ts +3 -5
  51. package/dist/methods.js +170 -178
  52. package/dist/services/device-validation.service.d.ts +8 -0
  53. package/dist/services/device-validation.service.js +55 -35
  54. package/dist/services/execution-plan.service.js +27 -15
  55. package/dist/services/execution-plan.utils.d.ts +3 -0
  56. package/dist/services/execution-plan.utils.js +10 -32
  57. package/dist/services/metadata-extractor.service.d.ts +0 -2
  58. package/dist/services/metadata-extractor.service.js +57 -57
  59. package/dist/services/moropo.service.js +25 -24
  60. package/dist/services/report-download.service.d.ts +12 -1
  61. package/dist/services/report-download.service.js +31 -20
  62. package/dist/services/results-polling.service.d.ts +6 -7
  63. package/dist/services/results-polling.service.js +80 -33
  64. package/dist/services/telemetry.service.d.ts +40 -0
  65. package/dist/services/telemetry.service.js +230 -0
  66. package/dist/services/test-submission.service.js +2 -1
  67. package/dist/services/version.service.d.ts +3 -2
  68. package/dist/services/version.service.js +27 -11
  69. package/dist/types/domain/auth.types.d.ts +12 -0
  70. package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
  71. package/dist/types/domain/live.types.d.ts +76 -0
  72. package/dist/types/domain/live.types.js +4 -0
  73. package/dist/utils/auth.d.ts +13 -0
  74. package/dist/utils/auth.js +142 -0
  75. package/dist/utils/cli.d.ts +35 -0
  76. package/dist/utils/cli.js +127 -0
  77. package/dist/utils/compatibility.d.ts +2 -1
  78. package/dist/utils/compatibility.js +2 -2
  79. package/dist/utils/config-store.d.ts +35 -0
  80. package/dist/utils/config-store.js +125 -0
  81. package/dist/utils/connectivity.js +7 -3
  82. package/dist/utils/expo.js +14 -3
  83. package/dist/utils/orgs.d.ts +11 -0
  84. package/dist/utils/orgs.js +40 -0
  85. package/dist/utils/paths.d.ts +11 -0
  86. package/dist/utils/paths.js +24 -0
  87. package/dist/utils/progress.d.ts +13 -0
  88. package/dist/utils/progress.js +50 -0
  89. package/dist/utils/styling.d.ts +13 -5
  90. package/dist/utils/styling.js +37 -7
  91. package/package.json +26 -38
  92. package/bin/dev.cmd +0 -3
  93. package/bin/dev.js +0 -6
  94. package/bin/run.cmd +0 -3
  95. package/bin/run.js +0 -7
  96. package/dist/types/schema.types.d.ts +0 -2702
  97. package/oclif.manifest.json +0 -884
@@ -1,204 +1,193 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const core_1 = require("@oclif/core");
4
- const constants_1 = require("../constants");
3
+ exports.statusCommand = void 0;
4
+ const citty_1 = require("citty");
5
+ const api_flags_1 = require("../config/flags/api.flags");
5
6
  const api_gateway_1 = require("../gateways/api-gateway");
6
7
  const methods_1 = require("../methods");
8
+ const auth_1 = require("../utils/auth");
9
+ const cli_1 = require("../utils/cli");
10
+ const config_store_1 = require("../utils/config-store");
7
11
  const connectivity_1 = require("../utils/connectivity");
8
12
  const styling_1 = require("../utils/styling");
9
- class Status extends core_1.Command {
10
- static description = 'Get the status of an upload by name or upload ID';
11
- static enableJsonFlag = true;
12
- static examples = [
13
- '<%= config.bin %> <%= command.id %> --name my-upload-name',
14
- '<%= config.bin %> <%= command.id %> --upload-id 123e4567-e89b-12d3-a456-426614174000 --json',
15
- ];
16
- static flags = {
17
- apiKey: constants_1.flags.apiKey,
18
- apiUrl: constants_1.flags.apiUrl,
19
- json: core_1.Flags.boolean({
13
+ /** Errors the API gateway surfaces for 4xx-class failures retrying is pointless. */
14
+ function isClientApiError(error) {
15
+ if (!error)
16
+ return false;
17
+ return (error.message.includes('Invalid request:') ||
18
+ error.message.includes('Resource not found') ||
19
+ error.message.includes('Authentication failed') ||
20
+ error.message.includes('Access denied') ||
21
+ error.message.includes('Invalid API key') ||
22
+ error.message.includes('Rate limit exceeded'));
23
+ }
24
+ function formatDateTime(isoString) {
25
+ try {
26
+ const date = new Date(isoString);
27
+ return date.toLocaleString();
28
+ }
29
+ catch {
30
+ return isoString;
31
+ }
32
+ }
33
+ exports.statusCommand = (0, citty_1.defineCommand)({
34
+ meta: {
35
+ name: 'status',
36
+ description: 'Get the status of an upload by name or upload ID',
37
+ },
38
+ args: {
39
+ ...api_flags_1.apiFlags,
40
+ json: {
41
+ type: 'boolean',
20
42
  description: 'output in json format',
21
- }),
22
- name: core_1.Flags.string({
43
+ },
44
+ name: {
45
+ type: 'string',
23
46
  description: 'Name of the upload to check status for',
24
- exclusive: ['upload-id'],
25
- }),
26
- 'upload-id': core_1.Flags.string({
47
+ },
48
+ 'upload-id': {
49
+ type: 'string',
27
50
  description: 'UUID of the upload to check status for',
28
- exclusive: ['name'],
29
- }),
30
- };
51
+ },
52
+ },
31
53
  // eslint-disable-next-line complexity
32
- async run() {
33
- const { flags } = await this.parse(Status);
34
- const { apiKey: apiKeyFlag, apiUrl, json, name, 'upload-id': uploadId, } = flags;
35
- const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY;
36
- if (!apiKey) {
37
- this.error('API key is required. Please provide it via --api-key flag or DEVICE_CLOUD_API_KEY environment variable.');
38
- return;
54
+ async run({ args }) {
55
+ const apiKeyFlag = args['api-key'];
56
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
57
+ const json = Boolean(args.json);
58
+ const name = args.name;
59
+ const uploadId = args['upload-id'];
60
+ try {
61
+ await statusMain({ apiKeyFlag, apiUrl, json, name, uploadId });
39
62
  }
40
- if (name && uploadId) {
41
- this.error('Cannot provide both --name and --upload-id. These options are mutually exclusive.');
42
- return;
63
+ catch (error) {
64
+ cli_1.logger.error(error, { exit: 1, json });
43
65
  }
44
- if (!name && !uploadId) {
45
- this.error('Either --name or --upload-id must be provided');
46
- return;
66
+ },
67
+ });
68
+ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
69
+ const auth = await (0, auth_1.resolveAuth)({ apiKeyFlag });
70
+ if (name && uploadId) {
71
+ throw new cli_1.CliError('Cannot provide both --name and --upload-id. These options are mutually exclusive.');
72
+ }
73
+ if (!name && !uploadId) {
74
+ throw new cli_1.CliError('Either --name or --upload-id must be provided');
75
+ }
76
+ let lastError = null;
77
+ let status = null;
78
+ let attemptsMade = 0;
79
+ for (let attempt = 1; attempt <= 5; attempt++) {
80
+ try {
81
+ attemptsMade = attempt;
82
+ status = (await api_gateway_1.ApiGateway.getUploadStatus(apiUrl, auth, {
83
+ name,
84
+ uploadId,
85
+ }));
86
+ break;
47
87
  }
48
- let lastError = null;
49
- let status = null;
50
- let attemptsMade = 0;
51
- for (let attempt = 1; attempt <= 5; attempt++) {
52
- try {
53
- attemptsMade = attempt;
54
- status = (await api_gateway_1.ApiGateway.getUploadStatus(apiUrl, apiKey, {
55
- name,
56
- uploadId,
57
- }));
88
+ catch (error) {
89
+ lastError = error;
90
+ const isNetworkError = lastError.name === 'NetworkError' ||
91
+ (error instanceof TypeError && lastError.message === 'fetch failed');
92
+ if (isClientApiError(lastError)) {
58
93
  break;
59
94
  }
60
- catch (error) {
61
- lastError = error;
62
- // Check if this is a retryable error (network/timeout issues)
63
- // Non-retryable errors: 4xx client errors (bad request, not found, unauthorized, forbidden)
64
- const isNetworkError = lastError.name === 'NetworkError' ||
65
- (error instanceof TypeError && lastError.message === 'fetch failed');
66
- const isClientError = lastError.message.includes('Invalid request:') ||
67
- lastError.message.includes('Resource not found') ||
68
- lastError.message.includes('Authentication failed') ||
69
- lastError.message.includes('Access denied') ||
70
- lastError.message.includes('Invalid API key') ||
71
- lastError.message.includes('Rate limit exceeded');
72
- // Don't retry client errors - they won't succeed on retry
73
- if (isClientError) {
74
- break;
75
- }
76
- // Only retry network errors
77
- if (attempt < 5 && isNetworkError) {
78
- this.log(`Network error on attempt ${attempt}/5. Retrying...`);
79
- await new Promise((resolve) => {
80
- setTimeout(resolve, 1000 * attempt);
81
- });
82
- }
83
- else if (attempt < 5) {
84
- // For other errors (server errors), retry but with different message
85
- this.log(`Request failed on attempt ${attempt}/5. Retrying...`);
86
- await new Promise((resolve) => {
87
- setTimeout(resolve, 1000 * attempt);
88
- });
89
- }
95
+ if (attempt < 5) {
96
+ cli_1.logger.log(isNetworkError
97
+ ? `Network error on attempt ${attempt}/5. Retrying...`
98
+ : `Request failed on attempt ${attempt}/5. Retrying...`);
99
+ await new Promise((resolve) => {
100
+ setTimeout(resolve, 1000 * attempt);
101
+ });
90
102
  }
91
103
  }
92
- if (!status) {
93
- // Check if this was a client error (non-retryable)
94
- const isClientError = lastError && (lastError.message.includes('Invalid request:') ||
95
- lastError.message.includes('Resource not found') ||
96
- lastError.message.includes('Authentication failed') ||
97
- lastError.message.includes('Access denied') ||
98
- lastError.message.includes('Invalid API key') ||
99
- lastError.message.includes('Rate limit exceeded'));
100
- if (isClientError) {
101
- // For client errors, show the error immediately without connectivity check
102
- const errorMessage = lastError?.message || 'Unknown error';
103
- if (json) {
104
- return {
105
- status: 'FAILED',
106
- error: errorMessage,
107
- attempts: attemptsMade,
108
- tests: [],
109
- };
110
- }
111
- this.error(errorMessage);
112
- }
113
- // Check if the failure is due to internet connectivity issues
114
- const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
115
- let errorMessage;
116
- if (connectivityCheck.connected) {
117
- errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}. Internet appears functional but unable to reach API. Last error: ${lastError?.message || 'Unknown error'}`;
118
- }
119
- else {
120
- // Build detailed error message with endpoint diagnostics
121
- const endpointDetails = connectivityCheck.endpointResults
122
- .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
123
- .join('\n');
124
- errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.\nLast API error: ${lastError?.message || 'Unknown error'}`;
125
- }
104
+ }
105
+ if (!status) {
106
+ if (isClientApiError(lastError)) {
107
+ const errorMessage = lastError?.message || 'Unknown error';
126
108
  if (json) {
127
- return {
109
+ // eslint-disable-next-line no-console
110
+ console.log(JSON.stringify({
128
111
  status: 'FAILED',
129
112
  error: errorMessage,
130
113
  attempts: attemptsMade,
131
- connectivityCheck: {
132
- connected: connectivityCheck.connected,
133
- endpointResults: connectivityCheck.endpointResults,
134
- message: connectivityCheck.message,
135
- },
136
114
  tests: [],
137
- };
115
+ }, null, 2));
116
+ return;
138
117
  }
139
- this.error(errorMessage);
118
+ throw new cli_1.CliError(errorMessage);
140
119
  }
141
- try {
142
- if (json) {
143
- // Reconstruct object to ensure tests appears at the bottom
144
- const { tests, ...rest } = status;
145
- return {
146
- ...rest,
147
- tests,
148
- };
149
- }
150
- this.log((0, styling_1.sectionHeader)('Upload Status'));
151
- // Display overall status
152
- this.log(` ${(0, styling_1.formatStatus)(status.status)}`);
153
- if (status.name) {
154
- this.log(` ${styling_1.colors.dim('Name:')} ${styling_1.colors.bold(status.name)}`);
155
- }
156
- if (status.uploadId) {
157
- this.log(` ${styling_1.colors.dim('Upload ID:')} ${(0, styling_1.formatId)(status.uploadId)}`);
158
- }
159
- if (status.appBinaryId) {
160
- this.log(` ${styling_1.colors.dim('Binary ID:')} ${(0, styling_1.formatId)(status.appBinaryId)}`);
161
- }
162
- if (status.createdAt) {
163
- this.log(` ${styling_1.colors.dim('Created:')} ${this.formatDateTime(status.createdAt)}`);
164
- }
165
- if (status.consoleUrl) {
166
- this.log(` ${styling_1.colors.dim('Console:')} ${(0, styling_1.formatUrl)(status.consoleUrl)}`);
167
- }
168
- if (status.tests.length > 0) {
169
- this.log((0, styling_1.sectionHeader)('Test Results'));
170
- for (const item of status.tests) {
171
- this.log(` ${(0, styling_1.formatStatus)(item.status)} ${styling_1.colors.bold(item.name)}`);
172
- if (item.status === 'FAILED' && item.failReason) {
173
- this.log(` ${styling_1.colors.error('Fail reason:')} ${item.failReason}`);
174
- }
175
- if (item.durationSeconds) {
176
- this.log(` ${styling_1.colors.dim('Duration:')} ${(0, methods_1.formatDurationSeconds)(item.durationSeconds)}`);
177
- }
178
- if (item.createdAt) {
179
- this.log(` ${styling_1.colors.dim('Created:')} ${this.formatDateTime(item.createdAt)}`);
180
- }
181
- this.log('');
182
- }
183
- }
120
+ const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
121
+ let errorMessage;
122
+ if (connectivityCheck.connected) {
123
+ errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}. Internet appears functional but unable to reach API. Last error: ${lastError?.message || 'Unknown error'}`;
184
124
  }
185
- catch (error) {
186
- this.error(`Failed to get status: ${error.message}`);
125
+ else {
126
+ const endpointDetails = connectivityCheck.endpointResults
127
+ .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
128
+ .join('\n');
129
+ errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.\nLast API error: ${lastError?.message || 'Unknown error'}`;
187
130
  }
131
+ if (json) {
132
+ // eslint-disable-next-line no-console
133
+ console.log(JSON.stringify({
134
+ status: 'FAILED',
135
+ error: errorMessage,
136
+ attempts: attemptsMade,
137
+ connectivityCheck: {
138
+ connected: connectivityCheck.connected,
139
+ endpointResults: connectivityCheck.endpointResults,
140
+ message: connectivityCheck.message,
141
+ },
142
+ tests: [],
143
+ }, null, 2));
144
+ return;
145
+ }
146
+ throw new cli_1.CliError(errorMessage);
188
147
  }
189
- /**
190
- * Format an ISO date string to a human-readable local date/time
191
- * @param isoString - ISO 8601 date string
192
- * @returns Formatted local date/time string
193
- */
194
- formatDateTime(isoString) {
195
- try {
196
- const date = new Date(isoString);
197
- return date.toLocaleString();
148
+ try {
149
+ if (json) {
150
+ const { tests, ...rest } = status;
151
+ // eslint-disable-next-line no-console
152
+ console.log(JSON.stringify({ ...rest, tests }, null, 2));
153
+ return;
154
+ }
155
+ cli_1.logger.log((0, styling_1.sectionHeader)('Upload Status'));
156
+ cli_1.logger.log(` ${(0, styling_1.formatStatus)(status.status)}`);
157
+ if (status.name) {
158
+ cli_1.logger.log(` ${styling_1.colors.dim('Name:')} ${styling_1.colors.bold(status.name)}`);
159
+ }
160
+ if (status.uploadId) {
161
+ cli_1.logger.log(` ${styling_1.colors.dim('Upload ID:')} ${(0, styling_1.formatId)(status.uploadId)}`);
198
162
  }
199
- catch {
200
- return isoString;
163
+ if (status.appBinaryId) {
164
+ cli_1.logger.log(` ${styling_1.colors.dim('Binary ID:')} ${(0, styling_1.formatId)(status.appBinaryId)}`);
201
165
  }
166
+ if (status.createdAt) {
167
+ cli_1.logger.log(` ${styling_1.colors.dim('Created:')} ${formatDateTime(status.createdAt)}`);
168
+ }
169
+ if (status.consoleUrl) {
170
+ cli_1.logger.log(` ${styling_1.colors.dim('Console:')} ${(0, styling_1.formatUrl)(status.consoleUrl)}`);
171
+ }
172
+ if (status.tests.length > 0) {
173
+ cli_1.logger.log((0, styling_1.sectionHeader)('Test Results'));
174
+ for (const item of status.tests) {
175
+ cli_1.logger.log(` ${(0, styling_1.formatStatus)(item.status)} ${styling_1.colors.bold(item.name)}`);
176
+ if (item.status === 'FAILED' && item.failReason) {
177
+ cli_1.logger.log(` ${styling_1.colors.error('Fail reason:')} ${item.failReason}`);
178
+ }
179
+ if (item.durationSeconds) {
180
+ cli_1.logger.log(` ${styling_1.colors.dim('Duration:')} ${(0, methods_1.formatDurationSeconds)(item.durationSeconds)}`);
181
+ }
182
+ if (item.createdAt) {
183
+ cli_1.logger.log(` ${styling_1.colors.dim('Created:')} ${formatDateTime(item.createdAt)}`);
184
+ }
185
+ cli_1.logger.log('');
186
+ }
187
+ }
188
+ }
189
+ catch (error) {
190
+ throw new cli_1.CliError(`Failed to get status: ${error.message}`);
202
191
  }
203
192
  }
204
- exports.default = Status;
193
+ exports.default = exports.statusCommand;
@@ -0,0 +1,12 @@
1
+ export declare const switchOrgCommand: import("citty").CommandDef<{
2
+ 'api-url': {
3
+ type: "string";
4
+ description: string;
5
+ };
6
+ org: {
7
+ type: "positional";
8
+ required: false;
9
+ description: string;
10
+ };
11
+ }>;
12
+ export default switchOrgCommand;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.switchOrgCommand = void 0;
4
+ /**
5
+ * `dcd switch-org` — changes the active org on the stored session.
6
+ *
7
+ * With an argument (`dcd switch-org <name>`): update config immediately after
8
+ * confirming the user is a member via GET /me/orgs.
9
+ * Without an argument: fetch orgs and prompt the user to pick one.
10
+ */
11
+ const citty_1 = require("citty");
12
+ const auth_1 = require("../utils/auth");
13
+ const cli_1 = require("../utils/cli");
14
+ const config_store_1 = require("../utils/config-store");
15
+ const orgs_1 = require("../utils/orgs");
16
+ const styling_1 = require("../utils/styling");
17
+ exports.switchOrgCommand = (0, citty_1.defineCommand)({
18
+ meta: {
19
+ name: 'switch-org',
20
+ description: 'Switch the active organization for the logged-in session',
21
+ },
22
+ args: {
23
+ 'api-url': {
24
+ type: 'string',
25
+ description: 'API base URL (defaults to the URL stored by `dcd login`)',
26
+ },
27
+ org: {
28
+ type: 'positional',
29
+ required: false,
30
+ description: 'Org name to switch to (omit for an interactive picker)',
31
+ },
32
+ },
33
+ async run({ args }) {
34
+ const config = (0, config_store_1.readConfig)();
35
+ if (!config?.session) {
36
+ throw new cli_1.CliError('Not logged in. Run `dcd login` first.');
37
+ }
38
+ // Honor the env the user logged into — defaulting to prod here would send
39
+ // a dev Bearer token to the prod API.
40
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
41
+ const target = args.org;
42
+ // sessionOnly: an exported DEVICE_CLOUD_API_KEY must not shadow the
43
+ // browser session this command requires.
44
+ const auth = await (0, auth_1.resolveAuth)({ apiKeyFlag: undefined, sessionOnly: true });
45
+ const orgs = await (0, orgs_1.fetchOrgs)(apiUrl, auth.headers);
46
+ let chosen;
47
+ if (target) {
48
+ chosen = matchOrg(orgs, target);
49
+ }
50
+ else {
51
+ chosen = await (0, orgs_1.pickOrg)(orgs);
52
+ }
53
+ (0, config_store_1.writeConfig)({
54
+ ...config,
55
+ current_org_id: chosen.id,
56
+ current_org_name: chosen.name,
57
+ });
58
+ cli_1.logger.log(`${styling_1.symbols.success} ${styling_1.colors.bold('Switched')} to ${styling_1.colors.highlight(chosen.name)}`);
59
+ },
60
+ });
61
+ /**
62
+ * Match a user-supplied string to an org. Prefers case-insensitive name match.
63
+ * Falls back to slug / id so existing scripts keep working, but error output
64
+ * only surfaces names.
65
+ */
66
+ function matchOrg(orgs, target) {
67
+ const needle = target.toLowerCase();
68
+ const nameMatches = orgs.filter((o) => o.name.toLowerCase() === needle);
69
+ if (nameMatches.length > 1) {
70
+ throw new cli_1.CliError(`Multiple orgs named "${target}". Run \`dcd switch-org\` without arguments to pick interactively.`);
71
+ }
72
+ const chosen = nameMatches[0] ?? orgs.find((o) => o.slug === target || o.id === target);
73
+ if (!chosen) {
74
+ throw new cli_1.CliError(`No org named "${target}". Available: ${orgs.map((o) => o.name).join(', ')}`);
75
+ }
76
+ return chosen;
77
+ }
78
+ exports.default = exports.switchOrgCommand;
@@ -0,0 +1,2 @@
1
+ export declare const upgradeCommand: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ export default upgradeCommand;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upgradeCommand = void 0;
4
+ /**
5
+ * `dcd upgrade` — in-place replace of the standalone binary.
6
+ *
7
+ * Only meaningful for the bun-compiled binary install. npm-installed users
8
+ * are redirected to `npm install -g`. Fetches the latest version from the
9
+ * release manifest, downloads the matching binary, verifies its SHA256
10
+ * against the published SHA256SUMS, then atomically renames over the running
11
+ * executable (the old inode stays alive until the process exits).
12
+ */
13
+ const node_crypto_1 = require("node:crypto");
14
+ const node_fs_1 = require("node:fs");
15
+ const node_stream_1 = require("node:stream");
16
+ const promises_1 = require("node:stream/promises");
17
+ const citty_1 = require("citty");
18
+ const version_service_1 = require("../services/version.service");
19
+ const cli_1 = require("../utils/cli");
20
+ const styling_1 = require("../utils/styling");
21
+ const DEFAULT_DOWNLOAD_BASE = 'https://get.devicecloud.dev';
22
+ // Maps `${process.platform}-${process.arch}` to the GitHub Release asset filename
23
+ // produced by scripts/build-binaries.mjs. Keys must stay in sync with that script.
24
+ const ASSET_BY_PLATFORM = {
25
+ 'darwin-arm64': 'dcd-darwin-arm64',
26
+ 'darwin-x64': 'dcd-darwin-x64',
27
+ 'linux-arm64': 'dcd-linux-arm64',
28
+ 'linux-x64': 'dcd-linux-x64',
29
+ 'win32-x64': 'dcd-windows-x64.exe',
30
+ };
31
+ exports.upgradeCommand = (0, citty_1.defineCommand)({
32
+ meta: {
33
+ name: 'upgrade',
34
+ description: 'Upgrade dcd in place to the latest released version',
35
+ },
36
+ async run() {
37
+ if ((0, cli_1.getInstallMethod)() !== 'binary') {
38
+ throw new cli_1.CliError('`dcd upgrade` only applies to the standalone binary install. ' +
39
+ 'Run: npm install -g @devicecloud.dev/dcd@latest');
40
+ }
41
+ const current = (0, cli_1.getCliVersion)();
42
+ const versionService = new version_service_1.VersionService();
43
+ const latest = await versionService.checkLatestCliVersion();
44
+ if (!latest) {
45
+ throw new cli_1.CliError('Could not reach the update manifest. Check your network connection and try again.');
46
+ }
47
+ if (!versionService.isOutdated(current, latest)) {
48
+ cli_1.logger.log(`${styling_1.symbols.success} Already on the latest version (${styling_1.colors.highlight(current)}).`);
49
+ return;
50
+ }
51
+ const platformKey = `${process.platform}-${process.arch}`;
52
+ const asset = ASSET_BY_PLATFORM[platformKey];
53
+ if (!asset) {
54
+ throw new cli_1.CliError(`No binary published for ${platformKey}. Supported platforms: ${Object.keys(ASSET_BY_PLATFORM).join(', ')}`);
55
+ }
56
+ if (process.platform === 'win32') {
57
+ // Windows can't replace a running .exe; defer to a re-run of the installer.
58
+ const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE;
59
+ throw new cli_1.CliError(`Automatic upgrade on Windows is not yet supported. Re-run the installer:\n irm ${base}/install.ps1 | iex`);
60
+ }
61
+ const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE;
62
+ const binaryUrl = `${base}/download/${latest}/${asset}`;
63
+ const sumsUrl = `${base}/download/${latest}/SHA256SUMS`;
64
+ cli_1.logger.log(`${styling_1.symbols.info} Upgrading ${styling_1.colors.highlight(current)} → ${styling_1.colors.highlight(latest)}`);
65
+ cli_1.logger.log(styling_1.colors.dim(` ${binaryUrl}`));
66
+ const execPath = process.execPath;
67
+ const tmpPath = `${execPath}.new`;
68
+ try {
69
+ await downloadToFile(binaryUrl, tmpPath);
70
+ const expected = await fetchExpectedChecksum(sumsUrl, asset);
71
+ const actual = await sha256File(tmpPath);
72
+ if (expected !== actual) {
73
+ throw new Error(`Checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
74
+ }
75
+ (0, node_fs_1.chmodSync)(tmpPath, 0o755);
76
+ // rename is atomic on the same filesystem; on POSIX the running process
77
+ // keeps the old inode open until exit, so this is safe to do mid-run.
78
+ (0, node_fs_1.renameSync)(tmpPath, execPath);
79
+ }
80
+ catch (e) {
81
+ safeUnlink(tmpPath);
82
+ throw new cli_1.CliError(`Upgrade failed: ${e.message}. The existing binary at ${execPath} was not modified.`);
83
+ }
84
+ cli_1.logger.log(`${styling_1.symbols.success} Upgraded to ${styling_1.colors.highlight(latest)}`);
85
+ },
86
+ });
87
+ async function downloadToFile(url, dest) {
88
+ const res = await fetch(url, { redirect: 'follow' });
89
+ if (!res.ok || !res.body) {
90
+ throw new Error(`HTTP ${res.status} fetching ${url}`);
91
+ }
92
+ // Node 22 exposes Readable.fromWeb for piping a WHATWG ReadableStream.
93
+ await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(res.body), (0, node_fs_1.createWriteStream)(dest));
94
+ }
95
+ async function fetchExpectedChecksum(sumsUrl, asset) {
96
+ const res = await fetch(sumsUrl, { redirect: 'follow' });
97
+ if (!res.ok)
98
+ throw new Error(`HTTP ${res.status} fetching ${sumsUrl}`);
99
+ const body = await res.text();
100
+ for (const line of body.split('\n')) {
101
+ const match = line.match(/^([a-f0-9]{64})\s+(.+)$/);
102
+ if (match && match[2].trim() === asset)
103
+ return match[1];
104
+ }
105
+ throw new Error(`SHA256SUMS has no entry for ${asset}`);
106
+ }
107
+ async function sha256File(path) {
108
+ const hash = (0, node_crypto_1.createHash)('sha256');
109
+ for await (const chunk of (0, node_fs_1.createReadStream)(path)) {
110
+ hash.update(chunk);
111
+ }
112
+ return hash.digest('hex');
113
+ }
114
+ function safeUnlink(path) {
115
+ try {
116
+ (0, node_fs_1.unlinkSync)(path);
117
+ }
118
+ catch {
119
+ // Best-effort cleanup; failure here would only leave a .new file.
120
+ }
121
+ }
122
+ exports.default = exports.upgradeCommand;
@@ -1,20 +1,35 @@
1
- import { Command } from '@oclif/core';
2
- export default class Upload extends Command {
3
- static args: {
4
- appFile: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
1
+ export declare const uploadCommand: import("citty").CommandDef<{
2
+ 'app-url': {
3
+ readonly type: "string";
4
+ readonly description: "Signed URL to an Expo iOS build (.tar.gz). The archive is downloaded and extracted automatically. Expo signed URLs expire after ~1 hour.";
5
5
  };
6
- static description: string;
7
- static enableJsonFlag: boolean;
8
- static examples: string[];
9
- static flags: {
10
- apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
- apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
- 'app-url': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
- debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
- 'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
+ 'ignore-sha-check': {
7
+ readonly type: "boolean";
8
+ readonly description: "Ignore the sha hash check and upload the binary regardless of whether it already exists (not recommended)";
15
9
  };
16
- run(): Promise<{
17
- appBinaryId: string;
18
- }>;
19
- protected toErrorJson(err: unknown): unknown;
20
- }
10
+ debug: {
11
+ readonly type: "boolean";
12
+ readonly default: false;
13
+ readonly description: "Enable detailed debug logging for troubleshooting issues";
14
+ };
15
+ json: {
16
+ readonly type: "boolean";
17
+ readonly description: "Output results in JSON format. Exit codes: 0 on success, 2 if the test run fails, 1 on CLI/infrastructure errors";
18
+ };
19
+ appFile: {
20
+ type: "positional";
21
+ required: false;
22
+ description: string;
23
+ };
24
+ 'api-key': {
25
+ readonly type: "string";
26
+ readonly alias: ["apiKey"];
27
+ readonly description: "API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.";
28
+ };
29
+ 'api-url': {
30
+ readonly type: "string";
31
+ readonly alias: ["apiURL", "apiUrl"];
32
+ readonly description: "API base URL (defaults to the URL stored by `dcd login`, else prod)";
33
+ };
34
+ }>;
35
+ export default uploadCommand;