create-claude-cabinet 0.34.0 → 0.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -0,0 +1,72 @@
1
+ import Cocoa
2
+
3
+ class InputDialog: NSObject {
4
+ func run(prompt: String) -> String? {
5
+ let alert = NSAlert()
6
+ alert.messageText = prompt
7
+ alert.addButton(withTitle: "OK")
8
+ alert.addButton(withTitle: "Cancel")
9
+
10
+ let stack = NSStackView(frame: NSRect(x: 0, y: 0, width: 300, height: 50))
11
+ stack.orientation = .vertical
12
+ stack.spacing = 8
13
+
14
+ let secureField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24))
15
+ secureField.placeholderString = "Enter passphrase"
16
+ let plainField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24))
17
+ plainField.placeholderString = "Enter passphrase"
18
+ plainField.isHidden = true
19
+
20
+ let toggle = NSButton(checkboxWithTitle: "Show passphrase", target: nil, action: nil)
21
+
22
+ class ToggleHandler: NSObject {
23
+ let secureField: NSSecureTextField
24
+ let plainField: NSTextField
25
+ let toggle: NSButton
26
+ init(s: NSSecureTextField, p: NSTextField, t: NSButton) {
27
+ secureField = s; plainField = p; toggle = t
28
+ }
29
+ @objc func toggled(_ sender: Any?) {
30
+ if toggle.state == .on {
31
+ plainField.stringValue = secureField.stringValue
32
+ secureField.isHidden = true
33
+ plainField.isHidden = false
34
+ plainField.becomeFirstResponder()
35
+ } else {
36
+ secureField.stringValue = plainField.stringValue
37
+ plainField.isHidden = true
38
+ secureField.isHidden = false
39
+ secureField.becomeFirstResponder()
40
+ }
41
+ }
42
+ }
43
+ let handler = ToggleHandler(s: secureField, p: plainField, t: toggle)
44
+ toggle.target = handler
45
+ toggle.action = #selector(ToggleHandler.toggled(_:))
46
+
47
+ stack.addArrangedSubview(secureField)
48
+ stack.addArrangedSubview(plainField)
49
+ stack.addArrangedSubview(toggle)
50
+
51
+ alert.accessoryView = stack
52
+ alert.window.initialFirstResponder = secureField
53
+
54
+ let response = alert.runModal()
55
+ if response == .alertFirstButtonReturn {
56
+ return toggle.state == .on ? plainField.stringValue : secureField.stringValue
57
+ }
58
+ return nil
59
+ }
60
+ }
61
+
62
+ let app = NSApplication.shared
63
+ app.setActivationPolicy(.accessory)
64
+
65
+ let dialog = InputDialog()
66
+ let prompt = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "Enter passphrase"
67
+ if let value = dialog.run(prompt: prompt) {
68
+ print(value)
69
+ } else {
70
+ fputs("CANCELLED\n", stderr)
71
+ exit(1)
72
+ }
@@ -1,7 +1,11 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
 
4
7
  const execFileAsync = promisify(execFile);
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
9
 
6
10
  export class DialogCancelledError extends Error {
7
11
  constructor() { super('User cancelled dialog'); this.code = 'CANCELLED'; }
@@ -27,7 +31,34 @@ function escapeAppleScript(str) {
27
31
  return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
28
32
  }
29
33
 
30
- async function captureViaMacOS(prompt) {
34
+ async function ensureNativeDialog() {
35
+ const cacheDir = join(process.env.HOME || '/tmp', '.claude-cabinet', 'bin');
36
+ const binPath = join(cacheDir, 'secure-input-dialog');
37
+ if (existsSync(binPath)) return binPath;
38
+
39
+ const srcPath = join(__dirname, 'secure-input-dialog.swift');
40
+ if (!existsSync(srcPath)) return null;
41
+
42
+ mkdirSync(cacheDir, { recursive: true });
43
+ try {
44
+ await execFileAsync('swiftc', ['-o', binPath, srcPath], { timeout: 30000 });
45
+ return binPath;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ async function captureViaNativeDialog(prompt, binPath) {
52
+ try {
53
+ const { stdout } = await execFileAsync(binPath, [prompt], { timeout: 120000 });
54
+ return stdout.trim();
55
+ } catch (err) {
56
+ if (err.stderr?.includes('CANCELLED')) throw new DialogCancelledError();
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ async function captureViaAppleScript(prompt) {
31
62
  const escaped = escapeAppleScript(prompt);
32
63
  const script = `display dialog "${escaped}" with hidden answer default answer ""`;
33
64
  try {
@@ -43,6 +74,12 @@ async function captureViaMacOS(prompt) {
43
74
  }
44
75
  }
45
76
 
77
+ async function captureViaMacOS(prompt) {
78
+ const binPath = await ensureNativeDialog();
79
+ if (binPath) return captureViaNativeDialog(prompt, binPath);
80
+ return captureViaAppleScript(prompt);
81
+ }
82
+
46
83
  async function captureViaZenity(prompt) {
47
84
  try {
48
85
  const { stdout } = await execFileAsync('zenity', ['--password', '--title', prompt]);
@@ -83,6 +120,12 @@ async function captureViaPowerShell(prompt) {
83
120
 
84
121
  /**
85
122
  * Capture a secret via a secure OS dialog.
123
+ *
124
+ * On macOS: uses a native Cocoa dialog with a "Show passphrase" toggle
125
+ * checkbox (compiled from secure-input-dialog.swift on first use, cached
126
+ * at ~/.claude-cabinet/bin/). Falls back to AppleScript (masked, no toggle)
127
+ * if Swift compilation fails.
128
+ *
86
129
  * @param {string} prompt - what to ask
87
130
  * @param {{ requireSecure?: boolean }} opts
88
131
  * requireSecure (default false): when true, REFUSE to fall back to the
@@ -100,7 +143,7 @@ export async function captureSecureInput(prompt, { requireSecure = false } = {})
100
143
  return await captureViaZenity(prompt);
101
144
  } catch (err) {
102
145
  if (err instanceof DialogUnavailableError) {
103
- if (requireSecure) throw err; // fail closed for credentials
146
+ if (requireSecure) throw err;
104
147
  return captureViaTerminal(prompt);
105
148
  }
106
149
  throw err;