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.
@@ -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 }}"
@@ -5,6 +5,9 @@ on:
5
5
  branches: [main]
6
6
  pull_request:
7
7
  branches: [main]
8
+ # Called by the release workflow so a release runs this exact matrix, rather
9
+ # than a subset of it that can be green while this one is red.
10
+ workflow_call:
8
11
 
9
12
  jobs:
10
13
  test:
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/p120ph37/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".
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/p120ph37/claude-as) use to give each project its own credentials. This tool follows suit:
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.0",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
- "repository": "https://github.com/p120ph37/claude-aws-mfa",
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
- return realpathSync(p);
62
+ real = realpathSync(p);
62
63
  } catch {
63
- return resolve(p);
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
- return real.replace(/[/.]/g, "-");
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: clean === home || clean.startsWith(home + "/") ? "~" + clean.slice(home.length) : clean,
98
+ path: underHome ? ("~" + clean.slice(home.length)).replace(/\\/g, "/") : clean,
95
99
  };
96
100
  }
97
101
 
@@ -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
- expect(profileSuffix("/opt/profiles/work")).toBe("-opt-profiles-work");
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("-home-me--claude-profiles-work");
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 (up to 20s).
56
- // Window.all() may throw if the window manager hasn't registered
57
- // EWMH atoms yet (e.g. _NET_CLIENT_LIST_STACKING on Linux), so
58
- // we catch and retry.
59
- const deadline = Date.now() + 20_000;
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
- const match = windows.find((w) => {
64
- try { return w.title()?.includes("Claude AWS MFA"); }
65
- catch { return false; }
71
+ titles = windows.map((w) => {
72
+ try { return w.title() ?? ""; } catch { return ""; }
66
73
  });
67
- if (match) {
68
- foundWindow = match;
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
- }, 30_000);
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();