@knpkv/atlassian-common 1.8.0 → 1.9.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/CHANGELOG.md +17 -0
- package/dist/bin.js +3 -2
- package/dist/bin.js.map +1 -1
- package/dist/cli/index.d.ts +40 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +115 -0
- package/dist/cli/index.js.map +1 -0
- package/package.json +6 -2
- package/src/bin.ts +3 -3
- package/src/cli/index.ts +136 -0
- package/test/ApiUpdateWorkflows.test.ts +4 -2
- package/test/AuthProfiles.test.ts +7 -4
- package/test/ProfileManager.test.ts +13 -5
- package/test/PublicExports.test.ts +1 -0
- package/test/cli.errorHandling.test.ts +145 -0
- package/test/cliAuth.authCommand.test.ts +0 -1
- package/test/cliAuth.guardrails.test.ts +6 -4
- package/test/cliAuth.login.test.ts +0 -1
- package/test/cliAuth.openBrowser.test.ts +0 -1
- package/test/tsconfig.json +14 -0
- package/vitest.config.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @knpkv/atlassian-common
|
|
2
2
|
|
|
3
|
+
## 1.9.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#579](https://github.com/knpkv/npm/pull/579) [`61c930e`](https://github.com/knpkv/npm/commit/61c930e88f4a9d735522d6c7b052b72d13450a6f) Thanks [@konopkov](https://github.com/konopkov)! - Tests typecheck as part of `check`. `externalizeAdfMetadata` returns the sidecar type `hydrateAdfMetadata` takes.
|
|
8
|
+
|
|
9
|
+
## 1.9.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#479](https://github.com/knpkv/npm/pull/479) [`1ef1847`](https://github.com/knpkv/npm/commit/1ef1847c867f83de237dec1c454b6bb5e40183ef) Thanks [@konopkov](https://github.com/konopkov)! - One error report for every Atlassian CLI (`jira`, `confluence`, `atlassian`), from the new `@knpkv/atlassian-common/cli` subpath (`handleCliError`, `withCliErrorHandling`, `Verbose`, `commandArgs`, `verboseRequested`).
|
|
14
|
+
|
|
15
|
+
- A failed command prints the error's message; a defect prints `Error: <message>`. The full cause, with stack traces, prints only with the new global `--verbose` flag or `DEBUG=1`.
|
|
16
|
+
- jira: stderr no longer prints the pretty-printed cause and stack trace by default.
|
|
17
|
+
- atlassian: tagged errors print their message instead of `String(error)`, and defects are reported too.
|
|
18
|
+
- Help and usage errors that the CLI already rendered are not printed a second time. Exit codes are unchanged.
|
|
19
|
+
|
|
3
20
|
## 1.8.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/dist/bin.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as Effect from "effect/Effect";
|
|
|
6
6
|
import * as Layer from "effect/Layer";
|
|
7
7
|
import * as Stdio from "effect/Stdio";
|
|
8
8
|
import pkg from "../package.json" with { type: "json" };
|
|
9
|
+
import { Verbose, withCliErrorHandling } from "./cli/index.js";
|
|
9
10
|
import { HomeDirectoryLive, inspectAllToolProfiles, migrateLegacyProfiles, refreshActiveProfiles, useProfileForAllTools } from "./config/index.js";
|
|
10
11
|
const profileArg = Args.String("profile").pipe(Args.withDescription("Profile ID, name, site URL, cloud ID, or account ID"));
|
|
11
12
|
const printStatuses = (statuses) => Effect.gen(function* () {
|
|
@@ -47,12 +48,12 @@ const profiles = Command.make("profiles").pipe(Command.withDescription("Manage s
|
|
|
47
48
|
const authRefresh = Command.make("refresh", {}, () => refreshActiveProfiles().pipe(Effect.flatMap(printStatuses)))
|
|
48
49
|
.pipe(Command.withDescription("Refresh expired active OAuth tokens"));
|
|
49
50
|
const auth = Command.make("auth").pipe(Command.withDescription("Manage Atlassian authentication"), Command.withSubcommands([authRefresh]));
|
|
50
|
-
const atlassian = Command.make("atlassian", {}, () => Console.log("Usage: atlassian profiles|auth")).pipe(Command.withDescription("Unified Atlassian profile manager"), Command.withSubcommands([profiles, auth]));
|
|
51
|
+
const atlassian = Command.make("atlassian", {}, () => Console.log("Usage: atlassian profiles|auth")).pipe(Command.withDescription("Unified Atlassian profile manager"), Command.withGlobalFlags([Verbose]), Command.withSubcommands([profiles, auth]));
|
|
51
52
|
const cli = Command.runWith(atlassian, { version: pkg.version });
|
|
52
53
|
const program = Effect.gen(function* () {
|
|
53
54
|
const stdio = yield* Stdio.Stdio;
|
|
54
55
|
const args = yield* stdio.args;
|
|
55
56
|
return yield* cli(args);
|
|
56
|
-
}).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerFetch, HomeDirectoryLive))
|
|
57
|
+
}).pipe(withCliErrorHandling, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerFetch, HomeDirectoryLive)));
|
|
57
58
|
NodeRuntime.runMain(program, { disableErrorReporting: true });
|
|
58
59
|
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an Atlassian command-line tool reports a failed run.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **The message is the report.** A failure prints its message; a defect prints `Error: <message>`;
|
|
7
|
+
* an interruption prints `Interrupted`. The full cause, with stack traces, is for whoever asked for
|
|
8
|
+
* it: `--verbose`, or `DEBUG=1` in the environment.
|
|
9
|
+
* - **What the CLI already showed is not repeated.** `Command.runWith` renders help and usage errors
|
|
10
|
+
* itself and then fails with them, so those reasons are skipped unless verbose.
|
|
11
|
+
* - **The exit code is untouched.** The cause is reported and re-failed as it was, so a help request
|
|
12
|
+
* still exits 0 and every failure still exits non-zero.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import * as Cause from "effect/Cause";
|
|
17
|
+
import { GlobalFlag } from "effect/cli";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as Stdio from "effect/Stdio";
|
|
20
|
+
/** `--verbose`: print the full cause, with stack traces, when a command fails. Register on the root command. */
|
|
21
|
+
export declare const Verbose: GlobalFlag.Setting<"verbose", boolean>;
|
|
22
|
+
/** Whether `args` turn {@link Verbose} on, as the flag parser would read them. */
|
|
23
|
+
export declare const verboseFlagIn: (args: ReadonlyArray<string>) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* `args` without the {@link Verbose} flag and the value it consumed, for code that routes on the
|
|
26
|
+
* command words before the CLI parses them, such as choosing which service layer to build.
|
|
27
|
+
*/
|
|
28
|
+
export declare const commandArgs: (args: ReadonlyArray<string>) => ReadonlyArray<string>;
|
|
29
|
+
/** `--verbose` in `args`, or `DEBUG=1`. Only `1`: `DEBUG` is also the debug package's namespace list. */
|
|
30
|
+
export declare const verboseRequested: (args: ReadonlyArray<string>) => Effect.Effect<boolean>;
|
|
31
|
+
/** Print a failed run's cause to stderr: each failure's message, or the full cause when verbose. */
|
|
32
|
+
export declare const handleCliError: <E>(cause: Cause.Cause<E>, options: {
|
|
33
|
+
readonly verbose: boolean;
|
|
34
|
+
}) => Effect.Effect<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Report a CLI program's failure with {@link handleCliError} and fail with the same cause. Apply it
|
|
37
|
+
* before providing `Stdio`, which it reads the arguments from.
|
|
38
|
+
*/
|
|
39
|
+
export declare const withCliErrorHandling: <A, E, R>(program: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | Stdio.Stdio>;
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EAAkB,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,gHAAgH;AAChH,eAAO,MAAM,OAAO,wCAKlB,CAAA;AAiDF,kFAAkF;AAClF,eAAO,MAAM,aAAa,SAAU,aAAa,CAAC,MAAM,CAAC,KAAG,OAA6C,CAAA;AAEzG;;;GAGG;AACH,eAAO,MAAM,WAAW,SAAU,aAAa,CAAC,MAAM,CAAC,KAAG,aAAa,CAAC,MAAM,CAA2B,CAAA;AAEzG,yGAAyG;AACzG,eAAO,MAAM,gBAAgB,SAAU,aAAa,CAAC,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAMtC,CAAA;AAc9C,oGAAoG;AACpG,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAS3F,CAAA;AASzB;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,WACjC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAC0E,CAAA"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an Atlassian command-line tool reports a failed run.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **The message is the report.** A failure prints its message; a defect prints `Error: <message>`;
|
|
7
|
+
* an interruption prints `Interrupted`. The full cause, with stack traces, is for whoever asked for
|
|
8
|
+
* it: `--verbose`, or `DEBUG=1` in the environment.
|
|
9
|
+
* - **What the CLI already showed is not repeated.** `Command.runWith` renders help and usage errors
|
|
10
|
+
* itself and then fails with them, so those reasons are skipped unless verbose.
|
|
11
|
+
* - **The exit code is untouched.** The cause is reported and re-failed as it was, so a help request
|
|
12
|
+
* still exits 0 and every failure still exits non-zero.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import * as Cause from "effect/Cause";
|
|
17
|
+
import { CliError, Flag, GlobalFlag } from "effect/cli";
|
|
18
|
+
import * as Config from "effect/Config";
|
|
19
|
+
import * as Console from "effect/Console";
|
|
20
|
+
import * as Effect from "effect/Effect";
|
|
21
|
+
import * as Option from "effect/Option";
|
|
22
|
+
import * as Predicate from "effect/Predicate";
|
|
23
|
+
import * as Stdio from "effect/Stdio";
|
|
24
|
+
/** `--verbose`: print the full cause, with stack traces, when a command fails. Register on the root command. */
|
|
25
|
+
export const Verbose = GlobalFlag.Setting("verbose")({
|
|
26
|
+
flag: Flag.Boolean("verbose").pipe(Flag.withDefault(false), Flag.withDescription("Print the full cause, with stack traces, when a command fails"))
|
|
27
|
+
});
|
|
28
|
+
const truthy = new Set(["true", "1", "y", "yes", "on"]);
|
|
29
|
+
const falsy = new Set(["false", "0", "n", "no", "off"]);
|
|
30
|
+
/** Exactly the literals `effect/cli` accepts for a boolean: lowercase only, so `FALSE` is not one. */
|
|
31
|
+
const booleanLiteral = (token) => token === undefined ? undefined : truthy.has(token) ? true : falsy.has(token) ? false : undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Read {@link Verbose} out of `args` the way `effect/cli` parses a boolean flag: only before `--`;
|
|
34
|
+
* `--verbose` may take a following boolean literal (`--verbose false`), `--verbose=<boolean>` and
|
|
35
|
+
* `--no-verbose` are the other spellings, and the first occurrence decides the value.
|
|
36
|
+
*/
|
|
37
|
+
const scanVerbose = (args) => {
|
|
38
|
+
const end = args.indexOf("--");
|
|
39
|
+
const head = end === -1 ? args : args.slice(0, end);
|
|
40
|
+
const tail = end === -1 ? [] : args.slice(end);
|
|
41
|
+
const rest = [];
|
|
42
|
+
let verbose;
|
|
43
|
+
const settle = (value) => {
|
|
44
|
+
if (verbose === undefined)
|
|
45
|
+
verbose = value;
|
|
46
|
+
};
|
|
47
|
+
for (let index = 0; index < head.length; index++) {
|
|
48
|
+
const token = head[index];
|
|
49
|
+
if (token === "--verbose") {
|
|
50
|
+
const literal = booleanLiteral(head[index + 1]);
|
|
51
|
+
if (literal !== undefined)
|
|
52
|
+
index++;
|
|
53
|
+
settle(literal ?? true);
|
|
54
|
+
}
|
|
55
|
+
else if (token === "--no-verbose") {
|
|
56
|
+
settle(false);
|
|
57
|
+
}
|
|
58
|
+
else if (token.startsWith("--verbose=")) {
|
|
59
|
+
// A value that is not a boolean makes the parser fail; it never turns verbose on.
|
|
60
|
+
const literal = booleanLiteral(token.slice("--verbose=".length));
|
|
61
|
+
if (literal !== undefined)
|
|
62
|
+
settle(literal);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
rest.push(token);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { rest: [...rest, ...tail], verbose };
|
|
69
|
+
};
|
|
70
|
+
/** Whether `args` turn {@link Verbose} on, as the flag parser would read them. */
|
|
71
|
+
export const verboseFlagIn = (args) => scanVerbose(args).verbose === true;
|
|
72
|
+
/**
|
|
73
|
+
* `args` without the {@link Verbose} flag and the value it consumed, for code that routes on the
|
|
74
|
+
* command words before the CLI parses them, such as choosing which service layer to build.
|
|
75
|
+
*/
|
|
76
|
+
export const commandArgs = (args) => scanVerbose(args).rest;
|
|
77
|
+
/** `--verbose` in `args`, or `DEBUG=1`. Only `1`: `DEBUG` is also the debug package's namespace list. */
|
|
78
|
+
export const verboseRequested = (args) => verboseFlagIn(args)
|
|
79
|
+
? Effect.succeed(true)
|
|
80
|
+
: Effect.gen(function* () {
|
|
81
|
+
const debug = yield* Config.option(Config.String("DEBUG"));
|
|
82
|
+
return Option.getOrUndefined(debug) === "1";
|
|
83
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
84
|
+
const describe = (error) => {
|
|
85
|
+
if (Predicate.hasProperty(error, "message") && Predicate.isString(error.message) && error.message !== "") {
|
|
86
|
+
return error.message;
|
|
87
|
+
}
|
|
88
|
+
if (Predicate.hasProperty(error, "_tag") && Predicate.isString(error._tag))
|
|
89
|
+
return error._tag;
|
|
90
|
+
return String(error);
|
|
91
|
+
};
|
|
92
|
+
/** `Command.runWith` prints help and user errors itself before failing with them; nothing else. */
|
|
93
|
+
const renderedByRunner = (error) => CliError.isCliError(error) && (error._tag === "ShowHelp" || error._tag === "UserError");
|
|
94
|
+
/** Print a failed run's cause to stderr: each failure's message, or the full cause when verbose. */
|
|
95
|
+
export const handleCliError = (cause, options) => options.verbose
|
|
96
|
+
? Console.error(Cause.pretty(cause))
|
|
97
|
+
: Effect.forEach(cause.reasons, (reason) => {
|
|
98
|
+
if (Cause.isFailReason(reason)) {
|
|
99
|
+
return renderedByRunner(reason.error) ? Effect.void : Console.error(describe(reason.error));
|
|
100
|
+
}
|
|
101
|
+
if (Cause.isDieReason(reason))
|
|
102
|
+
return Console.error(`Error: ${describe(reason.defect)}`);
|
|
103
|
+
return Console.error("Interrupted");
|
|
104
|
+
}, { discard: true });
|
|
105
|
+
const reportFailure = (cause) => Effect.gen(function* () {
|
|
106
|
+
const stdio = yield* Stdio.Stdio;
|
|
107
|
+
const verbose = yield* stdio.args.pipe(Effect.flatMap(verboseRequested), Effect.orElseSucceed(() => false));
|
|
108
|
+
yield* handleCliError(cause, { verbose });
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Report a CLI program's failure with {@link handleCliError} and fail with the same cause. Apply it
|
|
112
|
+
* before providing `Stdio`, which it reads the arguments from.
|
|
113
|
+
*/
|
|
114
|
+
export const withCliErrorHandling = (program) => program.pipe(Effect.catchCause((cause) => reportFailure(cause).pipe(Effect.andThen(Effect.failCause(cause)))));
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/atlassian-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Shared utilities for Atlassian tools (AST, serializers, auth, config)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"import": "./dist/auth/index.js",
|
|
43
43
|
"types": "./dist/auth/index.d.ts"
|
|
44
44
|
},
|
|
45
|
+
"./cli": {
|
|
46
|
+
"import": "./dist/cli/index.js",
|
|
47
|
+
"types": "./dist/cli/index.d.ts"
|
|
48
|
+
},
|
|
45
49
|
"./cli-auth": {
|
|
46
50
|
"import": "./dist/cli-auth/index.js",
|
|
47
51
|
"types": "./dist/cli-auth/index.d.ts"
|
|
@@ -99,6 +103,6 @@
|
|
|
99
103
|
"test:watch": "vitest",
|
|
100
104
|
"lint": "eslint src",
|
|
101
105
|
"lint:fix": "eslint src --fix",
|
|
102
|
-
"check": "tsc --noEmit"
|
|
106
|
+
"check": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit"
|
|
103
107
|
}
|
|
104
108
|
}
|
package/src/bin.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as Effect from "effect/Effect"
|
|
|
6
6
|
import * as Layer from "effect/Layer"
|
|
7
7
|
import * as Stdio from "effect/Stdio"
|
|
8
8
|
import pkg from "../package.json" with { type: "json" }
|
|
9
|
+
import { Verbose, withCliErrorHandling } from "./cli/index.js"
|
|
9
10
|
import {
|
|
10
11
|
HomeDirectoryLive,
|
|
11
12
|
inspectAllToolProfiles,
|
|
@@ -90,6 +91,7 @@ const auth = Command.make("auth").pipe(
|
|
|
90
91
|
|
|
91
92
|
const atlassian = Command.make("atlassian", {}, () => Console.log("Usage: atlassian profiles|auth")).pipe(
|
|
92
93
|
Command.withDescription("Unified Atlassian profile manager"),
|
|
94
|
+
Command.withGlobalFlags([Verbose]),
|
|
93
95
|
Command.withSubcommands([profiles, auth])
|
|
94
96
|
)
|
|
95
97
|
|
|
@@ -99,15 +101,13 @@ const program = Effect.gen(function*() {
|
|
|
99
101
|
const args = yield* stdio.args
|
|
100
102
|
return yield* cli(args)
|
|
101
103
|
}).pipe(
|
|
104
|
+
withCliErrorHandling,
|
|
102
105
|
Effect.provide(
|
|
103
106
|
Layer.mergeAll(
|
|
104
107
|
NodeServices.layer,
|
|
105
108
|
NodeHttpClient.layerFetch,
|
|
106
109
|
HomeDirectoryLive
|
|
107
110
|
)
|
|
108
|
-
),
|
|
109
|
-
Effect.catch(<UnparsedInput>(error: UnparsedInput) =>
|
|
110
|
-
Console.error(String(error)).pipe(Effect.andThen(Effect.fail(error)))
|
|
111
111
|
)
|
|
112
112
|
)
|
|
113
113
|
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an Atlassian command-line tool reports a failed run.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - **The message is the report.** A failure prints its message; a defect prints `Error: <message>`;
|
|
7
|
+
* an interruption prints `Interrupted`. The full cause, with stack traces, is for whoever asked for
|
|
8
|
+
* it: `--verbose`, or `DEBUG=1` in the environment.
|
|
9
|
+
* - **What the CLI already showed is not repeated.** `Command.runWith` renders help and usage errors
|
|
10
|
+
* itself and then fails with them, so those reasons are skipped unless verbose.
|
|
11
|
+
* - **The exit code is untouched.** The cause is reported and re-failed as it was, so a help request
|
|
12
|
+
* still exits 0 and every failure still exits non-zero.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import * as Cause from "effect/Cause"
|
|
17
|
+
import { CliError, Flag, GlobalFlag } from "effect/cli"
|
|
18
|
+
import * as Config from "effect/Config"
|
|
19
|
+
import * as Console from "effect/Console"
|
|
20
|
+
import * as Effect from "effect/Effect"
|
|
21
|
+
import * as Option from "effect/Option"
|
|
22
|
+
import * as Predicate from "effect/Predicate"
|
|
23
|
+
import * as Stdio from "effect/Stdio"
|
|
24
|
+
|
|
25
|
+
/** `--verbose`: print the full cause, with stack traces, when a command fails. Register on the root command. */
|
|
26
|
+
export const Verbose = GlobalFlag.Setting("verbose")({
|
|
27
|
+
flag: Flag.Boolean("verbose").pipe(
|
|
28
|
+
Flag.withDefault(false),
|
|
29
|
+
Flag.withDescription("Print the full cause, with stack traces, when a command fails")
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const truthy = new Set(["true", "1", "y", "yes", "on"])
|
|
34
|
+
const falsy = new Set(["false", "0", "n", "no", "off"])
|
|
35
|
+
|
|
36
|
+
/** Exactly the literals `effect/cli` accepts for a boolean: lowercase only, so `FALSE` is not one. */
|
|
37
|
+
const booleanLiteral = (token: string | undefined): boolean | undefined =>
|
|
38
|
+
token === undefined ? undefined : truthy.has(token) ? true : falsy.has(token) ? false : undefined
|
|
39
|
+
|
|
40
|
+
interface VerboseScan {
|
|
41
|
+
/** The value the flag parser settles on, or undefined when the flag is absent. */
|
|
42
|
+
readonly verbose: boolean | undefined
|
|
43
|
+
/** `args` with every token the flag consumed removed. */
|
|
44
|
+
readonly rest: ReadonlyArray<string>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read {@link Verbose} out of `args` the way `effect/cli` parses a boolean flag: only before `--`;
|
|
49
|
+
* `--verbose` may take a following boolean literal (`--verbose false`), `--verbose=<boolean>` and
|
|
50
|
+
* `--no-verbose` are the other spellings, and the first occurrence decides the value.
|
|
51
|
+
*/
|
|
52
|
+
const scanVerbose = (args: ReadonlyArray<string>): VerboseScan => {
|
|
53
|
+
const end = args.indexOf("--")
|
|
54
|
+
const head = end === -1 ? args : args.slice(0, end)
|
|
55
|
+
const tail = end === -1 ? [] : args.slice(end)
|
|
56
|
+
const rest: Array<string> = []
|
|
57
|
+
let verbose: boolean | undefined
|
|
58
|
+
const settle = (value: boolean) => {
|
|
59
|
+
if (verbose === undefined) verbose = value
|
|
60
|
+
}
|
|
61
|
+
for (let index = 0; index < head.length; index++) {
|
|
62
|
+
const token = head[index]!
|
|
63
|
+
if (token === "--verbose") {
|
|
64
|
+
const literal = booleanLiteral(head[index + 1])
|
|
65
|
+
if (literal !== undefined) index++
|
|
66
|
+
settle(literal ?? true)
|
|
67
|
+
} else if (token === "--no-verbose") {
|
|
68
|
+
settle(false)
|
|
69
|
+
} else if (token.startsWith("--verbose=")) {
|
|
70
|
+
// A value that is not a boolean makes the parser fail; it never turns verbose on.
|
|
71
|
+
const literal = booleanLiteral(token.slice("--verbose=".length))
|
|
72
|
+
if (literal !== undefined) settle(literal)
|
|
73
|
+
} else {
|
|
74
|
+
rest.push(token)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { rest: [...rest, ...tail], verbose }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether `args` turn {@link Verbose} on, as the flag parser would read them. */
|
|
81
|
+
export const verboseFlagIn = (args: ReadonlyArray<string>): boolean => scanVerbose(args).verbose === true
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `args` without the {@link Verbose} flag and the value it consumed, for code that routes on the
|
|
85
|
+
* command words before the CLI parses them, such as choosing which service layer to build.
|
|
86
|
+
*/
|
|
87
|
+
export const commandArgs = (args: ReadonlyArray<string>): ReadonlyArray<string> => scanVerbose(args).rest
|
|
88
|
+
|
|
89
|
+
/** `--verbose` in `args`, or `DEBUG=1`. Only `1`: `DEBUG` is also the debug package's namespace list. */
|
|
90
|
+
export const verboseRequested = (args: ReadonlyArray<string>): Effect.Effect<boolean> =>
|
|
91
|
+
verboseFlagIn(args)
|
|
92
|
+
? Effect.succeed(true)
|
|
93
|
+
: Effect.gen(function*() {
|
|
94
|
+
const debug = yield* Config.option(Config.String("DEBUG"))
|
|
95
|
+
return Option.getOrUndefined(debug) === "1"
|
|
96
|
+
}).pipe(Effect.orElseSucceed(() => false))
|
|
97
|
+
|
|
98
|
+
const describe = <E>(error: E): string => {
|
|
99
|
+
if (Predicate.hasProperty(error, "message") && Predicate.isString(error.message) && error.message !== "") {
|
|
100
|
+
return error.message
|
|
101
|
+
}
|
|
102
|
+
if (Predicate.hasProperty(error, "_tag") && Predicate.isString(error._tag)) return error._tag
|
|
103
|
+
return String(error)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `Command.runWith` prints help and user errors itself before failing with them; nothing else. */
|
|
107
|
+
const renderedByRunner = <E>(error: E): boolean =>
|
|
108
|
+
CliError.isCliError(error) && (error._tag === "ShowHelp" || error._tag === "UserError")
|
|
109
|
+
|
|
110
|
+
/** Print a failed run's cause to stderr: each failure's message, or the full cause when verbose. */
|
|
111
|
+
export const handleCliError = <E>(cause: Cause.Cause<E>, options: { readonly verbose: boolean }): Effect.Effect<void> =>
|
|
112
|
+
options.verbose
|
|
113
|
+
? Console.error(Cause.pretty(cause))
|
|
114
|
+
: Effect.forEach(cause.reasons, (reason) => {
|
|
115
|
+
if (Cause.isFailReason(reason)) {
|
|
116
|
+
return renderedByRunner(reason.error) ? Effect.void : Console.error(describe(reason.error))
|
|
117
|
+
}
|
|
118
|
+
if (Cause.isDieReason(reason)) return Console.error(`Error: ${describe(reason.defect)}`)
|
|
119
|
+
return Console.error("Interrupted")
|
|
120
|
+
}, { discard: true })
|
|
121
|
+
|
|
122
|
+
const reportFailure = <E>(cause: Cause.Cause<E>): Effect.Effect<void, never, Stdio.Stdio> =>
|
|
123
|
+
Effect.gen(function*() {
|
|
124
|
+
const stdio = yield* Stdio.Stdio
|
|
125
|
+
const verbose = yield* stdio.args.pipe(Effect.flatMap(verboseRequested), Effect.orElseSucceed(() => false))
|
|
126
|
+
yield* handleCliError(cause, { verbose })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Report a CLI program's failure with {@link handleCliError} and fail with the same cause. Apply it
|
|
131
|
+
* before providing `Stdio`, which it reads the arguments from.
|
|
132
|
+
*/
|
|
133
|
+
export const withCliErrorHandling = <A, E, R>(
|
|
134
|
+
program: Effect.Effect<A, E, R>
|
|
135
|
+
): Effect.Effect<A, E, R | Stdio.Stdio> =>
|
|
136
|
+
program.pipe(Effect.catchCause((cause) => reportFailure(cause).pipe(Effect.andThen(Effect.failCause(cause)))))
|
|
@@ -45,9 +45,11 @@ const PackageManifest = Schema.Struct({
|
|
|
45
45
|
|
|
46
46
|
// Public generated subpaths or generated export targets declare the contract;
|
|
47
47
|
// package names and consumer dependencies do not.
|
|
48
|
+
const isJsonArray = Schema.is(Schema.Array(Schema.Json))
|
|
49
|
+
|
|
48
50
|
const exposesGeneratedContract = (exports: Schema.Json): boolean => {
|
|
49
51
|
if (Predicate.isString(exports)) return /(?:^|\/)generated(?:\/|$)/u.test(exports)
|
|
50
|
-
if (
|
|
52
|
+
if (isJsonArray(exports)) return exports.some(exposesGeneratedContract)
|
|
51
53
|
if (!isRecord(exports)) return false
|
|
52
54
|
return Object.entries(exports).some(([name, target]) =>
|
|
53
55
|
/^\.\/generated(?:\/|$)/u.test(name) || exposesGeneratedContract(target)
|
|
@@ -178,7 +180,7 @@ const inspectApiUpdateWorkflows = Effect.fn("ApiUpdateWorkflows.inspectApiUpdate
|
|
|
178
180
|
}
|
|
179
181
|
const workflowRoot = path.join(root, ".github/workflows")
|
|
180
182
|
const diagnostics: Array<string> = []
|
|
181
|
-
for (const name of (yield* fileSystem.readDirectory(workflowRoot)).
|
|
183
|
+
for (const name of [...(yield* fileSystem.readDirectory(workflowRoot))].sort()) {
|
|
182
184
|
if (!/-api-update\.ya?ml$/u.test(name)) continue
|
|
183
185
|
const source = yield* fileSystem.readFileString(path.join(workflowRoot, name))
|
|
184
186
|
for (const diagnostic of patchGuidanceDiagnostics(source, generatedClientNames)) {
|
|
@@ -2,10 +2,11 @@ import { describe, expect, it } from "@effect/vitest"
|
|
|
2
2
|
import { ConfigProvider, Effect, Layer } from "effect"
|
|
3
3
|
import * as FileSystem from "effect/FileSystem"
|
|
4
4
|
import * as Path from "effect/Path"
|
|
5
|
-
import { SystemError } from "effect/PlatformError"
|
|
5
|
+
import { PlatformError, SystemError } from "effect/PlatformError"
|
|
6
6
|
import {
|
|
7
7
|
deleteProfileBySelector,
|
|
8
8
|
HomeDirectoryLive,
|
|
9
|
+
type HomeDirectoryTag,
|
|
9
10
|
loadActiveProfileToken,
|
|
10
11
|
loadProfiles,
|
|
11
12
|
type OAuthToken,
|
|
@@ -21,7 +22,9 @@ const profilesPath = `${basePath}/profiles.json`
|
|
|
21
22
|
|
|
22
23
|
const fsError = (method: string) =>
|
|
23
24
|
Effect.fail(
|
|
24
|
-
new
|
|
25
|
+
new PlatformError(
|
|
26
|
+
new SystemError({ _tag: "NotFound", module: "FileSystem", method, description: "mock not found" })
|
|
27
|
+
)
|
|
25
28
|
)
|
|
26
29
|
|
|
27
30
|
const makeToken = (n: number): OAuthToken => ({
|
|
@@ -57,7 +60,7 @@ const makeMockFS = (files: Record<string, string> = {}) => {
|
|
|
57
60
|
|
|
58
61
|
return {
|
|
59
62
|
store,
|
|
60
|
-
layer:
|
|
63
|
+
layer: FileSystem.layerNoop(partial)
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -66,7 +69,7 @@ const ConfigProviderLive = ConfigProvider.layer(
|
|
|
66
69
|
)
|
|
67
70
|
|
|
68
71
|
const run = <A>(
|
|
69
|
-
effect: Effect.Effect<A, unknown, FileSystem.FileSystem | Path.Path>,
|
|
72
|
+
effect: Effect.Effect<A, unknown, FileSystem.FileSystem | Path.Path | HomeDirectoryTag>,
|
|
70
73
|
files?: Record<string, string>
|
|
71
74
|
) => {
|
|
72
75
|
const mock = makeMockFS(files)
|
|
@@ -6,10 +6,11 @@ import * as Fiber from "effect/Fiber"
|
|
|
6
6
|
import * as FileSystem from "effect/FileSystem"
|
|
7
7
|
import { HttpClient, HttpClientResponse } from "effect/http"
|
|
8
8
|
import * as Path from "effect/Path"
|
|
9
|
-
import { SystemError } from "effect/PlatformError"
|
|
9
|
+
import { PlatformError, SystemError } from "effect/PlatformError"
|
|
10
10
|
import {
|
|
11
11
|
type AtlassianToolDefinition,
|
|
12
12
|
HomeDirectoryLive,
|
|
13
|
+
type HomeDirectoryTag,
|
|
13
14
|
inspectAllToolProfiles,
|
|
14
15
|
migrateLegacyProfiles,
|
|
15
16
|
MissingOAuthConfigError,
|
|
@@ -39,7 +40,11 @@ const sharedStoreTools: ReadonlyArray<AtlassianToolDefinition> = [
|
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
const fsError = (method: string) =>
|
|
42
|
-
Effect.fail(
|
|
43
|
+
Effect.fail(
|
|
44
|
+
new PlatformError(
|
|
45
|
+
new SystemError({ _tag: "NotFound", module: "FileSystem", method, description: "mock not found" })
|
|
46
|
+
)
|
|
47
|
+
)
|
|
43
48
|
|
|
44
49
|
const makeToken = (n: number, scope = "read:me offline_access", expiresAt = Date.now() + 60_000): OAuthToken => ({
|
|
45
50
|
access_token: `access-${n}`,
|
|
@@ -69,13 +74,13 @@ const makeMockFS = () => {
|
|
|
69
74
|
}
|
|
70
75
|
return {
|
|
71
76
|
store,
|
|
72
|
-
layer:
|
|
77
|
+
layer: FileSystem.layerNoop(partial)
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
const ConfigProviderLive = ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: TEST_HOME } }))
|
|
77
82
|
|
|
78
|
-
const run = async <A>(effect: Effect.Effect<A, unknown, FileSystem.FileSystem | Path.Path>) => {
|
|
83
|
+
const run = async <A>(effect: Effect.Effect<A, unknown, FileSystem.FileSystem | Path.Path | HomeDirectoryTag>) => {
|
|
79
84
|
const mock = makeMockFS()
|
|
80
85
|
const layer = Layer.mergeAll(mock.layer, Path.layer, HomeDirectoryLive, ConfigProviderLive)
|
|
81
86
|
const result = await Effect.runPromise(effect.pipe(Effect.provide(layer)))
|
|
@@ -242,7 +247,10 @@ describe("ProfileManager", () => {
|
|
|
242
247
|
Effect.gen(function*() {
|
|
243
248
|
yield* saveProfileToken("tool-a", makeToken(1, "read:me offline_access", Date.now() - 1_000))
|
|
244
249
|
return yield* refreshActiveProfiles([tools[0]!])
|
|
245
|
-
})
|
|
250
|
+
}).pipe(
|
|
251
|
+
// The refresh fails on configuration before any request; a request would be a regression.
|
|
252
|
+
Effect.provideService(HttpClient.HttpClient, HttpClient.make(() => Effect.die("no request expected")))
|
|
253
|
+
)
|
|
246
254
|
)
|
|
247
255
|
).rejects.toBeInstanceOf(MissingOAuthConfigError)
|
|
248
256
|
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** @effect-diagnostics strictEffectProvide:skip-file */
|
|
2
|
+
/**
|
|
3
|
+
* How every Atlassian CLI reports a failed run: the message by default, the full cause when asked,
|
|
4
|
+
* nothing for what `Command.runWith` already rendered, and the exit unchanged.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeServices } from "@effect/platform-node"
|
|
7
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
8
|
+
import { ConfigProvider } from "effect"
|
|
9
|
+
import * as Cause from "effect/Cause"
|
|
10
|
+
import { Argument, CliError, Command } from "effect/cli"
|
|
11
|
+
import * as Console from "effect/Console"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import * as Exit from "effect/Exit"
|
|
14
|
+
import * as Schema from "effect/Schema"
|
|
15
|
+
import * as Stdio from "effect/Stdio"
|
|
16
|
+
import {
|
|
17
|
+
commandArgs,
|
|
18
|
+
handleCliError,
|
|
19
|
+
Verbose,
|
|
20
|
+
verboseFlagIn,
|
|
21
|
+
verboseRequested,
|
|
22
|
+
withCliErrorHandling
|
|
23
|
+
} from "../src/cli/index.js"
|
|
24
|
+
|
|
25
|
+
class ProfileGone extends Schema.TaggedError<ProfileGone>()("ProfileGone", { message: Schema.String }) {}
|
|
26
|
+
class Bare extends Schema.TaggedError<Bare>()("Bare", {}) {}
|
|
27
|
+
|
|
28
|
+
const stderrOf = <E>(cause: Cause.Cause<E>, verbose: boolean) =>
|
|
29
|
+
Effect.gen(function*() {
|
|
30
|
+
const lines: Array<string> = []
|
|
31
|
+
const capture: Console.Console = Object.assign(Object.create(console), {
|
|
32
|
+
error: (...parts: ReadonlyArray<unknown>) => lines.push(parts.join(" "))
|
|
33
|
+
})
|
|
34
|
+
yield* handleCliError(cause, { verbose }).pipe(Effect.provideService(Console.Console, capture))
|
|
35
|
+
return lines
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("handleCliError", () => {
|
|
39
|
+
it.effect("prints a failure's message, a bare tagged error's tag, a defect and an interruption", () =>
|
|
40
|
+
Effect.gen(function*() {
|
|
41
|
+
expect(yield* stderrOf(Cause.fail(new ProfileGone({ message: "Profile not found: x" })), false)).toEqual([
|
|
42
|
+
"Profile not found: x"
|
|
43
|
+
])
|
|
44
|
+
expect(yield* stderrOf(Cause.fail(new Bare()), false)).toEqual(["Bare"])
|
|
45
|
+
expect(yield* stderrOf(Cause.die(new Error("boom")), false)).toEqual(["Error: boom"])
|
|
46
|
+
expect(yield* stderrOf(Cause.interrupt(), false)).toEqual(["Interrupted"])
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
it.effect("skips what Command.runWith already rendered", () =>
|
|
50
|
+
Effect.gen(function*() {
|
|
51
|
+
const help = new CliError.ShowHelp({ commandPath: ["jira"], errors: [] })
|
|
52
|
+
expect(yield* stderrOf(Cause.fail(help), false)).toEqual([])
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
it.effect("prints the full cause, stack included, when verbose", () =>
|
|
56
|
+
Effect.gen(function*() {
|
|
57
|
+
const lines = yield* stderrOf(Cause.die(new Error("boom")), true)
|
|
58
|
+
expect(lines.join("\n")).toContain("boom")
|
|
59
|
+
expect(lines.join("\n")).toContain("at ")
|
|
60
|
+
}))
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("verbose", () => {
|
|
64
|
+
// The scanner runs before the CLI parses, so it must agree with the parser it stands in for.
|
|
65
|
+
const parsed = (args: ReadonlyArray<string>) =>
|
|
66
|
+
Effect.gen(function*() {
|
|
67
|
+
const seen: Array<boolean> = []
|
|
68
|
+
const probe = Command.make("probe", { words: Argument.String("word").pipe(Argument.variadic()) }, () =>
|
|
69
|
+
Effect.gen(function*() {
|
|
70
|
+
seen.push(yield* Verbose)
|
|
71
|
+
}))
|
|
72
|
+
const root = Command.make("tool").pipe(Command.withSubcommands([probe]), Command.withGlobalFlags([Verbose]))
|
|
73
|
+
const exit = yield* Command.runWith(root, { version: "0" })(args).pipe(
|
|
74
|
+
Effect.provide(NodeServices.layer),
|
|
75
|
+
Effect.exit
|
|
76
|
+
)
|
|
77
|
+
return { exit, verbose: seen[0] }
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it.effect("reads --verbose and routes the command words exactly as the parser does", () =>
|
|
81
|
+
Effect.gen(function*() {
|
|
82
|
+
const cases: ReadonlyArray<ReadonlyArray<string>> = [
|
|
83
|
+
["probe", "x"],
|
|
84
|
+
["--verbose", "probe", "x"],
|
|
85
|
+
["--verbose", "false", "probe", "x"],
|
|
86
|
+
["--verbose", "true", "probe", "x"],
|
|
87
|
+
["--verbose=false", "probe", "x"],
|
|
88
|
+
["--verbose=yes", "probe", "x"],
|
|
89
|
+
["probe", "x", "--verbose"],
|
|
90
|
+
["--verbose", "--no-verbose", "probe", "x"],
|
|
91
|
+
["--no-verbose", "--verbose", "probe", "x"],
|
|
92
|
+
["probe", "--", "--verbose"],
|
|
93
|
+
["probe", "x", "--verbose", "FALSE"],
|
|
94
|
+
["probe", "x", "--verbose", "True"]
|
|
95
|
+
]
|
|
96
|
+
for (const args of cases) {
|
|
97
|
+
const { exit, verbose } = yield* parsed(args)
|
|
98
|
+
expect(Exit.isSuccess(exit), JSON.stringify(args)).toBe(true)
|
|
99
|
+
expect(verboseFlagIn(args), JSON.stringify(args)).toBe(verbose)
|
|
100
|
+
expect(commandArgs(args)[0], JSON.stringify(args)).toBe("probe")
|
|
101
|
+
// A literal the parser does not consume stays a positional word.
|
|
102
|
+
for (const word of ["FALSE", "True"]) {
|
|
103
|
+
if (args.includes(word)) expect(commandArgs(args), JSON.stringify(args)).toContain(word)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
it.effect("does not report verbose for a value the parser rejects, and the rejection is printed", () =>
|
|
109
|
+
Effect.gen(function*() {
|
|
110
|
+
expect(verboseFlagIn(["--verbose=bogus", "probe"])).toBe(false)
|
|
111
|
+
expect(verboseFlagIn(["--verbose=FALSE", "probe"])).toBe(false)
|
|
112
|
+
const { exit } = yield* parsed(["--verbose=bogus", "probe", "x"])
|
|
113
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
114
|
+
if (Exit.isFailure(exit)) {
|
|
115
|
+
const lines = yield* stderrOf(exit.cause, false)
|
|
116
|
+
expect(lines).toHaveLength(1)
|
|
117
|
+
expect(lines[0]).toContain("bogus")
|
|
118
|
+
}
|
|
119
|
+
}))
|
|
120
|
+
|
|
121
|
+
it.effect("turns on for DEBUG=1 only", () =>
|
|
122
|
+
Effect.gen(function*() {
|
|
123
|
+
const withEnv = (env: Record<string, string>) =>
|
|
124
|
+
verboseRequested([]).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))))
|
|
125
|
+
expect(yield* withEnv({ DEBUG: "1" })).toBe(true)
|
|
126
|
+
expect(yield* withEnv({ DEBUG: "express:*" })).toBe(false)
|
|
127
|
+
expect(yield* withEnv({})).toBe(false)
|
|
128
|
+
}))
|
|
129
|
+
|
|
130
|
+
it.effect("reports with --verbose from the arguments, and fails with the same cause", () =>
|
|
131
|
+
Effect.gen(function*() {
|
|
132
|
+
const lines: Array<string> = []
|
|
133
|
+
const capture: Console.Console = Object.assign(Object.create(console), {
|
|
134
|
+
error: (...parts: ReadonlyArray<unknown>) => lines.push(parts.join(" "))
|
|
135
|
+
})
|
|
136
|
+
const failure = new ProfileGone({ message: "Profile not found: x" })
|
|
137
|
+
const exit = yield* withCliErrorHandling(Effect.fail(failure)).pipe(
|
|
138
|
+
Effect.provideService(Console.Console, capture),
|
|
139
|
+
Effect.provide(Stdio.layerTest({ args: Effect.succeed(["auth", "use", "x", "--verbose"]) })),
|
|
140
|
+
Effect.exit
|
|
141
|
+
)
|
|
142
|
+
expect(Exit.isFailure(exit) && Cause.squash(exit.cause)).toBe(failure)
|
|
143
|
+
expect(lines.join("\n")).toContain("ProfileGone")
|
|
144
|
+
}))
|
|
145
|
+
})
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it } from "@effect/vitest"
|
|
|
4
4
|
import * as Effect from "effect/Effect"
|
|
5
5
|
import * as FileSystem from "effect/FileSystem"
|
|
6
6
|
import * as Path from "effect/Path"
|
|
7
|
+
import * as Predicate from "effect/Predicate"
|
|
7
8
|
import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
|
8
9
|
import * as Schema from "effect/Schema"
|
|
9
10
|
import * as Stream from "effect/Stream"
|
|
@@ -19,10 +20,11 @@ const Findings = Schema.fromJsonString(Schema.Array(Schema.Struct({ file: Schema
|
|
|
19
20
|
describe("CLI auth guardrails", () => {
|
|
20
21
|
for (const [name, config] of Object.entries(configs)) {
|
|
21
22
|
it(`${name} resolves CLI auth from source rather than stale build output`, () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
23
|
+
const aliases = config.resolve?.alias
|
|
24
|
+
const entries = Array.isArray(aliases) ? aliases : []
|
|
25
|
+
const specifier = "@knpkv/atlassian-common/cli-auth"
|
|
26
|
+
const cliAuth = entries.find(({ find }) => Predicate.isString(find) ? find === specifier : find.test(specifier))
|
|
27
|
+
expect(cliAuth?.replacement).toBe(new URL("../src/cli-auth/index.ts", import.meta.url).pathname)
|
|
26
28
|
})
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -118,7 +118,6 @@ const failingLaunchers = (launched: Deferred.Deferred<string>, attempts: Ref.Ref
|
|
|
118
118
|
isRunning: Effect.succeed(false),
|
|
119
119
|
kill: () => Effect.void,
|
|
120
120
|
pid: ChildProcessSpawner.ProcessId(1),
|
|
121
|
-
reref: Effect.void,
|
|
122
121
|
stderr: Stream.empty,
|
|
123
122
|
stdin: Sink.drain,
|
|
124
123
|
stdout: Stream.empty,
|
|
@@ -32,7 +32,6 @@ const launchers = (outcomes: Readonly<Record<string, number>>, attempts: Array<s
|
|
|
32
32
|
isRunning: Effect.succeed(false),
|
|
33
33
|
kill: () => Effect.void,
|
|
34
34
|
pid: ChildProcessSpawner.ProcessId(1),
|
|
35
|
-
reref: Effect.void,
|
|
36
35
|
stderr: Stream.empty,
|
|
37
36
|
stdin: Sink.drain,
|
|
38
37
|
stdout: Stream.empty,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"composite": false,
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"declarationMap": false,
|
|
8
|
+
"rootDir": "../../..",
|
|
9
|
+
"outDir": null,
|
|
10
|
+
"tsBuildInfoFile": null
|
|
11
|
+
},
|
|
12
|
+
"include": ["../src", "."],
|
|
13
|
+
"exclude": []
|
|
14
|
+
}
|
package/vitest.config.ts
CHANGED