@credibledata/cred-cli 0.0.102 → 0.0.103

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 (118) hide show
  1. package/README.md +1 -1
  2. package/coverage/coverage-summary.json +40 -0
  3. package/coverage/tmp/coverage-2152-1789496974457-0.json +1 -0
  4. package/coverage/tmp/coverage-2153-1789496954404-0.json +1 -0
  5. package/dist/client/api.js +4193 -1342
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/clientconfiguration.js +57 -4
  8. package/dist/clientconfiguration.js.map +1 -1
  9. package/dist/clientconfiguration.spec.js +90 -0
  10. package/dist/clientconfiguration.spec.js.map +1 -1
  11. package/dist/commands/add.js +18 -0
  12. package/dist/commands/add.js.map +1 -1
  13. package/dist/commands/environment/updateenvironment.js +63 -1
  14. package/dist/commands/environment/updateenvironment.js.map +1 -1
  15. package/dist/commands/environment/updateenvironment.spec.js +84 -0
  16. package/dist/commands/environment/updateenvironment.spec.js.map +1 -1
  17. package/dist/commands/get.js +52 -1
  18. package/dist/commands/get.js.map +1 -1
  19. package/dist/commands/list.js +78 -0
  20. package/dist/commands/list.js.map +1 -1
  21. package/dist/commands/materialization/cancelrun.js +62 -0
  22. package/dist/commands/materialization/cancelrun.js.map +1 -0
  23. package/dist/commands/materialization/getindex.js +52 -0
  24. package/dist/commands/materialization/getindex.js.map +1 -0
  25. package/dist/commands/materialization/getmaterialization.js +54 -0
  26. package/dist/commands/materialization/getmaterialization.js.map +1 -0
  27. package/dist/commands/materialization/getpreaggregate.js +55 -0
  28. package/dist/commands/materialization/getpreaggregate.js.map +1 -0
  29. package/dist/commands/materialization/getrun.js +54 -0
  30. package/dist/commands/materialization/getrun.js.map +1 -0
  31. package/dist/commands/materialization/listindexes.js +58 -0
  32. package/dist/commands/materialization/listindexes.js.map +1 -0
  33. package/dist/commands/materialization/listmaterializations.js +52 -0
  34. package/dist/commands/materialization/listmaterializations.js.map +1 -0
  35. package/dist/commands/materialization/listpreaggregates.js +61 -0
  36. package/dist/commands/materialization/listpreaggregates.js.map +1 -0
  37. package/dist/commands/materialization/listruns.js +60 -0
  38. package/dist/commands/materialization/listruns.js.map +1 -0
  39. package/dist/commands/materialization/rerun.js +111 -0
  40. package/dist/commands/materialization/rerun.js.map +1 -0
  41. package/dist/commands/misc/login.js +14 -0
  42. package/dist/commands/misc/login.js.map +1 -1
  43. package/dist/commands/misc/login.spec.js +92 -0
  44. package/dist/commands/misc/login.spec.js.map +1 -1
  45. package/dist/commands/package/listpackages.js +7 -0
  46. package/dist/commands/package/listpackages.js.map +1 -1
  47. package/dist/commands/package/listpackages.spec.js +12 -0
  48. package/dist/commands/package/listpackages.spec.js.map +1 -1
  49. package/dist/commands/package/publish.js +34 -11
  50. package/dist/commands/package/publish.js.map +1 -1
  51. package/dist/commands/package/publish.spec.js +26 -28
  52. package/dist/commands/package/publish.spec.js.map +1 -1
  53. package/dist/commands/permission/createpermission.js +123 -0
  54. package/dist/commands/permission/createpermission.js.map +1 -0
  55. package/dist/commands/permission/createpermission.spec.js +360 -0
  56. package/dist/commands/permission/createpermission.spec.js.map +1 -0
  57. package/dist/commands/permission/deletepermission.js +86 -0
  58. package/dist/commands/permission/deletepermission.js.map +1 -0
  59. package/dist/commands/permission/getpermission.js +85 -0
  60. package/dist/commands/permission/getpermission.js.map +1 -0
  61. package/dist/commands/permission/listpermissions.js +75 -0
  62. package/dist/commands/permission/listpermissions.js.map +1 -0
  63. package/dist/commands/permission/scope.js +140 -0
  64. package/dist/commands/permission/scope.js.map +1 -0
  65. package/dist/commands/rm.js +11 -0
  66. package/dist/commands/rm.js.map +1 -1
  67. package/dist/commands/update.js +18 -12
  68. package/dist/commands/update.js.map +1 -1
  69. package/dist/common/UserGroupId.js +1 -1
  70. package/dist/common/UserGroupId.js.map +1 -1
  71. package/dist/common/UserGroupId.spec.js +2 -4
  72. package/dist/common/UserGroupId.spec.js.map +1 -1
  73. package/dist/constants.js +48 -0
  74. package/dist/constants.js.map +1 -1
  75. package/dist/index.js +61 -6
  76. package/dist/index.js.map +1 -1
  77. package/dist/logger.js +5 -1
  78. package/dist/logger.js.map +1 -1
  79. package/openapitools.json +4 -1
  80. package/package.json +5 -3
  81. package/src/client/api.ts +6433 -1533
  82. package/src/clientconfiguration.spec.ts +126 -0
  83. package/src/clientconfiguration.ts +64 -4
  84. package/src/commands/add.ts +43 -0
  85. package/src/commands/environment/updateenvironment.spec.ts +167 -0
  86. package/src/commands/environment/updateenvironment.ts +85 -1
  87. package/src/commands/get.ts +110 -1
  88. package/src/commands/list.ts +171 -0
  89. package/src/commands/materialization/cancelrun.ts +70 -0
  90. package/src/commands/materialization/getindex.ts +55 -0
  91. package/src/commands/materialization/getmaterialization.ts +57 -0
  92. package/src/commands/materialization/getpreaggregate.ts +58 -0
  93. package/src/commands/materialization/getrun.ts +55 -0
  94. package/src/commands/materialization/listindexes.ts +69 -0
  95. package/src/commands/materialization/listmaterializations.ts +53 -0
  96. package/src/commands/materialization/listpreaggregates.ts +74 -0
  97. package/src/commands/materialization/listruns.ts +77 -0
  98. package/src/commands/materialization/rerun.ts +123 -0
  99. package/src/commands/misc/login.spec.ts +118 -2
  100. package/src/commands/misc/login.ts +14 -0
  101. package/src/commands/package/listpackages.spec.ts +16 -0
  102. package/src/commands/package/listpackages.ts +7 -0
  103. package/src/commands/package/publish.spec.ts +45 -30
  104. package/src/commands/package/publish.ts +31 -10
  105. package/src/commands/permission/createpermission.spec.ts +507 -0
  106. package/src/commands/permission/createpermission.ts +181 -0
  107. package/src/commands/permission/deletepermission.ts +117 -0
  108. package/src/commands/permission/getpermission.ts +115 -0
  109. package/src/commands/permission/listpermissions.ts +101 -0
  110. package/src/commands/permission/scope.ts +180 -0
  111. package/src/commands/rm.ts +29 -0
  112. package/src/commands/update.ts +50 -27
  113. package/src/common/UserGroupId.spec.ts +2 -4
  114. package/src/common/UserGroupId.ts +1 -1
  115. package/src/constants.ts +50 -1
  116. package/src/index.ts +94 -11
  117. package/src/logger.ts +5 -1
  118. package/bun.lock +0 -934
