@solana-mobile/dapp-store-cli 0.16.0 → 0.16.1
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/bin/dapp-store.js +3 -1
- package/lib/CliSetup.js +304 -505
- package/lib/CliUtils.js +6 -376
- package/lib/__tests__/CliSetupTest.js +484 -74
- package/lib/cli/__tests__/parseErrors.test.js +25 -0
- package/lib/cli/__tests__/signer.test.js +436 -0
- package/lib/cli/constants.js +23 -0
- package/lib/cli/messages.js +21 -0
- package/lib/cli/parseErrors.js +41 -0
- package/lib/{commands/publish/PublishCliSupport.js → cli/selfUpdate.js} +72 -38
- package/lib/{commands/publish/PublishCliRemove.js → cli/signer.js} +35 -56
- package/lib/index.js +96 -5
- package/lib/package.json +5 -24
- package/lib/portal/__tests__/releaseMetadata.test.js +647 -0
- package/lib/portal/__tests__/translators.test.js +76 -0
- package/lib/portal/__tests__/workflowClient.test.js +457 -0
- package/lib/portal/attestationClient.js +143 -0
- package/lib/portal/files.js +64 -0
- package/lib/portal/http.js +364 -0
- package/lib/portal/records.js +64 -0
- package/lib/portal/releaseMetadata.js +748 -0
- package/lib/portal/translators.js +460 -0
- package/lib/portal/types.js +1 -0
- package/lib/portal/workflowClient.js +704 -0
- package/lib/publication/PublicationProgressReporter.js +1051 -0
- package/lib/publication/__tests__/PublicationProgressReporter.test.js +174 -0
- package/lib/{commands/ValidateCommand.js → publication/__tests__/fundingPreflight.test.js} +90 -66
- package/lib/publication/__tests__/publicationSummary.test.js +26 -0
- package/lib/publication/cliValidation.js +482 -0
- package/lib/publication/fundingPreflight.js +246 -0
- package/lib/publication/publicationSummary.js +99 -0
- package/lib/{commands/utils.js → publication/runPublicationWorkflow.js} +16 -46
- package/package.json +5 -24
- package/src/CliSetup.ts +370 -505
- package/src/CliUtils.ts +9 -233
- package/src/__tests__/CliSetupTest.ts +272 -120
- package/src/cli/__tests__/parseErrors.test.ts +34 -0
- package/src/cli/__tests__/signer.test.ts +359 -0
- package/src/cli/constants.ts +3 -0
- package/src/cli/messages.ts +27 -0
- package/src/cli/parseErrors.ts +62 -0
- package/src/cli/selfUpdate.ts +59 -0
- package/src/cli/signer.ts +38 -0
- package/src/index.ts +31 -4
- package/src/portal/__tests__/releaseMetadata.test.ts +508 -0
- package/src/portal/__tests__/translators.test.ts +82 -0
- package/src/portal/__tests__/workflowClient.test.ts +278 -0
- package/src/portal/attestationClient.ts +19 -0
- package/src/portal/files.ts +73 -0
- package/src/portal/http.ts +170 -0
- package/src/portal/records.ts +38 -0
- package/src/portal/releaseMetadata.ts +489 -0
- package/src/portal/translators.ts +750 -0
- package/src/portal/types.ts +27 -0
- package/src/portal/workflowClient.ts +575 -0
- package/src/publication/PublicationProgressReporter.ts +1026 -0
- package/src/publication/__tests__/PublicationProgressReporter.test.ts +210 -0
- package/src/publication/__tests__/fundingPreflight.test.ts +78 -0
- package/src/publication/__tests__/publicationSummary.test.ts +30 -0
- package/src/publication/cliValidation.ts +264 -0
- package/src/publication/fundingPreflight.ts +123 -0
- package/src/publication/publicationSummary.ts +26 -0
- package/src/publication/runPublicationWorkflow.ts +46 -0
- package/lib/commands/create/CreateCliApp.js +0 -223
- package/lib/commands/create/CreateCliRelease.js +0 -290
- package/lib/commands/create/index.js +0 -40
- package/lib/commands/index.js +0 -3
- package/lib/commands/publish/PublishCliSubmit.js +0 -208
- package/lib/commands/publish/PublishCliUpdate.js +0 -211
- package/lib/commands/publish/index.js +0 -22
- package/lib/commands/scaffolding/ScaffoldInit.js +0 -15
- package/lib/commands/scaffolding/index.js +0 -1
- package/lib/config/EnvVariables.js +0 -59
- package/lib/config/PublishDetails.js +0 -915
- package/lib/config/S3StorageManager.js +0 -93
- package/lib/config/index.js +0 -2
- package/lib/generated/config_obj.json +0 -1
- package/lib/generated/config_schema.json +0 -1
- package/lib/prebuild_schema/publishing_source.yaml +0 -64
- package/lib/prebuild_schema/schemagen.js +0 -25
- package/lib/upload/CachedStorageDriver.js +0 -458
- package/lib/upload/TurboStorageDriver.js +0 -718
- package/lib/upload/__tests__/CachedStorageDriver.test.js +0 -437
- package/lib/upload/__tests__/TurboStorageDriver.test.js +0 -17
- package/lib/upload/__tests__/contentGateway.test.js +0 -17
- package/lib/upload/contentGateway.js +0 -23
- package/lib/upload/index.js +0 -2
- package/src/commands/ValidateCommand.ts +0 -82
- package/src/commands/create/CreateCliApp.ts +0 -93
- package/src/commands/create/CreateCliRelease.ts +0 -149
- package/src/commands/create/index.ts +0 -47
- package/src/commands/index.ts +0 -3
- package/src/commands/publish/PublishCliRemove.ts +0 -66
- package/src/commands/publish/PublishCliSubmit.ts +0 -93
- package/src/commands/publish/PublishCliSupport.ts +0 -66
- package/src/commands/publish/PublishCliUpdate.ts +0 -101
- package/src/commands/publish/index.ts +0 -29
- package/src/commands/scaffolding/ScaffoldInit.ts +0 -20
- package/src/commands/scaffolding/index.ts +0 -1
- package/src/commands/utils.ts +0 -33
- package/src/config/EnvVariables.ts +0 -39
- package/src/config/PublishDetails.ts +0 -456
- package/src/config/S3StorageManager.ts +0 -47
- package/src/config/index.ts +0 -2
- package/src/prebuild_schema/publishing_source.yaml +0 -64
- package/src/prebuild_schema/schemagen.js +0 -31
- package/src/upload/CachedStorageDriver.ts +0 -179
- package/src/upload/TurboStorageDriver.ts +0 -283
- package/src/upload/__tests__/CachedStorageDriver.test.ts +0 -246
- package/src/upload/__tests__/TurboStorageDriver.test.ts +0 -15
- package/src/upload/__tests__/contentGateway.test.ts +0 -31
- package/src/upload/contentGateway.ts +0 -37
- package/src/upload/index.ts +0 -2
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PublicationWorkflowLogger,
|
|
5
|
+
PublicationWorkflowResult,
|
|
6
|
+
} from './runPublicationWorkflow.js';
|
|
7
|
+
|
|
8
|
+
type PublicationProgressMode = 'new-version' | 'resume';
|
|
9
|
+
|
|
10
|
+
type ProgressPhaseKey =
|
|
11
|
+
| 'source'
|
|
12
|
+
| 'ingestion'
|
|
13
|
+
| 'context'
|
|
14
|
+
| 'mint'
|
|
15
|
+
| 'verify'
|
|
16
|
+
| 'attest'
|
|
17
|
+
| 'submit';
|
|
18
|
+
|
|
19
|
+
type ProgressPhaseState = 'pending' | 'active' | 'complete';
|
|
20
|
+
|
|
21
|
+
type ProgressMetadata = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
type ProgressSourceKind = 'apk-file' | 'apk-url' | 'resume';
|
|
24
|
+
|
|
25
|
+
type ProgressContext = Partial<{
|
|
26
|
+
sourceKind: ProgressSourceKind;
|
|
27
|
+
fileName: string;
|
|
28
|
+
apkUrl: string;
|
|
29
|
+
sourceReleaseId: string;
|
|
30
|
+
androidPackage: string;
|
|
31
|
+
versionName: string;
|
|
32
|
+
releaseId: string;
|
|
33
|
+
publicationSessionId: string;
|
|
34
|
+
ingestionSessionId: string;
|
|
35
|
+
mintAddress: string;
|
|
36
|
+
transactionSignature: string;
|
|
37
|
+
hubspotTicketId: string;
|
|
38
|
+
requestUniqueId: string;
|
|
39
|
+
fileSize: number;
|
|
40
|
+
bytesUploaded: number;
|
|
41
|
+
bytesTotal: number;
|
|
42
|
+
ingestionStatus: string;
|
|
43
|
+
ingestionProgress: number;
|
|
44
|
+
ingestionStage: string;
|
|
45
|
+
ingestionDetail: string;
|
|
46
|
+
activeStep: string;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
const SPINNER_FRAMES = ['|', '/', '-', '\\'] as const;
|
|
50
|
+
|
|
51
|
+
const PHASES: Array<{ key: ProgressPhaseKey; label: string }> = [
|
|
52
|
+
{ key: 'source', label: 'Prepare source' },
|
|
53
|
+
{ key: 'ingestion', label: 'Process APK' },
|
|
54
|
+
{ key: 'context', label: 'Load release context' },
|
|
55
|
+
{ key: 'mint', label: 'Mint release NFT' },
|
|
56
|
+
{ key: 'verify', label: 'Verify collection' },
|
|
57
|
+
{ key: 'attest', label: 'Create attestation' },
|
|
58
|
+
{ key: 'submit', label: 'Submit to review' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const PHASE_WEIGHT_PROFILES: Record<
|
|
62
|
+
ProgressSourceKind,
|
|
63
|
+
Record<ProgressPhaseKey, number>
|
|
64
|
+
> = {
|
|
65
|
+
'apk-file': {
|
|
66
|
+
source: 0.42,
|
|
67
|
+
ingestion: 0.2,
|
|
68
|
+
context: 0.08,
|
|
69
|
+
mint: 0.12,
|
|
70
|
+
verify: 0.08,
|
|
71
|
+
attest: 0.04,
|
|
72
|
+
submit: 0.06,
|
|
73
|
+
},
|
|
74
|
+
'apk-url': {
|
|
75
|
+
source: 0.05,
|
|
76
|
+
ingestion: 0.43,
|
|
77
|
+
context: 0.12,
|
|
78
|
+
mint: 0.16,
|
|
79
|
+
verify: 0.1,
|
|
80
|
+
attest: 0.05,
|
|
81
|
+
submit: 0.09,
|
|
82
|
+
},
|
|
83
|
+
resume: {
|
|
84
|
+
source: 0.42,
|
|
85
|
+
ingestion: 0.2,
|
|
86
|
+
context: 0.08,
|
|
87
|
+
mint: 0.12,
|
|
88
|
+
verify: 0.08,
|
|
89
|
+
attest: 0.04,
|
|
90
|
+
submit: 0.06,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const STEP_PROGRESS_RANGES: Record<
|
|
95
|
+
string,
|
|
96
|
+
{ start: number; end: number }
|
|
97
|
+
> = {
|
|
98
|
+
'source.prepare': { start: 0.02, end: 0.1 },
|
|
99
|
+
'source.upload': { start: 0.1, end: 1 },
|
|
100
|
+
'source.ready': { start: 0.2, end: 1 },
|
|
101
|
+
'ingestion.create': { start: 0.05, end: 0.2 },
|
|
102
|
+
'ingestion.wait': { start: 0.2, end: 1 },
|
|
103
|
+
'bundle.load': { start: 0.1, end: 0.55 },
|
|
104
|
+
'session.load': { start: 0.55, end: 1 },
|
|
105
|
+
'mint.prepare': { start: 0.05, end: 0.3 },
|
|
106
|
+
'mint.submit': { start: 0.3, end: 0.65 },
|
|
107
|
+
'mint.save': { start: 0.65, end: 1 },
|
|
108
|
+
'verify.prepare': { start: 0.05, end: 0.35 },
|
|
109
|
+
'verify.submit': { start: 0.35, end: 1 },
|
|
110
|
+
'attestation.create': { start: 0.05, end: 1 },
|
|
111
|
+
'submit.store': { start: 0.05, end: 1 },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const DEFAULT_RUNNING_STEP_PROGRESS = 0.2;
|
|
115
|
+
|
|
116
|
+
const STEP_TO_PHASE: Record<string, ProgressPhaseKey> = {
|
|
117
|
+
'source.prepare': 'source',
|
|
118
|
+
'source.upload': 'source',
|
|
119
|
+
'source.ready': 'source',
|
|
120
|
+
'ingestion.create': 'ingestion',
|
|
121
|
+
'ingestion.wait': 'ingestion',
|
|
122
|
+
'bundle.load': 'context',
|
|
123
|
+
'session.load': 'context',
|
|
124
|
+
'mint.prepare': 'mint',
|
|
125
|
+
'mint.submit': 'mint',
|
|
126
|
+
'mint.save': 'mint',
|
|
127
|
+
'verify.prepare': 'verify',
|
|
128
|
+
'verify.submit': 'verify',
|
|
129
|
+
'attestation.create': 'attest',
|
|
130
|
+
'submit.store': 'submit',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const PHASE_FINAL_STEPS: Record<ProgressPhaseKey, string[]> = {
|
|
134
|
+
source: ['source.ready', 'source.upload'],
|
|
135
|
+
ingestion: ['ingestion.wait'],
|
|
136
|
+
context: ['session.load'],
|
|
137
|
+
mint: ['mint.save'],
|
|
138
|
+
verify: ['verify.submit'],
|
|
139
|
+
attest: ['attestation.create'],
|
|
140
|
+
submit: ['submit.store'],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const STAGE_COMPLETED_PHASES: Record<string, ProgressPhaseKey[]> = {
|
|
144
|
+
PreparedForMint: ['source', 'ingestion', 'context'],
|
|
145
|
+
MintSubmitted: ['source', 'ingestion', 'context'],
|
|
146
|
+
MintSaved: ['source', 'ingestion', 'context', 'mint'],
|
|
147
|
+
VerificationSubmitted: ['source', 'ingestion', 'context', 'mint'],
|
|
148
|
+
Verified: ['source', 'ingestion', 'context', 'mint', 'verify'],
|
|
149
|
+
Attested: ['source', 'ingestion', 'context', 'mint', 'verify', 'attest'],
|
|
150
|
+
Submitted: [
|
|
151
|
+
'source',
|
|
152
|
+
'ingestion',
|
|
153
|
+
'context',
|
|
154
|
+
'mint',
|
|
155
|
+
'verify',
|
|
156
|
+
'attest',
|
|
157
|
+
'submit',
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const MAX_RECENT_EVENTS = 2;
|
|
162
|
+
const VERBOSE_PREFIX = 'Verbose';
|
|
163
|
+
|
|
164
|
+
export function createPublicationProgressReporter(input: {
|
|
165
|
+
title: string;
|
|
166
|
+
mode: PublicationProgressMode;
|
|
167
|
+
verbose?: boolean;
|
|
168
|
+
stream?: NodeJS.WriteStream;
|
|
169
|
+
}) {
|
|
170
|
+
return new PublicationProgressReporter(input);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
class PublicationProgressReporter {
|
|
174
|
+
readonly logger: PublicationWorkflowLogger;
|
|
175
|
+
|
|
176
|
+
private readonly title: string;
|
|
177
|
+
private readonly stream: NodeJS.WriteStream;
|
|
178
|
+
private readonly interactive: boolean;
|
|
179
|
+
private readonly verbose: boolean;
|
|
180
|
+
private readonly phaseStates: Record<ProgressPhaseKey, ProgressPhaseState>;
|
|
181
|
+
private readonly phaseProgress: Record<ProgressPhaseKey, number>;
|
|
182
|
+
private readonly verboseValues = new Map<string, string>();
|
|
183
|
+
|
|
184
|
+
private currentMessage: string;
|
|
185
|
+
private recentEvents: string[] = [];
|
|
186
|
+
private context: ProgressContext = {};
|
|
187
|
+
private spinnerIndex = 0;
|
|
188
|
+
private renderedLineCount = 0;
|
|
189
|
+
private intervalId?: ReturnType<typeof setInterval>;
|
|
190
|
+
private finalState?: 'complete' | 'failed';
|
|
191
|
+
|
|
192
|
+
constructor(input: {
|
|
193
|
+
title: string;
|
|
194
|
+
mode: PublicationProgressMode;
|
|
195
|
+
verbose?: boolean;
|
|
196
|
+
stream?: NodeJS.WriteStream;
|
|
197
|
+
}) {
|
|
198
|
+
this.title = input.title;
|
|
199
|
+
this.stream = input.stream ?? process.stdout;
|
|
200
|
+
this.interactive =
|
|
201
|
+
Boolean(this.stream.isTTY) && process.env.TERM !== 'dumb';
|
|
202
|
+
this.verbose = Boolean(input.verbose);
|
|
203
|
+
this.phaseStates = Object.fromEntries(
|
|
204
|
+
PHASES.map(({ key }) => [key, 'pending']),
|
|
205
|
+
) as Record<ProgressPhaseKey, ProgressPhaseState>;
|
|
206
|
+
this.phaseProgress = Object.fromEntries(
|
|
207
|
+
PHASES.map(({ key }) => [key, 0]),
|
|
208
|
+
) as Record<ProgressPhaseKey, number>;
|
|
209
|
+
this.currentMessage =
|
|
210
|
+
input.mode === 'resume'
|
|
211
|
+
? 'Loading existing publication state'
|
|
212
|
+
: 'Preparing publication workflow';
|
|
213
|
+
|
|
214
|
+
if (input.mode === 'resume') {
|
|
215
|
+
this.context.sourceKind = 'resume';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (input.mode === 'resume') {
|
|
219
|
+
this.phaseStates.source = 'complete';
|
|
220
|
+
this.phaseStates.ingestion = 'complete';
|
|
221
|
+
this.phaseProgress.source = 1;
|
|
222
|
+
this.phaseProgress.ingestion = 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.logger = {
|
|
226
|
+
debug: (message, metadata) => {
|
|
227
|
+
this.handleEvent('debug', message, metadata);
|
|
228
|
+
},
|
|
229
|
+
info: (message, metadata) => {
|
|
230
|
+
this.handleEvent('info', message, metadata);
|
|
231
|
+
},
|
|
232
|
+
warn: (message, metadata) => {
|
|
233
|
+
this.handleEvent('warn', message, metadata);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
start(input?: { message?: string; metadata?: ProgressMetadata }) {
|
|
239
|
+
if (input?.message) {
|
|
240
|
+
this.currentMessage = input.message;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (input?.metadata) {
|
|
244
|
+
this.updateContext(input.metadata);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.ensureActivePhase();
|
|
248
|
+
|
|
249
|
+
if (this.interactive) {
|
|
250
|
+
this.stream.write('\x1B[?25l');
|
|
251
|
+
this.render();
|
|
252
|
+
this.intervalId = setInterval(() => {
|
|
253
|
+
if (this.finalState) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
258
|
+
this.render();
|
|
259
|
+
}, 120);
|
|
260
|
+
this.intervalId.unref?.();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`${this.title}: ${this.currentMessage}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
complete(result: PublicationWorkflowResult) {
|
|
268
|
+
this.finalState = 'complete';
|
|
269
|
+
this.currentMessage = 'Publication workflow completed';
|
|
270
|
+
this.updateContext({
|
|
271
|
+
androidPackage: result.publicationBundle.release.androidPackage,
|
|
272
|
+
versionName: result.publicationBundle.release.versionName,
|
|
273
|
+
releaseId: result.releaseId,
|
|
274
|
+
publicationSessionId: result.publicationSessionId,
|
|
275
|
+
ingestionSessionId: result.ingestionSessionId,
|
|
276
|
+
mintAddress: result.releaseMintAddress,
|
|
277
|
+
transactionSignature:
|
|
278
|
+
result.collectionTransactionSignature ??
|
|
279
|
+
result.releaseTransactionSignature,
|
|
280
|
+
hubspotTicketId: result.hubspotTicketId,
|
|
281
|
+
requestUniqueId: result.attestationRequestUniqueId,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
for (const { key } of PHASES) {
|
|
285
|
+
this.phaseStates[key] = 'complete';
|
|
286
|
+
this.phaseProgress[key] = 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.pushRecentEvent('Publication workflow completed');
|
|
290
|
+
this.stopAndRenderFinalState();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fail(error: unknown) {
|
|
294
|
+
this.finalState = 'failed';
|
|
295
|
+
if (error instanceof Error && error.message.trim().length > 0) {
|
|
296
|
+
this.pushRecentEvent(`Failed: ${error.message.trim()}`);
|
|
297
|
+
}
|
|
298
|
+
this.stopAndRenderFinalState();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private handleEvent(
|
|
302
|
+
level: 'debug' | 'info' | 'warn',
|
|
303
|
+
message: string,
|
|
304
|
+
metadata?: ProgressMetadata,
|
|
305
|
+
) {
|
|
306
|
+
this.currentMessage = message.trim().length > 0 ? message : this.currentMessage;
|
|
307
|
+
|
|
308
|
+
if (metadata) {
|
|
309
|
+
this.updateContext(metadata);
|
|
310
|
+
this.updatePhaseState(metadata);
|
|
311
|
+
this.applyStageHint(metadata);
|
|
312
|
+
this.emitVerboseMetadata(metadata);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (level === 'warn') {
|
|
316
|
+
this.pushRecentEvent(`Warning: ${message}`);
|
|
317
|
+
} else if (
|
|
318
|
+
level === 'info' &&
|
|
319
|
+
this.readString(metadata, 'status') === 'complete'
|
|
320
|
+
) {
|
|
321
|
+
this.pushRecentEvent(message);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (this.interactive) {
|
|
325
|
+
this.render();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (level === 'debug') {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(this.buildLogLine(level, message, metadata));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private updatePhaseState(metadata: ProgressMetadata) {
|
|
337
|
+
const step = this.readString(metadata, 'step');
|
|
338
|
+
if (!step) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const phaseKey = STEP_TO_PHASE[step];
|
|
343
|
+
if (!phaseKey) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const phaseIndex = this.getPhaseIndex(phaseKey);
|
|
348
|
+
for (let index = 0; index < phaseIndex; index += 1) {
|
|
349
|
+
this.phaseStates[PHASES[index].key] = 'complete';
|
|
350
|
+
this.phaseProgress[PHASES[index].key] = 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (this.phaseStates[phaseKey] !== 'complete') {
|
|
354
|
+
this.phaseStates[phaseKey] = 'active';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const status = this.readString(metadata, 'status');
|
|
358
|
+
const resolvedProgress = this.resolvePhaseProgress(step, metadata, status);
|
|
359
|
+
if (resolvedProgress !== undefined) {
|
|
360
|
+
this.phaseProgress[phaseKey] = Math.max(
|
|
361
|
+
this.phaseProgress[phaseKey],
|
|
362
|
+
resolvedProgress,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (
|
|
367
|
+
status === 'complete' &&
|
|
368
|
+
PHASE_FINAL_STEPS[phaseKey].includes(step)
|
|
369
|
+
) {
|
|
370
|
+
this.phaseStates[phaseKey] = 'complete';
|
|
371
|
+
this.phaseProgress[phaseKey] = 1;
|
|
372
|
+
this.ensureActivePhase();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private resolvePhaseProgress(
|
|
377
|
+
step: string,
|
|
378
|
+
metadata: ProgressMetadata,
|
|
379
|
+
status: string | undefined,
|
|
380
|
+
): number | undefined {
|
|
381
|
+
const range = STEP_PROGRESS_RANGES[step];
|
|
382
|
+
const explicitProgress =
|
|
383
|
+
this.readProgress(metadata, 'stepProgress') ??
|
|
384
|
+
this.readByteProgress(metadata);
|
|
385
|
+
|
|
386
|
+
if (status === 'complete') {
|
|
387
|
+
return range?.end ?? 1;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!range) {
|
|
391
|
+
return explicitProgress;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (explicitProgress !== undefined) {
|
|
395
|
+
return this.interpolateProgress(range, explicitProgress);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (status === 'running') {
|
|
399
|
+
return this.interpolateProgress(range, DEFAULT_RUNNING_STEP_PROGRESS);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private interpolateProgress(
|
|
406
|
+
range: { start: number; end: number },
|
|
407
|
+
progress: number,
|
|
408
|
+
): number {
|
|
409
|
+
const clampedProgress = Math.max(0, Math.min(1, progress));
|
|
410
|
+
return range.start + (range.end - range.start) * clampedProgress;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private applyStageHint(metadata: ProgressMetadata) {
|
|
414
|
+
const stage = this.readString(metadata, 'stage');
|
|
415
|
+
if (!stage) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const completedPhases = STAGE_COMPLETED_PHASES[stage];
|
|
420
|
+
if (!completedPhases) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for (const phaseKey of completedPhases) {
|
|
425
|
+
this.phaseStates[phaseKey] = 'complete';
|
|
426
|
+
this.phaseProgress[phaseKey] = 1;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.ensureActivePhase();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private updateContext(metadata: ProgressMetadata) {
|
|
433
|
+
const sourceKind = this.readString(metadata, 'sourceKind');
|
|
434
|
+
if (
|
|
435
|
+
sourceKind === 'apk-file' ||
|
|
436
|
+
sourceKind === 'apk-url' ||
|
|
437
|
+
sourceKind === 'resume'
|
|
438
|
+
) {
|
|
439
|
+
this.context.sourceKind = sourceKind;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const fileName = this.readString(metadata, 'fileName');
|
|
443
|
+
if (fileName) {
|
|
444
|
+
this.context.fileName = fileName;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const apkUrl = this.readString(metadata, 'apkUrl');
|
|
448
|
+
if (apkUrl) {
|
|
449
|
+
this.context.apkUrl = apkUrl;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const sourceReleaseId = this.readString(metadata, 'sourceReleaseId');
|
|
453
|
+
if (sourceReleaseId) {
|
|
454
|
+
this.context.sourceReleaseId = sourceReleaseId;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const androidPackage = this.readString(metadata, 'androidPackage');
|
|
458
|
+
if (androidPackage) {
|
|
459
|
+
this.context.androidPackage = androidPackage;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const versionName = this.readString(metadata, 'versionName');
|
|
463
|
+
if (versionName) {
|
|
464
|
+
this.context.versionName = versionName;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const releaseId = this.readString(metadata, 'releaseId');
|
|
468
|
+
if (releaseId) {
|
|
469
|
+
this.context.releaseId = releaseId;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const publicationSessionId =
|
|
473
|
+
this.readString(metadata, 'publicationSessionId') ??
|
|
474
|
+
this.readString(metadata, 'sessionId');
|
|
475
|
+
if (publicationSessionId) {
|
|
476
|
+
this.context.publicationSessionId = publicationSessionId;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const ingestionSessionId = this.readString(metadata, 'ingestionSessionId');
|
|
480
|
+
if (ingestionSessionId) {
|
|
481
|
+
this.context.ingestionSessionId = ingestionSessionId;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const mintAddress = this.readString(metadata, 'mintAddress');
|
|
485
|
+
if (mintAddress) {
|
|
486
|
+
this.context.mintAddress = mintAddress;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const transactionSignature = this.readString(
|
|
490
|
+
metadata,
|
|
491
|
+
'transactionSignature',
|
|
492
|
+
);
|
|
493
|
+
if (transactionSignature) {
|
|
494
|
+
this.context.transactionSignature = transactionSignature;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const hubspotTicketId = this.readString(metadata, 'hubspotTicketId');
|
|
498
|
+
if (hubspotTicketId) {
|
|
499
|
+
this.context.hubspotTicketId = hubspotTicketId;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const requestUniqueId = this.readString(metadata, 'requestUniqueId');
|
|
503
|
+
if (requestUniqueId) {
|
|
504
|
+
this.context.requestUniqueId = requestUniqueId;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const fileSize = this.readNumber(metadata, 'fileSize');
|
|
508
|
+
if (fileSize !== undefined) {
|
|
509
|
+
this.context.fileSize = fileSize;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const bytesUploaded = this.readNumber(metadata, 'bytesUploaded');
|
|
513
|
+
if (bytesUploaded !== undefined) {
|
|
514
|
+
this.context.bytesUploaded = bytesUploaded;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const bytesTotal =
|
|
518
|
+
this.readNumber(metadata, 'bytesTotal') ??
|
|
519
|
+
this.readNumber(metadata, 'fileSize');
|
|
520
|
+
if (bytesTotal !== undefined) {
|
|
521
|
+
this.context.bytesTotal = bytesTotal;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const ingestionStatus = this.readString(metadata, 'ingestionStatus');
|
|
525
|
+
if (ingestionStatus) {
|
|
526
|
+
this.context.ingestionStatus = ingestionStatus;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const ingestionProgress = this.readNumber(metadata, 'ingestionProgress');
|
|
530
|
+
if (ingestionProgress !== undefined) {
|
|
531
|
+
this.context.ingestionProgress = Math.max(
|
|
532
|
+
0,
|
|
533
|
+
Math.min(100, ingestionProgress),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const ingestionStage = this.readString(metadata, 'ingestionStage');
|
|
538
|
+
if (ingestionStage) {
|
|
539
|
+
this.context.ingestionStage = ingestionStage;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const ingestionDetail = this.readString(metadata, 'ingestionDetail');
|
|
543
|
+
if (ingestionDetail) {
|
|
544
|
+
this.context.ingestionDetail = ingestionDetail;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const activeStep = this.readString(metadata, 'step');
|
|
548
|
+
if (activeStep) {
|
|
549
|
+
this.context.activeStep = activeStep;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private stopAndRenderFinalState() {
|
|
554
|
+
if (this.intervalId) {
|
|
555
|
+
clearInterval(this.intervalId);
|
|
556
|
+
this.intervalId = undefined;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (this.interactive) {
|
|
560
|
+
this.render();
|
|
561
|
+
this.stream.write('\n');
|
|
562
|
+
this.stream.write('\x1B[?25h');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const statusLabel =
|
|
567
|
+
this.finalState === 'complete' ? 'completed' : 'failed';
|
|
568
|
+
console.log(`${this.title}: ${statusLabel}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private ensureActivePhase() {
|
|
572
|
+
if (this.finalState) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const activePhase = PHASES.find(
|
|
577
|
+
({ key }) => this.phaseStates[key] === 'active',
|
|
578
|
+
);
|
|
579
|
+
if (activePhase) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const nextPendingPhase = PHASES.find(
|
|
584
|
+
({ key }) => this.phaseStates[key] === 'pending',
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (nextPendingPhase) {
|
|
588
|
+
this.phaseStates[nextPendingPhase.key] = 'active';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private render() {
|
|
593
|
+
const lines = this.buildLines();
|
|
594
|
+
|
|
595
|
+
if (this.renderedLineCount > 0) {
|
|
596
|
+
readline.moveCursor(this.stream, 0, -(this.renderedLineCount - 1));
|
|
597
|
+
readline.cursorTo(this.stream, 0);
|
|
598
|
+
readline.clearScreenDown(this.stream);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.stream.write(lines.join('\n'));
|
|
602
|
+
this.renderedLineCount = lines.length;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private buildLines(): string[] {
|
|
606
|
+
const completedCount = PHASES.filter(
|
|
607
|
+
({ key }) => this.phaseStates[key] === 'complete',
|
|
608
|
+
).length;
|
|
609
|
+
const activePhase = this.getActivePhaseKey();
|
|
610
|
+
const phaseLabel =
|
|
611
|
+
activePhase && this.finalState !== 'complete'
|
|
612
|
+
? PHASES[this.getPhaseIndex(activePhase)].label
|
|
613
|
+
: 'Complete';
|
|
614
|
+
const percent = this.getProgressPercent();
|
|
615
|
+
const bar = this.buildProgressBar(percent);
|
|
616
|
+
const statusToken = this.getStatusToken();
|
|
617
|
+
const lines = [
|
|
618
|
+
this.fitToWidth(
|
|
619
|
+
`${this.title} [${statusToken}] ${completedCount}/${PHASES.length} complete | ${phaseLabel}`,
|
|
620
|
+
),
|
|
621
|
+
this.fitToWidth(`${bar} ${Math.round(percent * 100)}%`),
|
|
622
|
+
this.fitToWidth(`Working: ${this.currentMessage}`),
|
|
623
|
+
...this.buildDetailLines(),
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
return lines;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private buildDetailLines(): string[] {
|
|
630
|
+
const targetTokens = [
|
|
631
|
+
this.context.androidPackage
|
|
632
|
+
? `app ${this.truncateMiddle(this.context.androidPackage, 42)}`
|
|
633
|
+
: null,
|
|
634
|
+
this.context.versionName
|
|
635
|
+
? `version ${this.truncateMiddle(this.context.versionName, 24)}`
|
|
636
|
+
: null,
|
|
637
|
+
this.context.fileName
|
|
638
|
+
? `file ${this.truncateMiddle(this.context.fileName, 26)}`
|
|
639
|
+
: null,
|
|
640
|
+
this.context.apkUrl
|
|
641
|
+
? `url ${this.truncateMiddle(this.compactUrl(this.context.apkUrl), 32)}`
|
|
642
|
+
: null,
|
|
643
|
+
this.context.sourceReleaseId
|
|
644
|
+
? `source ${this.compactIdentifier(this.context.sourceReleaseId)}`
|
|
645
|
+
: null,
|
|
646
|
+
].filter((token): token is string => token !== null);
|
|
647
|
+
|
|
648
|
+
const lines: string[] = [];
|
|
649
|
+
if (targetTokens.length > 0) {
|
|
650
|
+
lines.push(this.fitToWidth(`Target: ${targetTokens.join(' | ')}`));
|
|
651
|
+
}
|
|
652
|
+
const uploadLine = this.buildUploadLine();
|
|
653
|
+
if (uploadLine) {
|
|
654
|
+
lines.push(this.fitToWidth(uploadLine));
|
|
655
|
+
}
|
|
656
|
+
const ingestionLine = this.buildIngestionLine();
|
|
657
|
+
if (ingestionLine) {
|
|
658
|
+
lines.push(this.fitToWidth(ingestionLine));
|
|
659
|
+
}
|
|
660
|
+
if (this.recentEvents.length > 0) {
|
|
661
|
+
lines.push(
|
|
662
|
+
this.fitToWidth(
|
|
663
|
+
`Recent: ${this.recentEvents[this.recentEvents.length - 1]}`,
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return lines;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private buildProgressBar(percent: number): string {
|
|
672
|
+
const columns = this.stream.columns ?? 100;
|
|
673
|
+
const barWidth = Math.min(32, Math.max(10, columns - 44));
|
|
674
|
+
const filled = Math.max(0, Math.min(barWidth, Math.round(percent * barWidth)));
|
|
675
|
+
const empty = Math.max(0, barWidth - filled);
|
|
676
|
+
return `[${'#'.repeat(filled)}${'-'.repeat(empty)}]`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private buildUploadLine(): string | null {
|
|
680
|
+
if (
|
|
681
|
+
this.context.bytesTotal === undefined ||
|
|
682
|
+
this.context.bytesUploaded === undefined
|
|
683
|
+
) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const activeStep = this.context.activeStep;
|
|
688
|
+
if (activeStep !== 'source.upload' && this.finalState !== 'complete') {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const ratio =
|
|
693
|
+
this.context.bytesTotal > 0
|
|
694
|
+
? this.context.bytesUploaded / this.context.bytesTotal
|
|
695
|
+
: 1;
|
|
696
|
+
|
|
697
|
+
return `Upload: ${this.formatBytes(this.context.bytesUploaded)} / ${this.formatBytes(this.context.bytesTotal)} (${Math.round(
|
|
698
|
+
Math.max(0, Math.min(1, ratio)) * 100,
|
|
699
|
+
)}%)`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private buildIngestionLine(): string | null {
|
|
703
|
+
if (
|
|
704
|
+
!this.context.ingestionStatus &&
|
|
705
|
+
!this.context.ingestionStage &&
|
|
706
|
+
this.context.ingestionProgress === undefined
|
|
707
|
+
) {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const activePhase = this.getActivePhaseKey();
|
|
712
|
+
if (activePhase !== 'ingestion' && this.finalState !== 'complete') {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const detail =
|
|
717
|
+
this.context.ingestionDetail ??
|
|
718
|
+
this.context.ingestionStage ??
|
|
719
|
+
this.context.ingestionStatus;
|
|
720
|
+
|
|
721
|
+
const progress =
|
|
722
|
+
this.context.ingestionProgress === undefined
|
|
723
|
+
? null
|
|
724
|
+
: `${Math.round(this.context.ingestionProgress)}%`;
|
|
725
|
+
|
|
726
|
+
if (detail && progress) {
|
|
727
|
+
return `Ingestion: ${detail} (${progress})`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (detail) {
|
|
731
|
+
return `Ingestion: ${detail}`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (progress) {
|
|
735
|
+
return `Ingestion: ${progress}`;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private getProgressPercent(): number {
|
|
742
|
+
if (this.finalState === 'complete') {
|
|
743
|
+
return 1;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const weights = this.getPhaseWeights();
|
|
747
|
+
const progress = PHASES.reduce((total, { key }) => {
|
|
748
|
+
const phaseProgress = Math.max(0, Math.min(1, this.phaseProgress[key]));
|
|
749
|
+
return total + phaseProgress * weights[key];
|
|
750
|
+
}, 0);
|
|
751
|
+
|
|
752
|
+
return Math.max(0, Math.min(0.99, progress));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private getStatusToken(): string {
|
|
756
|
+
if (this.finalState === 'complete') {
|
|
757
|
+
return 'done';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (this.finalState === 'failed') {
|
|
761
|
+
return 'fail';
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return SPINNER_FRAMES[this.spinnerIndex];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private getActivePhaseKey(): ProgressPhaseKey | undefined {
|
|
768
|
+
const activePhase = PHASES.find(
|
|
769
|
+
({ key }) => this.phaseStates[key] === 'active',
|
|
770
|
+
);
|
|
771
|
+
if (activePhase) {
|
|
772
|
+
return activePhase.key;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const pendingPhase = PHASES.find(
|
|
776
|
+
({ key }) => this.phaseStates[key] === 'pending',
|
|
777
|
+
);
|
|
778
|
+
return pendingPhase?.key;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private getPhaseIndex(phaseKey: ProgressPhaseKey): number {
|
|
782
|
+
return PHASES.findIndex(({ key }) => key === phaseKey);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private emitVerboseMetadata(metadata: ProgressMetadata) {
|
|
786
|
+
if (!this.verbose) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const lines = this.buildVerboseMetadataLines(metadata);
|
|
791
|
+
if (lines.length === 0) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (this.interactive && this.renderedLineCount > 0) {
|
|
796
|
+
readline.moveCursor(this.stream, 0, -(this.renderedLineCount - 1));
|
|
797
|
+
readline.cursorTo(this.stream, 0);
|
|
798
|
+
readline.clearScreenDown(this.stream);
|
|
799
|
+
this.renderedLineCount = 0;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (const line of lines) {
|
|
803
|
+
this.stream.write(`${line}\n`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private buildVerboseMetadataLines(metadata: ProgressMetadata): string[] {
|
|
808
|
+
const lines: string[] = [];
|
|
809
|
+
const step = this.readString(metadata, 'step');
|
|
810
|
+
|
|
811
|
+
this.appendVerboseField(
|
|
812
|
+
lines,
|
|
813
|
+
'releaseId',
|
|
814
|
+
'Release ID',
|
|
815
|
+
this.readString(metadata, 'releaseId'),
|
|
816
|
+
);
|
|
817
|
+
this.appendVerboseField(
|
|
818
|
+
lines,
|
|
819
|
+
'publicationSessionId',
|
|
820
|
+
'Publication session ID',
|
|
821
|
+
this.readString(metadata, 'publicationSessionId') ??
|
|
822
|
+
this.readString(metadata, 'sessionId'),
|
|
823
|
+
);
|
|
824
|
+
this.appendVerboseField(
|
|
825
|
+
lines,
|
|
826
|
+
'ingestionSessionId',
|
|
827
|
+
'Ingestion session ID',
|
|
828
|
+
this.readString(metadata, 'ingestionSessionId'),
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const transactionSignature = this.readString(metadata, 'transactionSignature');
|
|
832
|
+
if (transactionSignature) {
|
|
833
|
+
const transactionKey =
|
|
834
|
+
step === 'verify.submit'
|
|
835
|
+
? 'collectionTransactionSignature'
|
|
836
|
+
: 'releaseTransactionSignature';
|
|
837
|
+
const transactionLabel =
|
|
838
|
+
step === 'verify.submit'
|
|
839
|
+
? 'Collection transaction signature'
|
|
840
|
+
: 'Release transaction signature';
|
|
841
|
+
|
|
842
|
+
this.appendVerboseField(
|
|
843
|
+
lines,
|
|
844
|
+
transactionKey,
|
|
845
|
+
transactionLabel,
|
|
846
|
+
transactionSignature,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
this.appendVerboseField(
|
|
851
|
+
lines,
|
|
852
|
+
'attestationRequestUniqueId',
|
|
853
|
+
'Attestation request ID',
|
|
854
|
+
this.readString(metadata, 'requestUniqueId'),
|
|
855
|
+
);
|
|
856
|
+
this.appendVerboseField(
|
|
857
|
+
lines,
|
|
858
|
+
'ticketId',
|
|
859
|
+
'Ticket ID',
|
|
860
|
+
this.readString(metadata, 'hubspotTicketId'),
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
return lines;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private appendVerboseField(
|
|
867
|
+
lines: string[],
|
|
868
|
+
key: string,
|
|
869
|
+
label: string,
|
|
870
|
+
value: string | undefined,
|
|
871
|
+
) {
|
|
872
|
+
if (!value) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const previousValue = this.verboseValues.get(key);
|
|
877
|
+
if (previousValue === value) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
this.verboseValues.set(key, value);
|
|
882
|
+
lines.push(`${VERBOSE_PREFIX}: ${label}: ${value}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private pushRecentEvent(message: string) {
|
|
886
|
+
this.recentEvents.push(this.truncateMiddle(message.trim(), 96));
|
|
887
|
+
if (this.recentEvents.length > MAX_RECENT_EVENTS) {
|
|
888
|
+
this.recentEvents = this.recentEvents.slice(-MAX_RECENT_EVENTS);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private buildLogLine(
|
|
893
|
+
level: 'info' | 'warn',
|
|
894
|
+
message: string,
|
|
895
|
+
metadata?: ProgressMetadata,
|
|
896
|
+
): string {
|
|
897
|
+
const step = this.readString(metadata, 'step');
|
|
898
|
+
const phaseKey = step ? STEP_TO_PHASE[step] : this.getActivePhaseKey();
|
|
899
|
+
const phaseIndex = phaseKey ? this.getPhaseIndex(phaseKey) + 1 : 0;
|
|
900
|
+
const phaseLabel = phaseKey
|
|
901
|
+
? PHASES[this.getPhaseIndex(phaseKey)].label
|
|
902
|
+
: 'Publication workflow';
|
|
903
|
+
const prefix =
|
|
904
|
+
level === 'warn'
|
|
905
|
+
? 'warning'
|
|
906
|
+
: phaseIndex > 0
|
|
907
|
+
? `${phaseIndex}/${PHASES.length}`
|
|
908
|
+
: 'info';
|
|
909
|
+
|
|
910
|
+
return `${prefix} ${phaseLabel}: ${message}`;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private compactIdentifier(value: string): string {
|
|
914
|
+
return this.truncateMiddle(value, 18);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private getPhaseWeights(): Record<ProgressPhaseKey, number> {
|
|
918
|
+
const profile =
|
|
919
|
+
this.context.sourceKind ??
|
|
920
|
+
(this.context.apkUrl ? 'apk-url' : 'apk-file');
|
|
921
|
+
return PHASE_WEIGHT_PROFILES[profile];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private compactUrl(value: string): string {
|
|
925
|
+
try {
|
|
926
|
+
const url = new URL(value);
|
|
927
|
+
const path = url.pathname === '/' ? '' : url.pathname;
|
|
928
|
+
return `${url.host}${path}`;
|
|
929
|
+
} catch {
|
|
930
|
+
return value;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private fitToWidth(value: string): string {
|
|
935
|
+
const width = this.stream.columns ?? 100;
|
|
936
|
+
if (width <= 3) {
|
|
937
|
+
return value.slice(0, Math.max(0, width));
|
|
938
|
+
}
|
|
939
|
+
return value.length <= width ? value : `${value.slice(0, width - 3)}...`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private truncateMiddle(value: string, maxLength: number): string {
|
|
943
|
+
if (value.length <= maxLength) {
|
|
944
|
+
return value;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (maxLength <= 3) {
|
|
948
|
+
return value.slice(0, maxLength);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const startLength = Math.ceil((maxLength - 3) / 2);
|
|
952
|
+
const endLength = Math.floor((maxLength - 3) / 2);
|
|
953
|
+
return `${value.slice(0, startLength)}...${value.slice(-endLength)}`;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private readString(
|
|
957
|
+
metadata: ProgressMetadata | undefined,
|
|
958
|
+
key: string,
|
|
959
|
+
): string | undefined {
|
|
960
|
+
const value = metadata?.[key];
|
|
961
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
962
|
+
? value.trim()
|
|
963
|
+
: undefined;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private readNumber(
|
|
967
|
+
metadata: ProgressMetadata | undefined,
|
|
968
|
+
key: string,
|
|
969
|
+
): number | undefined {
|
|
970
|
+
const value = metadata?.[key];
|
|
971
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
private readProgress(
|
|
975
|
+
metadata: ProgressMetadata | undefined,
|
|
976
|
+
key: string,
|
|
977
|
+
): number | undefined {
|
|
978
|
+
const value = this.readNumber(metadata, key);
|
|
979
|
+
if (value === undefined) {
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (value > 1) {
|
|
984
|
+
return Math.max(0, Math.min(1, value / 100));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return Math.max(0, Math.min(1, value));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private readByteProgress(
|
|
991
|
+
metadata: ProgressMetadata | undefined,
|
|
992
|
+
): number | undefined {
|
|
993
|
+
const bytesUploaded = this.readNumber(metadata, 'bytesUploaded');
|
|
994
|
+
const bytesTotal =
|
|
995
|
+
this.readNumber(metadata, 'bytesTotal') ??
|
|
996
|
+
this.readNumber(metadata, 'fileSize');
|
|
997
|
+
|
|
998
|
+
if (
|
|
999
|
+
bytesUploaded === undefined ||
|
|
1000
|
+
bytesTotal === undefined ||
|
|
1001
|
+
bytesTotal <= 0
|
|
1002
|
+
) {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return Math.max(0, Math.min(1, bytesUploaded / bytesTotal));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private formatBytes(value: number): string {
|
|
1010
|
+
if (value < 1024) {
|
|
1011
|
+
return `${Math.round(value)} B`;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
1015
|
+
let size = value;
|
|
1016
|
+
let unitIndex = -1;
|
|
1017
|
+
|
|
1018
|
+
do {
|
|
1019
|
+
size /= 1024;
|
|
1020
|
+
unitIndex += 1;
|
|
1021
|
+
} while (size >= 1024 && unitIndex < units.length - 1);
|
|
1022
|
+
|
|
1023
|
+
const digits = size >= 100 ? 0 : size >= 10 ? 1 : 2;
|
|
1024
|
+
return `${size.toFixed(digits)} ${units[unitIndex]}`;
|
|
1025
|
+
}
|
|
1026
|
+
}
|