claude-aws-mfa 1.2.0 → 1.2.2
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/.github/workflows/release.yml +9 -3
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +13 -1
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/config.ts +9 -5
- package/test/config.test.ts +7 -3
- package/test/gui-screenshot.test.ts +28 -11
|
@@ -110,9 +110,17 @@ jobs:
|
|
|
110
110
|
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
|
|
111
111
|
echo "Detected version=$VERSION prerelease=$PRERELEASE"
|
|
112
112
|
|
|
113
|
-
release
|
|
113
|
+
# v1.2.0 shipped while the Test workflow was red: the release ran its own
|
|
114
|
+
# `bun test` on ubuntu only, so a Windows-only failure never reached it.
|
|
115
|
+
# Gate on the whole matrix instead, and only when actually releasing.
|
|
116
|
+
test:
|
|
114
117
|
needs: detect-release
|
|
115
118
|
if: needs.detect-release.outputs.version != ''
|
|
119
|
+
uses: ./.github/workflows/test.yml
|
|
120
|
+
|
|
121
|
+
release:
|
|
122
|
+
needs: [detect-release, test]
|
|
123
|
+
if: needs.detect-release.outputs.version != ''
|
|
116
124
|
runs-on: ubuntu-latest
|
|
117
125
|
permissions:
|
|
118
126
|
contents: read
|
|
@@ -128,8 +136,6 @@ jobs:
|
|
|
128
136
|
|
|
129
137
|
- run: bun install --frozen-lockfile
|
|
130
138
|
|
|
131
|
-
- run: bun test
|
|
132
|
-
|
|
133
139
|
- name: Set package version
|
|
134
140
|
run: |
|
|
135
141
|
VERSION="${{ needs.detect-release.outputs.version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -22,10 +22,22 @@ Releases are automated via CI. To create a new release:
|
|
|
22
22
|
Stable versions (e.g. `v1.2.0`) publish to npm `@latest`. Pre-release versions
|
|
23
23
|
(e.g. `v1.2.0-rc.1`) publish to npm `@next` and create a GitHub pre-release.
|
|
24
24
|
|
|
25
|
+
## [v1.2.2] - 2026-10-09
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Changed GitHub username
|
|
29
|
+
|
|
30
|
+
## [v1.2.1] - 2026-08-10
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **Profile scoping works on Windows.** The per-profile config filename encoded only `/` and `.`, so a Windows `CLAUDE_CONFIG_DIR` kept its backslashes and drive colon — `~/.config/claude-aws-mfa<encoded>.json` then named an NTFS alternate data stream instead of a per-profile file. Separators are normalized and the colon encoded, so a Windows profile gets its own config and lock like everywhere else.
|
|
34
|
+
- **A release can no longer publish over a red build.** The release workflow ran its own `bun test` on Linux only, so v1.2.0 shipped while the Test workflow's Windows leg was failing. It now calls that workflow directly and waits for the full matrix — every OS, GUI tests included — before publishing.
|
|
35
|
+
- Dialog titles on Windows showed the full `C:\Users\you\.claude-profiles\work` instead of `~/.claude-profiles/work`: the home-prefix shortening only recognized `/` as a separator.
|
|
36
|
+
|
|
25
37
|
## [v1.2.0] - 2026-08-10
|
|
26
38
|
|
|
27
39
|
### Added
|
|
28
|
-
- **Claude Code profile awareness**: `--setup` now reads and writes `$CLAUDE_CONFIG_DIR/settings.json` rather than always `~/.claude/settings.json`, so a tool like [claude-as](https://github.com/
|
|
40
|
+
- **Claude Code profile awareness**: `--setup` now reads and writes `$CLAUDE_CONFIG_DIR/settings.json` rather than always `~/.claude/settings.json`, so a tool like [claude-as](https://github.com/aaronmeriwether/claude-as) can give one project a Bedrock profile while others stay on OAuth. The saved config and lock file are scoped to the profile as well (`~/.config/claude-aws-mfa<encoded-config-dir>.json`), since separate profiles generally mean separate AWS accounts and a shared cached session would otherwise hand a profile credentials for the wrong account. The default `~/.claude` is unsuffixed, so existing installs keep their current config file with no migration. Since the lock is per-profile, two profiles can prompt at the same time, so the credential, error, and setup dialogs all name their profile in the window title and in a banner under the heading. The default config dir is named too — to a multi-profile user an unlabelled dialog reads equally as "this is the default profile" and "this build does not label profiles".
|
|
29
41
|
- **`--no-ui` mode**: skip the GUI entirely and drive the tool from the command line — useful for scripting and for letting an LLM operate the tool during setup/debugging. Prints credentials JSON to stdout on success, keeping stdout cleanly parsable; on failure, prints the underlying error (or names the specific missing config field(s) when the config is incomplete) to stderr. Never imports the GUI toolkit, so it also works in headless environments.
|
|
30
42
|
- **Error dialog**: authentication failures (STS or MFA command) now show a native dialog box instead of writing to stderr, since Claude Code doesn't surface a failing command's stderr to the user. Suppressed in `--no-ui` mode, where the error is printed to stderr instead.
|
|
31
43
|
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ On subsequent runs, all fields are pre-populated from the saved config — just
|
|
|
58
58
|
|
|
59
59
|
### Claude Code profiles
|
|
60
60
|
|
|
61
|
-
Claude Code reads its settings from `$CLAUDE_CONFIG_DIR` when that is set, which tools such as [claude-as](https://github.com/
|
|
61
|
+
Claude Code reads its settings from `$CLAUDE_CONFIG_DIR` when that is set, which tools such as [claude-as](https://github.com/aaronmeriwether/claude-as) use to give each project its own credentials. This tool follows suit:
|
|
62
62
|
|
|
63
63
|
- `--setup` reads and writes `$CLAUDE_CONFIG_DIR/settings.json`, so a profile can be Bedrock-only while others stay on OAuth.
|
|
64
64
|
- The config file is scoped per profile — `~/.config/claude-aws-mfa<profile>.json`, where `<profile>` is the config dir path encoded the way Claude encodes project dirs (`/` and `.` both become `-`). Different profiles usually mean different AWS accounts, and a shared cached session would hand a profile credentials for the wrong one.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-aws-mfa",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"repository": "https://github.com/
|
|
5
|
+
"repository": "https://github.com/aaronmeriwether/claude-aws-mfa",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-aws-mfa": "./src/index.ts"
|
|
8
8
|
},
|
package/src/config.ts
CHANGED
|
@@ -57,15 +57,18 @@ export interface Config {
|
|
|
57
57
|
export function profileSuffix(dir = process.env.CLAUDE_CONFIG_DIR): string {
|
|
58
58
|
if (!dir) return "";
|
|
59
59
|
const canonical = (p: string) => {
|
|
60
|
+
let real: string;
|
|
60
61
|
try {
|
|
61
|
-
|
|
62
|
+
real = realpathSync(p);
|
|
62
63
|
} catch {
|
|
63
|
-
|
|
64
|
+
real = resolve(p);
|
|
64
65
|
}
|
|
66
|
+
return real.replace(/\\/g, "/");
|
|
65
67
|
};
|
|
66
68
|
const real = canonical(dir);
|
|
67
69
|
if (real === canonical(join(homedir(), ".claude"))) return "";
|
|
68
|
-
|
|
70
|
+
// `:` goes too, or a Windows drive letter would make the filename an NTFS stream.
|
|
71
|
+
return real.replace(/[/.:]/g, "-");
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface ActiveProfile {
|
|
@@ -87,11 +90,12 @@ export const DEFAULT_PROFILE: ActiveProfile = { name: "default", path: "~/.claud
|
|
|
87
90
|
*/
|
|
88
91
|
export function activeProfile(dir = process.env.CLAUDE_CONFIG_DIR): ActiveProfile {
|
|
89
92
|
if (!dir || !profileSuffix(dir)) return DEFAULT_PROFILE;
|
|
90
|
-
const clean = dir.replace(
|
|
93
|
+
const clean = dir.replace(/[/\\]+$/, "");
|
|
91
94
|
const home = homedir();
|
|
95
|
+
const underHome = clean === home || (clean.startsWith(home) && /^[/\\]/.test(clean.slice(home.length)));
|
|
92
96
|
return {
|
|
93
97
|
name: basename(clean),
|
|
94
|
-
path:
|
|
98
|
+
path: underHome ? ("~" + clean.slice(home.length)).replace(/\\/g, "/") : clean,
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
|
package/test/config.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync, statSync, openSync, closeSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { activeProfile, missingConfigFields, profileSuffix } from "../src/config";
|
|
@@ -13,10 +13,14 @@ describe("profileSuffix", () => {
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
test("encodes the full path, not the basename, with / and . both as -", () => {
|
|
16
|
-
|
|
16
|
+
// resolve() puts a drive letter in front on Windows: "-" on POSIX, "D--" there.
|
|
17
|
+
const root = resolve("/").replace(/[/\\:]/g, "-");
|
|
18
|
+
expect(profileSuffix("/opt/profiles/work")).toBe(root + "opt-profiles-work");
|
|
17
19
|
// basename alone would collide; the full path keeps them distinct
|
|
18
20
|
expect(profileSuffix("/a/work")).not.toBe(profileSuffix("/b/work"));
|
|
19
|
-
expect(profileSuffix("/home/me/.claude-profiles/work")).toBe("
|
|
21
|
+
expect(profileSuffix("/home/me/.claude-profiles/work")).toBe(root + "home-me--claude-profiles-work");
|
|
22
|
+
// whatever the platform, the result has to be usable as a filename component
|
|
23
|
+
expect(profileSuffix("/opt/profiles/work")).not.toMatch(/[/\\:]/);
|
|
20
24
|
});
|
|
21
25
|
});
|
|
22
26
|
|
|
@@ -41,7 +41,10 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
|
|
|
41
41
|
typeof import("node-screenshots").Window
|
|
42
42
|
> | null = null;
|
|
43
43
|
|
|
44
|
+
let titles: string[] = [];
|
|
45
|
+
|
|
44
46
|
beforeAll(async () => {
|
|
47
|
+
const start = Date.now();
|
|
45
48
|
windowModule = await import("node-screenshots");
|
|
46
49
|
|
|
47
50
|
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
@@ -52,28 +55,42 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
|
|
|
52
55
|
stderr: "inherit",
|
|
53
56
|
});
|
|
54
57
|
|
|
55
|
-
// Poll for the window to appear
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
58
|
+
// Poll for the window to appear. The budget is 90s rather than a snug
|
|
59
|
+
// multiple of the ~8s a warm runner takes: on Windows the first WebView2
|
|
60
|
+
// launch on a fresh image bootstraps its runtime and user-data folder
|
|
61
|
+
// before any window exists, which is what made this test flaky in CI.
|
|
62
|
+
// Waiting longer costs nothing when the window shows up on schedule.
|
|
63
|
+
//
|
|
64
|
+
// Window.all() may throw if the window manager hasn't registered EWMH
|
|
65
|
+
// atoms yet (e.g. _NET_CLIENT_LIST_STACKING on Linux), so we catch and
|
|
66
|
+
// retry.
|
|
67
|
+
const deadline = Date.now() + 90_000;
|
|
60
68
|
while (Date.now() < deadline) {
|
|
61
69
|
try {
|
|
62
70
|
const windows = windowModule.Window.all();
|
|
63
|
-
|
|
64
|
-
try { return w.title()
|
|
65
|
-
catch { return false; }
|
|
71
|
+
titles = windows.map((w) => {
|
|
72
|
+
try { return w.title() ?? ""; } catch { return ""; }
|
|
66
73
|
});
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const i = titles.findIndex((t) => t.includes("Claude AWS MFA"));
|
|
75
|
+
if (i >= 0) {
|
|
76
|
+
foundWindow = windows[i]!;
|
|
69
77
|
break;
|
|
70
78
|
}
|
|
71
79
|
} catch {
|
|
72
80
|
// Window.all() not yet available — WM still initializing
|
|
73
81
|
}
|
|
82
|
+
// A dead launcher will never produce a window; stop and let the
|
|
83
|
+
// "app launches without error" assertion report the exit code.
|
|
84
|
+
if (proc.exitCode !== null) break;
|
|
74
85
|
await Bun.sleep(500);
|
|
75
86
|
}
|
|
76
|
-
|
|
87
|
+
if (!foundWindow) {
|
|
88
|
+
// Without this a failure is a bare `null` — no way to tell "window never
|
|
89
|
+
// opened" from "opened under a title we didn't match".
|
|
90
|
+
console.log(`No matching window after ${Math.round((Date.now() - start) / 1000)}s.`);
|
|
91
|
+
console.log(`Launcher exitCode: ${proc.exitCode}. Visible titles: ${JSON.stringify(titles)}`);
|
|
92
|
+
}
|
|
93
|
+
}, 120_000);
|
|
77
94
|
|
|
78
95
|
afterAll(() => {
|
|
79
96
|
proc?.kill();
|