claude-aws-mfa 1.2.0 → 1.2.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.
@@ -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,6 +22,13 @@ 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.1] - 2026-08-10
26
+
27
+ ### Fixed
28
+ - **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.
29
+ - **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.
30
+ - 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.
31
+
25
32
  ## [v1.2.0] - 2026-08-10
26
33
 
27
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-aws-mfa",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/p120ph37/claude-aws-mfa",
6
6
  "bin": {
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();