@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.
- package/README.md +1 -1
- package/coverage/coverage-summary.json +40 -0
- package/coverage/tmp/coverage-2152-1789496974457-0.json +1 -0
- package/coverage/tmp/coverage-2153-1789496954404-0.json +1 -0
- package/dist/client/api.js +4193 -1342
- package/dist/client/api.js.map +1 -1
- package/dist/clientconfiguration.js +57 -4
- package/dist/clientconfiguration.js.map +1 -1
- package/dist/clientconfiguration.spec.js +90 -0
- package/dist/clientconfiguration.spec.js.map +1 -1
- package/dist/commands/add.js +18 -0
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/environment/updateenvironment.js +63 -1
- package/dist/commands/environment/updateenvironment.js.map +1 -1
- package/dist/commands/environment/updateenvironment.spec.js +84 -0
- package/dist/commands/environment/updateenvironment.spec.js.map +1 -1
- package/dist/commands/get.js +52 -1
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/list.js +78 -0
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/materialization/cancelrun.js +62 -0
- package/dist/commands/materialization/cancelrun.js.map +1 -0
- package/dist/commands/materialization/getindex.js +52 -0
- package/dist/commands/materialization/getindex.js.map +1 -0
- package/dist/commands/materialization/getmaterialization.js +54 -0
- package/dist/commands/materialization/getmaterialization.js.map +1 -0
- package/dist/commands/materialization/getpreaggregate.js +55 -0
- package/dist/commands/materialization/getpreaggregate.js.map +1 -0
- package/dist/commands/materialization/getrun.js +54 -0
- package/dist/commands/materialization/getrun.js.map +1 -0
- package/dist/commands/materialization/listindexes.js +58 -0
- package/dist/commands/materialization/listindexes.js.map +1 -0
- package/dist/commands/materialization/listmaterializations.js +52 -0
- package/dist/commands/materialization/listmaterializations.js.map +1 -0
- package/dist/commands/materialization/listpreaggregates.js +61 -0
- package/dist/commands/materialization/listpreaggregates.js.map +1 -0
- package/dist/commands/materialization/listruns.js +60 -0
- package/dist/commands/materialization/listruns.js.map +1 -0
- package/dist/commands/materialization/rerun.js +111 -0
- package/dist/commands/materialization/rerun.js.map +1 -0
- package/dist/commands/misc/login.js +14 -0
- package/dist/commands/misc/login.js.map +1 -1
- package/dist/commands/misc/login.spec.js +92 -0
- package/dist/commands/misc/login.spec.js.map +1 -1
- package/dist/commands/package/listpackages.js +7 -0
- package/dist/commands/package/listpackages.js.map +1 -1
- package/dist/commands/package/listpackages.spec.js +12 -0
- package/dist/commands/package/listpackages.spec.js.map +1 -1
- package/dist/commands/package/publish.js +34 -11
- package/dist/commands/package/publish.js.map +1 -1
- package/dist/commands/package/publish.spec.js +26 -28
- package/dist/commands/package/publish.spec.js.map +1 -1
- package/dist/commands/permission/createpermission.js +123 -0
- package/dist/commands/permission/createpermission.js.map +1 -0
- package/dist/commands/permission/createpermission.spec.js +360 -0
- package/dist/commands/permission/createpermission.spec.js.map +1 -0
- package/dist/commands/permission/deletepermission.js +86 -0
- package/dist/commands/permission/deletepermission.js.map +1 -0
- package/dist/commands/permission/getpermission.js +85 -0
- package/dist/commands/permission/getpermission.js.map +1 -0
- package/dist/commands/permission/listpermissions.js +75 -0
- package/dist/commands/permission/listpermissions.js.map +1 -0
- package/dist/commands/permission/scope.js +140 -0
- package/dist/commands/permission/scope.js.map +1 -0
- package/dist/commands/rm.js +11 -0
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/update.js +18 -12
- package/dist/commands/update.js.map +1 -1
- package/dist/common/UserGroupId.js +1 -1
- package/dist/common/UserGroupId.js.map +1 -1
- package/dist/common/UserGroupId.spec.js +2 -4
- package/dist/common/UserGroupId.spec.js.map +1 -1
- package/dist/constants.js +48 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +61 -6
- package/dist/index.js.map +1 -1
- package/dist/logger.js +5 -1
- package/dist/logger.js.map +1 -1
- package/openapitools.json +4 -1
- package/package.json +5 -3
- package/src/client/api.ts +6433 -1533
- package/src/clientconfiguration.spec.ts +126 -0
- package/src/clientconfiguration.ts +64 -4
- package/src/commands/add.ts +43 -0
- package/src/commands/environment/updateenvironment.spec.ts +167 -0
- package/src/commands/environment/updateenvironment.ts +85 -1
- package/src/commands/get.ts +110 -1
- package/src/commands/list.ts +171 -0
- package/src/commands/materialization/cancelrun.ts +70 -0
- package/src/commands/materialization/getindex.ts +55 -0
- package/src/commands/materialization/getmaterialization.ts +57 -0
- package/src/commands/materialization/getpreaggregate.ts +58 -0
- package/src/commands/materialization/getrun.ts +55 -0
- package/src/commands/materialization/listindexes.ts +69 -0
- package/src/commands/materialization/listmaterializations.ts +53 -0
- package/src/commands/materialization/listpreaggregates.ts +74 -0
- package/src/commands/materialization/listruns.ts +77 -0
- package/src/commands/materialization/rerun.ts +123 -0
- package/src/commands/misc/login.spec.ts +118 -2
- package/src/commands/misc/login.ts +14 -0
- package/src/commands/package/listpackages.spec.ts +16 -0
- package/src/commands/package/listpackages.ts +7 -0
- package/src/commands/package/publish.spec.ts +45 -30
- package/src/commands/package/publish.ts +31 -10
- package/src/commands/permission/createpermission.spec.ts +507 -0
- package/src/commands/permission/createpermission.ts +181 -0
- package/src/commands/permission/deletepermission.ts +117 -0
- package/src/commands/permission/getpermission.ts +115 -0
- package/src/commands/permission/listpermissions.ts +101 -0
- package/src/commands/permission/scope.ts +180 -0
- package/src/commands/rm.ts +29 -0
- package/src/commands/update.ts +50 -27
- package/src/common/UserGroupId.spec.ts +2 -4
- package/src/common/UserGroupId.ts +1 -1
- package/src/constants.ts +50 -1
- package/src/index.ts +94 -11
- package/src/logger.ts +5 -1
- 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
|
}
|