@chriscode/hush 3.0.1 → 3.1.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAyF9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAwK9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
@@ -18,6 +18,79 @@ function promptViaMacOSDialog(key) {
18
18
  return null;
19
19
  }
20
20
  }
21
+ function promptViaWindowsDialog(key) {
22
+ try {
23
+ const psScript = `
24
+ Add-Type -AssemblyName System.Windows.Forms
25
+ Add-Type -AssemblyName System.Drawing
26
+
27
+ $form = New-Object System.Windows.Forms.Form
28
+ $form.Text = 'Hush - Set Secret'
29
+ $form.Size = New-Object System.Drawing.Size(300,150)
30
+ $form.StartPosition = 'CenterScreen'
31
+
32
+ $label = New-Object System.Windows.Forms.Label
33
+ $label.Location = New-Object System.Drawing.Point(10,20)
34
+ $label.Size = New-Object System.Drawing.Size(280,20)
35
+ $label.Text = 'Enter value for ${key}:'
36
+ $form.Controls.Add($label)
37
+
38
+ $textBox = New-Object System.Windows.Forms.TextBox
39
+ $textBox.Location = New-Object System.Drawing.Point(10,50)
40
+ $textBox.Size = New-Object System.Drawing.Size(260,20)
41
+ $textBox.PasswordChar = '*'
42
+ $form.Controls.Add($textBox)
43
+
44
+ $okButton = New-Object System.Windows.Forms.Button
45
+ $okButton.Location = New-Object System.Drawing.Point(10,80)
46
+ $okButton.Size = New-Object System.Drawing.Size(75,23)
47
+ $okButton.Text = 'OK'
48
+ $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
49
+ $form.AcceptButton = $okButton
50
+ $form.Controls.Add($okButton)
51
+
52
+ $form.TopMost = $true
53
+
54
+ $result = $form.ShowDialog()
55
+
56
+ if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
57
+ Write-Output $textBox.Text
58
+ } else {
59
+ exit 1
60
+ }
61
+ `;
62
+ const encodedCommand = Buffer.from(psScript, 'utf16le').toString('base64');
63
+ const result = execSync(`powershell -EncodedCommand "${encodedCommand}"`, {
64
+ encoding: 'utf-8',
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ });
67
+ return result.trim();
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ function promptViaLinuxDialog(key) {
74
+ try {
75
+ const result = execSync(`zenity --password --title="Hush - Set Secret" --text="Enter value for ${key}:"`, {
76
+ encoding: 'utf-8',
77
+ stdio: ['pipe', 'pipe', 'pipe'],
78
+ });
79
+ return result.trim();
80
+ }
81
+ catch {
82
+ try {
83
+ const result = execSync(`kdialog --password "Enter value for ${key}:" --title "Hush - Set Secret"`, {
84
+ encoding: 'utf-8',
85
+ stdio: ['pipe', 'pipe', 'pipe'],
86
+ });
87
+ return result.trim();
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ }
21
94
  function promptViaTTY(key) {
22
95
  return new Promise((resolve, reject) => {
23
96
  process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
@@ -60,26 +133,29 @@ function promptViaTTY(key) {
60
133
  });
61
134
  }
62
135
  async function promptForValue(key, forceGui) {
63
- if (forceGui && platform() === 'darwin') {
64
- console.log(pc.dim('Opening dialog for secret input...'));
65
- const value = promptViaMacOSDialog(key);
66
- if (value !== null) {
67
- return value;
68
- }
69
- throw new Error('Dialog cancelled or failed');
70
- }
71
- if (process.stdin.isTTY) {
136
+ if (process.stdin.isTTY && !forceGui) {
72
137
  return promptViaTTY(key);
73
138
  }
74
- if (platform() === 'darwin') {
75
- console.log(pc.dim('Opening dialog for secret input...'));
76
- const value = promptViaMacOSDialog(key);
77
- if (value !== null) {
78
- return value;
79
- }
80
- throw new Error('Dialog cancelled or failed');
139
+ console.log(pc.dim('Opening dialog for secret input...'));
140
+ let value = null;
141
+ switch (platform()) {
142
+ case 'darwin':
143
+ value = promptViaMacOSDialog(key);
144
+ break;
145
+ case 'win32':
146
+ value = promptViaWindowsDialog(key);
147
+ break;
148
+ case 'linux':
149
+ value = promptViaLinuxDialog(key);
150
+ break;
151
+ }
152
+ if (value !== null) {
153
+ return value;
154
+ }
155
+ if (platform() === 'linux') {
156
+ throw new Error('GUI prompt failed. Please install "zenity" or "kdialog".');
81
157
  }
82
- throw new Error('Interactive input requires a terminal (TTY) or macOS');
158
+ throw new Error('Dialog cancelled or failed. Interactive input requires a terminal (TTY) or a supported GUI environment.');
83
159
  }
84
160
  export async function setCommand(options) {
85
161
  const { root, file, key, gui } = options;
@@ -1 +1 @@
1
- {"version":3,"file":"sops.d.ts","sourceRoot":"","sources":["../../src/core/sops.ts"],"names":[],"mappings":"AA0BA,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6BhD;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuBnE;AAED,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAqB3C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAqDzE"}
1
+ {"version":3,"file":"sops.d.ts","sourceRoot":"","sources":["../../src/core/sops.ts"],"names":[],"mappings":"AA0BA,wBAAgB,eAAe,IAAI,OAAO,CAUzC;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6BhD;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAsBnE;AAED,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAsB3C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CA+CzE"}
package/dist/core/sops.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { execSync, spawnSync } from 'node:child_process';
2
2
  import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
4
+ import { tmpdir, homedir } from 'node:os';
5
5
  function getAgeKeyFile() {
6
6
  if (process.env.SOPS_AGE_KEY_FILE) {
7
7
  return process.env.SOPS_AGE_KEY_FILE;
8
8
  }
9
- const defaultPath = join(process.env.HOME || '~', '.config/sops/age/key.txt');
9
+ const defaultPath = join(homedir(), '.config', 'sops', 'age', 'key.txt');
10
10
  if (existsSync(defaultPath)) {
11
11
  return defaultPath;
12
12
  }
@@ -21,8 +21,11 @@ function getSopsEnv() {
21
21
  }
22
22
  export function isSopsInstalled() {
23
23
  try {
24
- execSync('which sops', { stdio: 'ignore' });
25
- return true;
24
+ const result = spawnSync('sops', ['--version'], {
25
+ stdio: 'ignore',
26
+ shell: true
27
+ });
28
+ return result.status === 0;
26
29
  }
27
30
  catch {
28
31
  return false;
@@ -36,7 +39,7 @@ export function decrypt(filePath) {
36
39
  throw new Error(`Encrypted file not found: ${filePath}`);
37
40
  }
38
41
  if (!isSopsInstalled()) {
39
- throw new Error('SOPS is not installed. Install with: brew install sops');
42
+ throw new Error('SOPS is not installed. Install with: brew install sops (Mac) or scoop install sops (Windows)');
40
43
  }
41
44
  try {
42
45
  const result = execSync(`sops --input-type dotenv --output-type dotenv --decrypt "${filePath}"`, {
@@ -60,12 +63,11 @@ export function encrypt(inputPath, outputPath) {
60
63
  throw new Error(`Input file not found: ${inputPath}`);
61
64
  }
62
65
  if (!isSopsInstalled()) {
63
- throw new Error('SOPS is not installed. Install with: brew install sops');
66
+ throw new Error('SOPS is not installed. Install with: brew install sops (Mac) or scoop install sops (Windows)');
64
67
  }
65
68
  try {
66
69
  execSync(`sops --input-type dotenv --output-type dotenv --encrypt "${inputPath}" > "${outputPath}"`, {
67
70
  encoding: 'utf-8',
68
- shell: '/bin/bash',
69
71
  stdio: ['pipe', 'pipe', 'pipe'],
70
72
  env: getSopsEnv(),
71
73
  });
@@ -85,6 +87,7 @@ export function edit(filePath) {
85
87
  const result = spawnSync('sops', ['--input-type', 'dotenv', '--output-type', 'dotenv', filePath], {
86
88
  stdio: 'inherit',
87
89
  env: getSopsEnv(),
90
+ shell: true // Required to find executable on Windows
88
91
  });
89
92
  if (result.status !== 0) {
90
93
  throw new Error(`SOPS edit failed with exit code ${result.status}`);
@@ -99,13 +102,10 @@ export function setKey(filePath, key, value) {
99
102
  throw new Error('SOPS is not installed. Install with: brew install sops');
100
103
  }
101
104
  let content = '';
102
- // If file exists, decrypt it first
103
105
  if (existsSync(filePath)) {
104
106
  content = decrypt(filePath);
105
107
  }
106
- // Parse existing content into lines
107
108
  const lines = content.split('\n').filter(line => line.trim() !== '');
108
- // Find and update or add the key
109
109
  let found = false;
110
110
  const updatedLines = lines.map(line => {
111
111
  const match = line.match(/^([^=]+)=/);
@@ -122,16 +122,13 @@ export function setKey(filePath, key, value) {
122
122
  const tempFile = join(tmpdir(), `hush-temp-${Date.now()}.env`);
123
123
  try {
124
124
  writeFileSync(tempFile, newContent, 'utf-8');
125
- // Encrypt temp file to the target
126
125
  execSync(`sops --input-type dotenv --output-type dotenv --encrypt "${tempFile}" > "${filePath}"`, {
127
126
  encoding: 'utf-8',
128
- shell: '/bin/bash',
129
127
  stdio: ['pipe', 'pipe', 'pipe'],
130
128
  env: getSopsEnv(),
131
129
  });
132
130
  }
133
131
  finally {
134
- // Always clean up temp file
135
132
  if (existsSync(tempFile)) {
136
133
  unlinkSync(tempFile);
137
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {