subto 9.0.0 → 9.0.2
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/dist/package/index.js +84 -2
- package/index.js +77 -2
- package/package.json +1 -1
package/dist/package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const chalk = (_chalk && _chalk.default) ? _chalk.default : _chalk;
|
|
|
11
11
|
const CONFIG_DIR = path.join(os.homedir(), '.subto');
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
13
|
const DEFAULT_API_BASE = 'https://subto.one';
|
|
14
|
-
const CLIENT_META = { name: 'subto-cli', version: '9.0.
|
|
14
|
+
const CLIENT_META = { name: 'subto-cli', version: '9.0.2' };
|
|
15
15
|
|
|
16
16
|
function configFilePath() { return CONFIG_PATH; }
|
|
17
17
|
|
|
@@ -122,9 +122,52 @@ function printScanSummary(obj) {
|
|
|
122
122
|
if (keys.length) console.log(chalk.dim(' Additional keys: ' + keys.join(', ')));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async function fetchWithKey(url, opts, apiKey) {
|
|
126
|
+
const fetchFn = global.fetch || (function(){ try{ const u = require('undici'); if (u && typeof u.fetch === 'function') { global.fetch = u.fetch; return global.fetch; } } catch(e){} return null; })();
|
|
127
|
+
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Install Node 18+ or run `npm install undici`.');
|
|
128
|
+
const baseHeaders = (opts && opts.headers) ? Object.assign({}, opts.headers) : {};
|
|
129
|
+
const headers = Object.assign({}, baseHeaders, { 'Authorization': `Bearer ${apiKey}`, 'x-api-key': String(apiKey) });
|
|
130
|
+
return fetchFn(url, Object.assign({}, opts || {}, { headers }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function fetchJsonWithKey(url, opts, apiKey) {
|
|
134
|
+
const res = await fetchWithKey(url, opts, apiKey);
|
|
135
|
+
const text = await res.text();
|
|
136
|
+
let data = null;
|
|
137
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
|
|
138
|
+
return { status: res.status, headers: res.headers, body: data };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatCount(value) {
|
|
142
|
+
const numeric = Number(value);
|
|
143
|
+
if (!Number.isFinite(numeric)) return '0';
|
|
144
|
+
return numeric.toLocaleString('en-US');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatMemberSince(value) {
|
|
148
|
+
if (!value) return 'Unknown';
|
|
149
|
+
const parsed = new Date(value);
|
|
150
|
+
if (Number.isNaN(parsed.getTime())) return String(value);
|
|
151
|
+
return parsed.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printAccountSummary(payload) {
|
|
155
|
+
const account = payload && payload.account ? payload.account : {};
|
|
156
|
+
const stats = account && account.stats ? account.stats : {};
|
|
157
|
+
const title = account.name || payload.accountId || payload.userId || 'Account';
|
|
158
|
+
|
|
159
|
+
console.log(chalk.bold('Account summary:'));
|
|
160
|
+
console.log(chalk.green(' Name:') + ' ' + title);
|
|
161
|
+
if (account.email) console.log(chalk.green(' Email:') + ' ' + account.email);
|
|
162
|
+
if (payload.accountId) console.log(chalk.green(' Account ID:') + ' ' + payload.accountId);
|
|
163
|
+
console.log(chalk.green(' API calls:') + ' ' + formatCount(stats.apiCalls));
|
|
164
|
+
console.log(chalk.green(' Scans:') + ' ' + formatCount(stats.scans));
|
|
165
|
+
console.log(chalk.green(' Member since:') + ' ' + formatMemberSince(account.createdAt));
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
async function run(argv) {
|
|
126
169
|
const program = new Command();
|
|
127
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '
|
|
170
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.2');
|
|
128
171
|
|
|
129
172
|
program.command('login').description('Store your API key in ~/.subto/config.json').action(async () => {
|
|
130
173
|
try {
|
|
@@ -143,6 +186,45 @@ async function run(argv) {
|
|
|
143
186
|
} catch (err) { if (err && err.message === 'Aborted') { console.log('\n' + chalk.yellow('Login aborted.')); process.exit(1); } throw err; }
|
|
144
187
|
});
|
|
145
188
|
|
|
189
|
+
program
|
|
190
|
+
.command('account')
|
|
191
|
+
.description('Show the current account name, API calls, and scans')
|
|
192
|
+
.option('--json', 'Output raw JSON')
|
|
193
|
+
.action(async (opts) => {
|
|
194
|
+
const cfg = await readConfig();
|
|
195
|
+
if (!cfg || !cfg.apiKey) {
|
|
196
|
+
console.error(chalk.red('Missing API key. Run:'), chalk.cyan('subto login'));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const endpoint = new URL('/api/v1/account/me', process.env.SUBTO_API_BASE_URL || DEFAULT_API_BASE).toString();
|
|
202
|
+
const resp = await fetchJsonWithKey(endpoint, { headers: { 'Accept': 'application/json' } }, cfg.apiKey);
|
|
203
|
+
|
|
204
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
205
|
+
console.error(chalk.red('Authentication failed. Check your API key with `subto login`.'));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
209
|
+
let msg = `Request failed with status ${resp.status}`;
|
|
210
|
+
if (resp.body && typeof resp.body === 'object' && resp.body.error) msg += ': ' + resp.body.error;
|
|
211
|
+
console.error(chalk.red(msg));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (opts.json) {
|
|
216
|
+
console.log(JSON.stringify(resp.body, null, 2));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
printAccountSummary(resp.body);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const msg = err && err.message ? err.message : String(err);
|
|
223
|
+
console.error(chalk.red('Network error:'), msg);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
146
228
|
program
|
|
147
229
|
.command('scan <url>')
|
|
148
230
|
.description('Request a scan for <url> via the Subto API')
|
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const chalk = (_chalk && _chalk.default) ? _chalk.default : _chalk;
|
|
|
11
11
|
const CONFIG_DIR = path.join(os.homedir(), '.subto');
|
|
12
12
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
13
|
const DEFAULT_API_BASE = 'https://subto.one/api/v1';
|
|
14
|
-
const CLIENT_META = { name: 'subto-cli', version: '9.0.
|
|
14
|
+
const CLIENT_META = { name: 'subto-cli', version: '9.0.2' };
|
|
15
15
|
const cp = require('child_process');
|
|
16
16
|
|
|
17
17
|
// Normalize SUBTO API base so callers can set either
|
|
@@ -296,6 +296,41 @@ function printScanSummary(obj) {
|
|
|
296
296
|
if (keys.length) console.log(chalk.dim(' Additional keys: ' + keys.join(', ')));
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
async function fetchJsonWithKey(url, opts, apiKey, debug=false) {
|
|
300
|
+
const res = await fetchWithKey(url, opts, apiKey, debug);
|
|
301
|
+
const text = typeof res._cached_text === 'string' ? res._cached_text : await res.text();
|
|
302
|
+
let data = null;
|
|
303
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
|
|
304
|
+
return { status: res.status, headers: res.headers, body: data };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function formatCount(value) {
|
|
308
|
+
const numeric = Number(value);
|
|
309
|
+
if (!Number.isFinite(numeric)) return '0';
|
|
310
|
+
return numeric.toLocaleString('en-US');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function formatMemberSince(value) {
|
|
314
|
+
if (!value) return 'Unknown';
|
|
315
|
+
const parsed = new Date(value);
|
|
316
|
+
if (Number.isNaN(parsed.getTime())) return String(value);
|
|
317
|
+
return parsed.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function printAccountSummary(payload) {
|
|
321
|
+
const account = payload && payload.account ? payload.account : {};
|
|
322
|
+
const stats = account && account.stats ? account.stats : {};
|
|
323
|
+
const title = account.name || payload.accountId || payload.userId || 'Account';
|
|
324
|
+
|
|
325
|
+
console.log(chalk.bold('Account summary:'));
|
|
326
|
+
console.log(chalk.green(' Name:') + ' ' + title);
|
|
327
|
+
if (account.email) console.log(chalk.green(' Email:') + ' ' + account.email);
|
|
328
|
+
if (payload.accountId) console.log(chalk.green(' Account ID:') + ' ' + payload.accountId);
|
|
329
|
+
console.log(chalk.green(' API calls:') + ' ' + formatCount(stats.apiCalls));
|
|
330
|
+
console.log(chalk.green(' Scans:') + ' ' + formatCount(stats.scans));
|
|
331
|
+
console.log(chalk.green(' Member since:') + ' ' + formatMemberSince(account.createdAt));
|
|
332
|
+
}
|
|
333
|
+
|
|
299
334
|
async function printFullReport(data) {
|
|
300
335
|
const scan = (data && data.results) ? data.results : data;
|
|
301
336
|
if (!scan || typeof scan !== 'object') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
@@ -577,7 +612,7 @@ async function startChatREPL(scanData){
|
|
|
577
612
|
|
|
578
613
|
async function run(argv) {
|
|
579
614
|
const program = new Command();
|
|
580
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.
|
|
615
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.2');
|
|
581
616
|
program.option('-v, --verbose', 'Show verbose HTTP logs');
|
|
582
617
|
program.option('--debug', 'Show debug HTTP headers and responses');
|
|
583
618
|
program.option('--chat', 'Start local AI assistant (no command required)');
|
|
@@ -603,6 +638,46 @@ async function run(argv) {
|
|
|
603
638
|
} catch (err) { if (err && err.message === 'Aborted') { console.log('\n' + chalk.yellow('Login aborted.')); process.exit(1); } throw err; }
|
|
604
639
|
});
|
|
605
640
|
|
|
641
|
+
program
|
|
642
|
+
.command('account')
|
|
643
|
+
.description('Show the current account name, API calls, and scans')
|
|
644
|
+
.option('--json', 'Output raw JSON')
|
|
645
|
+
.action(async (opts) => {
|
|
646
|
+
const cfg = await readConfig();
|
|
647
|
+
if (!cfg || !cfg.apiKey) {
|
|
648
|
+
console.error(chalk.red('Missing API key. Run:'), chalk.cyan('subto login'));
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
const DEBUG = (program && typeof program.opts === 'function') ? Boolean(program.opts().debug) : false;
|
|
654
|
+
const endpoint = new URL('account/me', SUBTO_API_BASE_SLASH).toString();
|
|
655
|
+
const resp = await fetchJsonWithKey(endpoint, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
656
|
+
|
|
657
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
658
|
+
console.error(chalk.red('Authentication failed. Check your API key with `subto login`.'));
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
662
|
+
let msg = `Request failed with status ${resp.status}`;
|
|
663
|
+
if (resp.body && typeof resp.body === 'object' && resp.body.error) msg += ': ' + resp.body.error;
|
|
664
|
+
console.error(chalk.red(msg));
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (opts.json) {
|
|
669
|
+
console.log(JSON.stringify(resp.body, null, 2));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
printAccountSummary(resp.body);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
const msg = err && err.message ? err.message : String(err);
|
|
676
|
+
console.error(chalk.red('Network error:'), msg);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
606
681
|
program
|
|
607
682
|
.command('scan <url>')
|
|
608
683
|
.description('Request a scan for <url> via the Subto API')
|