@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
package/src/CliSetup.ts
CHANGED
|
@@ -1,544 +1,409 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
publishSupportCommand,
|
|
8
|
-
publishUpdateCommand
|
|
9
|
-
} from "./commands/publish/index.js";
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import * as dotenv from 'dotenv';
|
|
5
|
+
import { Command, Option } from 'commander';
|
|
6
|
+
|
|
10
7
|
import {
|
|
11
8
|
checkForSelfUpdate,
|
|
12
|
-
checkSubmissionNetwork,
|
|
13
9
|
Constants,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
createPortalAttestationClient,
|
|
11
|
+
createPortalWorkflowClient,
|
|
12
|
+
createPublicationSignerFromKeypair,
|
|
17
13
|
parseKeypair,
|
|
18
14
|
showMessage,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
} from './CliUtils.js';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_API_KEY_ENV,
|
|
18
|
+
DEFAULT_PRODUCTION_PORTAL_URL,
|
|
19
|
+
resolveApiKey,
|
|
20
|
+
resolvePortalTargets,
|
|
21
|
+
validateNewVersionArgs,
|
|
22
|
+
validateResumeArgs,
|
|
23
|
+
type NewVersionCliOptions,
|
|
24
|
+
type ResumeCliOptions,
|
|
25
|
+
type ResolvedPortalTargets,
|
|
26
|
+
} from './publication/cliValidation.js';
|
|
27
|
+
import { createPublicationProgressReporter } from './publication/PublicationProgressReporter.js';
|
|
28
|
+
import { extractPublicationSummaryLines } from './publication/publicationSummary.js';
|
|
29
|
+
import { runPublicationWorkflow } from './publication/runPublicationWorkflow.js';
|
|
30
|
+
import { ensurePublicationSignerBalance } from './publication/fundingPreflight.js';
|
|
31
|
+
import type {
|
|
32
|
+
PublicationResumeInput,
|
|
33
|
+
PublicationWorkflowInput,
|
|
34
|
+
} from './publication/runPublicationWorkflow.js';
|
|
35
|
+
import type { Keypair } from '@solana/web3.js';
|
|
24
36
|
|
|
25
37
|
dotenv.config();
|
|
26
38
|
|
|
27
|
-
const hasAddressInConfig = ({ address }: { address: string }) => {
|
|
28
|
-
return !!address;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
39
|
export const mainCli = new Command();
|
|
32
40
|
|
|
33
|
-
function resolveBuildToolsPath(buildToolsPath: string | undefined) {
|
|
34
|
-
// If a path was specified on the command line, use that
|
|
35
|
-
if (buildToolsPath !== undefined) {
|
|
36
|
-
return buildToolsPath;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// If a path is specified in a .env file, use that
|
|
40
|
-
if (process.env.ANDROID_TOOLS_DIR !== undefined) {
|
|
41
|
-
return process.env.ANDROID_TOOLS_DIR;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// No path was specified
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* This method should be updated with each new release of the CLI, and just do nothing when there isn't anything to report
|
|
50
|
-
*/
|
|
51
|
-
function latestReleaseMessage() {
|
|
52
|
-
const messages = [
|
|
53
|
-
`- Banner Graphic image of size 1200x600px is now manadatory for publishing updates.`,
|
|
54
|
-
`- Feature Graphic image of size 1200x1200px is required to be featured in Editor's choice carousel. (optional)`,
|
|
55
|
-
`- Release metadata now publishes publisher.support_email when provided; otherwise we reuse publisher.email for end-user support.`,
|
|
56
|
-
].join('\n\n')
|
|
57
|
-
showMessage(
|
|
58
|
-
`Publishing Tools Version ${ Constants.CLI_VERSION }`,
|
|
59
|
-
messages,
|
|
60
|
-
"warning"
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function tryWithErrorMessage(block: () => Promise<any>) {
|
|
65
|
-
try {
|
|
66
|
-
await block()
|
|
67
|
-
} catch (e) {
|
|
68
|
-
const errorMsg = (e as Error | null)?.message ?? "";
|
|
69
|
-
|
|
70
|
-
showMessage("Error", errorMsg, "error");
|
|
71
|
-
process.exit(-1)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
41
|
mainCli
|
|
76
|
-
.name(
|
|
42
|
+
.name('dapp-store')
|
|
77
43
|
.version(Constants.CLI_VERSION)
|
|
78
|
-
.description(
|
|
44
|
+
.description('Portal-backed CLI for Solana Mobile dApp version publishing')
|
|
45
|
+
.showHelpAfterError();
|
|
79
46
|
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
export const createCliCmd = mainCli
|
|
92
|
-
.command("create")
|
|
93
|
-
.description("Create a `app`, or `release`")
|
|
94
|
-
|
|
95
|
-
createCliCmd.addHelpText(
|
|
96
|
-
"after",
|
|
97
|
-
[
|
|
98
|
-
"",
|
|
99
|
-
"Release metadata notes:",
|
|
100
|
-
" We include publisher.support_email when provided; if omitted we fall back to publisher.email.",
|
|
101
|
-
].join("\n")
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
export const createAppCliCmd = createCliCmd
|
|
105
|
-
.command("app")
|
|
106
|
-
.description("Create a app")
|
|
107
|
-
.requiredOption(
|
|
108
|
-
"-k, --keypair <path-to-keypair-file>",
|
|
109
|
-
"Path to keypair file"
|
|
110
|
-
)
|
|
111
|
-
.option("-u, --url <url>", "RPC URL", Constants.DEFAULT_RPC_DEVNET)
|
|
112
|
-
.option("-d, --dry-run", "Flag for dry run. Doesn't mint an NFT")
|
|
113
|
-
.option("-s, --storage-config <storage-config>", "Provide alternative storage configuration details")
|
|
114
|
-
.option("-p, --priority-fee-lamports <priority-fee-lamports>", "Priority Fee lamports")
|
|
115
|
-
.action(async ({keypair, url, dryRun, storageConfig, priorityFeeLamports }) => {
|
|
116
|
-
await tryWithErrorMessage(async () => {
|
|
117
|
-
showNetworkWarningIfApplicable(url)
|
|
118
|
-
latestReleaseMessage();
|
|
119
|
-
await checkForSelfUpdate();
|
|
120
|
-
|
|
121
|
-
const signer = parseKeypair(keypair);
|
|
122
|
-
if (signer) {
|
|
123
|
-
const result = await createAppCommand({
|
|
124
|
-
signer,
|
|
125
|
-
url,
|
|
126
|
-
dryRun,
|
|
127
|
-
storageParams: storageConfig,
|
|
128
|
-
priorityFeeLamports: priorityFeeLamports,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (dryRun) {
|
|
132
|
-
dryRunSuccessMessage()
|
|
133
|
-
} else {
|
|
134
|
-
const displayUrl = `https://explorer.solana.com/address/${result.appAddress}${generateNetworkSuffix(url)}`;
|
|
135
|
-
const transactionUrl = `https://explorer.solana.com/tx/${result.transactionSignature}${generateNetworkSuffix(url)}`;
|
|
136
|
-
const resultText = `App NFT successfully minted:\n${displayUrl}\n${transactionUrl}`;
|
|
137
|
-
showMessage("Success", resultText);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
export const createReleaseCliCmd = createCliCmd
|
|
144
|
-
.command("release")
|
|
145
|
-
.description("Create a release")
|
|
146
|
-
.requiredOption(
|
|
147
|
-
"-k, --keypair <path-to-keypair-file>",
|
|
148
|
-
"Path to keypair file"
|
|
47
|
+
mainCli
|
|
48
|
+
.option('--apk-file <path>', 'Path to the APK file to publish')
|
|
49
|
+
.option('--apk-url <url>', 'HTTPS URL for an externally hosted APK')
|
|
50
|
+
.option('--whats-new <text>', 'What changed in this version')
|
|
51
|
+
.option('--portal-url <url>', 'Publishing portal base URL')
|
|
52
|
+
.option(
|
|
53
|
+
'--api-key-env <name>',
|
|
54
|
+
'Environment variable that contains the portal API key',
|
|
55
|
+
DEFAULT_API_KEY_ENV,
|
|
149
56
|
)
|
|
150
57
|
.option(
|
|
151
|
-
|
|
152
|
-
|
|
58
|
+
'--api-key-stdin',
|
|
59
|
+
'Read the portal API key from stdin instead of an env var',
|
|
153
60
|
)
|
|
154
|
-
.option(
|
|
155
|
-
.
|
|
61
|
+
.option('--keypair <path>', 'Path to the Solana signer keypair')
|
|
62
|
+
.addOption(new Option('--rpc-url <url>', 'Solana RPC URL').hideHelp())
|
|
63
|
+
.option('--local-dev', 'Allow localhost portal endpoints and skip gating')
|
|
156
64
|
.option(
|
|
157
|
-
|
|
158
|
-
|
|
65
|
+
'--skip-self-update',
|
|
66
|
+
'Bypass the self-update check when working against a local portal',
|
|
159
67
|
)
|
|
160
|
-
.option(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
await tryWithErrorMessage(async () => {
|
|
164
|
-
showNetworkWarningIfApplicable(url)
|
|
165
|
-
latestReleaseMessage();
|
|
166
|
-
await checkForSelfUpdate();
|
|
167
|
-
|
|
168
|
-
const resolvedBuildToolsPath = resolveBuildToolsPath(buildToolsPath);
|
|
169
|
-
if (resolvedBuildToolsPath === undefined) {
|
|
170
|
-
throw new Error("Please specify an Android build tools directory in the .env file or via the command line argument.")
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const config = await loadPublishDetailsWithChecks();
|
|
174
|
-
if (!hasAddressInConfig(config.app) && !appMintAddress) {
|
|
175
|
-
throw new Error("Either specify an app mint address in the config file or specify as a CLI argument to this command")
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const signer = parseKeypair(keypair);
|
|
179
|
-
if (signer) {
|
|
180
|
-
const result = await createReleaseCommand({
|
|
181
|
-
appMintAddress: appMintAddress,
|
|
182
|
-
buildToolsPath: resolvedBuildToolsPath,
|
|
183
|
-
signer,
|
|
184
|
-
url,
|
|
185
|
-
dryRun,
|
|
186
|
-
storageParams: storageConfig,
|
|
187
|
-
priorityFeeLamports: priorityFeeLamports
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
if (dryRun) {
|
|
191
|
-
dryRunSuccessMessage()
|
|
192
|
-
} else {
|
|
193
|
-
const displayUrl = `https://explorer.solana.com/address/${result?.releaseAddress}${generateNetworkSuffix(url)}`;
|
|
194
|
-
const transactionUrl = `https://explorer.solana.com/tx/${result.transactionSignature}${generateNetworkSuffix(url)}`;
|
|
195
|
-
const resultText = `Release NFT successfully minted:\n${displayUrl}\n${transactionUrl}`;
|
|
196
|
-
|
|
197
|
-
showMessage("Success", resultText);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
mainCli
|
|
205
|
-
.command("validate")
|
|
206
|
-
.description("Validates details prior to publishing")
|
|
207
|
-
.requiredOption(
|
|
208
|
-
"-k, --keypair <path-to-keypair-file>",
|
|
209
|
-
"Path to keypair file"
|
|
68
|
+
.option(
|
|
69
|
+
'--idempotency-key <key>',
|
|
70
|
+
'Optional idempotency key for safe retries',
|
|
210
71
|
)
|
|
211
72
|
.option(
|
|
212
|
-
|
|
213
|
-
|
|
73
|
+
'--verbose',
|
|
74
|
+
'Print detailed publication identifiers as they are emitted',
|
|
214
75
|
)
|
|
215
|
-
.action(async (
|
|
216
|
-
await
|
|
217
|
-
latestReleaseMessage();
|
|
218
|
-
await checkForSelfUpdate();
|
|
219
|
-
|
|
220
|
-
const resolvedBuildToolsPath = resolveBuildToolsPath(buildToolsPath);
|
|
221
|
-
if (resolvedBuildToolsPath === undefined) {
|
|
222
|
-
throw new Error("Please specify an Android build tools directory in the .env file or via the command line argument.")
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const signer = parseKeypair(keypair);
|
|
226
|
-
if (signer) {
|
|
227
|
-
await validateCommand({
|
|
228
|
-
signer,
|
|
229
|
-
buildToolsPath: resolvedBuildToolsPath,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
});
|
|
76
|
+
.action(async () => {
|
|
77
|
+
await runRootAction();
|
|
233
78
|
});
|
|
234
79
|
|
|
235
|
-
const
|
|
236
|
-
.command("publish")
|
|
237
|
-
.description(
|
|
238
|
-
"Submit a publishing request (`submit`, `update`, `remove`, or `support`) to the Solana Mobile dApp publisher portal"
|
|
239
|
-
);
|
|
80
|
+
const resumeCommand = mainCli.command('resume');
|
|
240
81
|
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
.
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.requiredOption(
|
|
253
|
-
"--requestor-is-authorized",
|
|
254
|
-
"An attestation that the party making this Solana dApp publisher portal request is authorized to do so"
|
|
82
|
+
resumeCommand
|
|
83
|
+
.description('Resume a partially completed publication session')
|
|
84
|
+
.option('--release-id <id>', 'Publication release identifier')
|
|
85
|
+
.option('--resume-release <id>', 'Alias for --release-id')
|
|
86
|
+
.option('--session-id <id>', 'Publication session identifier')
|
|
87
|
+
.option('--resume-session <id>', 'Alias for --session-id')
|
|
88
|
+
.option('--portal-url <url>', 'Publishing portal base URL')
|
|
89
|
+
.option(
|
|
90
|
+
'--api-key-env <name>',
|
|
91
|
+
'Environment variable that contains the portal API key',
|
|
92
|
+
DEFAULT_API_KEY_ENV,
|
|
255
93
|
)
|
|
256
94
|
.option(
|
|
257
|
-
|
|
258
|
-
|
|
95
|
+
'--api-key-stdin',
|
|
96
|
+
'Read the portal API key from stdin instead of an env var',
|
|
259
97
|
)
|
|
98
|
+
.option('--keypair <path>', 'Path to the Solana signer keypair')
|
|
99
|
+
.addOption(new Option('--rpc-url <url>', 'Solana RPC URL').hideHelp())
|
|
100
|
+
.option('--local-dev', 'Allow localhost portal endpoints and skip gating')
|
|
260
101
|
.option(
|
|
261
|
-
|
|
262
|
-
|
|
102
|
+
'--skip-self-update',
|
|
103
|
+
'Bypass the self-update check when working against a local portal',
|
|
263
104
|
)
|
|
264
|
-
.option("-u, --url <url>", "RPC URL", Constants.DEFAULT_RPC_DEVNET)
|
|
265
105
|
.option(
|
|
266
|
-
|
|
267
|
-
|
|
106
|
+
'--verbose',
|
|
107
|
+
'Print detailed publication identifiers as they are emitted',
|
|
268
108
|
)
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
critical: false,
|
|
307
|
-
alphaTest: alpha,
|
|
308
|
-
});
|
|
309
|
-
} else {
|
|
310
|
-
await publishSubmitCommand({
|
|
311
|
-
appMintAddress: appMintAddress,
|
|
312
|
-
releaseMintAddress: releaseMintAddress,
|
|
313
|
-
signer: signer,
|
|
314
|
-
url: url,
|
|
315
|
-
dryRun: dryRun,
|
|
316
|
-
compliesWithSolanaDappStorePolicies: compliesWithSolanaDappStorePolicies,
|
|
317
|
-
requestorIsAuthorized: requestorIsAuthorized,
|
|
318
|
-
alphaTest: alpha,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (dryRun) {
|
|
323
|
-
dryRunSuccessMessage()
|
|
324
|
-
} else {
|
|
325
|
-
showMessage("Success", "Successfully submitted to the Solana Mobile dApp publisher portal");
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
});
|
|
109
|
+
.action(async (options: ResumeCliOptions) => {
|
|
110
|
+
await runResumeAction(options);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
mainCli.addHelpText(
|
|
114
|
+
'after',
|
|
115
|
+
[
|
|
116
|
+
'',
|
|
117
|
+
'Usage:',
|
|
118
|
+
' dapp-store --apk-file ./app.apk --whats-new "Bug fixes"',
|
|
119
|
+
' dapp-store --apk-url https://example.com/app.apk --whats-new "Bug fixes"',
|
|
120
|
+
' dapp-store resume --release-id <release-id> [--session-id <session-id>]',
|
|
121
|
+
'',
|
|
122
|
+
'Portal:',
|
|
123
|
+
' Set DAPP_STORE_PORTAL_URL to the portal origin (for example https://staging.publish.solanamobile.com).',
|
|
124
|
+
' The CLI derives the /api endpoint from that URL for the active publication workflow.',
|
|
125
|
+
` If unset, it defaults to ${DEFAULT_PRODUCTION_PORTAL_URL}.`,
|
|
126
|
+
' The target app must already exist in the portal and already have its App NFT.',
|
|
127
|
+
' The portal decides whether the submission is the first release for an existing app or a later update.',
|
|
128
|
+
'',
|
|
129
|
+
'Secrets:',
|
|
130
|
+
` Portal API key defaults to ${DEFAULT_API_KEY_ENV} or the name passed via --api-key-env.`,
|
|
131
|
+
' Use --api-key-stdin to read the portal API key from stdin.',
|
|
132
|
+
'',
|
|
133
|
+
'Local development:',
|
|
134
|
+
' Pass --local-dev to allow localhost portal endpoints and to skip self-update gating.',
|
|
135
|
+
' Local-dev mode rejects non-local portal URLs.',
|
|
136
|
+
].join('\n'),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
async function runRootAction() {
|
|
140
|
+
await runWithUserFacingErrors(async () => {
|
|
141
|
+
const options = mainCli.opts() as NewVersionCliOptions;
|
|
142
|
+
|
|
143
|
+
if (!hasPublicationInputs(options)) {
|
|
144
|
+
mainCli.outputHelp();
|
|
145
|
+
return;
|
|
329
146
|
}
|
|
330
|
-
);
|
|
331
147
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.requiredOption(
|
|
346
|
-
"--requestor-is-authorized",
|
|
347
|
-
"An attestation that the party making this Solana dApp publisher portal request is authorized to do so"
|
|
348
|
-
)
|
|
349
|
-
.option(
|
|
350
|
-
"-a, --app-mint-address <app-mint-address>",
|
|
351
|
-
"The mint address of the app NFT. If not specified, the value from your config file will be used."
|
|
352
|
-
)
|
|
353
|
-
.option(
|
|
354
|
-
"-r, --release-mint-address <release-mint-address>",
|
|
355
|
-
"The mint address of the release NFT. If not specified, the value from your config file will be used."
|
|
356
|
-
)
|
|
357
|
-
.option("-c, --critical", "Flag for a critical app update request")
|
|
358
|
-
.option("-u, --url <url>", "RPC URL", Constants.DEFAULT_RPC_DEVNET)
|
|
359
|
-
.option(
|
|
360
|
-
"-d, --dry-run",
|
|
361
|
-
"Flag for dry run. Doesn't submit the request to the publisher portal."
|
|
362
|
-
)
|
|
363
|
-
.option("-l, --alpha", "Flag to mark the submission as alpha test.")
|
|
364
|
-
.action(
|
|
365
|
-
async ({
|
|
366
|
-
appMintAddress,
|
|
367
|
-
releaseMintAddress,
|
|
368
|
-
keypair,
|
|
369
|
-
url,
|
|
370
|
-
compliesWithSolanaDappStorePolicies,
|
|
371
|
-
requestorIsAuthorized,
|
|
372
|
-
critical,
|
|
373
|
-
dryRun,
|
|
374
|
-
alpha,
|
|
375
|
-
}) => {
|
|
376
|
-
await tryWithErrorMessage(async () => {
|
|
377
|
-
await checkForSelfUpdate();
|
|
378
|
-
checkSubmissionNetwork(url);
|
|
379
|
-
|
|
380
|
-
const config = await loadPublishDetails(Constants.getConfigFilePath())
|
|
381
|
-
|
|
382
|
-
if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
|
|
383
|
-
throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (alpha) {
|
|
387
|
-
alphaAppSubmissionMessage()
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const signer = parseKeypair(keypair);
|
|
391
|
-
if (signer) {
|
|
392
|
-
await publishUpdateCommand({
|
|
393
|
-
appMintAddress,
|
|
394
|
-
releaseMintAddress,
|
|
395
|
-
signer,
|
|
396
|
-
url,
|
|
397
|
-
dryRun,
|
|
398
|
-
compliesWithSolanaDappStorePolicies,
|
|
399
|
-
requestorIsAuthorized,
|
|
400
|
-
critical,
|
|
401
|
-
alphaTest: alpha,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (dryRun) {
|
|
405
|
-
dryRunSuccessMessage()
|
|
406
|
-
} else {
|
|
407
|
-
showMessage("Success", "dApp successfully updated on the publisher portal");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
});
|
|
148
|
+
validateNewVersionArgs(options);
|
|
149
|
+
enforceSelfUpdatePolicy(options);
|
|
150
|
+
|
|
151
|
+
const targets = resolvePortalTargets(options);
|
|
152
|
+
const apiKey = await resolveApiKey(options);
|
|
153
|
+
const signerKeypair = loadSignerKeypair(options.keypair);
|
|
154
|
+
const balanceWarning = await ensurePublicationSignerBalance({
|
|
155
|
+
publicKey: signerKeypair.publicKey.toBase58(),
|
|
156
|
+
rpcUrl: options.rpcUrl,
|
|
157
|
+
localDev: options.localDev,
|
|
158
|
+
});
|
|
159
|
+
if (balanceWarning) {
|
|
160
|
+
showMessage('Warning', balanceWarning, 'warning');
|
|
411
161
|
}
|
|
412
|
-
|
|
162
|
+
const signer = createPublicationSignerFromKeypair(signerKeypair);
|
|
163
|
+
const clients = createPortalClients(targets, apiKey);
|
|
164
|
+
const progress = createPublicationProgressReporter({
|
|
165
|
+
title: 'Publishing version',
|
|
166
|
+
mode: 'new-version',
|
|
167
|
+
verbose: options.verbose,
|
|
168
|
+
});
|
|
413
169
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
"-d, --dry-run",
|
|
439
|
-
"Flag for dry run. Doesn't submit the request to the publisher portal."
|
|
440
|
-
)
|
|
441
|
-
.action(
|
|
442
|
-
async ({
|
|
443
|
-
appMintAddress,
|
|
444
|
-
releaseMintAddress,
|
|
445
|
-
keypair,
|
|
446
|
-
url,
|
|
447
|
-
requestorIsAuthorized,
|
|
448
|
-
critical,
|
|
449
|
-
dryRun,
|
|
450
|
-
}) => {
|
|
451
|
-
await tryWithErrorMessage(async () => {
|
|
452
|
-
await checkForSelfUpdate();
|
|
453
|
-
checkSubmissionNetwork(url);
|
|
454
|
-
|
|
455
|
-
const config = await loadPublishDetails(Constants.getConfigFilePath())
|
|
456
|
-
|
|
457
|
-
if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
|
|
458
|
-
throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const signer = parseKeypair(keypair);
|
|
462
|
-
if (signer) {
|
|
463
|
-
await publishRemoveCommand({
|
|
464
|
-
appMintAddress,
|
|
465
|
-
releaseMintAddress,
|
|
466
|
-
signer,
|
|
467
|
-
url,
|
|
468
|
-
dryRun,
|
|
469
|
-
requestorIsAuthorized,
|
|
470
|
-
critical,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
if (dryRun) {
|
|
474
|
-
dryRunSuccessMessage()
|
|
475
|
-
} else {
|
|
476
|
-
showMessage("Success", "dApp successfully removed from the publisher portal");
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
})
|
|
170
|
+
progress.start({
|
|
171
|
+
message: 'Connecting to publishing portal',
|
|
172
|
+
metadata: buildNewVersionProgressMetadata(options),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await runPublicationWorkflow({
|
|
177
|
+
mode: 'new-version',
|
|
178
|
+
client: clients.workflowClient,
|
|
179
|
+
input: buildNewVersionWorkflowInput(
|
|
180
|
+
options,
|
|
181
|
+
signer,
|
|
182
|
+
clients.attestationClient,
|
|
183
|
+
),
|
|
184
|
+
options: {
|
|
185
|
+
logger: progress.logger,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
progress.complete(result);
|
|
190
|
+
showPublicationSummary('Version publication completed', result);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
progress.fail(error);
|
|
193
|
+
throw error;
|
|
480
194
|
}
|
|
481
|
-
);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
482
197
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const config = await loadPublishDetails(Constants.getConfigFilePath())
|
|
519
|
-
|
|
520
|
-
if (!hasAddressInConfig(config.release) && !releaseMintAddress) {
|
|
521
|
-
throw new Error("Either specify a release mint address in the config file or specify as a CLI argument to this command.")
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const signer = parseKeypair(keypair);
|
|
525
|
-
if (signer) {
|
|
526
|
-
await publishSupportCommand({
|
|
527
|
-
appMintAddress,
|
|
528
|
-
releaseMintAddress,
|
|
529
|
-
signer,
|
|
530
|
-
url,
|
|
531
|
-
dryRun,
|
|
532
|
-
requestorIsAuthorized,
|
|
533
|
-
requestDetails,
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
if (dryRun) {
|
|
537
|
-
dryRunSuccessMessage()
|
|
538
|
-
} else {
|
|
539
|
-
showMessage("Success", "Support request sent successfully");
|
|
540
|
-
}
|
|
541
|
-
}
|
|
198
|
+
async function runResumeAction(options: ResumeCliOptions) {
|
|
199
|
+
await runWithUserFacingErrors(async () => {
|
|
200
|
+
validateResumeArgs(options);
|
|
201
|
+
enforceSelfUpdatePolicy(options);
|
|
202
|
+
|
|
203
|
+
const targets = resolvePortalTargets(options);
|
|
204
|
+
const apiKey = await resolveApiKey(options);
|
|
205
|
+
const signer = createPublicationSignerFromKeypair(
|
|
206
|
+
loadSignerKeypair(options.keypair)
|
|
207
|
+
);
|
|
208
|
+
const clients = createPortalClients(targets, apiKey);
|
|
209
|
+
const progress = createPublicationProgressReporter({
|
|
210
|
+
title: 'Resuming publication',
|
|
211
|
+
mode: 'resume',
|
|
212
|
+
verbose: options.verbose,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
progress.start({
|
|
216
|
+
message: 'Connecting to publishing portal',
|
|
217
|
+
metadata: buildResumeProgressMetadata(options),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const result = await runPublicationWorkflow({
|
|
222
|
+
mode: 'resume',
|
|
223
|
+
client: clients.workflowClient,
|
|
224
|
+
input: buildResumeWorkflowInput(
|
|
225
|
+
options,
|
|
226
|
+
signer,
|
|
227
|
+
clients.attestationClient,
|
|
228
|
+
),
|
|
229
|
+
options: {
|
|
230
|
+
logger: progress.logger,
|
|
231
|
+
},
|
|
542
232
|
});
|
|
233
|
+
|
|
234
|
+
progress.complete(result);
|
|
235
|
+
showPublicationSummary('Publication resume completed', result);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
progress.fail(error);
|
|
238
|
+
throw error;
|
|
543
239
|
}
|
|
544
|
-
);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createPortalClients(
|
|
244
|
+
targets: ResolvedPortalTargets,
|
|
245
|
+
apiKey: string,
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
workflowClient: createPortalWorkflowClient({
|
|
249
|
+
apiBaseUrl: targets.apiBaseUrl,
|
|
250
|
+
apiKey,
|
|
251
|
+
}),
|
|
252
|
+
attestationClient: createPortalAttestationClient({
|
|
253
|
+
apiBaseUrl: targets.apiBaseUrl,
|
|
254
|
+
apiKey,
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildNewVersionWorkflowInput(
|
|
260
|
+
options: NewVersionCliOptions,
|
|
261
|
+
signer: ReturnType<typeof createPublicationSignerFromKeypair>,
|
|
262
|
+
attestationClient: PublicationWorkflowInput['attestationClient'],
|
|
263
|
+
): PublicationWorkflowInput {
|
|
264
|
+
return {
|
|
265
|
+
source: buildPublicationSource(options),
|
|
266
|
+
whatsNew: options.whatsNew ?? '',
|
|
267
|
+
idempotencyKey: options.idempotencyKey ?? randomUUID(),
|
|
268
|
+
signer,
|
|
269
|
+
attestationClient,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildResumeWorkflowInput(
|
|
274
|
+
options: ResumeCliOptions,
|
|
275
|
+
signer: ReturnType<typeof createPublicationSignerFromKeypair>,
|
|
276
|
+
attestationClient: PublicationResumeInput['attestationClient'],
|
|
277
|
+
): PublicationResumeInput {
|
|
278
|
+
return {
|
|
279
|
+
publicationSessionId: resolveResumeSessionId(options),
|
|
280
|
+
releaseId: resolveResumeReleaseId(options),
|
|
281
|
+
signer,
|
|
282
|
+
attestationClient,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildPublicationSource(options: NewVersionCliOptions) {
|
|
287
|
+
if (options.apkFile) {
|
|
288
|
+
return {
|
|
289
|
+
kind: 'apk-file' as const,
|
|
290
|
+
filePath: options.apkFile,
|
|
291
|
+
fileName: path.basename(options.apkFile),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!options.apkUrl) {
|
|
296
|
+
throw new Error('`--apk-file` or `--apk-url` is required.');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
kind: 'apk-url' as const,
|
|
301
|
+
url: options.apkUrl,
|
|
302
|
+
fileName: inferFileNameFromUrl(options.apkUrl),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildNewVersionProgressMetadata(
|
|
307
|
+
options: NewVersionCliOptions,
|
|
308
|
+
): Record<string, string> {
|
|
309
|
+
if (options.apkFile) {
|
|
310
|
+
return {
|
|
311
|
+
sourceKind: 'apk-file',
|
|
312
|
+
fileName: path.basename(options.apkFile),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (options.apkUrl) {
|
|
317
|
+
const fileName = inferFileNameFromUrl(options.apkUrl);
|
|
318
|
+
return {
|
|
319
|
+
sourceKind: 'apk-url',
|
|
320
|
+
apkUrl: options.apkUrl,
|
|
321
|
+
...(fileName ? { fileName } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildResumeProgressMetadata(
|
|
329
|
+
options: ResumeCliOptions,
|
|
330
|
+
): Record<string, string> {
|
|
331
|
+
const publicationSessionId = resolveResumeSessionId(options);
|
|
332
|
+
const releaseId = resolveResumeReleaseId(options);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
...(publicationSessionId ? { publicationSessionId } : {}),
|
|
336
|
+
...(releaseId ? { releaseId } : {}),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function inferFileNameFromUrl(url: string): string | undefined {
|
|
341
|
+
try {
|
|
342
|
+
const pathname = new URL(url).pathname;
|
|
343
|
+
const fileName = pathname.split('/').filter(Boolean).pop();
|
|
344
|
+
return fileName || undefined;
|
|
345
|
+
} catch {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function enforceSelfUpdatePolicy(
|
|
351
|
+
options: NewVersionCliOptions | ResumeCliOptions,
|
|
352
|
+
) {
|
|
353
|
+
if (options.skipSelfUpdate && !options.localDev) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
'`--skip-self-update` is only allowed together with `--local-dev`.',
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function resolveResumeReleaseId(options: ResumeCliOptions): string | undefined {
|
|
361
|
+
return options.releaseId ?? options.resumeRelease;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resolveResumeSessionId(options: ResumeCliOptions): string | undefined {
|
|
365
|
+
return options.sessionId ?? options.resumeSession;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function loadSignerKeypair(keypairPath?: string): Keypair {
|
|
369
|
+
if (!keypairPath) {
|
|
370
|
+
throw new Error('`--keypair` is required.');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const keypair = parseKeypair(keypairPath);
|
|
374
|
+
if (!keypair) {
|
|
375
|
+
throw new Error('Failed to load the signer keypair.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return keypair;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function hasPublicationInputs(options: NewVersionCliOptions): boolean {
|
|
382
|
+
return Boolean(
|
|
383
|
+
options.apkFile ||
|
|
384
|
+
options.apkUrl ||
|
|
385
|
+
options.whatsNew ||
|
|
386
|
+
options.portalUrl ||
|
|
387
|
+
options.keypair ||
|
|
388
|
+
options.rpcUrl ||
|
|
389
|
+
options.idempotencyKey ||
|
|
390
|
+
options.dappId ||
|
|
391
|
+
options.verbose,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function showPublicationSummary(title: string, result: unknown) {
|
|
396
|
+
const summaryLines = extractPublicationSummaryLines(result);
|
|
397
|
+
showMessage(title, summaryLines.join('\n'), 'standard');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function runWithUserFacingErrors(block: () => Promise<void>) {
|
|
401
|
+
try {
|
|
402
|
+
await block();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const message =
|
|
405
|
+
error instanceof Error ? error.message : 'An unexpected error occurred';
|
|
406
|
+
showMessage('Error', message, 'error');
|
|
407
|
+
process.exitCode = 1;
|
|
408
|
+
}
|
|
409
|
+
}
|