@@ -0,0 +1,53 @@
1
+ import { MaterializationsApi } from "../../client/api";
2
+ import { Configuration } from "../../client/configuration";
3
+ import { ClientConfiguration } from "../../clientconfiguration";
4
+ import { logger } from "../../logger";
5
+ import { prettifyJson, printOutError } from "../../utils";
6
+
7
+ const materializationsApi = new MaterializationsApi(new Configuration());
8
+
9
+ /**
10
+ * List the Malloy Persistence materializations bound to a package version.
11
+ * Each materialization prints its `status` and, when FAILED, the `error`
12
+ * explaining why the build failed (e.g. a warehouse permission denial) — so a
13
+ * failing materialization is diagnosable from the CLI without opening the app.
14
+ */
15
+ export async function listMaterializations(
16
+ controlPlane: string,
17
+ packagePath: string,
18
+ versionId: string,
19
+ debug: boolean,
20
+ ) {
21
+ const clientConfig = new ClientConfiguration();
22
+ const organizationName = clientConfig.getOrganization();
23
+ const environmentName = clientConfig.getProject();
24
+
25
+ if (!clientConfig.doesConfigExists()) {
26
+ logger.error("Please, make sure that you are logged in");
27
+ return;
28
+ }
29
+
30
+ if (!environmentName) {
31
+ logger.error("Please, make sure to set an environment");
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const packageName = packagePath.replace("/", "");
37
+ const response = await materializationsApi.listMaterializations(
38
+ organizationName,
39
+ environmentName,
40
+ packageName,
41
+ versionId,
42
+ {
43
+ baseURL: controlPlane,
44
+ headers: clientConfig.getHeaders(),
45
+ },
46
+ );
47
+
48
+ console.log(prettifyJson(response.data));
49
+ } catch (e) {
50
+ logger.error("Failed to list materializations");
51
+ printOutError(e, debug);
52
+ }
53
+ }
@@ -0,0 +1,74 @@
1
+ import { PreaggregatesApi } from "../../client/api";
2
+ import { Configuration } from "../../client/configuration";
3
+ import { ClientConfiguration } from "../../clientconfiguration";
4
+ import { logger } from "../../logger";
5
+ import {
6
+ prettifyJson,
7
+ printOutError,
8
+ parsePaginationParams,
9
+ extractTotalCount,
10
+ } from "../../utils";
11
+
12
+ const preaggregatesApi = new PreaggregatesApi(new Configuration());
13
+
14
+ /**
15
+ * List the pre-aggregated measures of a package version (one entry per `#@ preaggregate`
16
+ * measure). Each entry prints its `status` and, when FAILED, the `error`; plus the
17
+ * `grainDimensions` it is rolled up to, the serving `rowCount`, `lastBuiltAt`, freshness
18
+ * policy, and the `stale`/`staleReasons` triple. `--source` narrows to the measures of one
19
+ * authored source. The total available count is logged to stderr.
20
+ *
21
+ * <p>One rollup table can serve several measures, so entries here outnumber the tables
22
+ * behind them: the measure is what a modeler wrote, and it is the unit this lists.
23
+ */
24
+ export async function listPreaggregates(
25
+ controlPlane: string,
26
+ packagePath: string,
27
+ versionId: string,
28
+ sourceName: string | undefined,
29
+ debug: boolean,
30
+ page?: string | number,
31
+ pageSize?: string | number,
32
+ ) {
33
+ const clientConfig = new ClientConfiguration();
34
+ const organizationName = clientConfig.getOrganization();
35
+ const environmentName = clientConfig.getProject();
36
+
37
+ if (!clientConfig.doesConfigExists()) {
38
+ logger.error("Please, make sure that you are logged in");
39
+ return;
40
+ }
41
+
42
+ if (!environmentName) {
43
+ logger.error("Please, make sure to set an environment");
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const packageName = packagePath.replace("/", "");
49
+ const { limit, offset } = parsePaginationParams(page, pageSize);
50
+ const response = await preaggregatesApi.listPreaggregates(
51
+ organizationName,
52
+ environmentName,
53
+ packageName,
54
+ versionId,
55
+ sourceName,
56
+ limit,
57
+ offset,
58
+ {
59
+ baseURL: controlPlane,
60
+ headers: clientConfig.getHeaders(),
61
+ },
62
+ );
63
+
64
+ const totalCount = extractTotalCount(response.headers);
65
+ if (totalCount !== undefined) {
66
+ logger.info(`Total pre-aggregations available: ${totalCount}`);
67
+ }
68
+
69
+ console.log(prettifyJson(response.data));
70
+ } catch (e) {
71
+ logger.error("Failed to list pre-aggregations");
72
+ printOutError(e, debug);
73
+ }
74
+ }
@@ -0,0 +1,77 @@
1
+ import { RunsApi } from "../../client/api";
2
+ import { Configuration } from "../../client/configuration";
3
+ import { ClientConfiguration } from "../../clientconfiguration";
4
+ import { logger } from "../../logger";
5
+ import {
6
+ prettifyJson,
7
+ printOutError,
8
+ parsePaginationParams,
9
+ extractTotalCount,
10
+ } from "../../utils";
11
+
12
+ const runsApi = new RunsApi(new Configuration());
13
+
14
+ /**
15
+ * List the build/refresh runs for a package (most recent first). Runs are one
16
+ * package-scoped resource spanning both families — materialized sources and
17
+ * dimensional indexes. Optional filters narrow the list: `versionId` limits to
18
+ * the runs that impacted a version; `sourceName`/`dimension`/`measure` limit to runs
19
+ * targeting a specific unit (Phase C per-unit dispatch). Each run prints its
20
+ * `status`, `trigger`, `buildNumber`, target scope, and, when failed, its
21
+ * `error`. The per-unit build-plan graph is available via `cred get run`.
22
+ */
23
+ export async function listRuns(
24
+ controlPlane: string,
25
+ packagePath: string,
26
+ versionId: string | undefined,
27
+ debug: boolean,
28
+ sourceName?: string,
29
+ dimension?: string,
30
+ measure?: string,
31
+ page?: string | number,
32
+ pageSize?: string | number,
33
+ ) {
34
+ const clientConfig = new ClientConfiguration();
35
+ const organizationName = clientConfig.getOrganization();
36
+ const environmentName = clientConfig.getProject();
37
+
38
+ if (!clientConfig.doesConfigExists()) {
39
+ logger.error("Please, make sure that you are logged in");
40
+ return;
41
+ }
42
+
43
+ if (!environmentName) {
44
+ logger.error("Please, make sure to set an environment");
45
+ return;
46
+ }
47
+
48
+ try {
49
+ const packageName = packagePath.replace("/", "");
50
+ const { limit, offset } = parsePaginationParams(page, pageSize);
51
+ const response = await runsApi.listRuns(
52
+ organizationName,
53
+ environmentName,
54
+ packageName,
55
+ versionId,
56
+ sourceName,
57
+ measure,
58
+ dimension,
59
+ limit,
60
+ offset,
61
+ {
62
+ baseURL: controlPlane,
63
+ headers: clientConfig.getHeaders(),
64
+ },
65
+ );
66
+
67
+ const totalCount = extractTotalCount(response.headers);
68
+ if (totalCount !== undefined) {
69
+ logger.info(`Total runs available: ${totalCount}`);
70
+ }
71
+
72
+ console.log(prettifyJson(response.data));
73
+ } catch (e) {
74
+ logger.error("Failed to list runs");
75
+ printOutError(e, debug);
76
+ }
77
+ }
@@ -0,0 +1,123 @@
1
+ import { RunsApi, CreateRunRequest } from "../../client/api";
2
+ import { Configuration } from "../../client/configuration";
3
+ import { ClientConfiguration } from "../../clientconfiguration";
4
+ import { logger } from "../../logger";
5
+ import { getConfirmation, prettifyJson, printOutError } from "../../utils";
6
+
7
+ const runsApi = new RunsApi(new Configuration());
8
+
9
+ export interface RerunScope {
10
+ source?: string;
11
+ dimension?: string;
12
+ measure?: string;
13
+ modelFile?: string;
14
+ includeUpstream?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Trigger an on-demand run (rebuild/refresh) for a package version. With no
19
+ * scope selectors this is a full run: re-materialize every persist source and
20
+ * rebuild every indexed dimension. The optional `--source` / `--dimension` /
21
+ * `--measure` / `--model-file` selectors narrow it to a single unit (Phase C
22
+ * per-unit dispatch); `--measure` rebuilds the rollup serving that
23
+ * pre-aggregated measure, and fails when the measure is declared at more than
24
+ * one grain (pass `--source` to disambiguate); `--include-upstream` additionally force-rebuilds the target's
25
+ * upstream persisted closure. Prints the started run (poll it with
26
+ * `cred get run <package> <runId>`).
27
+ */
28
+ export async function rerun(
29
+ controlPlane: string,
30
+ packagePath: string,
31
+ versionId: string,
32
+ scope: RerunScope,
33
+ debug: boolean,
34
+ autoConfirm: boolean,
35
+ ) {
36
+ const clientConfig = new ClientConfiguration();
37
+ const organizationName = clientConfig.getOrganization();
38
+ const environmentName = clientConfig.getProject();
39
+
40
+ if (!clientConfig.doesConfigExists()) {
41
+ logger.error("Please, make sure that you are logged in");
42
+ return;
43
+ }
44
+
45
+ if (!environmentName) {
46
+ logger.error("Please, make sure to set an environment");
47
+ return;
48
+ }
49
+
50
+ if (!versionId) {
51
+ logger.error("Please provide a version ID");
52
+ return;
53
+ }
54
+
55
+ // Caught here rather than at the server so the message can name the flags. A
56
+ // source may carry a dimension and a measure of the same name, so which one
57
+ // you pass decides what gets rebuilt — there is no safe default.
58
+ if (scope.dimension && scope.measure) {
59
+ logger.error(
60
+ "--dimension and --measure both narrow a run to one field; pass only one",
61
+ );
62
+ return;
63
+ }
64
+
65
+ const packageName = packagePath.replace("/", "");
66
+
67
+ const request: CreateRunRequest = { versionId };
68
+ if (scope.source) request.sourceName = scope.source;
69
+ if (scope.dimension) request.dimension = scope.dimension;
70
+ if (scope.measure) request.measure = scope.measure;
71
+ if (scope.modelFile) request.modelFilePath = scope.modelFile;
72
+ if (scope.includeUpstream) request.includeUpstream = true;
73
+
74
+ const targetLabel = describeScope(scope);
75
+
76
+ if (!autoConfirm) {
77
+ const confirmation = await getConfirmation(
78
+ `Trigger a run for ${packageName}/${versionId} (${targetLabel})?`,
79
+ );
80
+ if (!confirmation) {
81
+ return;
82
+ }
83
+ }
84
+
85
+ logger.info(`Starting run for ${packageName}/${versionId} (${targetLabel})`);
86
+ try {
87
+ const response = await runsApi.createRun(
88
+ organizationName,
89
+ environmentName,
90
+ packageName,
91
+ request,
92
+ {
93
+ baseURL: controlPlane,
94
+ headers: clientConfig.getHeaders(),
95
+ },
96
+ );
97
+
98
+ console.log(prettifyJson(response.data));
99
+ } catch (e) {
100
+ logger.error("Failed to start run");
101
+ printOutError(e, debug);
102
+ }
103
+ }
104
+
105
+ function describeScope(scope: RerunScope): string {
106
+ const parts: string[] = [];
107
+ if (scope.source) parts.push(`source=${scope.source}`);
108
+ if (scope.dimension) parts.push(`dimension=${scope.dimension}`);
109
+ if (scope.measure) parts.push(`measure=${scope.measure}`);
110
+ if (scope.modelFile) parts.push(`modelFile=${scope.modelFile}`);
111
+ if (parts.length === 0) {
112
+ return "full run — all sources, indexes and pre-aggregations";
113
+ }
114
+ if (scope.includeUpstream) parts.push("includeUpstream");
115
+ const scoped = `scoped: ${parts.join(", ")}`;
116
+ // A measure names a COLUMN in a rollup table, and the whole table is what
117
+ // gets rebuilt — so `measure=revenue` alone understates what the caller is
118
+ // agreeing to. This string is the confirmation prompt, so the sibling
119
+ // measures have to be named as consequences before the y/n, not after.
120
+ return scope.measure
121
+ ? `${scoped} — rebuilds the measure's whole rollup table, and so every measure declared at the same grain`
122
+ : scoped;
123
+ }
@@ -1,12 +1,13 @@
1
+ import { spawn } from "child_process";
1
2
  import axios from "axios";
2
3
  import { expect } from "chai";
3
4
  import fs from "fs";
4
5
  import os from "os";
5
- import { join } from "path";
6
+ import { join, resolve } from "path";
6
7
  import sinon from "sinon";
7
8
  import * as credConstants from "../../constants";
8
9
  import { ClientConfiguration } from "../../clientconfiguration";
9
- import { login } from "./login";
10
+ import { isLoginInvocation, login } from "./login";
10
11
  import { OrganizationsApi } from "../../client/api";
11
12
  import * as loopbackOAuth from "./loopbackOAuth";
12
13
 
@@ -253,3 +254,118 @@ describe("login", () => {
253
254
  );
254
255
  });
