aegiscode 6.3.2 → 6.5.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 +77 -11
- package/bin/aegiscode.js +161 -2
- package/package.json +2 -2
- package/scripts/predist.mjs +5 -0
- package/src/app.js +178 -43
- package/src/cloudsync.js +401 -0
- package/src/commands.js +285 -17
- package/src/config.js +8 -6
- package/src/credentials.js +30 -0
- package/src/history.js +120 -8
- package/src/screens.js +159 -9
- package/src/secret.js +56 -0
- package/src/shared.js +45 -0
- package/vendor/client/credentials.js +386 -0
- package/vendor/client/session-store.js +418 -0
package/src/screens.js
CHANGED
|
@@ -36,6 +36,7 @@ const { welcomeArtParts } = require('./art.js');
|
|
|
36
36
|
const { renderDiffPreview } = require('./markdown.js');
|
|
37
37
|
const render = require('./render.js');
|
|
38
38
|
const { updateConfig, configExists } = require('./config.js');
|
|
39
|
+
const credentials = require('./credentials.js');
|
|
39
40
|
|
|
40
41
|
const VERSION = require('../package.json').version;
|
|
41
42
|
|
|
@@ -387,6 +388,115 @@ async function showWelcome(ctx, firstRun = true) {
|
|
|
387
388
|
}
|
|
388
389
|
}
|
|
389
390
|
|
|
391
|
+
// ── the account key ──────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
const KEY_URL = 'https://aegiscloud.org';
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The key screen's lines. Pure, so what the user is told is asserted directly.
|
|
397
|
+
*
|
|
398
|
+
* `value` is echoed back as bullets: this is the one screen in the product
|
|
399
|
+
* where the thing being typed is a secret, and a screen that echoes it would
|
|
400
|
+
* put the key in a scrollback buffer, a screenshot and a screen share.
|
|
401
|
+
*/
|
|
402
|
+
function keyLines(ctx, cols, { value = '', error = null, verify = false } = {}) {
|
|
403
|
+
const t = themeOf(ctx);
|
|
404
|
+
const lines = [];
|
|
405
|
+
lines.push([span(t.gold, '─'.repeat(Math.max(1, cols)))]);
|
|
406
|
+
lines.push([span(t.white + BOLD, 'Connect your AEGIS account')]);
|
|
407
|
+
lines.push([span('', '')]);
|
|
408
|
+
for (const l of wrapped(
|
|
409
|
+
`Paste an API key to use AEGIS Cloud. Get one free at ${KEY_URL}. ` +
|
|
410
|
+
'It is stored in your user config directory with owner-only permissions, so later launches and scripts pick it up without an export.',
|
|
411
|
+
cols,
|
|
412
|
+
t.white
|
|
413
|
+
)) {
|
|
414
|
+
lines.push(l);
|
|
415
|
+
}
|
|
416
|
+
lines.push([span('', '')]);
|
|
417
|
+
lines.push([
|
|
418
|
+
span(t.lavender, GLYPH.cursor),
|
|
419
|
+
span(t.gray, ' API key: '),
|
|
420
|
+
span(t.white, '•'.repeat(String(value).length)),
|
|
421
|
+
span(verify ? t.cyan : t.gray, verify ? ' verifying…' : ''),
|
|
422
|
+
]);
|
|
423
|
+
if (error) lines.push([span(t.coral, ` ${error}`)]);
|
|
424
|
+
lines.push([span('', '')]);
|
|
425
|
+
lines.push([
|
|
426
|
+
span(t.gray, ` ${GLYPH.check} `),
|
|
427
|
+
span(t.gray, 'Enter to save'),
|
|
428
|
+
span(t.gray, ' · '),
|
|
429
|
+
span(t.gray, 'Esc to skip'),
|
|
430
|
+
]);
|
|
431
|
+
return lines;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Ask for the account key. Resolves `{key}` when submitted, `{skipped:true}`
|
|
436
|
+
* when the user declines, `{exit:true}` on ctrl+c.
|
|
437
|
+
*
|
|
438
|
+
* `submit(key)` is the caller's verification+persistence step; its rejection
|
|
439
|
+
* message is shown on the screen and the user stays on it, because the failure
|
|
440
|
+
* mode this prevents is a key that is accepted into the config while the
|
|
441
|
+
* account behind it rejects every call.
|
|
442
|
+
*/
|
|
443
|
+
async function requestApiKey(ctx, o = {}) {
|
|
444
|
+
const submit = o.submit || (async () => ({ ok: true }));
|
|
445
|
+
const { cols } = getSize();
|
|
446
|
+
let value = '';
|
|
447
|
+
let error = null;
|
|
448
|
+
let verify = false;
|
|
449
|
+
const paintScreen = () => paint(keyLines(ctx, cols, { value, error, verify }));
|
|
450
|
+
paintScreen();
|
|
451
|
+
|
|
452
|
+
for (;;) {
|
|
453
|
+
const key = await nextKey();
|
|
454
|
+
if (key.name === KEY.ENTER) {
|
|
455
|
+
if (!value.trim()) return { skipped: true };
|
|
456
|
+
verify = true;
|
|
457
|
+
error = null;
|
|
458
|
+
paintScreen();
|
|
459
|
+
let res;
|
|
460
|
+
try {
|
|
461
|
+
res = await submit(value);
|
|
462
|
+
} catch (e) {
|
|
463
|
+
res = { ok: false, message: (e && e.message) || String(e) };
|
|
464
|
+
}
|
|
465
|
+
verify = false;
|
|
466
|
+
if (res && res.ok === false) {
|
|
467
|
+
error = res.message || 'that key was refused';
|
|
468
|
+
paintScreen();
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
return { key: value, result: res };
|
|
472
|
+
}
|
|
473
|
+
if (key.name === KEY.ESC) return { skipped: true };
|
|
474
|
+
if (key.name === KEY.CTRL_C || key.name === KEY.CTRL_D) return { exit: true };
|
|
475
|
+
if (key.name === KEY.BACKSPACE) {
|
|
476
|
+
value = value.slice(0, -1);
|
|
477
|
+
error = null;
|
|
478
|
+
paintScreen();
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (key.name === 'char') {
|
|
482
|
+
const ch = key.ch;
|
|
483
|
+
if (ch && ch >= ' ') {
|
|
484
|
+
value += ch;
|
|
485
|
+
error = null;
|
|
486
|
+
paintScreen();
|
|
487
|
+
}
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
// A paste arrives as its own event (events.js rushes multi-char reads);
|
|
491
|
+
// without this a pasted key is dropped on the floor.
|
|
492
|
+
if (key.name === 'paste' && key.text) {
|
|
493
|
+
value = (value + String(key.text)).replace(/\s+/g, '');
|
|
494
|
+
error = null;
|
|
495
|
+
paintScreen();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
390
500
|
// ── the sequence ─────────────────────────────────────────────────────────────
|
|
391
501
|
|
|
392
502
|
/**
|
|
@@ -398,8 +508,12 @@ async function showWelcome(ctx, firstRun = true) {
|
|
|
398
508
|
* @param {() => boolean} [o.seen] an explicit "has run before" probe; defaults
|
|
399
509
|
* to the config file's existence
|
|
400
510
|
* @param {(patch:object)=>void} [o.save] persist patch; defaults to updateConfig
|
|
401
|
-
* @
|
|
402
|
-
*
|
|
511
|
+
* @param {() => boolean} [o.needsKey] true when no account key is configured —
|
|
512
|
+
* the key screen is shown only then
|
|
513
|
+
* @param {(key:string)=>Promise<object>} [o.submitKey] verify + persist a key
|
|
514
|
+
* @returns {Promise<{ok:boolean, firstRun:boolean, themeIndex:number,
|
|
515
|
+
* key:{set:boolean, skipped:boolean}}>} `ok` is false when the user
|
|
516
|
+
* declined the trust check or asked to exit.
|
|
403
517
|
*/
|
|
404
518
|
async function runOnboarding(ctx, o = {}) {
|
|
405
519
|
const seen = o.seen || configExists;
|
|
@@ -407,8 +521,29 @@ async function runOnboarding(ctx, o = {}) {
|
|
|
407
521
|
// Injectable so the *sequence* — which screens run, in what order, and what is
|
|
408
522
|
// persisted — can be asserted without a terminal. The screens themselves are
|
|
409
523
|
// tested directly through their pure line-builders.
|
|
410
|
-
const ui =
|
|
411
|
-
|
|
524
|
+
const ui = {
|
|
525
|
+
showTrustCheck,
|
|
526
|
+
showThemePicker,
|
|
527
|
+
showWelcome,
|
|
528
|
+
requestApiKey: (c, opts) =>
|
|
529
|
+
requestApiKey(c, { submit: o.submitKey || (async () => ({ ok: true })), ...opts }),
|
|
530
|
+
// An injected `ui` overrides the screens it names and inherits the rest, so
|
|
531
|
+
// a caller testing the sequence does not have to supply a key screen it
|
|
532
|
+
// never wants to exercise.
|
|
533
|
+
...(o.ui || {}),
|
|
534
|
+
};
|
|
535
|
+
// No key is the one state where the session cannot do anything at all, so it
|
|
536
|
+
// is asked for in-band rather than left to a shell export the user has to
|
|
537
|
+
// discover. Default reads the credential store so a caller that forgets to
|
|
538
|
+
// pass it still gets the right behaviour.
|
|
539
|
+
const needsKey = o.needsKey || (() => !credentials.hasApiKey());
|
|
540
|
+
// …but only where a question can actually be asked. Without this a library
|
|
541
|
+
// caller with no TTY reaches a screen that waits on a key queue nothing will
|
|
542
|
+
// ever feed — a hang instead of a missing credential.
|
|
543
|
+
const canPrompt = o.canPrompt || (() => !!(process.stdin && process.stdin.isTTY));
|
|
544
|
+
// Declared out here because both the sequence and its key step report on it.
|
|
545
|
+
const key = { set: false, skipped: false };
|
|
546
|
+
if (o.continue) return { ok: true, firstRun: false, themeIndex: ctx.themeIndex, key: { set: false, skipped: false } };
|
|
412
547
|
|
|
413
548
|
// Onboarding runs *before* the session loop, and the session loop is what
|
|
414
549
|
// normally attaches the key pump — so without this the first screen paints and
|
|
@@ -429,17 +564,29 @@ async function runOnboarding(ctx, o = {}) {
|
|
|
429
564
|
// neither is ever shown again. Re-running this every launch greeted
|
|
430
565
|
// returning users with "Let's get started." and discarded their session.
|
|
431
566
|
const trusted = await ui.showTrustCheck(ctx);
|
|
432
|
-
if (!trusted) return { ok: false, firstRun: true, themeIndex: ctx.themeIndex };
|
|
567
|
+
if (!trusted) return { ok: false, firstRun: true, themeIndex: ctx.themeIndex, key };
|
|
433
568
|
await ui.showThemePicker(ctx);
|
|
434
569
|
save({ themeIndex: ctx.themeIndex, light: ctx.light });
|
|
570
|
+
if (await askKey()) return { ok: false, firstRun: true, themeIndex: ctx.themeIndex, key };
|
|
435
571
|
const welcome = await ui.showWelcome(ctx, true);
|
|
436
|
-
if (welcome && welcome.exit) return { ok: false, firstRun: true, themeIndex: ctx.themeIndex };
|
|
437
|
-
return { ok: true, firstRun: true, themeIndex: ctx.themeIndex };
|
|
572
|
+
if (welcome && welcome.exit) return { ok: false, firstRun: true, themeIndex: ctx.themeIndex, key };
|
|
573
|
+
return { ok: true, firstRun: true, themeIndex: ctx.themeIndex, key };
|
|
438
574
|
}
|
|
439
575
|
|
|
576
|
+
if (await askKey()) return { ok: false, firstRun: false, themeIndex: ctx.themeIndex, key };
|
|
440
577
|
const welcome = await ui.showWelcome(ctx, false);
|
|
441
|
-
if (welcome && welcome.exit) return { ok: false, firstRun: false, themeIndex: ctx.themeIndex };
|
|
442
|
-
return { ok: true, firstRun: false, themeIndex: ctx.themeIndex };
|
|
578
|
+
if (welcome && welcome.exit) return { ok: false, firstRun: false, themeIndex: ctx.themeIndex, key };
|
|
579
|
+
return { ok: true, firstRun: false, themeIndex: ctx.themeIndex, key };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** @returns {Promise<boolean>} true when the user asked to exit. */
|
|
583
|
+
async function askKey() {
|
|
584
|
+
if (!needsKey() || !canPrompt()) return false;
|
|
585
|
+
const res = await ui.requestApiKey(ctx);
|
|
586
|
+
if (res && res.exit) return true;
|
|
587
|
+
if (res && res.key) key.set = true;
|
|
588
|
+
else key.skipped = true;
|
|
589
|
+
return false;
|
|
443
590
|
}
|
|
444
591
|
}
|
|
445
592
|
|
|
@@ -449,6 +596,7 @@ module.exports = {
|
|
|
449
596
|
WHATS_NEW,
|
|
450
597
|
trustLines,
|
|
451
598
|
themePickerLines,
|
|
599
|
+
keyLines,
|
|
452
600
|
applyTheme,
|
|
453
601
|
welcomeLines,
|
|
454
602
|
boxes,
|
|
@@ -458,6 +606,8 @@ module.exports = {
|
|
|
458
606
|
wrapPlain,
|
|
459
607
|
showTrustCheck,
|
|
460
608
|
showThemePicker,
|
|
609
|
+
requestApiKey,
|
|
461
610
|
showWelcome,
|
|
462
611
|
runOnboarding,
|
|
612
|
+
KEY_URL,
|
|
463
613
|
};
|
package/src/secret.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read a secret from the terminal without echoing it.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from app.js so the non-interactive entry point can use the same
|
|
7
|
+
* prompt as the in-session commands: `aegiscode login` asks for the account key
|
|
8
|
+
* on the same terms `/key` does, and a second copy of this loop would be a
|
|
9
|
+
* second place for a key to end up echoed to the screen or in a shell history.
|
|
10
|
+
*
|
|
11
|
+
* Raw mode when the stream supports it; a stream that cannot mask (no TTY, no
|
|
12
|
+
* setRawMode) resolves to '' rather than silently reading an echoed secret —
|
|
13
|
+
* every caller treats '' as "nothing given" and says so.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const MASK = '\u2022'; // •
|
|
17
|
+
|
|
18
|
+
function readSecret(promptText, o = {}) {
|
|
19
|
+
const stdin = o.stdin || process.stdin;
|
|
20
|
+
const stdout = o.stdout || process.stdout;
|
|
21
|
+
const canMask = !!stdin.isTTY && typeof stdin.setRawMode === 'function';
|
|
22
|
+
if (!canMask) return Promise.resolve('');
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
stdout.write(promptText);
|
|
25
|
+
let buf = '';
|
|
26
|
+
const done = (value) => {
|
|
27
|
+
try {
|
|
28
|
+
stdin.setRawMode(false);
|
|
29
|
+
} catch {}
|
|
30
|
+
stdin.removeListener('data', onData);
|
|
31
|
+
stdout.write('\n');
|
|
32
|
+
resolve(value);
|
|
33
|
+
};
|
|
34
|
+
const onData = (chunk) => {
|
|
35
|
+
// Drop terminal escape sequences whole: skipping only the ESC leaves the
|
|
36
|
+
// CSI tail ([A, [3~, …) to be appended to the key.
|
|
37
|
+
const text = String(chunk).replace(/\x1b\[[0-9;?]*[A-Za-z~]/g, '').replace(/\x1b./g, '');
|
|
38
|
+
for (const ch of text) {
|
|
39
|
+
if (ch === '\r' || ch === '\n') return done(buf);
|
|
40
|
+
if (ch === '\x03' || ch === '\x04') return done(''); // ctrl+c / ctrl+d
|
|
41
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
42
|
+
buf = buf.slice(0, -1);
|
|
43
|
+
stdout.write('\b \b');
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
buf += ch;
|
|
47
|
+
stdout.write(MASK);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
stdin.setRawMode(true);
|
|
51
|
+
stdin.resume();
|
|
52
|
+
stdin.on('data', onData);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { readSecret, MASK };
|
package/src/shared.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the CLI finds the modules it shares with the other hosts.
|
|
5
|
+
*
|
|
6
|
+
* `src/deps.js` does this for the transport/engine/registry trio, but it pulls
|
|
7
|
+
* the whole agent loop in at require time — far too heavy for `config.js` and
|
|
8
|
+
* `history.js`, which every command and every launch touches. This module
|
|
9
|
+
* resolves the two *pure* shared modules those files need, with the same
|
|
10
|
+
* two-layout rule and the same by-existence lookup:
|
|
11
|
+
*
|
|
12
|
+
* in-repo <repo>/cli/src/shared.js -> <repo>/client/*.js
|
|
13
|
+
* npm <pkg>/src/shared.js -> <pkg>/vendor/client/*.js
|
|
14
|
+
*
|
|
15
|
+
* A missing module is a loud error rather than a silent fallback to a second
|
|
16
|
+
* copy: a duplicate credential store or session store is exactly the drift this
|
|
17
|
+
* whole arrangement exists to prevent.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
function candidates(name) {
|
|
24
|
+
return [
|
|
25
|
+
path.join(__dirname, '..', '..', 'client', name), // repo checkout
|
|
26
|
+
path.join(__dirname, '..', 'vendor', 'client', name), // staged package
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveClientModule(name) {
|
|
31
|
+
const tried = candidates(name);
|
|
32
|
+
for (const candidate of tried) {
|
|
33
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`aegiscode: cannot find client/${name}. Expected it beside cli/ (in the repo) ` +
|
|
37
|
+
'or under cli/vendor/client/ (installed package). Reinstall the package, or ' +
|
|
38
|
+
'run `node scripts/predist.mjs` from cli/ if this is a source checkout.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const credentials = require(resolveClientModule('credentials.js'));
|
|
43
|
+
const sessionStore = require(resolveClientModule('session-store.js'));
|
|
44
|
+
|
|
45
|
+
module.exports = { credentials, sessionStore, resolveClientModule };
|