@promptcellar/pc 0.5.2 → 0.5.4
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/README.md +9 -4
- package/bin/pc.js +2 -2
- package/hooks/codex-capture.js +1 -1
- package/hooks/gemini-capture.js +1 -1
- package/hooks/prompt-capture.js +1 -1
- package/package.json +2 -2
- package/src/commands/login.js +28 -5
- package/src/commands/setup.js +7 -7
- package/src/commands/status.js +1 -1
- package/src/index.js +1 -1
- package/src/lib/config.js +22 -2
- package/src/lib/context.js +15 -1
- package/src/lib/keychain.js +7 -2
- package/src/lib/websocket.js +1 -1
- package/tests/config.test.js +29 -0
- package/tests/context.test.js +23 -0
- package/tests/device-login.test.js +28 -0
- package/tests/keychain.test.js +34 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Prompt Cellar CLI
|
|
2
2
|
|
|
3
|
-
Command-line tool for capturing, managing, and reusing AI prompts with [
|
|
3
|
+
Command-line tool for capturing, managing, and reusing AI prompts with [Prompt Cellar](https://prompts.weldedanvil.com).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -103,11 +103,16 @@ Configuration is stored in:
|
|
|
103
103
|
- Linux: `~/.config/promptcellar-nodejs/`
|
|
104
104
|
- Windows: `%APPDATA%/promptcellar-nodejs/`
|
|
105
105
|
|
|
106
|
+
Security flags and requirements:
|
|
107
|
+
- `PC_VAULT_KEY`: overrides the vault key used by the CLI.
|
|
108
|
+
- `PC_VAULT_KEY_FALLBACK=1`: enables file fallback when the keychain is locked.
|
|
109
|
+
- `apiUrl` and `accountUrl` must use HTTPS (`http://localhost` is allowed).
|
|
110
|
+
|
|
106
111
|
## API
|
|
107
112
|
|
|
108
|
-
The CLI communicates with your
|
|
113
|
+
The CLI communicates with your Prompt Cellar instance. Get your API key from:
|
|
109
114
|
|
|
110
|
-
Settings > API Keys in your
|
|
115
|
+
Settings > API Keys in your Prompt Cellar dashboard.
|
|
111
116
|
|
|
112
117
|
## License
|
|
113
118
|
|
package/bin/pc.js
CHANGED
|
@@ -16,12 +16,12 @@ const pkg = require('../package.json');
|
|
|
16
16
|
|
|
17
17
|
program
|
|
18
18
|
.name('pc')
|
|
19
|
-
.description('
|
|
19
|
+
.description('Prompt Cellar CLI - sync prompts between your terminal and the cloud')
|
|
20
20
|
.version(pkg.version);
|
|
21
21
|
|
|
22
22
|
program
|
|
23
23
|
.command('login')
|
|
24
|
-
.description('Authenticate with
|
|
24
|
+
.description('Authenticate with Prompt Cellar via browser')
|
|
25
25
|
.option('-u, --url <url>', 'API URL (for self-hosted instances)')
|
|
26
26
|
.option('-a, --account-url <url>', 'Account URL (for self-hosted instances)')
|
|
27
27
|
.option('-c, --client-id <id>', 'OIDC client ID (default: prompts-prod)')
|
package/hooks/codex-capture.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Codex CLI notify hook for capturing prompts to
|
|
4
|
+
* Codex CLI notify hook for capturing prompts to Prompt Cellar.
|
|
5
5
|
*
|
|
6
6
|
* Codex calls this script with a JSON argument containing:
|
|
7
7
|
* - type: event type (e.g., 'agent-turn-complete')
|
package/hooks/gemini-capture.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Gemini CLI BeforeAgent hook for capturing prompts to
|
|
4
|
+
* Gemini CLI BeforeAgent hook for capturing prompts to Prompt Cellar.
|
|
5
5
|
*
|
|
6
6
|
* Gemini hooks receive JSON via stdin with:
|
|
7
7
|
* - hook_event_name: the hook event (e.g., 'BeforeAgent')
|
package/hooks/prompt-capture.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Claude Code UserPromptSubmit hook for capturing prompts to
|
|
4
|
+
* Claude Code UserPromptSubmit hook for capturing prompts to Prompt Cellar.
|
|
5
5
|
*
|
|
6
6
|
* This script is called by Claude Code on every user prompt submission.
|
|
7
7
|
* UserPromptSubmit hooks receive JSON via stdin with:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptcellar/pc",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "CLI for
|
|
3
|
+
"version": "0.5.4",
|
|
4
|
+
"description": "CLI for Prompt Cellar - sync prompts between your terminal and the cloud",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
package/src/commands/login.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes } from 'crypto';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import {
|
|
5
|
+
normalizeAndValidateUrl,
|
|
5
6
|
setApiKey, setApiUrl, setAccountUrl,
|
|
6
7
|
setVaultAvailable,
|
|
7
8
|
isLoggedIn
|
|
@@ -123,6 +124,26 @@ export async function finalizeDeviceLogin({
|
|
|
123
124
|
return exchangeTokenForApiKeyFn(apiUrl, approval.access_token, deviceName);
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export function persistLoginState({
|
|
128
|
+
apiUrl,
|
|
129
|
+
accountUrl,
|
|
130
|
+
apiKey,
|
|
131
|
+
vaultAvailable = true,
|
|
132
|
+
normalizeFn = normalizeAndValidateUrl,
|
|
133
|
+
setApiUrlFn = setApiUrl,
|
|
134
|
+
setAccountUrlFn = setAccountUrl,
|
|
135
|
+
setApiKeyFn = setApiKey,
|
|
136
|
+
setVaultAvailableFn = setVaultAvailable,
|
|
137
|
+
}) {
|
|
138
|
+
const normalizedApiUrl = normalizeFn(apiUrl);
|
|
139
|
+
const normalizedAccountUrl = normalizeFn(accountUrl);
|
|
140
|
+
|
|
141
|
+
setApiUrlFn(normalizedApiUrl);
|
|
142
|
+
setAccountUrlFn(normalizedAccountUrl);
|
|
143
|
+
setApiKeyFn(apiKey);
|
|
144
|
+
setVaultAvailableFn(vaultAvailable);
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
function getDeviceName() {
|
|
127
148
|
const os = process.platform;
|
|
128
149
|
const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'CLI';
|
|
@@ -145,7 +166,7 @@ export async function login(options) {
|
|
|
145
166
|
return;
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
console.log(chalk.bold('\
|
|
169
|
+
console.log(chalk.bold('\nPrompt Cellar CLI Login\n'));
|
|
149
170
|
|
|
150
171
|
const spinner = ora('Connecting...').start();
|
|
151
172
|
|
|
@@ -181,10 +202,12 @@ export async function login(options) {
|
|
|
181
202
|
});
|
|
182
203
|
|
|
183
204
|
// Step 5: Store credentials
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
persistLoginState({
|
|
206
|
+
apiUrl,
|
|
207
|
+
accountUrl,
|
|
208
|
+
apiKey: result.api_key,
|
|
209
|
+
vaultAvailable: true,
|
|
210
|
+
});
|
|
188
211
|
|
|
189
212
|
spinner.succeed(chalk.green('Logged in successfully!'));
|
|
190
213
|
console.log(`\nWelcome, ${chalk.cyan(result.email)}!`);
|
package/src/commands/setup.js
CHANGED
|
@@ -133,7 +133,7 @@ function saveGeminiSettings(settings) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
export async function setup() {
|
|
136
|
-
console.log(chalk.bold('\
|
|
136
|
+
console.log(chalk.bold('\nPrompt Cellar CLI Setup\n'));
|
|
137
137
|
|
|
138
138
|
const installedTools = detectInstalledTools();
|
|
139
139
|
|
|
@@ -178,7 +178,7 @@ export async function setup() {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
console.log(chalk.green('\nSetup complete!'));
|
|
181
|
-
console.log('Your prompts will be automatically captured to
|
|
181
|
+
console.log('Your prompts will be automatically captured to Prompt Cellar.\n');
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
async function setupClaudeCode() {
|
|
@@ -261,7 +261,7 @@ async function setupCodex() {
|
|
|
261
261
|
|
|
262
262
|
// Remove existing notify line and comment
|
|
263
263
|
config = config.split('\n')
|
|
264
|
-
.filter(line => !line.includes('pc-codex-capture') && !line.includes('#
|
|
264
|
+
.filter(line => !line.includes('pc-codex-capture') && !line.includes('# Prompt Cellar capture hook'))
|
|
265
265
|
.join('\n');
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -278,10 +278,10 @@ async function setupCodex() {
|
|
|
278
278
|
const insertPos = firstTableMatch.index;
|
|
279
279
|
const before = config.slice(0, insertPos).trimEnd();
|
|
280
280
|
const after = config.slice(insertPos);
|
|
281
|
-
config = before + '\n\n#
|
|
281
|
+
config = before + '\n\n# Prompt Cellar capture hook\n' + notifyLine + '\n\n' + after;
|
|
282
282
|
} else {
|
|
283
283
|
// No table sections — safe to append
|
|
284
|
-
config = config.trimEnd() + '\n\n#
|
|
284
|
+
config = config.trimEnd() + '\n\n# Prompt Cellar capture hook\n' + notifyLine + '\n';
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
@@ -339,7 +339,7 @@ async function setupGemini() {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
export async function unsetup() {
|
|
342
|
-
console.log(chalk.bold('\nRemoving
|
|
342
|
+
console.log(chalk.bold('\nRemoving Prompt Cellar hooks...\n'));
|
|
343
343
|
|
|
344
344
|
let removed = false;
|
|
345
345
|
|
|
@@ -366,7 +366,7 @@ export async function unsetup() {
|
|
|
366
366
|
let codexConfig = getCodexConfig();
|
|
367
367
|
if (isCodexHookInstalled(codexConfig)) {
|
|
368
368
|
codexConfig = codexConfig.split('\n')
|
|
369
|
-
.filter(line => !line.includes('pc-codex-capture') && !line.includes('#
|
|
369
|
+
.filter(line => !line.includes('pc-codex-capture') && !line.includes('# Prompt Cellar capture hook'))
|
|
370
370
|
.join('\n');
|
|
371
371
|
// Clean up extra blank lines left behind
|
|
372
372
|
codexConfig = codexConfig.replace(/\n{3,}/g, '\n\n');
|
package/src/commands/status.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getGitContext } from '../lib/context.js';
|
|
|
5
5
|
import { loadVaultKey } from '../lib/keychain.js';
|
|
6
6
|
|
|
7
7
|
export async function status() {
|
|
8
|
-
console.log(chalk.bold('\
|
|
8
|
+
console.log(chalk.bold('\nPrompt Cellar CLI Status\n'));
|
|
9
9
|
|
|
10
10
|
// Auth status
|
|
11
11
|
if (isLoggedIn()) {
|
package/src/index.js
CHANGED
package/src/lib/config.js
CHANGED
|
@@ -50,8 +50,28 @@ export function getApiUrl() {
|
|
|
50
50
|
return config.get('apiUrl');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export function normalizeAndValidateUrl(rawUrl) {
|
|
54
|
+
if (typeof rawUrl !== 'string' || !rawUrl.trim()) {
|
|
55
|
+
throw new Error('URL is required');
|
|
56
|
+
}
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(rawUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error('Invalid URL');
|
|
62
|
+
}
|
|
63
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
64
|
+
throw new Error('URL must be http or https');
|
|
65
|
+
}
|
|
66
|
+
const isLocalhost = ['localhost', '127.0.0.1'].includes(url.hostname);
|
|
67
|
+
if (url.protocol !== 'https:' && !isLocalhost) {
|
|
68
|
+
throw new Error('URL must use https');
|
|
69
|
+
}
|
|
70
|
+
return url.origin;
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
export function setApiUrl(url) {
|
|
54
|
-
config.set('apiUrl', url);
|
|
74
|
+
config.set('apiUrl', normalizeAndValidateUrl(url));
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
export function getCaptureLevel() {
|
|
@@ -79,7 +99,7 @@ export function getAccountUrl() {
|
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
export function setAccountUrl(url) {
|
|
82
|
-
config.set('accountUrl', url);
|
|
102
|
+
config.set('accountUrl', normalizeAndValidateUrl(url));
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
export function isVaultAvailable() {
|
package/src/lib/context.js
CHANGED
|
@@ -9,6 +9,20 @@ function execGit(...args) {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function sanitizeGitRemote(remote) {
|
|
13
|
+
if (!remote) return remote;
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(remote);
|
|
16
|
+
url.username = '';
|
|
17
|
+
url.password = '';
|
|
18
|
+
url.search = '';
|
|
19
|
+
url.hash = '';
|
|
20
|
+
return `${url.origin}${url.pathname}`;
|
|
21
|
+
} catch {
|
|
22
|
+
return remote.replace(/\/\/[^@]+@/, '//');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
export function getGitContext() {
|
|
13
27
|
const level = getCaptureLevel();
|
|
14
28
|
|
|
@@ -28,7 +42,7 @@ export function getGitContext() {
|
|
|
28
42
|
|
|
29
43
|
// Rich: add remote URL and commit hash
|
|
30
44
|
if (level === 'rich' && topLevel) {
|
|
31
|
-
context.git_remote_url = execGit('remote', 'get-url', 'origin');
|
|
45
|
+
context.git_remote_url = sanitizeGitRemote(execGit('remote', 'get-url', 'origin'));
|
|
32
46
|
context.git_commit = execGit('rev-parse', 'HEAD');
|
|
33
47
|
}
|
|
34
48
|
|
package/src/lib/keychain.js
CHANGED
|
@@ -43,19 +43,24 @@ export async function storeVaultKey(keyB64url) {
|
|
|
43
43
|
try {
|
|
44
44
|
await keytar.setPassword(SERVICE, ACCOUNT, keyB64url);
|
|
45
45
|
return;
|
|
46
|
-
} catch {
|
|
47
|
-
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (process.env.PC_VAULT_KEY_FALLBACK !== '1') {
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
48
50
|
}
|
|
49
51
|
writeFallback(keyB64url);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export async function loadVaultKey() {
|
|
55
|
+
const envVaultKey = process.env.PC_VAULT_KEY;
|
|
56
|
+
if (envVaultKey) return envVaultKey;
|
|
53
57
|
try {
|
|
54
58
|
const val = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
55
59
|
if (val) return val;
|
|
56
60
|
} catch {
|
|
57
61
|
// Keychain unavailable — try file fallback
|
|
58
62
|
}
|
|
63
|
+
if (process.env.PC_VAULT_KEY_FALLBACK !== '1') return null;
|
|
59
64
|
return readFallback();
|
|
60
65
|
}
|
|
61
66
|
|
package/src/lib/websocket.js
CHANGED
|
@@ -37,7 +37,7 @@ export function connect(tool, onPromptReceived) {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
socket.on('registered', (data) => {
|
|
40
|
-
console.log(`Connected to
|
|
40
|
+
console.log(`Connected to Prompt Cellar (session: ${data.session_id})`);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
socket.on('prompt_pushed', (data) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { normalizeAndValidateUrl } from '../src/lib/config.js';
|
|
4
|
+
|
|
5
|
+
describe('normalizeAndValidateUrl', () => {
|
|
6
|
+
it('accepts https URLs', () => {
|
|
7
|
+
const url = normalizeAndValidateUrl('https://prompts.weldedanvil.com');
|
|
8
|
+
assert.equal(url, 'https://prompts.weldedanvil.com');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('accepts localhost http URLs', () => {
|
|
12
|
+
const url = normalizeAndValidateUrl('http://localhost:3000');
|
|
13
|
+
assert.equal(url, 'http://localhost:3000');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('rejects non-https for non-local hosts', () => {
|
|
17
|
+
assert.throws(
|
|
18
|
+
() => normalizeAndValidateUrl('http://example.com'),
|
|
19
|
+
/https/i,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects non-http schemes', () => {
|
|
24
|
+
assert.throws(
|
|
25
|
+
() => normalizeAndValidateUrl('ftp://example.com'),
|
|
26
|
+
/http/i,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { sanitizeGitRemote } from '../src/lib/context.js';
|
|
4
|
+
|
|
5
|
+
describe('sanitizeGitRemote', () => {
|
|
6
|
+
it('strips credentials and query/fragment', () => {
|
|
7
|
+
const input = 'https://token:secret@github.com/org/repo.git?foo=bar#frag';
|
|
8
|
+
const output = sanitizeGitRemote(input);
|
|
9
|
+
assert.equal(output, 'https://github.com/org/repo.git');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('removes userinfo from https URL', () => {
|
|
13
|
+
const input = 'https://token@github.com/org/repo.git';
|
|
14
|
+
const output = sanitizeGitRemote(input);
|
|
15
|
+
assert.equal(output, 'https://github.com/org/repo.git');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('leaves scp-style remotes intact', () => {
|
|
19
|
+
const input = 'git@github.com:org/repo.git';
|
|
20
|
+
const output = sanitizeGitRemote(input);
|
|
21
|
+
assert.equal(output, input);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -118,4 +118,32 @@ describe('device login helpers', () => {
|
|
|
118
118
|
assert.equal(exchangeCalled, false);
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
describe('persistLoginState', () => {
|
|
123
|
+
it('does not persist when validation fails', () => {
|
|
124
|
+
const { persistLoginState } = login;
|
|
125
|
+
assert.equal(typeof persistLoginState, 'function', 'persistLoginState should be exported');
|
|
126
|
+
|
|
127
|
+
const calls = [];
|
|
128
|
+
const normalizeFn = () => {
|
|
129
|
+
throw new Error('invalid url');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
assert.throws(
|
|
133
|
+
() => persistLoginState({
|
|
134
|
+
apiUrl: 'http://bad',
|
|
135
|
+
accountUrl: 'http://bad',
|
|
136
|
+
apiKey: 'api-key',
|
|
137
|
+
normalizeFn,
|
|
138
|
+
setApiUrlFn: () => calls.push('apiUrl'),
|
|
139
|
+
setAccountUrlFn: () => calls.push('accountUrl'),
|
|
140
|
+
setApiKeyFn: () => calls.push('apiKey'),
|
|
141
|
+
setVaultAvailableFn: () => calls.push('vaultAvailable'),
|
|
142
|
+
}),
|
|
143
|
+
/invalid url/i,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
assert.deepEqual(calls, []);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
121
149
|
});
|
package/tests/keychain.test.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import keytar from 'keytar';
|
|
3
4
|
import * as keychain from '../src/lib/keychain.js';
|
|
5
|
+
import { loadVaultKey, storeVaultKey } from '../src/lib/keychain.js';
|
|
6
|
+
|
|
7
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
8
|
+
|
|
9
|
+
function resetEnv() {
|
|
10
|
+
process.env = { ...ORIGINAL_ENV };
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
describe('requireVaultKey', () => {
|
|
6
14
|
it('returns key when available', async () => {
|
|
@@ -38,3 +46,28 @@ describe('requireVaultKey', () => {
|
|
|
38
46
|
assert.equal(key, null);
|
|
39
47
|
});
|
|
40
48
|
});
|
|
49
|
+
|
|
50
|
+
describe('vault key env handling', () => {
|
|
51
|
+
it('prefers PC_VAULT_KEY when set', async () => {
|
|
52
|
+
resetEnv();
|
|
53
|
+
process.env.PC_VAULT_KEY = 'env-key';
|
|
54
|
+
mock.method(keytar, 'getPassword', async () => {
|
|
55
|
+
throw new Error('should not call keytar');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const key = await loadVaultKey();
|
|
59
|
+
assert.equal(key, 'env-key');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws when keytar fails and fallback disabled', async () => {
|
|
63
|
+
resetEnv();
|
|
64
|
+
mock.method(keytar, 'setPassword', async () => {
|
|
65
|
+
throw new Error('keytar locked');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
() => storeVaultKey('vault-key'),
|
|
70
|
+
/keytar locked/i,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|