255
256
  });
257
+
258
+ describe("isLoginInvocation", () => {
259
+ it("is true for a `cred login <org>` invocation", () => {
260
+ expect(isLoginInvocation(["node", "/x/index.js", "login", "myorg"])).to.be
261
+ .true;
262
+ });
263
+
264
+ it("is false for other commands (they stay fail-loud on a corrupt config)", () => {
265
+ expect(isLoginInvocation(["node", "/x/index.js", "status"])).to.be.false;
266
+ expect(isLoginInvocation(["node", "/x/index.js", "publish"])).to.be.false;
267
+ expect(isLoginInvocation(["node", "/x/index.js"])).to.be.false;
268
+ });
269
+ });
270
+
271
+ // Drives the real binary entrypoint (src/index.ts), not recoverIfCorrupt() in
272
+ // isolation. index.ts reads the active environment at module load (axios
273
+ // baseURL + command registration), so a corrupt ~/.cred used to throw before
274
+ // the login action ran — dead-ending the documented recovery path. This guards
275
+ // that ordering: with HOME pointed at a fixture dir holding a corrupt config,
276
+ // `index.ts login <org>` must reset the file rather than crash with the
277
+ // structure error.
278
+ describe("entrypoint recovery for a corrupt ~/.cred (real index.ts)", () => {
279
+ let tmpHome: string;
280
+ const indexPath = resolve(__dirname, "../../index.ts");
281
+ const cliRoot = resolve(__dirname, "../../..");
282
+
283
+ beforeEach(() => {
284
+ tmpHome = fs.mkdtempSync(join(os.tmpdir(), "cred-entrypoint-"));
285
+ });
286
+
287
+ afterEach(() => {
288
+ fs.rmSync(tmpHome, { recursive: true, force: true });
289
+ });
290
+
291
+ const findBackup = (): string | undefined =>
292
+ fs.readdirSync(tmpHome).find((f) => f.startsWith(".cred.corrupt-"));
293
+
294
+ it("backs up the corrupt file and resets instead of crashing", async function () {
295
+ this.timeout(60000);
296
+
297
+ // A structurally-invalid config: parses as JSON but has neither
298
+ // activeEnvironment nor configPerEnvironment — exactly what triggered the
299
+ // customer's "re-run cred login" loop.
300
+ const credPath = join(tmpHome, ".cred");
301
+ fs.writeFileSync(credPath, JSON.stringify({ garbage: true }));
302
+
303
+ const child = spawn(
304
+ process.execPath,
305
+ ["-r", "ts-node/register", indexPath, "login", "testorg"],
306
+ {
307
+ cwd: cliRoot,
308
+ env: {
309
+ ...process.env,
310
+ HOME: tmpHome,
311
+ USERPROFILE: tmpHome,
312
+ // Runtime behavior is what we're testing; skip type-checking the
313
+ // whole import graph so an unrelated type error can't derail it.
314
+ TS_NODE_TRANSPILE_ONLY: "1",
315
+ },
316
+ },
317
+ );
318
+
319
+ let output = "";
320
+ child.stdout.on("data", (d) => (output += d.toString()));
321
+ child.stderr.on("data", (d) => (output += d.toString()));
322
+
323
+ // Recovery runs synchronously at module load, before login() touches the
324
+ // network. It renames the corrupt file aside (.cred.corrupt-*) and THEN
325
+ // rewrites .cred to a valid default. Gate on the rewrite, not the backup:
326
+ // the backup appears first, so killing the instant it shows can catch the
327
+ // child in the gap before .cred is rewritten — a flaky ENOENT on the read
328
+ // below. resetComplete() only goes true once .cred is the valid default
329
+ // (which implies the backup already happened). Then stop the process
330
+ // (login otherwise blocks on the device-code flow).
331
+ const resetComplete = (): boolean => {
332
+ try {
333
+ return (
334
+ JSON.parse(fs.readFileSync(credPath, "utf8"))
335
+ .activeEnvironment === "production"
336
+ );
337
+ } catch {
338
+ return false; // .cred absent or mid-rewrite
339
+ }
340
+ };
341
+ await new Promise<void>((resolveWait) => {
342
+ const deadline = Date.now() + 55000;
343
+ const timer = setInterval(() => {
344
+ if (resetComplete() || Date.now() > deadline) {
345
+ clearInterval(timer);
346
+ resolveWait();
347
+ }
348
+ }, 200);
349
+ child.on("exit", () => {
350
+ clearInterval(timer);
351
+ resolveWait();
352
+ });
353
+ });
354
+ child.kill("SIGKILL");
355
+
356
+ const backup = findBackup();
357
+ expect(backup, "corrupt config should be backed up aside").to.be.a(
358
+ "string",
359
+ );
360
+ // The bad contents are preserved in the backup, not destroyed.
361
+ expect(
362
+ JSON.parse(fs.readFileSync(join(tmpHome, backup!), "utf8")),
363
+ ).to.deep.equal({ garbage: true });
364
+ // ~/.cred is rewritten to a valid default the next read can load.
365
+ const reset = JSON.parse(fs.readFileSync(credPath, "utf8"));
366
+ expect(reset.activeEnvironment).to.equal("production");
367
+ expect(reset.configPerEnvironment).to.be.an("object");
368
+ // The startup read no longer dead-ends on the structure error.
369
+ expect(output).to.not.match(/Invalid config structure/);
370
+ });
371
+ });
@@ -18,6 +18,20 @@ import * as loopbackOAuth from "./loopbackOAuth";
18
18
 
