@solana-mobile/dapp-store-cli 0.15.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.
Files changed (105) hide show
  1. package/bin/dapp-store.js +3 -1
  2. package/lib/CliSetup.js +304 -505
  3. package/lib/CliUtils.js +6 -376
  4. package/lib/__tests__/CliSetupTest.js +484 -74
  5. package/lib/cli/__tests__/parseErrors.test.js +25 -0
  6. package/lib/cli/__tests__/signer.test.js +436 -0
  7. package/lib/cli/constants.js +23 -0
  8. package/lib/cli/messages.js +21 -0
  9. package/lib/cli/parseErrors.js +41 -0
  10. package/lib/{commands/publish/PublishCliSupport.js → cli/selfUpdate.js} +72 -38
  11. package/lib/{commands/publish/PublishCliRemove.js → cli/signer.js} +35 -56
  12. package/lib/index.js +96 -5
  13. package/lib/package.json +5 -24
  14. package/lib/portal/__tests__/releaseMetadata.test.js +647 -0
  15. package/lib/portal/__tests__/translators.test.js +76 -0
  16. package/lib/portal/__tests__/workflowClient.test.js +457 -0
  17. package/lib/portal/attestationClient.js +143 -0
  18. package/lib/portal/files.js +64 -0
  19. package/lib/portal/http.js +364 -0
  20. package/lib/portal/records.js +64 -0
  21. package/lib/portal/releaseMetadata.js +748 -0
  22. package/lib/portal/translators.js +460 -0
  23. package/lib/portal/types.js +1 -0
  24. package/lib/portal/workflowClient.js +704 -0
  25. package/lib/publication/PublicationProgressReporter.js +1051 -0
  26. package/lib/publication/__tests__/PublicationProgressReporter.test.js +174 -0
  27. package/lib/{commands/ValidateCommand.js → publication/__tests__/fundingPreflight.test.js} +90 -66
  28. package/lib/publication/__tests__/publicationSummary.test.js +26 -0
  29. package/lib/publication/cliValidation.js +482 -0
  30. package/lib/publication/fundingPreflight.js +246 -0
  31. package/lib/publication/publicationSummary.js +99 -0
  32. package/lib/{commands/utils.js → publication/runPublicationWorkflow.js} +16 -46
  33. package/package.json +5 -24
  34. package/src/CliSetup.ts +370 -505
  35. package/src/CliUtils.ts +9 -233
  36. package/src/__tests__/CliSetupTest.ts +272 -120
  37. package/src/cli/__tests__/parseErrors.test.ts +34 -0
  38. package/src/cli/__tests__/signer.test.ts +359 -0
  39. package/src/cli/constants.ts +3 -0
  40. package/src/cli/messages.ts +27 -0
  41. package/src/cli/parseErrors.ts +62 -0
  42. package/src/cli/selfUpdate.ts +59 -0
  43. package/src/cli/signer.ts +38 -0
  44. package/src/index.ts +31 -4
  45. package/src/portal/__tests__/releaseMetadata.test.ts +508 -0
  46. package/src/portal/__tests__/translators.test.ts +82 -0
  47. package/src/portal/__tests__/workflowClient.test.ts +278 -0
  48. package/src/portal/attestationClient.ts +19 -0
  49. package/src/portal/files.ts +73 -0
  50. package/src/portal/http.ts +170 -0
  51. package/src/portal/records.ts +38 -0
  52. package/src/portal/releaseMetadata.ts +489 -0
  53. package/src/portal/translators.ts +750 -0
  54. package/src/portal/types.ts +27 -0
  55. package/src/portal/workflowClient.ts +575 -0
  56. package/src/publication/PublicationProgressReporter.ts +1026 -0
  57. package/src/publication/__tests__/PublicationProgressReporter.test.ts +210 -0
  58. package/src/publication/__tests__/fundingPreflight.test.ts +78 -0
  59. package/src/publication/__tests__/publicationSummary.test.ts +30 -0
  60. package/src/publication/cliValidation.ts +264 -0
  61. package/src/publication/fundingPreflight.ts +123 -0
  62. package/src/publication/publicationSummary.ts +26 -0
  63. package/src/publication/runPublicationWorkflow.ts +46 -0
  64. package/lib/commands/create/CreateCliApp.js +0 -223
  65. package/lib/commands/create/CreateCliRelease.js +0 -290
  66. package/lib/commands/create/index.js +0 -40
  67. package/lib/commands/index.js +0 -3
  68. package/lib/commands/publish/PublishCliSubmit.js +0 -208
  69. package/lib/commands/publish/PublishCliUpdate.js +0 -211
  70. package/lib/commands/publish/index.js +0 -22
  71. package/lib/commands/scaffolding/ScaffoldInit.js +0 -15
  72. package/lib/commands/scaffolding/index.js +0 -1
  73. package/lib/config/EnvVariables.js +0 -59
  74. package/lib/config/PublishDetails.js +0 -915
  75. package/lib/config/S3StorageManager.js +0 -93
  76. package/lib/config/index.js +0 -2
  77. package/lib/generated/config_obj.json +0 -1
  78. package/lib/generated/config_schema.json +0 -1
  79. package/lib/prebuild_schema/publishing_source.yaml +0 -64
  80. package/lib/prebuild_schema/schemagen.js +0 -25
  81. package/lib/upload/CachedStorageDriver.js +0 -293
  82. package/lib/upload/TurboStorageDriver.js +0 -718
  83. package/lib/upload/index.js +0 -2
  84. package/src/commands/ValidateCommand.ts +0 -82
  85. package/src/commands/create/CreateCliApp.ts +0 -93
  86. package/src/commands/create/CreateCliRelease.ts +0 -149
  87. package/src/commands/create/index.ts +0 -47
  88. package/src/commands/index.ts +0 -3
  89. package/src/commands/publish/PublishCliRemove.ts +0 -66
  90. package/src/commands/publish/PublishCliSubmit.ts +0 -93
  91. package/src/commands/publish/PublishCliSupport.ts +0 -66
  92. package/src/commands/publish/PublishCliUpdate.ts +0 -101
  93. package/src/commands/publish/index.ts +0 -29
  94. package/src/commands/scaffolding/ScaffoldInit.ts +0 -20
  95. package/src/commands/scaffolding/index.ts +0 -1
  96. package/src/commands/utils.ts +0 -33
  97. package/src/config/EnvVariables.ts +0 -39
  98. package/src/config/PublishDetails.ts +0 -456
  99. package/src/config/S3StorageManager.ts +0 -47
  100. package/src/config/index.ts +0 -2
  101. package/src/prebuild_schema/publishing_source.yaml +0 -64
  102. package/src/prebuild_schema/schemagen.js +0 -31
  103. package/src/upload/CachedStorageDriver.ts +0 -99
  104. package/src/upload/TurboStorageDriver.ts +0 -277
  105. package/src/upload/index.ts +0 -2
