@cuytamvan/cuypwm 1.0.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.
- package/.prettierrc +5 -0
- package/.vscode/settings.json +4 -0
- package/CLAUDE.md +106 -0
- package/README.md +101 -0
- package/bun.lock +97 -0
- package/dist/cuypwm.js +6819 -0
- package/package.json +29 -0
- package/src/cli.ts +249 -0
- package/src/command-pages/addCredential.ts +190 -0
- package/src/command-pages/generatePassword.ts +13 -0
- package/src/command-pages/listCredentials.ts +21 -0
- package/src/command-pages/manageUsers.ts +98 -0
- package/src/command-pages/viewCredential.ts +345 -0
- package/src/config.ts +15 -0
- package/src/crypto.ts +79 -0
- package/src/generator.ts +84 -0
- package/src/index.ts +64 -0
- package/src/keys.ts +167 -0
- package/src/store.ts +74 -0
- package/src/types.ts +43 -0
- package/src/ui.ts +212 -0
- package/src/utils/checkCancel.ts +10 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/passwordOrGenerate.ts +31 -0
- package/src/utils/resolvePath.ts +6 -0
- package/src/utils/userFilter.ts +37 -0
- package/tsconfig.json +30 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cuytamvan/cuypwm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"module": "src/index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cuypwm": "dist/cuypwm.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "bun run src/index.ts",
|
|
11
|
+
"dev": "bun run src/index.ts --watch",
|
|
12
|
+
"build": "bun build ./src/index.ts --compile --outfile=./dist/cuypwm",
|
|
13
|
+
"build:node": "bun build ./src/index.ts --outfile=./dist/cuypwm.js --target=node",
|
|
14
|
+
"pretty": "prettier --write src/**/*.ts"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/bun": "latest",
|
|
18
|
+
"prettier": "^3.9.4"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"typescript": "^5"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@clack/prompts": "^1.7.0",
|
|
25
|
+
"boxen": "^8.0.1",
|
|
26
|
+
"chalk": "^5.6.2",
|
|
27
|
+
"cli-table3": "^0.6.5"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadEntries, loadUsers } from './store';
|
|
3
|
+
import { readPrivateKeyPem, getPrivateKeyPassphraseIfNeeded } from './keys';
|
|
4
|
+
import { decryptData } from './crypto';
|
|
5
|
+
import { entriesTable, TYPE_META } from './ui';
|
|
6
|
+
import type { PasswordEntry, CredentialType } from './types';
|
|
7
|
+
|
|
8
|
+
function showHelp() {
|
|
9
|
+
const T = chalk.bold.cyan;
|
|
10
|
+
const D = chalk.dim;
|
|
11
|
+
const types = Object.keys(TYPE_META).join(', ');
|
|
12
|
+
console.log(`
|
|
13
|
+
${T('cuypwm')} — CLI Password Manager
|
|
14
|
+
${D('powered by Bun & OpenSSL')}
|
|
15
|
+
|
|
16
|
+
${chalk.bold('Usage:')}
|
|
17
|
+
cuypwm Launch interactive TUI
|
|
18
|
+
cuypwm -h Show this help
|
|
19
|
+
|
|
20
|
+
${chalk.bold('Commands:')}
|
|
21
|
+
${T('cuypwm ls')}
|
|
22
|
+
List all credentials (table view).
|
|
23
|
+
|
|
24
|
+
${T('cuypwm get')} ${chalk.yellow('<cred_id>')}
|
|
25
|
+
Show credential detail without decrypting password.
|
|
26
|
+
|
|
27
|
+
${T('cuypwm get')} ${chalk.yellow('<cred_id>')} ${chalk.green('--copy-password')}
|
|
28
|
+
Decrypt and copy password to clipboard (macOS / Linux).
|
|
29
|
+
|
|
30
|
+
${T('cuypwm search')} ${chalk.yellow('<query>')}
|
|
31
|
+
Search across source, username, host, port, description.
|
|
32
|
+
|
|
33
|
+
${T('cuypwm search')} ${chalk.green('--type')} ${chalk.yellow('<type>')}
|
|
34
|
+
Filter by credential type.
|
|
35
|
+
Available types: ${D(types)}
|
|
36
|
+
|
|
37
|
+
${T('cuypwm search')} ${chalk.yellow('<query>')} ${chalk.green('--type')} ${chalk.yellow('<type>')}
|
|
38
|
+
Combine text search and type filter.
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function copyToClipboard(text: string): Promise<void> {
|
|
43
|
+
let cmd: string[];
|
|
44
|
+
if (process.platform === 'darwin') {
|
|
45
|
+
cmd = ['pbcopy'];
|
|
46
|
+
} else {
|
|
47
|
+
// try xclip, fallback to xsel
|
|
48
|
+
const which = Bun.spawnSync(['which', 'xclip']);
|
|
49
|
+
cmd =
|
|
50
|
+
which.exitCode === 0
|
|
51
|
+
? ['xclip', '-selection', 'clipboard']
|
|
52
|
+
: ['xsel', '--clipboard', '--input'];
|
|
53
|
+
}
|
|
54
|
+
const proc = Bun.spawn(cmd, { stdin: 'pipe' });
|
|
55
|
+
proc.stdin!.write(text);
|
|
56
|
+
await proc.stdin!.end();
|
|
57
|
+
await proc.exited;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchEntry(entry: PasswordEntry, query: string): boolean {
|
|
61
|
+
const q = query.toLowerCase();
|
|
62
|
+
if (entry.source.toLowerCase().includes(q)) return true;
|
|
63
|
+
if (entry.description.toLowerCase().includes(q)) return true;
|
|
64
|
+
if (entry.type === 'ssh_cred') {
|
|
65
|
+
if (entry.username.toLowerCase().includes(q)) return true;
|
|
66
|
+
if (entry.host.toLowerCase().includes(q)) return true;
|
|
67
|
+
if (String(entry.port).includes(q)) return true;
|
|
68
|
+
}
|
|
69
|
+
if (entry.type !== 'ssh_key') {
|
|
70
|
+
if ((entry as { username: string }).username.toLowerCase().includes(q))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function cmdLs(rest: string[]) {
|
|
77
|
+
const showIdFlag = rest.includes('--show-id');
|
|
78
|
+
|
|
79
|
+
const entries = loadEntries();
|
|
80
|
+
const users = loadUsers();
|
|
81
|
+
if (!entries.length) {
|
|
82
|
+
console.log(chalk.yellow('No credentials saved yet.'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(entriesTable(entries, users, !!showIdFlag));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function cmdGet(rest: string[]) {
|
|
89
|
+
const idArg = rest.find((a) => !a.startsWith('-'));
|
|
90
|
+
const copyFlag = rest.includes('--copy-password');
|
|
91
|
+
const showPrivateKeyFlag = rest.includes('--show-private-key');
|
|
92
|
+
|
|
93
|
+
if (!idArg) {
|
|
94
|
+
console.error(
|
|
95
|
+
chalk.red(
|
|
96
|
+
'Usage: cuypwm get <cred_id> [--copy-password] [--show-private-key]',
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const entries = loadEntries();
|
|
103
|
+
const users = loadUsers();
|
|
104
|
+
const entry = entries.find((e) => e.id === idArg);
|
|
105
|
+
|
|
106
|
+
if (!entry) {
|
|
107
|
+
console.error(chalk.red(`Credential with id '${idArg}' not found.`));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const userMap = new Map(users.map((u) => [u.id, u.name]));
|
|
112
|
+
const meta = TYPE_META[entry.type];
|
|
113
|
+
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(
|
|
116
|
+
` ${chalk.hex(meta.color).bold(`${meta.icon} ${entry.source}`)} ${chalk.dim(`[${entry.type}]`)}`,
|
|
117
|
+
);
|
|
118
|
+
console.log(` ${chalk.dim('ID')} : ${chalk.dim(entry.id)}`);
|
|
119
|
+
if (entry.cred_user_id) {
|
|
120
|
+
const userName = userMap.get(entry.cred_user_id) ?? chalk.dim('?');
|
|
121
|
+
console.log(
|
|
122
|
+
` ${chalk.dim('User')} : ${chalk.whiteBright(userName)}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
console.log(
|
|
126
|
+
` ${chalk.dim('Description')} : ${entry.description || chalk.dim('-')}`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (entry.type === 'ssh_key') {
|
|
130
|
+
console.log(
|
|
131
|
+
` ${chalk.dim('Public Key')} :\n${chalk.white(entry.public_key.trim())}`,
|
|
132
|
+
);
|
|
133
|
+
if (copyFlag) {
|
|
134
|
+
console.log(
|
|
135
|
+
chalk.yellow('\n --copy-password is not applicable for SSH Keys.'),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (showPrivateKeyFlag) {
|
|
139
|
+
const privateKey = await decryptEntry(entry.encrypted_private_key);
|
|
140
|
+
console.log(
|
|
141
|
+
` ${chalk.dim('Private Key')} :\n${chalk.white(privateKey.trim())}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
} else if (entry.type === 'ssh_cred') {
|
|
145
|
+
console.log(
|
|
146
|
+
` ${chalk.dim('Host')} : ${chalk.whiteBright(entry.host)}`,
|
|
147
|
+
);
|
|
148
|
+
console.log(
|
|
149
|
+
` ${chalk.dim('Port')} : ${chalk.whiteBright(String(entry.port))}`,
|
|
150
|
+
);
|
|
151
|
+
console.log(
|
|
152
|
+
` ${chalk.dim('Username')} : ${chalk.whiteBright(entry.username)}`,
|
|
153
|
+
);
|
|
154
|
+
console.log(
|
|
155
|
+
` ${chalk.dim('Password')} : ${chalk.dim('(encrypted — use --copy-password)')}`,
|
|
156
|
+
);
|
|
157
|
+
if (copyFlag) {
|
|
158
|
+
const pw = await decryptEntry(entry.encrypted_password);
|
|
159
|
+
await copyToClipboard(pw);
|
|
160
|
+
console.log(chalk.green('\n ✔ Password copied to clipboard.'));
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
console.log(
|
|
164
|
+
` ${chalk.dim('Username')} : ${chalk.whiteBright(entry.username)}`,
|
|
165
|
+
);
|
|
166
|
+
console.log(
|
|
167
|
+
` ${chalk.dim('Password')} : ${chalk.dim('(encrypted — use --copy-password)')}`,
|
|
168
|
+
);
|
|
169
|
+
if (entry.extra) {
|
|
170
|
+
for (const [k, v] of Object.entries(entry.extra)) {
|
|
171
|
+
console.log(` ${chalk.dim(k.padEnd(11))} : ${chalk.whiteBright(v)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (copyFlag) {
|
|
175
|
+
const pw = await decryptEntry(entry.encrypted_password);
|
|
176
|
+
await copyToClipboard(pw);
|
|
177
|
+
console.log(chalk.green('\n ✔ Password copied to clipboard.'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function decryptEntry(encryptedPassword: string): Promise<string> {
|
|
184
|
+
const privateKeyPem = readPrivateKeyPem();
|
|
185
|
+
const passphrase = await getPrivateKeyPassphraseIfNeeded();
|
|
186
|
+
return decryptData(privateKeyPem, passphrase, encryptedPassword);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function cmdSearch(rest: string[]) {
|
|
190
|
+
const typeIdx = rest.indexOf('--type');
|
|
191
|
+
const typeFilter: CredentialType | undefined =
|
|
192
|
+
typeIdx !== -1 ? (rest[typeIdx + 1] as CredentialType) : undefined;
|
|
193
|
+
|
|
194
|
+
const queryTokens = rest.filter((a, i) => {
|
|
195
|
+
if (a.startsWith('-')) return false;
|
|
196
|
+
if (typeIdx !== -1 && i === typeIdx + 1) return false; // skip the type value
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
const query = queryTokens.join(' ').trim();
|
|
200
|
+
|
|
201
|
+
if (!query && !typeFilter) {
|
|
202
|
+
console.error(chalk.red('Usage: cuypwm search <query> [--type <type>]'));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const entries = loadEntries();
|
|
207
|
+
const users = loadUsers();
|
|
208
|
+
let results = entries;
|
|
209
|
+
|
|
210
|
+
if (typeFilter) {
|
|
211
|
+
results = results.filter((e) => e.type === typeFilter);
|
|
212
|
+
}
|
|
213
|
+
if (query) {
|
|
214
|
+
results = results.filter((e) => matchEntry(e, query));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!results.length) {
|
|
218
|
+
console.log(chalk.yellow('No matching results found.'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(entriesTable(results, users));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function runCli(args: string[]): Promise<boolean> {
|
|
226
|
+
const [cmd, ...rest] = args;
|
|
227
|
+
|
|
228
|
+
if (cmd === '-h' || cmd === '--help') {
|
|
229
|
+
showHelp();
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (cmd === 'ls') {
|
|
234
|
+
await cmdLs(rest);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (cmd === 'get') {
|
|
239
|
+
await cmdGet(rest);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (cmd === 'search') {
|
|
244
|
+
await cmdSearch(rest);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { readPublicKey } from '../keys';
|
|
5
|
+
import { encryptData } from '../crypto';
|
|
6
|
+
import { loadUsers, addEntry, newId } from '../store';
|
|
7
|
+
import { credentialTypeOptions } from '../ui';
|
|
8
|
+
import { checkCancel, resolvePath, passwordOrGenerate } from '../utils';
|
|
9
|
+
import type {
|
|
10
|
+
SimpleCredentialEntry,
|
|
11
|
+
SshCredEntry,
|
|
12
|
+
SshKeyEntry,
|
|
13
|
+
CredentialType,
|
|
14
|
+
} from '../types';
|
|
15
|
+
|
|
16
|
+
export async function addCredentialFlow() {
|
|
17
|
+
const type = checkCancel(
|
|
18
|
+
await p.select({
|
|
19
|
+
message: 'Select credential type',
|
|
20
|
+
options: credentialTypeOptions(),
|
|
21
|
+
}),
|
|
22
|
+
) as CredentialType;
|
|
23
|
+
|
|
24
|
+
const users = loadUsers();
|
|
25
|
+
let cred_user_id: string | null = null;
|
|
26
|
+
if (users.length) {
|
|
27
|
+
const userChoice = checkCancel(
|
|
28
|
+
await p.select({
|
|
29
|
+
message: 'Link to a user?',
|
|
30
|
+
options: [
|
|
31
|
+
{ value: '__none__', label: chalk.dim('— No User —') },
|
|
32
|
+
...users.map((u) => ({ value: u.id, label: u.name })),
|
|
33
|
+
],
|
|
34
|
+
}),
|
|
35
|
+
) as string;
|
|
36
|
+
cred_user_id = userChoice === '__none__' ? null : userChoice;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const publicKey = readPublicKey();
|
|
40
|
+
const id = newId();
|
|
41
|
+
const createdAt = new Date().toISOString();
|
|
42
|
+
|
|
43
|
+
if (type === 'ssh_key') {
|
|
44
|
+
const source = checkCancel(
|
|
45
|
+
await p.text({
|
|
46
|
+
message: 'Name / label (source)',
|
|
47
|
+
placeholder: 'e.g. server-prod',
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
const description = checkCancel(
|
|
51
|
+
await p.text({ message: 'Description (optional)', defaultValue: '' }),
|
|
52
|
+
);
|
|
53
|
+
const privateKeyPath = checkCancel(
|
|
54
|
+
await p.text({
|
|
55
|
+
message: 'Private key file path',
|
|
56
|
+
placeholder: '~/.ssh/id_rsa',
|
|
57
|
+
validate(v) {
|
|
58
|
+
if (!v) return 'Required';
|
|
59
|
+
if (!existsSync(resolvePath(v))) return 'File not found';
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
const publicKeyPath = checkCancel(
|
|
64
|
+
await p.text({
|
|
65
|
+
message: 'Public key file path',
|
|
66
|
+
placeholder: '~/.ssh/id_rsa.pub',
|
|
67
|
+
validate(v) {
|
|
68
|
+
if (!v) return 'Required';
|
|
69
|
+
if (!existsSync(resolvePath(v))) return 'File not found';
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const privateKeyContent = readFileSync(resolvePath(privateKeyPath), 'utf8');
|
|
75
|
+
const publicKeyContent = readFileSync(resolvePath(publicKeyPath), 'utf8');
|
|
76
|
+
|
|
77
|
+
const entry: SshKeyEntry = {
|
|
78
|
+
id,
|
|
79
|
+
type,
|
|
80
|
+
source,
|
|
81
|
+
description: description || '',
|
|
82
|
+
createdAt,
|
|
83
|
+
cred_user_id,
|
|
84
|
+
encrypted_private_key: encryptData(publicKey, privateKeyContent),
|
|
85
|
+
public_key: publicKeyContent,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
addEntry(entry);
|
|
89
|
+
p.log.success(
|
|
90
|
+
chalk.green(`✔ SSH Key '${chalk.bold(source)}' saved successfully.`),
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (type === 'ssh_cred') {
|
|
96
|
+
const source = checkCancel(
|
|
97
|
+
await p.text({
|
|
98
|
+
message: 'Name / label (source)',
|
|
99
|
+
placeholder: 'e.g. server-prod',
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
const host = checkCancel(
|
|
103
|
+
await p.text({ message: 'Host', placeholder: '192.168.1.1' }),
|
|
104
|
+
);
|
|
105
|
+
const portInput = checkCancel(
|
|
106
|
+
await p.text({
|
|
107
|
+
message: 'Port',
|
|
108
|
+
placeholder: '22',
|
|
109
|
+
initialValue: '22',
|
|
110
|
+
validate(v) {
|
|
111
|
+
if (v && Number.isNaN(Number(v))) return 'Port must be a number';
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
const username = checkCancel(await p.text({ message: 'Username' }));
|
|
116
|
+
const password = await passwordOrGenerate();
|
|
117
|
+
const description = checkCancel(
|
|
118
|
+
await p.text({ message: 'Description (optional)', defaultValue: '' }),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const entry: SshCredEntry = {
|
|
122
|
+
id,
|
|
123
|
+
type,
|
|
124
|
+
source,
|
|
125
|
+
description: description || '',
|
|
126
|
+
createdAt,
|
|
127
|
+
cred_user_id,
|
|
128
|
+
host,
|
|
129
|
+
port: Number(portInput || '22'),
|
|
130
|
+
username,
|
|
131
|
+
encrypted_password: encryptData(publicKey, password),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
addEntry(entry);
|
|
135
|
+
p.log.success(
|
|
136
|
+
chalk.green(
|
|
137
|
+
`✔ SSH Credential '${chalk.bold(source)}' saved successfully.`,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// github, gitlab, gmail, bank, website
|
|
144
|
+
const source = checkCancel(
|
|
145
|
+
await p.text({
|
|
146
|
+
message: 'Source / Name (e.g. github.com, bank name, website URL)',
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
const username = checkCancel(
|
|
150
|
+
await p.text({ message: 'Username / Email / Account number' }),
|
|
151
|
+
);
|
|
152
|
+
const password = await passwordOrGenerate();
|
|
153
|
+
const description = checkCancel(
|
|
154
|
+
await p.text({ message: 'Description (optional)', defaultValue: '' }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
let extra: Record<string, string> | undefined;
|
|
158
|
+
|
|
159
|
+
if (type === 'bank') {
|
|
160
|
+
const accountNumber = checkCancel(
|
|
161
|
+
await p.text({ message: 'Account number (optional)', defaultValue: '' }),
|
|
162
|
+
);
|
|
163
|
+
if (accountNumber)
|
|
164
|
+
extra = { ...(extra || {}), account_number: accountNumber };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (type === 'website') {
|
|
168
|
+
const url = checkCancel(
|
|
169
|
+
await p.text({ message: 'Website URL (optional)', defaultValue: '' }),
|
|
170
|
+
);
|
|
171
|
+
if (url) extra = { ...(extra || {}), url };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const entry: SimpleCredentialEntry = {
|
|
175
|
+
id,
|
|
176
|
+
type,
|
|
177
|
+
source,
|
|
178
|
+
description: description || '',
|
|
179
|
+
createdAt,
|
|
180
|
+
cred_user_id,
|
|
181
|
+
username,
|
|
182
|
+
encrypted_password: encryptData(publicKey, password),
|
|
183
|
+
extra,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
addEntry(entry);
|
|
187
|
+
p.log.success(
|
|
188
|
+
chalk.green(`✔ Credential '${chalk.bold(source)}' saved successfully.`),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { promptGeneratePassword } from '../generator';
|
|
4
|
+
import { passwordBox } from '../ui';
|
|
5
|
+
|
|
6
|
+
export async function generatePasswordFlow() {
|
|
7
|
+
const generated = await promptGeneratePassword();
|
|
8
|
+
if (!generated) {
|
|
9
|
+
p.log.warn(chalk.yellow('Generate password dibatalkan.'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log(passwordBox(generated));
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { entriesTable } from '../ui';
|
|
4
|
+
import { pickFilteredEntries } from '../utils';
|
|
5
|
+
|
|
6
|
+
export async function listCredentialsFlow() {
|
|
7
|
+
const { entries, users, total } = await pickFilteredEntries();
|
|
8
|
+
|
|
9
|
+
if (!total) {
|
|
10
|
+
p.log.warn(chalk.yellow('No credentials saved yet.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!entries.length) {
|
|
15
|
+
p.log.warn(chalk.yellow('No credentials found for this user.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(entriesTable(entries, users));
|
|
21
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { loadUsers, addUser, updateUser, deleteUser, newId } from '../store';
|
|
4
|
+
import { printBanner, pressEnterHint } from '../ui';
|
|
5
|
+
import { checkCancel } from '../utils';
|
|
6
|
+
|
|
7
|
+
export async function manageUsersFlow() {
|
|
8
|
+
while (true) {
|
|
9
|
+
printBanner();
|
|
10
|
+
const users = loadUsers();
|
|
11
|
+
|
|
12
|
+
if (users.length) {
|
|
13
|
+
console.log();
|
|
14
|
+
const lines = users.map(
|
|
15
|
+
(u, i) =>
|
|
16
|
+
` ${chalk.dim(`${i + 1}.`)} ${chalk.whiteBright(u.name)} ${chalk.dim(u.id)}`,
|
|
17
|
+
);
|
|
18
|
+
console.log(chalk.bold.cyan('👤 Users:'));
|
|
19
|
+
console.log(lines.join('\n'));
|
|
20
|
+
console.log();
|
|
21
|
+
} else {
|
|
22
|
+
console.log(chalk.dim('\n No users registered yet.\n'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const action = checkCancel(
|
|
26
|
+
await p.select({
|
|
27
|
+
message: 'Manage Users',
|
|
28
|
+
options: [
|
|
29
|
+
{ value: 'add', label: `${chalk.green('+')} Add User` },
|
|
30
|
+
{ value: 'edit', label: `${chalk.yellow('✏️ ')} Edit User` },
|
|
31
|
+
{ value: 'delete', label: `${chalk.red('🗑 ')} Delete User` },
|
|
32
|
+
{ value: 'back', label: chalk.dim('← Back') },
|
|
33
|
+
],
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (action === 'back') return;
|
|
38
|
+
|
|
39
|
+
if (action === 'add') {
|
|
40
|
+
const name = checkCancel(
|
|
41
|
+
await p.text({ message: 'User name', placeholder: 'e.g. John Doe' }),
|
|
42
|
+
);
|
|
43
|
+
addUser({ id: newId(), name });
|
|
44
|
+
p.log.success(
|
|
45
|
+
chalk.green(`✔ User '${chalk.bold(name)}' added successfully.`),
|
|
46
|
+
);
|
|
47
|
+
} else if (action === 'edit') {
|
|
48
|
+
if (!users.length) {
|
|
49
|
+
p.log.warn(chalk.yellow('No users yet.'));
|
|
50
|
+
} else {
|
|
51
|
+
const selectedId = checkCancel(
|
|
52
|
+
await p.select({
|
|
53
|
+
message: 'Select user to edit',
|
|
54
|
+
options: users.map((u) => ({ value: u.id, label: u.name })),
|
|
55
|
+
}),
|
|
56
|
+
) as string;
|
|
57
|
+
const user = users.find((u) => u.id === selectedId)!;
|
|
58
|
+
const name = checkCancel(
|
|
59
|
+
await p.text({ message: 'New name', initialValue: user.name }),
|
|
60
|
+
);
|
|
61
|
+
updateUser({ ...user, name });
|
|
62
|
+
p.log.success(
|
|
63
|
+
chalk.green(`✔ User '${chalk.bold(name)}' updated successfully.`),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
} else if (action === 'delete') {
|
|
67
|
+
if (!users.length) {
|
|
68
|
+
p.log.warn(chalk.yellow('No users yet.'));
|
|
69
|
+
} else {
|
|
70
|
+
const selectedId = checkCancel(
|
|
71
|
+
await p.select({
|
|
72
|
+
message: 'Select user to delete',
|
|
73
|
+
options: users.map((u) => ({ value: u.id, label: u.name })),
|
|
74
|
+
}),
|
|
75
|
+
) as string;
|
|
76
|
+
const user = users.find((u) => u.id === selectedId)!;
|
|
77
|
+
const confirmed = checkCancel(
|
|
78
|
+
await p.confirm({
|
|
79
|
+
message: `Delete user '${chalk.bold(user.name)}'? Linked credentials will not be deleted.`,
|
|
80
|
+
initialValue: false,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
if (confirmed) {
|
|
84
|
+
deleteUser(user.id);
|
|
85
|
+
p.log.success(
|
|
86
|
+
chalk.green(
|
|
87
|
+
`✔ User '${chalk.bold(user.name)}' deleted successfully.`,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
p.log.info(chalk.dim('Cancelled.'));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await p.text({ message: pressEnterHint(), defaultValue: '' });
|
|
97
|
+
}
|
|
98
|
+
}
|