19
19
  const organizationsApi = new OrganizationsApi(new Configuration());
20
20
 
21
+ /**
22
+ * True when argv is a `cred login ...` invocation (argv = [node, script,
23
+ * command, ...args]).
24
+ *
25
+ * Recovery from a corrupt `~/.cred` is scoped to this command only and must run
26
+ * at startup (see index.ts), before the eager config reads there. `cred login`
27
+ * is the one command whose job is to (re)establish config and the documented
28
+ * reset path; every other command must stay fail-loud so a stray read never
29
+ * silently resets a user's tokens.
30
+ */
31
+ export function isLoginInvocation(argv: string[]): boolean {
32
+ return argv[2] === "login";
33
+ }
34
+
21
35
  export async function login(
22
36
  organizationName: string,
23
37
  controlPlaneBaseURL?: string,
@@ -144,4 +144,20 @@ describe("list-packages", () => {
144
144
 
145
145
  expect(consoleLogStub.called).to.be.true;
146
146
  });
147
+
148
+ it("should exit non-zero and not print on failure in printOutPackages", async () => {
149
+ // On error, listPackages returns undefined. printOutPackages must NOT print
150
+ // `undefined` to stdout (which breaks downstream JSON.parse) and must signal
151
+ // failure with a non-zero exit code.
152
+ listPackagesStub.rejects(new Error("API Error"));
153
+ const prevExitCode = process.exitCode;
154
+ process.exitCode = 0;
155
+
156
+ await printOutPackages("http://test-server", false);
157
+
158
+ expect(consoleLogStub.called).to.be.false;
159
+ expect(process.exitCode).to.equal(1);
160
+
161
+ process.exitCode = prevExitCode;
162
+ });
147
163
  });
@@ -63,5 +63,12 @@ export async function printOutPackages(
63
63
  pageSize?: string | number,
64
64
  ) {
65
65
  const packages = await listPackages(controlPlane, debug, page, pageSize);
66
+ if (!packages) {
67
+ // listPackages already logged the failure (to stderr). Signal failure with
68
+ // a non-zero exit code instead of printing `undefined` to stdout and
69
+ // exiting 0 — otherwise scripts/tests that JSON.parse stdout choke on it.
70
+ process.exitCode = 1;
71
+ return;
72
+ }
66
73
  console.log(prettifyJson(packages));
67
74
  }