@@ -1,183 +1,335 @@
1
- import { beforeEach, expect } from "@jest/globals";
2
- import {
3
- createAppCliCmd,
4
- createCliCmd,
5
- createReleaseCliCmd,
6
- initCliCmd,
7
- mainCli
8
- } from "../CliSetup";
9
- import { Constants } from "../CliUtils";
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
10
4
 
11
- describe("Cli Setup & Execution", () => {
12
- const outputHelpReference = "(outputHelp)"
5
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
13
6
 
14
- let errorOutput: string = ""
15
- let otherOutput: string = ""
7
+ import { mainCli } from "../CliSetup";
8
+ import {
9
+ DEFAULT_API_KEY_ENV,
10
+ DEFAULT_LOCAL_PORTAL_URL,
11
+ DEFAULT_PRODUCTION_PORTAL_URL,
12
+ UPDATED_PUBLISHING_CLI_DOCS_URL,
13
+ formatUpdatedCliUsageError,
14
+ resolveApiKey,
15
+ resolvePortalTargets,
16
+ validateNewVersionArgs,
17
+ validateResumeArgs,
18
+ } from "../publication/cliValidation";
19
+
20
+ describe("CLI surface", () => {
21
+ const outputHelpReference = "(outputHelp)";
22
+ const trackedEnvKeys = [
23
+ DEFAULT_API_KEY_ENV,
24
+ "ALT_DAPP_STORE_API_KEY",
25
+ "DAPP_STORE_PORTAL_URL",
26
+ "DAPP_STORE_PORTAL_WEB_URL",
27
+ "DAPP_STORE_PORTAL_API_BASE_URL",
28
+ ] as const;
29
+
30
+ let errorOutput = "";
31
+ let otherOutput = "";
32
+ let tempDir = "";
33
+ let originalEnv: Record<string, string | undefined> = {};
16
34
 
17
35
  beforeEach(() => {
18
36
  errorOutput = "";
19
37
  otherOutput = "";
20
- mainCli.exitOverride();
38
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "dapp-store-cli-test-"));
39
+ originalEnv = Object.fromEntries(
40
+ trackedEnvKeys.map((key) => [key, process.env[key]])
41
+ );
42
+ for (const key of trackedEnvKeys) {
43
+ delete process.env[key];
44
+ }
21
45
 
46
+ mainCli.exitOverride();
22
47
  mainCli.configureOutput({
23
- getOutHelpWidth(): number { return 250; },
24
- getErrHelpWidth(): number { return 250;},
25
-
48
+ getOutHelpWidth(): number {
49
+ return 200;
50
+ },
51
+ getErrHelpWidth(): number {
52
+ return 200;
53
+ },
26
54
  writeOut(str: string) {
27
55
  otherOutput += str;
28
56
  },
29
-
30
57
  writeErr(str: string) {
31
58
  errorOutput += str;
32
- }
59
+ },
33
60
  });
61
+ process.exitCode = 0;
34
62
  });
35
63
 
36
- test("Cli version argument reports correct version", () => {
37
-
38
- expect(() => {
39
- mainCli.parse(["npx", "dapp-store", "-V"]);
40
- }).toThrow(Constants.CLI_VERSION)
64
+ afterEach(() => {
65
+ fs.rmSync(tempDir, { recursive: true, force: true });
66
+ for (const [key, value] of Object.entries(originalEnv)) {
67
+ if (typeof value === "undefined") {
68
+ delete process.env[key];
69
+ } else {
70
+ process.env[key] = value;
71
+ }
72
+ }
41
73
  });
42
74
 
43
- test("Calling cli with no parameters displays general help", () => {
44
- expect(() => {
45
- mainCli.parse(["npx", "dapp-store"]);
46
- }
47
- ).toThrow(outputHelpReference);
75
+ function createTempApkFile(fileName = "app-release.apk") {
76
+ const apkPath = path.join(tempDir, fileName);
77
+ fs.writeFileSync(apkPath, "apk");
78
+ return apkPath;
79
+ }
48
80
 
49
- expect(generalHelp).toEqual(errorOutput)
81
+ test("version reports the package version", () => {
82
+ expect(() => {
83
+ mainCli.parse(["node", "dapp-store", "-V"]);
84
+ }).toThrow();
50
85
  });
51
86
 
52
- test("Calling init command with help parameter shows contextual help info", () => {
53
- initCliCmd.exitOverride()
54
-
87
+ test("help advertises the default publication surface", () => {
55
88
  expect(() => {
56
- initCliCmd.parse(["dapp-store", "init", "-h"])
57
- }).toThrow(outputHelpReference)
89
+ mainCli.parse(["node", "dapp-store", "--help"]);
90
+ }).toThrow(outputHelpReference);
91
+
92
+ expect(otherOutput).not.toContain("--new-version");
93
+ expect(otherOutput).toContain("resume");
94
+ expect(otherOutput).toContain("--apk-file");
95
+ expect(otherOutput).toContain("--apk-url");
96
+ expect(otherOutput).toContain("--keypair");
97
+ expect(otherOutput).toContain("--portal-url");
98
+ expect(otherOutput).not.toContain("--dapp-id");
99
+ expect(otherOutput).not.toContain("--fee-payer-keypair");
100
+ expect(otherOutput).not.toContain("--signer-keypair");
101
+ expect(errorOutput).toBe("");
102
+ });
58
103
 
59
- expect(otherOutput).toEqual(initHelp)
60
- })
104
+ test("new-version validation rejects ambiguous APK sources", () => {
105
+ expect(() =>
106
+ validateNewVersionArgs({
107
+ apkFile: createTempApkFile(),
108
+ apkUrl: "https://example.com/app.apk",
109
+ whatsNew: "Fixes",
110
+ keypair: "/tmp/signer.json",
111
+ })
112
+ ).toThrow("exactly one of `--apk-file` or `--apk-url`");
113
+ });
61
114
 
62
- test("Calling create command with no options lists all options", () => {
63
- createCliCmd.exitOverride()
115
+ test("new-version validation rejects missing APK source", () => {
116
+ expect(() =>
117
+ validateNewVersionArgs({
118
+ whatsNew: "Fixes",
119
+ keypair: "/tmp/signer.json",
120
+ })
121
+ ).toThrow("exactly one of `--apk-file` or `--apk-url`");
122
+ });
64
123
 
65
- expect(() => {
66
- createCliCmd.parse(["dapp-store", "create"]);
67
- }
68
- ).toThrow(outputHelpReference);
124
+ test("new-version validation rejects blank release notes", () => {
125
+ expect(() =>
126
+ validateNewVersionArgs({
127
+ apkUrl: "https://example.com/app.apk",
128
+ whatsNew: " ",
129
+ keypair: "/tmp/signer.json",
130
+ })
131
+ ).toThrow("`--whats-new` is required.");
132
+ });
69
133
 
70
- expect(errorOutput).toEqual(createHelp)
134
+ test("new-version validation rejects missing keypair", () => {
135
+ expect(() =>
136
+ validateNewVersionArgs({
137
+ apkUrl: "https://example.com/app.apk",
138
+ whatsNew: "Fixes",
139
+ })
140
+ ).toThrow("`--keypair` is required.");
71
141
  });
72
142
 
73
- test("Calling create app command with no arguments warns about required argument", () => {
74
- createAppCliCmd.exitOverride()
143
+ test("new-version validation rejects a missing local APK file", () => {
144
+ expect(() =>
145
+ validateNewVersionArgs({
146
+ apkFile: path.join(tempDir, "missing.apk"),
147
+ whatsNew: "Fixes",
148
+ keypair: "/tmp/signer.json",
149
+ })
150
+ ).toThrow("APK file not found");
151
+ });
75
152
 
76
- expect(() => {
77
- createAppCliCmd.parse(["dapp-store", "create", "app"]);
78
- }
79
- ).toThrow(keyPairArgHelp);
153
+ test("new-version validation rejects non-HTTPS APK URLs", () => {
154
+ expect(() =>
155
+ validateNewVersionArgs({
156
+ apkUrl: "http://example.com/app.apk",
157
+ whatsNew: "Fixes",
158
+ keypair: "/tmp/signer.json",
159
+ })
160
+ ).toThrow("`--apk-url` must use HTTPS.");
80
161
  });
81
162
 
82
- test("Calling create app command with help flag shows contextual help", () => {
83
- createAppCliCmd.exitOverride()
163
+ test("new-version validation accepts an existing APK file", () => {
164
+ expect(() =>
165
+ validateNewVersionArgs({
166
+ apkFile: createTempApkFile(),
167
+ whatsNew: "Fixes",
168
+ keypair: "/tmp/signer.json",
169
+ })
170
+ ).not.toThrow();
171
+ });
84
172
 
85
- expect(() => {
86
- createAppCliCmd.parse(["dapp-store", "create", "app", "-h"]);
87
- }).toThrow(outputHelpReference)
173
+ test("new-version validation accepts a single HTTPS APK URL", () => {
174
+ expect(() =>
175
+ validateNewVersionArgs({
176
+ apkUrl: "https://example.com/app.apk",
177
+ whatsNew: "Fixes",
178
+ keypair: "/tmp/signer.json",
179
+ })
180
+ ).not.toThrow();
181
+ });
88
182
 
89
- expect(otherOutput).toEqual(createAppHelp)
183
+ test("resume validation requires a single target", () => {
184
+ expect(() =>
185
+ validateResumeArgs({
186
+ releaseId: "release-1",
187
+ sessionId: "session-1",
188
+ keypair: "/tmp/signer.json",
189
+ })
190
+ ).toThrow("exactly one of `--release-id` or `--session-id`");
90
191
  });
91
192
 
92
- test("Calling create release command with no arguments warns about required argument", () => {
93
- createReleaseCliCmd.exitOverride()
193
+ test("resume validation accepts alias flags", () => {
194
+ expect(() =>
195
+ validateResumeArgs({
196
+ resumeRelease: "release-1",
197
+ keypair: "/tmp/signer.json",
198
+ })
199
+ ).not.toThrow();
200
+
201
+ expect(() =>
202
+ validateResumeArgs({
203
+ resumeSession: "session-1",
204
+ keypair: "/tmp/signer.json",
205
+ })
206
+ ).not.toThrow();
207
+ });
94
208
 
95
- expect(() => {
96
- createReleaseCliCmd.parse(["dapp-store", "create", "release"]);
97
- }
98
- ).toThrow(keyPairArgHelp);
209
+ test("resume validation rejects conflicting release aliases", () => {
210
+ expect(() =>
211
+ validateResumeArgs({
212
+ releaseId: "release-1",
213
+ resumeRelease: "release-2",
214
+ keypair: "/tmp/signer.json",
215
+ })
216
+ ).toThrow(
217
+ "Conflicting values were provided for --release-id and --resume-release."
218
+ );
99
219
  });
100
220
 
101
- test("Calling create release command with help flag shows contextual help", () => {
102
- createReleaseCliCmd.exitOverride()
221
+ test("resume validation accepts a release id", () => {
222
+ expect(() =>
223
+ validateResumeArgs({
224
+ releaseId: "release-1",
225
+ keypair: "/tmp/signer.json",
226
+ })
227
+ ).not.toThrow();
228
+ });
103
229
 
104
- expect(() => {
105
- createReleaseCliCmd.parse(["dapp-store", "create", "release", "-h"]);
106
- }).toThrow(outputHelpReference)
230
+ test("portal targets default to production when unset", () => {
231
+ const targets = resolvePortalTargets({});
107
232
 
108
- expect(otherOutput).toEqual(createReleaseHelp)
233
+ expect(targets.apiBaseUrl).toBe(`${DEFAULT_PRODUCTION_PORTAL_URL}/api`);
109
234
  });
110
235
 
111
- //--------------------------------------------------
236
+ test("portal targets default to localhost in local-dev mode", () => {
237
+ const targets = resolvePortalTargets({
238
+ localDev: true,
239
+ });
112
240
 
113
- const generalHelp = `Usage: dapp-store [options] [command]
241
+ expect(targets.apiBaseUrl).toBe(`${DEFAULT_LOCAL_PORTAL_URL}/api`);
242
+ });
114
243
 
115
- CLI to assist with publishing to the Saga Dapp Store
244
+ test("portal targets derive the API base URL from the configured portal URL", () => {
245
+ const targets = resolvePortalTargets({
246
+ portalUrl: "https://staging.publish.solanamobile.com",
247
+ });
116
248
 
117
- Options:
118
- -V, --version output the version number
119
- -h, --help display help for command
249
+ expect(targets.apiBaseUrl).toBe(
250
+ "https://staging.publish.solanamobile.com/api"
251
+ );
252
+ });
120
253
 
121
- Commands:
122
- init First-time initialization of tooling configuration
123
- create Create a \`app\`, or \`release\`
124
- validate [options] Validates details prior to publishing
125
- publish Submit a publishing request (\`submit\`, \`update\`, \`remove\`, or \`support\`) to the Solana Mobile dApp publisher portal
126
- help [command] display help for command
127
- `;
254
+ test("portal targets preserve portal subpaths when deriving /api", () => {
255
+ const targets = resolvePortalTargets({
256
+ portalUrl: "https://portal.example.com/publishing",
257
+ });
128
258
 
129
- const initHelp = `Usage: dapp-store init [options]
259
+ expect(targets.apiBaseUrl).toBe(
260
+ "https://portal.example.com/publishing/api"
261
+ );
262
+ });
130
263
 
131
- First-time initialization of tooling configuration
264
+ test("portal targets honor DAPP_STORE_PORTAL_URL from the environment", () => {
265
+ process.env.DAPP_STORE_PORTAL_URL = "https://env.publish.solanamobile.com";
132
266
 
133
- Options:
134
- -h, --help display help for command
135
- `;
267
+ const targets = resolvePortalTargets({});
136
268
 
137
- const keyPairArgHelp = "error: required option '-k, --keypair <path-to-keypair-file>' not specified"
269
+ expect(targets.apiBaseUrl).toBe("https://env.publish.solanamobile.com/api");
270
+ });
138
271
 
139
- const createHelp = `Usage: dapp-store create [options] [command]
272
+ test("portal targets ignore removed legacy portal env vars", () => {
273
+ process.env.DAPP_STORE_PORTAL_WEB_URL =
274
+ "https://legacy-web.publish.solanamobile.com";
275
+ process.env.DAPP_STORE_PORTAL_API_BASE_URL =
276
+ "https://legacy.publish.solanamobile.com/root/api";
140
277
 
141
- Create a \`app\`, or \`release\`
278
+ const targets = resolvePortalTargets({});
142
279
 
143
- Options:
144
- -h, --help display help for command
280
+ expect(targets.apiBaseUrl).toBe(`${DEFAULT_PRODUCTION_PORTAL_URL}/api`);
281
+ });
145
282
 
146
- Commands:
147
- app [options] Create a app
148
- release [options] Create a release
149
- help [command] display help for command
283
+ test("local-dev mode rejects non-local portal URLs", () => {
284
+ expect(() =>
285
+ resolvePortalTargets({
286
+ localDev: true,
287
+ portalUrl: "https://portal.example.com",
288
+ })
289
+ ).toThrow("only allows localhost portal endpoints");
290
+ });
150
291
 
151
- Release metadata notes:
152
- We include publisher.support_email when provided; if omitted we fall back to publisher.email.
153
- `;
292
+ test("non-local portal endpoints must use HTTPS", () => {
293
+ expect(() =>
294
+ resolvePortalTargets({
295
+ portalUrl: "http://portal.example.com",
296
+ })
297
+ ).toThrow("Portal endpoints must use HTTPS unless --local-dev is set.");
298
+ });
154
299
 
155
- const createAppHelp = `Usage: dapp-store create app [options]
300
+ test("portal targets honor the configured API key env name", () => {
301
+ expect(DEFAULT_API_KEY_ENV).toBe("DAPP_STORE_API_KEY");
302
+ });
156
303
 
157
- Create a app
304
+ test("resolveApiKey reads the default API key env var", async () => {
305
+ process.env.DAPP_STORE_API_KEY = "portal-secret";
158
306
 
159
- Options:
160
- -k, --keypair <path-to-keypair-file> Path to keypair file
161
- -u, --url <url> RPC URL (default: "https://api.devnet.solana.com")
162
- -d, --dry-run Flag for dry run. Doesn't mint an NFT
163
- -s, --storage-config <storage-config> Provide alternative storage configuration details
164
- -p, --priority-fee-lamports <priority-fee-lamports> Priority Fee lamports
165
- -h, --help display help for command
166
- `;
307
+ await expect(resolveApiKey({})).resolves.toBe("portal-secret");
308
+ });
167
309
 
168
- const createReleaseHelp = `Usage: dapp-store create release [options]
310
+ test("resolveApiKey reads a custom API key env var", async () => {
311
+ process.env.ALT_DAPP_STORE_API_KEY = "alt-secret";
169
312
 
170
- Create a release
313
+ await expect(
314
+ resolveApiKey({ apiKeyEnv: "ALT_DAPP_STORE_API_KEY" })
315
+ ).resolves.toBe("alt-secret");
316
+ });
171
317
 
172
- Options:
173
- -k, --keypair <path-to-keypair-file> Path to keypair file
174
- -a, --app-mint-address <app-mint-address> The mint address of the app NFT
175
- -u, --url <url> RPC URL (default: "https://api.devnet.solana.com")
176
- -d, --dry-run Flag for dry run. Doesn't mint an NFT
177
- -b, --build-tools-path <build-tools-path> Path to Android build tools which contains AAPT2
178
- -s, --storage-config <storage-config> Provide alternative storage configuration details
179
- -p, --priority-fee-lamports <priority-fee-lamports> Priority Fee lamports
180
- -h, --help display help for command
181
- `;
318
+ test("resolveApiKey rejects when no API key is available", async () => {
319
+ await expect(resolveApiKey({})).rejects.toThrow(
320
+ "Portal API key is required."
321
+ );
322
+ await expect(resolveApiKey({})).rejects.toThrow(
323
+ UPDATED_PUBLISHING_CLI_DOCS_URL
324
+ );
325
+ });
182
326
 
327
+ test("formatUpdatedCliUsageError converts unknown-option errors into docs guidance", () => {
328
+ expect(formatUpdatedCliUsageError("error: unknown option '-k'")).toContain(
329
+ "Unknown option '-k'."
330
+ );
331
+ expect(formatUpdatedCliUsageError("error: unknown option '-k'")).toContain(
332
+ UPDATED_PUBLISHING_CLI_DOCS_URL
333
+ );
334
+ });
183
335
  });
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+
3
+ import {
4
+ getCommanderUserFacingError,
5
+ isCommanderLifecycleExit,
6
+ } from "../parseErrors";
7
+ import { UPDATED_PUBLISHING_CLI_DOCS_URL } from "../../publication/cliValidation";
8
+
9
+ describe("CLI parse error handling", () => {
10
+ test("legacy parse errors point to the updated publishing docs", () => {
11
+ const error = getCommanderUserFacingError({
12
+ code: "commander.unknownOption",
13
+ exitCode: 1,
14
+ message: "error: unknown option '-k'",
15
+ name: "CommanderError",
16
+ });
17
+
18
+ expect(error).not.toBeNull();
19
+ expect(error?.exitCode).toBe(1);
20
+ expect(error?.message).toContain("Unknown option '-k'.");
21
+ expect(error?.message).toContain(UPDATED_PUBLISHING_CLI_DOCS_URL);
22
+ });
23
+
24
+ test("help exits are ignored", () => {
25
+ expect(
26
+ isCommanderLifecycleExit({
27
+ code: "commander.helpDisplayed",
28
+ exitCode: 0,
29
+ message: "(outputHelp)",
30
+ name: "CommanderError",
31
+ })
32
+ ).toBe(true);
33
+ });
34
+ });