@id-wispera/cli 0.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.
- package/README.md +250 -0
- package/dist/commands/audit.d.ts +6 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +82 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +310 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +88 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/exec.d.ts +8 -0
- package/dist/commands/exec.d.ts.map +1 -0
- package/dist/commands/exec.js +163 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/import.d.ts +7 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +1166 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +91 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate.d.ts +7 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +105 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/provision.d.ts +7 -0
- package/dist/commands/provision.d.ts.map +1 -0
- package/dist/commands/provision.js +303 -0
- package/dist/commands/provision.js.map +1 -0
- package/dist/commands/revoke.d.ts +6 -0
- package/dist/commands/revoke.d.ts.map +1 -0
- package/dist/commands/revoke.js +70 -0
- package/dist/commands/revoke.js.map +1 -0
- package/dist/commands/scan.d.ts +16 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +700 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/share.d.ts +6 -0
- package/dist/commands/share.d.ts.map +1 -0
- package/dist/commands/share.js +144 -0
- package/dist/commands/share.js.map +1 -0
- package/dist/commands/show.d.ts +6 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +64 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/display.d.ts +78 -0
- package/dist/utils/display.d.ts.map +1 -0
- package/dist/utils/display.js +290 -0
- package/dist/utils/display.js.map +1 -0
- package/dist/utils/prompts.d.ts +67 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +353 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/vault-helpers.d.ts +21 -0
- package/dist/utils/vault-helpers.d.ts.map +1 -0
- package/dist/utils/vault-helpers.js +45 -0
- package/dist/utils/vault-helpers.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import command: idw import <file>
|
|
3
|
+
* Supports .env, .json, and OpenClaw format imports
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { readFile, readdir, access, stat } from 'fs/promises';
|
|
8
|
+
import { basename, extname, join, resolve, relative } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { unlockVault, createPassport, detectCredentials, classifyCredential, vaultExists, getDefaultVaultPath, } from '@id-wispera/core';
|
|
12
|
+
import { promptPassphrase, confirmImport } from '../utils/prompts.js';
|
|
13
|
+
import { error, success, info, warning, maskCredential, title } from '../utils/display.js';
|
|
14
|
+
import { walkDirectory, scanFile } from './scan.js';
|
|
15
|
+
/**
|
|
16
|
+
* Parse .env file
|
|
17
|
+
*/
|
|
18
|
+
function parseEnvFile(content) {
|
|
19
|
+
const credentials = [];
|
|
20
|
+
const lines = content.split('\n');
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i]?.trim();
|
|
23
|
+
if (!line || line.startsWith('#'))
|
|
24
|
+
continue;
|
|
25
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
26
|
+
if (match) {
|
|
27
|
+
const [, name, value] = match;
|
|
28
|
+
if (name && value) {
|
|
29
|
+
// Remove quotes if present
|
|
30
|
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
31
|
+
// Check if it looks like a credential
|
|
32
|
+
const type = classifyCredential(cleanValue);
|
|
33
|
+
const looksLikeSecret = type !== 'custom' ||
|
|
34
|
+
name.toLowerCase().includes('key') ||
|
|
35
|
+
name.toLowerCase().includes('secret') ||
|
|
36
|
+
name.toLowerCase().includes('token') ||
|
|
37
|
+
name.toLowerCase().includes('password') ||
|
|
38
|
+
name.toLowerCase().includes('api');
|
|
39
|
+
if (looksLikeSecret && cleanValue.length > 5) {
|
|
40
|
+
credentials.push({
|
|
41
|
+
name,
|
|
42
|
+
type: type === 'custom' ? 'secret' : type,
|
|
43
|
+
value: cleanValue,
|
|
44
|
+
line: i + 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return credentials;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Parse JSON config file
|
|
54
|
+
*/
|
|
55
|
+
function parseJsonFile(content, filename) {
|
|
56
|
+
const credentials = [];
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.parse(content);
|
|
59
|
+
function findCredentials(obj, path = '') {
|
|
60
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
61
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
62
|
+
if (typeof value === 'string' && value.length > 5) {
|
|
63
|
+
const type = classifyCredential(value);
|
|
64
|
+
const looksLikeSecret = type !== 'custom' ||
|
|
65
|
+
key.toLowerCase().includes('key') ||
|
|
66
|
+
key.toLowerCase().includes('secret') ||
|
|
67
|
+
key.toLowerCase().includes('token') ||
|
|
68
|
+
key.toLowerCase().includes('password') ||
|
|
69
|
+
key.toLowerCase().includes('api');
|
|
70
|
+
if (looksLikeSecret) {
|
|
71
|
+
credentials.push({
|
|
72
|
+
name: currentPath,
|
|
73
|
+
type: type === 'custom' ? 'secret' : type,
|
|
74
|
+
value,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (typeof value === 'object' && value !== null) {
|
|
79
|
+
findCredentials(value, currentPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
findCredentials(json);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not valid JSON, try regex detection
|
|
87
|
+
const detected = detectCredentials(content);
|
|
88
|
+
for (const result of detected) {
|
|
89
|
+
credentials.push({
|
|
90
|
+
name: `${filename}:${result.line}`,
|
|
91
|
+
type: result.type,
|
|
92
|
+
value: result.value,
|
|
93
|
+
line: result.line,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return credentials;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Guess platform from credential name/value
|
|
101
|
+
*/
|
|
102
|
+
function guessPlatform(name, value) {
|
|
103
|
+
const nameLower = name.toLowerCase();
|
|
104
|
+
if (nameLower.includes('anthropic') || value.startsWith('sk-ant-'))
|
|
105
|
+
return 'anthropic';
|
|
106
|
+
if (nameLower.includes('openai') || value.startsWith('sk-'))
|
|
107
|
+
return 'openai';
|
|
108
|
+
if (nameLower.includes('github') || value.startsWith('ghp_') || value.startsWith('gho_'))
|
|
109
|
+
return 'github';
|
|
110
|
+
if (nameLower.includes('aws') || value.startsWith('AKIA'))
|
|
111
|
+
return 'aws';
|
|
112
|
+
if (nameLower.includes('azure'))
|
|
113
|
+
return 'azure-ai';
|
|
114
|
+
if (nameLower.includes('google') || value.startsWith('AIza'))
|
|
115
|
+
return 'google-a2a';
|
|
116
|
+
if (nameLower.includes('stripe') || value.startsWith('sk_live_') || value.startsWith('sk_test_'))
|
|
117
|
+
return 'custom';
|
|
118
|
+
return 'custom';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Capitalize first letter
|
|
122
|
+
*/
|
|
123
|
+
function capitalizeFirst(str) {
|
|
124
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Scan and collect all OpenClaw credentials
|
|
128
|
+
*/
|
|
129
|
+
async function scanOpenClawCredentials() {
|
|
130
|
+
const credentials = [];
|
|
131
|
+
const openclawPath = join(homedir(), '.openclaw');
|
|
132
|
+
try {
|
|
133
|
+
await access(openclawPath);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return credentials;
|
|
137
|
+
}
|
|
138
|
+
// Scan WhatsApp credentials
|
|
139
|
+
const whatsappPath = join(openclawPath, 'credentials', 'whatsapp');
|
|
140
|
+
try {
|
|
141
|
+
const accounts = await readdir(whatsappPath);
|
|
142
|
+
for (const accountId of accounts) {
|
|
143
|
+
const credsPath = join(whatsappPath, accountId, 'creds.json');
|
|
144
|
+
try {
|
|
145
|
+
const content = await readFile(credsPath, 'utf-8');
|
|
146
|
+
const data = JSON.parse(content);
|
|
147
|
+
credentials.push({
|
|
148
|
+
name: `OpenClaw — WhatsApp Session (${accountId})`,
|
|
149
|
+
type: 'session-keys',
|
|
150
|
+
visaType: 'access',
|
|
151
|
+
value: content,
|
|
152
|
+
platforms: ['openclaw'],
|
|
153
|
+
tags: ['openclaw', 'whatsapp', 'session', 'imported'],
|
|
154
|
+
filePath: credsPath,
|
|
155
|
+
metadata: { accountId, botName: data.me?.name },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Skip unreadable files
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// WhatsApp dir doesn't exist
|
|
165
|
+
}
|
|
166
|
+
// Scan auth profiles (LLM API keys)
|
|
167
|
+
const agentsPath = join(openclawPath, 'agents');
|
|
168
|
+
try {
|
|
169
|
+
const agents = await readdir(agentsPath);
|
|
170
|
+
for (const agentId of agents) {
|
|
171
|
+
const authPath = join(agentsPath, agentId, 'agent', 'auth-profiles.json');
|
|
172
|
+
try {
|
|
173
|
+
const content = await readFile(authPath, 'utf-8');
|
|
174
|
+
const data = JSON.parse(content);
|
|
175
|
+
for (const [provider, profile] of Object.entries(data)) {
|
|
176
|
+
if (profile.type === 'api-key' && profile.key) {
|
|
177
|
+
credentials.push({
|
|
178
|
+
name: `OpenClaw — ${capitalizeFirst(provider)} API Key`,
|
|
179
|
+
type: 'api-key',
|
|
180
|
+
visaType: 'privilege',
|
|
181
|
+
value: profile.key,
|
|
182
|
+
platforms: ['openclaw', 'mcp', provider].filter(Boolean),
|
|
183
|
+
tags: ['openclaw', provider, 'api-key', 'llm', 'imported'],
|
|
184
|
+
filePath: authPath,
|
|
185
|
+
metadata: { agentId, provider, model: profile.model },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Skip unreadable files
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Agents dir doesn't exist
|
|
197
|
+
}
|
|
198
|
+
// Scan OAuth tokens
|
|
199
|
+
const oauthPath = join(openclawPath, 'credentials', 'oauth.json');
|
|
200
|
+
try {
|
|
201
|
+
const content = await readFile(oauthPath, 'utf-8');
|
|
202
|
+
const data = JSON.parse(content);
|
|
203
|
+
for (const [provider, tokens] of Object.entries(data)) {
|
|
204
|
+
if (tokens.access_token) {
|
|
205
|
+
const expiresAt = tokens.expires_at ? new Date(tokens.expires_at * 1000) : undefined;
|
|
206
|
+
credentials.push({
|
|
207
|
+
name: `OpenClaw — ${capitalizeFirst(provider)} OAuth Token`,
|
|
208
|
+
type: 'oauth-token',
|
|
209
|
+
visaType: 'access',
|
|
210
|
+
value: tokens.access_token,
|
|
211
|
+
platforms: ['openclaw'],
|
|
212
|
+
tags: ['openclaw', provider, 'oauth', 'imported'],
|
|
213
|
+
filePath: oauthPath,
|
|
214
|
+
metadata: { provider, hasRefreshToken: !!tokens.refresh_token },
|
|
215
|
+
expiresAt,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// OAuth file doesn't exist
|
|
222
|
+
}
|
|
223
|
+
// Scan openclaw.json for channel tokens
|
|
224
|
+
const configPath = join(openclawPath, 'openclaw.json');
|
|
225
|
+
try {
|
|
226
|
+
const content = await readFile(configPath, 'utf-8');
|
|
227
|
+
const data = JSON.parse(content);
|
|
228
|
+
// Telegram
|
|
229
|
+
if (data.channels?.telegram?.token) {
|
|
230
|
+
credentials.push({
|
|
231
|
+
name: 'OpenClaw — Telegram Bot Token',
|
|
232
|
+
type: 'bot-token',
|
|
233
|
+
visaType: 'access',
|
|
234
|
+
value: data.channels.telegram.token,
|
|
235
|
+
platforms: ['openclaw'],
|
|
236
|
+
tags: ['openclaw', 'telegram', 'bot-token', 'imported'],
|
|
237
|
+
filePath: configPath,
|
|
238
|
+
metadata: { channel: 'telegram' },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Slack Bot Token
|
|
242
|
+
if (data.channels?.slack?.botToken) {
|
|
243
|
+
credentials.push({
|
|
244
|
+
name: 'OpenClaw — Slack Bot Token',
|
|
245
|
+
type: 'bot-token',
|
|
246
|
+
visaType: 'access',
|
|
247
|
+
value: data.channels.slack.botToken,
|
|
248
|
+
platforms: ['openclaw'],
|
|
249
|
+
tags: ['openclaw', 'slack', 'bot-token', 'imported'],
|
|
250
|
+
filePath: configPath,
|
|
251
|
+
metadata: { channel: 'slack' },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// Slack App Token
|
|
255
|
+
if (data.channels?.slack?.appToken) {
|
|
256
|
+
credentials.push({
|
|
257
|
+
name: 'OpenClaw — Slack App Token',
|
|
258
|
+
type: 'bot-token',
|
|
259
|
+
visaType: 'access',
|
|
260
|
+
value: data.channels.slack.appToken,
|
|
261
|
+
platforms: ['openclaw'],
|
|
262
|
+
tags: ['openclaw', 'slack', 'app-token', 'imported'],
|
|
263
|
+
filePath: configPath,
|
|
264
|
+
metadata: { channel: 'slack' },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Discord
|
|
268
|
+
if (data.channels?.discord?.token) {
|
|
269
|
+
credentials.push({
|
|
270
|
+
name: 'OpenClaw — Discord Bot Token',
|
|
271
|
+
type: 'bot-token',
|
|
272
|
+
visaType: 'access',
|
|
273
|
+
value: data.channels.discord.token,
|
|
274
|
+
platforms: ['openclaw'],
|
|
275
|
+
tags: ['openclaw', 'discord', 'bot-token', 'imported'],
|
|
276
|
+
filePath: configPath,
|
|
277
|
+
metadata: { channel: 'discord' },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Gateway token
|
|
281
|
+
if (data.gateway?.token) {
|
|
282
|
+
credentials.push({
|
|
283
|
+
name: 'OpenClaw — Gateway Token',
|
|
284
|
+
type: 'api-key',
|
|
285
|
+
visaType: 'privilege',
|
|
286
|
+
value: data.gateway.token,
|
|
287
|
+
platforms: ['openclaw'],
|
|
288
|
+
tags: ['openclaw', 'gateway', 'admin', 'imported'],
|
|
289
|
+
filePath: configPath,
|
|
290
|
+
metadata: { gatewayPort: data.gateway.port },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Config file doesn't exist
|
|
296
|
+
}
|
|
297
|
+
// Scan allowlists
|
|
298
|
+
const credsPath = join(openclawPath, 'credentials');
|
|
299
|
+
try {
|
|
300
|
+
const files = await readdir(credsPath);
|
|
301
|
+
for (const file of files) {
|
|
302
|
+
if (file.endsWith('-allowFrom.json')) {
|
|
303
|
+
const filePath = join(credsPath, file);
|
|
304
|
+
try {
|
|
305
|
+
const content = await readFile(filePath, 'utf-8');
|
|
306
|
+
const data = JSON.parse(content);
|
|
307
|
+
const channel = file.replace('-allowFrom.json', '');
|
|
308
|
+
const pairedCount = Object.keys(data).length;
|
|
309
|
+
credentials.push({
|
|
310
|
+
name: `OpenClaw — ${capitalizeFirst(channel)} Pairing Allowlist`,
|
|
311
|
+
type: 'custom',
|
|
312
|
+
visaType: 'access',
|
|
313
|
+
value: content,
|
|
314
|
+
platforms: ['openclaw'],
|
|
315
|
+
tags: ['openclaw', channel, 'allowlist', 'imported'],
|
|
316
|
+
filePath,
|
|
317
|
+
metadata: { channel, pairedCount },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Skip unreadable files
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Credentials dir doesn't exist
|
|
328
|
+
}
|
|
329
|
+
return credentials;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Display OpenClaw credentials for selection
|
|
333
|
+
*/
|
|
334
|
+
function displayOpenClawCredentials(credentials) {
|
|
335
|
+
console.log();
|
|
336
|
+
title('OpenClaw Credentials Found');
|
|
337
|
+
console.log();
|
|
338
|
+
for (let i = 0; i < credentials.length; i++) {
|
|
339
|
+
const cred = credentials[i];
|
|
340
|
+
if (!cred)
|
|
341
|
+
continue;
|
|
342
|
+
const visaLabel = cred.visaType === 'privilege' ? chalk.red('Privilege') : chalk.green('Access');
|
|
343
|
+
const valuePreview = maskCredential(cred.value, 6);
|
|
344
|
+
console.log(` ${chalk.cyan(`[${i + 1}]`)} ${cred.name}`);
|
|
345
|
+
console.log(` Type: ${cred.type} | Visa: ${visaLabel}`);
|
|
346
|
+
console.log(` Preview: ${chalk.dim(valuePreview)}`);
|
|
347
|
+
console.log(` Source: ${chalk.dim(cred.filePath)}`);
|
|
348
|
+
console.log();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Discover .env files in a directory (non-recursive, top-level only)
|
|
353
|
+
*/
|
|
354
|
+
async function discoverEnvFiles(dir) {
|
|
355
|
+
const found = [];
|
|
356
|
+
try {
|
|
357
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
if (entry.isFile() && (entry.name === '.env' || entry.name.startsWith('.env.'))) {
|
|
360
|
+
found.push(join(dir, entry.name));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Directory not readable
|
|
366
|
+
}
|
|
367
|
+
return found.sort();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Discover JSON config files in a directory (non-recursive, top-level only)
|
|
371
|
+
* Excludes well-known non-credential files
|
|
372
|
+
*/
|
|
373
|
+
const JSON_SKIP = new Set([
|
|
374
|
+
'package.json',
|
|
375
|
+
'package-lock.json',
|
|
376
|
+
'tsconfig.json',
|
|
377
|
+
'tsconfig.base.json',
|
|
378
|
+
'tsconfig.build.json',
|
|
379
|
+
'turbo.json',
|
|
380
|
+
'lerna.json',
|
|
381
|
+
'nx.json',
|
|
382
|
+
'jest.config.json',
|
|
383
|
+
'biome.json',
|
|
384
|
+
'.eslintrc.json',
|
|
385
|
+
'.prettierrc.json',
|
|
386
|
+
'renovate.json',
|
|
387
|
+
'manifest.json',
|
|
388
|
+
'composer.json',
|
|
389
|
+
]);
|
|
390
|
+
async function discoverJsonFiles(dir) {
|
|
391
|
+
const found = [];
|
|
392
|
+
try {
|
|
393
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
394
|
+
for (const entry of entries) {
|
|
395
|
+
if (entry.isFile() && entry.name.endsWith('.json') && !JSON_SKIP.has(entry.name)) {
|
|
396
|
+
found.push(join(dir, entry.name));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Directory not readable
|
|
402
|
+
}
|
|
403
|
+
return found.sort();
|
|
404
|
+
}
|
|
405
|
+
export function createImportCommand() {
|
|
406
|
+
const command = new Command('import')
|
|
407
|
+
.description('Import credentials from files, directory scans, or OpenClaw')
|
|
408
|
+
.argument('[path]', 'File or directory to import from')
|
|
409
|
+
.option('--format <format>', 'Import format: env, json, openclaw')
|
|
410
|
+
.option('--all', 'Import all detected credentials from a directory scan')
|
|
411
|
+
.option('--min-confidence <level>', 'Minimum confidence threshold for scan import (0-1)')
|
|
412
|
+
.option('--owner <owner>', 'Human owner email')
|
|
413
|
+
.option('--auto-name', 'Auto-generate passport names')
|
|
414
|
+
.option('-y, --yes', 'Import all without confirmation')
|
|
415
|
+
.option('-p, --path <path>', 'Custom vault path')
|
|
416
|
+
.addHelpText('after', `
|
|
417
|
+
Examples:
|
|
418
|
+
$ idw import .env # Import from a specific .env file
|
|
419
|
+
$ idw import config.json # Import from a JSON config file
|
|
420
|
+
$ idw import --format env # Auto-discover .env files in current directory
|
|
421
|
+
$ idw import --format json # Auto-discover JSON config files in current directory
|
|
422
|
+
$ idw import --format openclaw # Auto-scan ~/.openclaw credentials
|
|
423
|
+
$ idw import ./project --all # Scan directory and import all detected credentials
|
|
424
|
+
$ idw import --all # Scan current directory for all credentials
|
|
425
|
+
$ idw import --min-confidence 0.9 # Only import high-confidence detections
|
|
426
|
+
$ idw import --format env -y # Auto-discover .env files and import all without prompting
|
|
427
|
+
`)
|
|
428
|
+
.action(async (file, options) => {
|
|
429
|
+
const vaultPath = options.path ?? getDefaultVaultPath();
|
|
430
|
+
// Check vault exists
|
|
431
|
+
if (!(await vaultExists(vaultPath))) {
|
|
432
|
+
error('Vault not found. Run `idw init` first.');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
// Handle OpenClaw format
|
|
436
|
+
if (options.format === 'openclaw') {
|
|
437
|
+
console.log();
|
|
438
|
+
title('OpenClaw Credential Import');
|
|
439
|
+
console.log();
|
|
440
|
+
const spinner = ora('Scanning OpenClaw credentials...').start();
|
|
441
|
+
const openclawCreds = await scanOpenClawCredentials();
|
|
442
|
+
spinner.stop();
|
|
443
|
+
if (openclawCreds.length === 0) {
|
|
444
|
+
info('No OpenClaw credentials found at ~/.openclaw/');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
console.log(chalk.green(`Found ${openclawCreds.length} credential(s) in OpenClaw installation`));
|
|
448
|
+
// Display credentials
|
|
449
|
+
displayOpenClawCredentials(openclawCreds);
|
|
450
|
+
// Confirm import
|
|
451
|
+
let selectedIndexes;
|
|
452
|
+
if (options.yes) {
|
|
453
|
+
selectedIndexes = openclawCreds.map((_, i) => i);
|
|
454
|
+
console.log(chalk.yellow(`Importing all ${openclawCreds.length} credentials (--yes flag)`));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const inquirer = await import('inquirer');
|
|
458
|
+
const { selected } = await inquirer.default.prompt([
|
|
459
|
+
{
|
|
460
|
+
type: 'checkbox',
|
|
461
|
+
name: 'selected',
|
|
462
|
+
message: 'Select credentials to import:',
|
|
463
|
+
choices: openclawCreds.map((c, i) => ({
|
|
464
|
+
name: `${c.name} (${c.type})`,
|
|
465
|
+
value: i,
|
|
466
|
+
checked: true,
|
|
467
|
+
})),
|
|
468
|
+
},
|
|
469
|
+
]);
|
|
470
|
+
selectedIndexes = selected;
|
|
471
|
+
}
|
|
472
|
+
if (selectedIndexes.length === 0) {
|
|
473
|
+
console.log('No credentials selected for import.');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// Get owner if not provided
|
|
477
|
+
let owner = options.owner;
|
|
478
|
+
if (!owner) {
|
|
479
|
+
const inquirer = await import('inquirer');
|
|
480
|
+
const { ownerEmail } = await inquirer.default.prompt([
|
|
481
|
+
{
|
|
482
|
+
type: 'input',
|
|
483
|
+
name: 'ownerEmail',
|
|
484
|
+
message: 'Human owner email (who is responsible for these credentials):',
|
|
485
|
+
validate: (input) => input.includes('@') || 'Valid email required',
|
|
486
|
+
},
|
|
487
|
+
]);
|
|
488
|
+
owner = ownerEmail;
|
|
489
|
+
}
|
|
490
|
+
// Unlock vault
|
|
491
|
+
const passphrase = await promptPassphrase();
|
|
492
|
+
const unlockSpinner = ora('Unlocking vault...').start();
|
|
493
|
+
let vault;
|
|
494
|
+
try {
|
|
495
|
+
vault = await unlockVault(passphrase, vaultPath);
|
|
496
|
+
unlockSpinner.succeed('Vault unlocked');
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
unlockSpinner.fail('Failed to unlock vault');
|
|
500
|
+
error(err instanceof Error ? err.message : 'Invalid passphrase');
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
// Import selected credentials
|
|
504
|
+
const importSpinner = ora('Importing credentials...').start();
|
|
505
|
+
let imported = 0;
|
|
506
|
+
let failed = 0;
|
|
507
|
+
for (const idx of selectedIndexes) {
|
|
508
|
+
const cred = openclawCreds[idx];
|
|
509
|
+
if (!cred)
|
|
510
|
+
continue;
|
|
511
|
+
try {
|
|
512
|
+
// Build delegation chain
|
|
513
|
+
const delegationChain = [
|
|
514
|
+
{
|
|
515
|
+
from: owner,
|
|
516
|
+
to: 'OpenClaw Instance',
|
|
517
|
+
grantedAt: new Date().toISOString(),
|
|
518
|
+
scope: ['*'],
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
if (cred.metadata?.agentId) {
|
|
522
|
+
delegationChain.push({
|
|
523
|
+
from: 'OpenClaw Instance',
|
|
524
|
+
to: `Agent: ${cred.metadata.agentId}`,
|
|
525
|
+
grantedAt: new Date().toISOString(),
|
|
526
|
+
scope: ['*'],
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
await createPassport(vault, {
|
|
530
|
+
name: cred.name,
|
|
531
|
+
credentialType: cred.type,
|
|
532
|
+
credentialValue: cred.value,
|
|
533
|
+
visaType: cred.visaType,
|
|
534
|
+
issuingAuthority: 'OpenClaw (self-managed)',
|
|
535
|
+
platforms: cred.platforms,
|
|
536
|
+
scope: ['*'],
|
|
537
|
+
validFrom: new Date().toISOString(),
|
|
538
|
+
validUntil: cred.expiresAt?.toISOString(),
|
|
539
|
+
delegationChain,
|
|
540
|
+
humanOwner: owner,
|
|
541
|
+
tags: cred.tags,
|
|
542
|
+
notes: `Imported from OpenClaw: ${cred.filePath}`,
|
|
543
|
+
});
|
|
544
|
+
imported++;
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
failed++;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
importSpinner.stop();
|
|
551
|
+
console.log();
|
|
552
|
+
if (imported > 0) {
|
|
553
|
+
success(`Imported ${imported} credential(s) as passports.`);
|
|
554
|
+
}
|
|
555
|
+
if (failed > 0) {
|
|
556
|
+
warning(`Failed to import ${failed} credential(s).`);
|
|
557
|
+
}
|
|
558
|
+
console.log();
|
|
559
|
+
info('Run `idw list --tag openclaw` to see your OpenClaw passports.');
|
|
560
|
+
warning('Source files still contain plaintext credentials. Consider securely deleting them.');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Auto-discovery for --format env (no path given)
|
|
564
|
+
if (options.format === 'env' && !file) {
|
|
565
|
+
const searchDir = resolve('.');
|
|
566
|
+
console.log();
|
|
567
|
+
title('Environment File Auto-Discovery');
|
|
568
|
+
console.log();
|
|
569
|
+
const spinner = ora('Searching for .env files...').start();
|
|
570
|
+
const envFiles = await discoverEnvFiles(searchDir);
|
|
571
|
+
spinner.stop();
|
|
572
|
+
if (envFiles.length === 0) {
|
|
573
|
+
info(`No .env files found in ${searchDir}`);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
console.log(chalk.green(`Found ${envFiles.length} .env file(s):`));
|
|
577
|
+
for (const f of envFiles) {
|
|
578
|
+
console.log(` ${chalk.dim('•')} ${relative(searchDir, f) || basename(f)}`);
|
|
579
|
+
}
|
|
580
|
+
console.log();
|
|
581
|
+
// Parse all env files and combine credentials
|
|
582
|
+
const allDetected = [];
|
|
583
|
+
for (const envFile of envFiles) {
|
|
584
|
+
try {
|
|
585
|
+
const content = await readFile(envFile, 'utf-8');
|
|
586
|
+
const creds = parseEnvFile(content);
|
|
587
|
+
for (const c of creds) {
|
|
588
|
+
allDetected.push({ ...c, sourceFile: envFile });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
warning(`Cannot read: ${relative(searchDir, envFile) || basename(envFile)}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (allDetected.length === 0) {
|
|
596
|
+
info('No credentials detected in .env files.');
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
info(`Found ${allDetected.length} potential credential(s)`);
|
|
600
|
+
console.log();
|
|
601
|
+
for (let i = 0; i < allDetected.length; i++) {
|
|
602
|
+
const d = allDetected[i];
|
|
603
|
+
if (!d)
|
|
604
|
+
continue;
|
|
605
|
+
const relFile = relative(searchDir, d.sourceFile) || basename(d.sourceFile);
|
|
606
|
+
console.log(` ${chalk.cyan(`[${i + 1}]`)} ${chalk.bold(d.name)}`);
|
|
607
|
+
console.log(` File: ${chalk.dim(relFile)}${d.line ? ` | Line: ${d.line}` : ''}`);
|
|
608
|
+
console.log(` Type: ${d.type} | Value: ${chalk.dim(maskCredential(d.value, 4))}`);
|
|
609
|
+
console.log();
|
|
610
|
+
}
|
|
611
|
+
// Confirm import
|
|
612
|
+
let selectedIndexes;
|
|
613
|
+
if (options.yes) {
|
|
614
|
+
selectedIndexes = allDetected.map((_, i) => i);
|
|
615
|
+
console.log(chalk.yellow(`Importing all ${allDetected.length} credentials (--yes flag)`));
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
const inquirer = await import('inquirer');
|
|
619
|
+
const { selected } = await inquirer.default.prompt([
|
|
620
|
+
{
|
|
621
|
+
type: 'checkbox',
|
|
622
|
+
name: 'selected',
|
|
623
|
+
message: 'Select credentials to import:',
|
|
624
|
+
choices: allDetected.map((c, i) => ({
|
|
625
|
+
name: `${c.name} (${c.type}) — ${relative(searchDir, c.sourceFile) || basename(c.sourceFile)}`,
|
|
626
|
+
value: i,
|
|
627
|
+
checked: true,
|
|
628
|
+
})),
|
|
629
|
+
},
|
|
630
|
+
]);
|
|
631
|
+
selectedIndexes = selected;
|
|
632
|
+
}
|
|
633
|
+
if (selectedIndexes.length === 0) {
|
|
634
|
+
console.log('No credentials selected for import.');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// Get owner if not provided
|
|
638
|
+
let envOwner = options.owner;
|
|
639
|
+
if (!envOwner) {
|
|
640
|
+
const inquirer = await import('inquirer');
|
|
641
|
+
const { ownerEmail } = await inquirer.default.prompt([
|
|
642
|
+
{
|
|
643
|
+
type: 'input',
|
|
644
|
+
name: 'ownerEmail',
|
|
645
|
+
message: 'Human owner email:',
|
|
646
|
+
validate: (input) => input.includes('@') || 'Valid email required',
|
|
647
|
+
},
|
|
648
|
+
]);
|
|
649
|
+
envOwner = ownerEmail;
|
|
650
|
+
}
|
|
651
|
+
// Unlock vault
|
|
652
|
+
const envPassphrase = await promptPassphrase();
|
|
653
|
+
const unlockSpinner = ora('Unlocking vault...').start();
|
|
654
|
+
let envVault;
|
|
655
|
+
try {
|
|
656
|
+
envVault = await unlockVault(envPassphrase, vaultPath);
|
|
657
|
+
unlockSpinner.succeed('Vault unlocked');
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
unlockSpinner.fail('Failed to unlock vault');
|
|
661
|
+
error(err instanceof Error ? err.message : 'Invalid passphrase');
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
// Import selected credentials
|
|
665
|
+
const importSpinner = ora('Importing credentials...').start();
|
|
666
|
+
let envImported = 0;
|
|
667
|
+
let envFailed = 0;
|
|
668
|
+
for (const idx of selectedIndexes) {
|
|
669
|
+
const cred = allDetected[idx];
|
|
670
|
+
if (!cred)
|
|
671
|
+
continue;
|
|
672
|
+
try {
|
|
673
|
+
const platform = guessPlatform(cred.name, cred.value);
|
|
674
|
+
const sourceTag = basename(cred.sourceFile).replace(/\./g, '-');
|
|
675
|
+
await createPassport(envVault, {
|
|
676
|
+
name: options.autoName ? `${cred.name} (imported)` : cred.name,
|
|
677
|
+
credentialType: cred.type,
|
|
678
|
+
credentialValue: cred.value,
|
|
679
|
+
visaType: 'access',
|
|
680
|
+
platforms: [platform],
|
|
681
|
+
scope: [],
|
|
682
|
+
humanOwner: envOwner,
|
|
683
|
+
tags: ['imported', sourceTag],
|
|
684
|
+
notes: `Imported from ${cred.sourceFile}${cred.line ? ` (line ${cred.line})` : ''}`,
|
|
685
|
+
});
|
|
686
|
+
envImported++;
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
envFailed++;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
importSpinner.stop();
|
|
693
|
+
console.log();
|
|
694
|
+
if (envImported > 0) {
|
|
695
|
+
success(`Imported ${envImported} credential(s) as passports.`);
|
|
696
|
+
}
|
|
697
|
+
if (envFailed > 0) {
|
|
698
|
+
warning(`Failed to import ${envFailed} credential(s).`);
|
|
699
|
+
}
|
|
700
|
+
info('Run `idw list` to see your passports.');
|
|
701
|
+
warning('Source files still contain plaintext credentials. Consider securely deleting them.');
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// Auto-discovery for --format json (no path given)
|
|
705
|
+
if (options.format === 'json' && !file) {
|
|
706
|
+
const searchDir = resolve('.');
|
|
707
|
+
console.log();
|
|
708
|
+
title('JSON Config Auto-Discovery');
|
|
709
|
+
console.log();
|
|
710
|
+
const spinner = ora('Searching for JSON config files...').start();
|
|
711
|
+
const jsonFiles = await discoverJsonFiles(searchDir);
|
|
712
|
+
spinner.stop();
|
|
713
|
+
if (jsonFiles.length === 0) {
|
|
714
|
+
info(`No JSON config files found in ${searchDir}`);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
console.log(chalk.green(`Found ${jsonFiles.length} JSON file(s):`));
|
|
718
|
+
for (const f of jsonFiles) {
|
|
719
|
+
console.log(` ${chalk.dim('•')} ${relative(searchDir, f) || basename(f)}`);
|
|
720
|
+
}
|
|
721
|
+
console.log();
|
|
722
|
+
// Parse all JSON files and combine credentials
|
|
723
|
+
const allDetected = [];
|
|
724
|
+
for (const jsonFile of jsonFiles) {
|
|
725
|
+
try {
|
|
726
|
+
const content = await readFile(jsonFile, 'utf-8');
|
|
727
|
+
const creds = parseJsonFile(content, basename(jsonFile));
|
|
728
|
+
for (const c of creds) {
|
|
729
|
+
allDetected.push({ ...c, sourceFile: jsonFile });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
warning(`Cannot read: ${relative(searchDir, jsonFile) || basename(jsonFile)}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (allDetected.length === 0) {
|
|
737
|
+
info('No credentials detected in JSON files.');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
info(`Found ${allDetected.length} potential credential(s)`);
|
|
741
|
+
console.log();
|
|
742
|
+
for (let i = 0; i < allDetected.length; i++) {
|
|
743
|
+
const d = allDetected[i];
|
|
744
|
+
if (!d)
|
|
745
|
+
continue;
|
|
746
|
+
const relFile = relative(searchDir, d.sourceFile) || basename(d.sourceFile);
|
|
747
|
+
console.log(` ${chalk.cyan(`[${i + 1}]`)} ${chalk.bold(d.name)}`);
|
|
748
|
+
console.log(` File: ${chalk.dim(relFile)}${d.line ? ` | Line: ${d.line}` : ''}`);
|
|
749
|
+
console.log(` Type: ${d.type} | Value: ${chalk.dim(maskCredential(d.value, 4))}`);
|
|
750
|
+
console.log();
|
|
751
|
+
}
|
|
752
|
+
// Confirm import
|
|
753
|
+
let selectedIndexes;
|
|
754
|
+
if (options.yes) {
|
|
755
|
+
selectedIndexes = allDetected.map((_, i) => i);
|
|
756
|
+
console.log(chalk.yellow(`Importing all ${allDetected.length} credentials (--yes flag)`));
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
const inquirer = await import('inquirer');
|
|
760
|
+
const { selected } = await inquirer.default.prompt([
|
|
761
|
+
{
|
|
762
|
+
type: 'checkbox',
|
|
763
|
+
name: 'selected',
|
|
764
|
+
message: 'Select credentials to import:',
|
|
765
|
+
choices: allDetected.map((c, i) => ({
|
|
766
|
+
name: `${c.name} (${c.type}) — ${relative(searchDir, c.sourceFile) || basename(c.sourceFile)}`,
|
|
767
|
+
value: i,
|
|
768
|
+
checked: true,
|
|
769
|
+
})),
|
|
770
|
+
},
|
|
771
|
+
]);
|
|
772
|
+
selectedIndexes = selected;
|
|
773
|
+
}
|
|
774
|
+
if (selectedIndexes.length === 0) {
|
|
775
|
+
console.log('No credentials selected for import.');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
// Get owner if not provided
|
|
779
|
+
let jsonOwner = options.owner;
|
|
780
|
+
if (!jsonOwner) {
|
|
781
|
+
const inquirer = await import('inquirer');
|
|
782
|
+
const { ownerEmail } = await inquirer.default.prompt([
|
|
783
|
+
{
|
|
784
|
+
type: 'input',
|
|
785
|
+
name: 'ownerEmail',
|
|
786
|
+
message: 'Human owner email:',
|
|
787
|
+
validate: (input) => input.includes('@') || 'Valid email required',
|
|
788
|
+
},
|
|
789
|
+
]);
|
|
790
|
+
jsonOwner = ownerEmail;
|
|
791
|
+
}
|
|
792
|
+
// Unlock vault
|
|
793
|
+
const jsonPassphrase = await promptPassphrase();
|
|
794
|
+
const unlockSpinner = ora('Unlocking vault...').start();
|
|
795
|
+
let jsonVault;
|
|
796
|
+
try {
|
|
797
|
+
jsonVault = await unlockVault(jsonPassphrase, vaultPath);
|
|
798
|
+
unlockSpinner.succeed('Vault unlocked');
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
unlockSpinner.fail('Failed to unlock vault');
|
|
802
|
+
error(err instanceof Error ? err.message : 'Invalid passphrase');
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
// Import selected credentials
|
|
806
|
+
const importSpinner = ora('Importing credentials...').start();
|
|
807
|
+
let jsonImported = 0;
|
|
808
|
+
let jsonFailed = 0;
|
|
809
|
+
for (const idx of selectedIndexes) {
|
|
810
|
+
const cred = allDetected[idx];
|
|
811
|
+
if (!cred)
|
|
812
|
+
continue;
|
|
813
|
+
try {
|
|
814
|
+
const platform = guessPlatform(cred.name, cred.value);
|
|
815
|
+
const sourceTag = basename(cred.sourceFile).replace(/\./g, '-');
|
|
816
|
+
await createPassport(jsonVault, {
|
|
817
|
+
name: options.autoName ? `${cred.name} (imported)` : cred.name,
|
|
818
|
+
credentialType: cred.type,
|
|
819
|
+
credentialValue: cred.value,
|
|
820
|
+
visaType: 'access',
|
|
821
|
+
platforms: [platform],
|
|
822
|
+
scope: [],
|
|
823
|
+
humanOwner: jsonOwner,
|
|
824
|
+
tags: ['imported', sourceTag],
|
|
825
|
+
notes: `Imported from ${cred.sourceFile}${cred.line ? ` (line ${cred.line})` : ''}`,
|
|
826
|
+
});
|
|
827
|
+
jsonImported++;
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
jsonFailed++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
importSpinner.stop();
|
|
834
|
+
console.log();
|
|
835
|
+
if (jsonImported > 0) {
|
|
836
|
+
success(`Imported ${jsonImported} credential(s) as passports.`);
|
|
837
|
+
}
|
|
838
|
+
if (jsonFailed > 0) {
|
|
839
|
+
warning(`Failed to import ${jsonFailed} credential(s).`);
|
|
840
|
+
}
|
|
841
|
+
info('Run `idw list` to see your passports.');
|
|
842
|
+
warning('Source files still contain plaintext credentials. Consider securely deleting them.');
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// Scan-based import (--all or --min-confidence)
|
|
846
|
+
if (options.all || options.minConfidence) {
|
|
847
|
+
const scanPath = resolve(file || '.');
|
|
848
|
+
const minConf = options.all ? 0 : parseFloat(options.minConfidence);
|
|
849
|
+
console.log();
|
|
850
|
+
title('Scan-Based Import');
|
|
851
|
+
info(`Scanning: ${scanPath}`);
|
|
852
|
+
if (!options.all) {
|
|
853
|
+
info(`Minimum confidence: ${minConf}`);
|
|
854
|
+
}
|
|
855
|
+
console.log();
|
|
856
|
+
const spinner = ora('Scanning for credentials...').start();
|
|
857
|
+
const toLine = (r) => r.line ?? 0;
|
|
858
|
+
const toCol = (r) => r.column ?? 0;
|
|
859
|
+
const detections = [];
|
|
860
|
+
let filesScanned = 0;
|
|
861
|
+
try {
|
|
862
|
+
const pathStats = await stat(scanPath);
|
|
863
|
+
if (pathStats.isFile()) {
|
|
864
|
+
filesScanned = 1;
|
|
865
|
+
const results = await scanFile(scanPath);
|
|
866
|
+
const fileName = basename(scanPath);
|
|
867
|
+
for (const r of results) {
|
|
868
|
+
if (r.confidence >= minConf) {
|
|
869
|
+
detections.push({
|
|
870
|
+
file: scanPath,
|
|
871
|
+
name: `${r.pattern || r.type} in ${fileName}`,
|
|
872
|
+
type: r.type,
|
|
873
|
+
value: r.value,
|
|
874
|
+
line: toLine(r),
|
|
875
|
+
column: toCol(r),
|
|
876
|
+
confidence: r.confidence,
|
|
877
|
+
pattern: r.pattern || String(r.type),
|
|
878
|
+
context: r.context,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
for await (const filePath of walkDirectory(scanPath)) {
|
|
885
|
+
filesScanned++;
|
|
886
|
+
if (filesScanned % 100 === 0) {
|
|
887
|
+
spinner.text = `Scanned ${filesScanned} files...`;
|
|
888
|
+
}
|
|
889
|
+
const results = await scanFile(filePath);
|
|
890
|
+
const fileName = basename(filePath);
|
|
891
|
+
for (const r of results) {
|
|
892
|
+
if (r.confidence >= minConf) {
|
|
893
|
+
detections.push({
|
|
894
|
+
file: filePath,
|
|
895
|
+
name: `${r.pattern || r.type} in ${fileName}`,
|
|
896
|
+
type: r.type,
|
|
897
|
+
value: r.value,
|
|
898
|
+
line: toLine(r),
|
|
899
|
+
column: toCol(r),
|
|
900
|
+
confidence: r.confidence,
|
|
901
|
+
pattern: r.pattern || String(r.type),
|
|
902
|
+
context: r.context,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
catch (err) {
|
|
910
|
+
spinner.fail('Scan failed');
|
|
911
|
+
error(err instanceof Error ? err.message : 'Cannot access path');
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
spinner.succeed(`Scanned ${filesScanned} file(s)`);
|
|
915
|
+
if (detections.length === 0) {
|
|
916
|
+
info('No credentials detected matching criteria.');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
console.log();
|
|
920
|
+
warning(`Found ${detections.length} credential(s):`);
|
|
921
|
+
console.log();
|
|
922
|
+
// Display detections
|
|
923
|
+
for (let i = 0; i < detections.length; i++) {
|
|
924
|
+
const d = detections[i];
|
|
925
|
+
if (!d)
|
|
926
|
+
continue;
|
|
927
|
+
const confLabel = d.confidence >= 0.9 ? chalk.red('HIGH') :
|
|
928
|
+
d.confidence >= 0.7 ? chalk.yellow('MEDIUM') :
|
|
929
|
+
chalk.dim('LOW');
|
|
930
|
+
const relPath = relative(resolve(scanPath), d.file) || basename(d.file);
|
|
931
|
+
console.log(` ${chalk.cyan(`[${i + 1}]`)} ${chalk.bold(d.pattern)}`);
|
|
932
|
+
console.log(` File: ${chalk.dim(relPath)}`);
|
|
933
|
+
console.log(` Line: ${d.line} | Confidence: ${confLabel} (${d.confidence.toFixed(2)})`);
|
|
934
|
+
console.log(` Value: ${chalk.dim(maskCredential(d.value, 4))}`);
|
|
935
|
+
console.log();
|
|
936
|
+
}
|
|
937
|
+
// Confirm import
|
|
938
|
+
let scanSelectedIndexes;
|
|
939
|
+
if (options.yes) {
|
|
940
|
+
scanSelectedIndexes = detections.map((_, i) => i);
|
|
941
|
+
console.log(chalk.yellow(`Importing all ${detections.length} credentials (--yes flag)`));
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
const inquirer = await import('inquirer');
|
|
945
|
+
const { selected } = await inquirer.default.prompt([
|
|
946
|
+
{
|
|
947
|
+
type: 'checkbox',
|
|
948
|
+
name: 'selected',
|
|
949
|
+
message: 'Select credentials to import:',
|
|
950
|
+
choices: detections.map((d, i) => ({
|
|
951
|
+
name: `${d.name} (confidence: ${d.confidence.toFixed(2)})`,
|
|
952
|
+
value: i,
|
|
953
|
+
checked: true,
|
|
954
|
+
})),
|
|
955
|
+
},
|
|
956
|
+
]);
|
|
957
|
+
scanSelectedIndexes = selected;
|
|
958
|
+
}
|
|
959
|
+
if (scanSelectedIndexes.length === 0) {
|
|
960
|
+
console.log('No credentials selected for import.');
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// Get owner if not provided
|
|
964
|
+
let scanOwner = options.owner;
|
|
965
|
+
if (!scanOwner) {
|
|
966
|
+
const inquirer = await import('inquirer');
|
|
967
|
+
const { ownerEmail } = await inquirer.default.prompt([
|
|
968
|
+
{
|
|
969
|
+
type: 'input',
|
|
970
|
+
name: 'ownerEmail',
|
|
971
|
+
message: 'Human owner email:',
|
|
972
|
+
validate: (input) => input.includes('@') || 'Valid email required',
|
|
973
|
+
},
|
|
974
|
+
]);
|
|
975
|
+
scanOwner = ownerEmail;
|
|
976
|
+
}
|
|
977
|
+
// Unlock vault
|
|
978
|
+
const scanPassphrase = await promptPassphrase();
|
|
979
|
+
const unlockSpinner = ora('Unlocking vault...').start();
|
|
980
|
+
let scanVault;
|
|
981
|
+
try {
|
|
982
|
+
scanVault = await unlockVault(scanPassphrase, vaultPath);
|
|
983
|
+
unlockSpinner.succeed('Vault unlocked');
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
unlockSpinner.fail('Failed to unlock vault');
|
|
987
|
+
error(err instanceof Error ? err.message : 'Invalid passphrase');
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
// Import selected credentials
|
|
991
|
+
const importSpinner = ora('Importing credentials...').start();
|
|
992
|
+
let scanImported = 0;
|
|
993
|
+
let scanFailed = 0;
|
|
994
|
+
for (const idx of scanSelectedIndexes) {
|
|
995
|
+
const d = detections[idx];
|
|
996
|
+
if (!d)
|
|
997
|
+
continue;
|
|
998
|
+
try {
|
|
999
|
+
const platform = guessPlatform(d.pattern, d.value);
|
|
1000
|
+
const confLevel = d.confidence >= 0.9 ? 'high' : d.confidence >= 0.7 ? 'medium' : 'low';
|
|
1001
|
+
const fileTag = basename(d.file).replace(/\./g, '-');
|
|
1002
|
+
await createPassport(scanVault, {
|
|
1003
|
+
name: `${d.pattern} in ${basename(d.file)}`,
|
|
1004
|
+
credentialType: d.type,
|
|
1005
|
+
credentialValue: d.value,
|
|
1006
|
+
visaType: 'access',
|
|
1007
|
+
platforms: [platform],
|
|
1008
|
+
scope: [],
|
|
1009
|
+
humanOwner: scanOwner,
|
|
1010
|
+
tags: ['imported', 'scan', `confidence-${confLevel}`, fileTag],
|
|
1011
|
+
notes: `Detected in ${d.file} at line ${d.line}, col ${d.column}. Confidence: ${d.confidence.toFixed(2)}. Pattern: ${d.pattern}.`,
|
|
1012
|
+
});
|
|
1013
|
+
scanImported++;
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
scanFailed++;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
importSpinner.stop();
|
|
1020
|
+
console.log();
|
|
1021
|
+
if (scanImported > 0) {
|
|
1022
|
+
success(`Imported ${scanImported} credential(s) as passports.`);
|
|
1023
|
+
}
|
|
1024
|
+
if (scanFailed > 0) {
|
|
1025
|
+
warning(`Failed to import ${scanFailed} credential(s).`);
|
|
1026
|
+
}
|
|
1027
|
+
info('Run `idw list --tag scan` to see your scanned passports.');
|
|
1028
|
+
warning('Source files still contain plaintext credentials. Consider securely deleting them.');
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
// File-based import (existing behavior)
|
|
1032
|
+
if (!file) {
|
|
1033
|
+
error('No file path provided. Try one of these:');
|
|
1034
|
+
console.log();
|
|
1035
|
+
console.log(` ${chalk.cyan('idw import .env')} Import a specific file`);
|
|
1036
|
+
console.log(` ${chalk.cyan('idw import --format env')} Auto-discover .env files in current directory`);
|
|
1037
|
+
console.log(` ${chalk.cyan('idw import --format json')} Auto-discover JSON config files`);
|
|
1038
|
+
console.log(` ${chalk.cyan('idw import --format openclaw')} Auto-scan ~/.openclaw credentials`);
|
|
1039
|
+
console.log(` ${chalk.cyan('idw import --all')} Scan current directory for all credentials`);
|
|
1040
|
+
console.log(` ${chalk.cyan('idw import --min-confidence 0.9')} Import only high-confidence detections`);
|
|
1041
|
+
console.log();
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
1044
|
+
// Read file
|
|
1045
|
+
let content;
|
|
1046
|
+
try {
|
|
1047
|
+
content = await readFile(file, 'utf-8');
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
error(`Cannot read file: ${file}`);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
// Parse credentials based on file type
|
|
1054
|
+
const ext = extname(file).toLowerCase();
|
|
1055
|
+
const filename = basename(file);
|
|
1056
|
+
let detected;
|
|
1057
|
+
if (ext === '.env' || filename.startsWith('.env')) {
|
|
1058
|
+
detected = parseEnvFile(content);
|
|
1059
|
+
}
|
|
1060
|
+
else if (ext === '.json') {
|
|
1061
|
+
detected = parseJsonFile(content, filename);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
// Generic detection
|
|
1065
|
+
const results = detectCredentials(content);
|
|
1066
|
+
detected = results.map(r => ({
|
|
1067
|
+
name: `${filename}:${r.line}`,
|
|
1068
|
+
type: r.type,
|
|
1069
|
+
value: r.value,
|
|
1070
|
+
line: r.line,
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
1073
|
+
if (detected.length === 0) {
|
|
1074
|
+
info('No credentials detected in file.');
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
console.log();
|
|
1078
|
+
info(`Found ${detected.length} potential credential(s) in ${file}`);
|
|
1079
|
+
console.log();
|
|
1080
|
+
// Confirm import
|
|
1081
|
+
let selectedIndexes;
|
|
1082
|
+
if (options.yes) {
|
|
1083
|
+
selectedIndexes = detected.map((_, i) => i.toString());
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
selectedIndexes = await confirmImport(detected.map(c => ({
|
|
1087
|
+
name: c.name,
|
|
1088
|
+
type: c.type,
|
|
1089
|
+
preview: maskCredential(c.value, 4),
|
|
1090
|
+
})));
|
|
1091
|
+
}
|
|
1092
|
+
if (selectedIndexes.length === 0) {
|
|
1093
|
+
console.log('No credentials selected for import.');
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
// Get owner if not provided
|
|
1097
|
+
let owner = options.owner;
|
|
1098
|
+
if (!owner) {
|
|
1099
|
+
const inquirer = await import('inquirer');
|
|
1100
|
+
const { ownerEmail } = await inquirer.default.prompt([
|
|
1101
|
+
{
|
|
1102
|
+
type: 'input',
|
|
1103
|
+
name: 'ownerEmail',
|
|
1104
|
+
message: 'Human owner email:',
|
|
1105
|
+
validate: (input) => input.includes('@') || 'Valid email required',
|
|
1106
|
+
},
|
|
1107
|
+
]);
|
|
1108
|
+
owner = ownerEmail;
|
|
1109
|
+
}
|
|
1110
|
+
// Unlock vault
|
|
1111
|
+
const passphrase = await promptPassphrase();
|
|
1112
|
+
const spinner = ora('Unlocking vault...').start();
|
|
1113
|
+
let vault;
|
|
1114
|
+
try {
|
|
1115
|
+
vault = await unlockVault(passphrase, vaultPath);
|
|
1116
|
+
spinner.succeed('Vault unlocked');
|
|
1117
|
+
}
|
|
1118
|
+
catch (err) {
|
|
1119
|
+
spinner.fail('Failed to unlock vault');
|
|
1120
|
+
error(err instanceof Error ? err.message : 'Invalid passphrase');
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
// Import selected credentials
|
|
1124
|
+
spinner.start('Importing credentials...');
|
|
1125
|
+
let imported = 0;
|
|
1126
|
+
let failed = 0;
|
|
1127
|
+
for (const idx of selectedIndexes) {
|
|
1128
|
+
const cred = detected[parseInt(idx, 10)];
|
|
1129
|
+
if (!cred)
|
|
1130
|
+
continue;
|
|
1131
|
+
try {
|
|
1132
|
+
const platform = guessPlatform(cred.name, cred.value);
|
|
1133
|
+
const passportName = options.autoName
|
|
1134
|
+
? `${cred.name} (imported)`
|
|
1135
|
+
: cred.name;
|
|
1136
|
+
await createPassport(vault, {
|
|
1137
|
+
name: passportName,
|
|
1138
|
+
credentialType: cred.type,
|
|
1139
|
+
credentialValue: cred.value,
|
|
1140
|
+
visaType: 'access',
|
|
1141
|
+
platforms: [platform],
|
|
1142
|
+
scope: [],
|
|
1143
|
+
humanOwner: owner,
|
|
1144
|
+
tags: ['imported', filename.replace(/\./g, '-')],
|
|
1145
|
+
notes: `Imported from ${file}${cred.line ? ` (line ${cred.line})` : ''}`,
|
|
1146
|
+
});
|
|
1147
|
+
imported++;
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
failed++;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
spinner.stop();
|
|
1154
|
+
console.log();
|
|
1155
|
+
if (imported > 0) {
|
|
1156
|
+
success(`Imported ${imported} credential(s) as passports.`);
|
|
1157
|
+
}
|
|
1158
|
+
if (failed > 0) {
|
|
1159
|
+
warning(`Failed to import ${failed} credential(s).`);
|
|
1160
|
+
}
|
|
1161
|
+
info('Run `idw list` to see your passports.');
|
|
1162
|
+
warning('Source file still contains plaintext credentials. Consider securely deleting it.');
|
|
1163
|
+
});
|
|
1164
|
+
return command;
|
|
1165
|
+
}
|
|
1166
|
+
//# sourceMappingURL=import.js.map
|