aegis-desktop 0.8.19 → 0.8.21
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/lib/local/engine.js +119 -24
- package/lib/local/local.js +293 -27
- package/lib/local/prompt.js +51 -2
- package/lib/local/tools.js +134 -23
- package/package.json +1 -1
- package/vendor/aegis.js +317 -33
- package/vendor/agent-gate.mjs +55 -6
- package/vendor/env-file.js +11 -4
- package/vendor/platform.js +259 -0
package/vendor/platform.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
const fs = require('node:fs');
|
|
42
|
+
const os = require('node:os');
|
|
42
43
|
const path = require('node:path');
|
|
43
44
|
const { spawnSync } = require('node:child_process');
|
|
44
45
|
|
|
@@ -80,6 +81,23 @@ function defaultProbe(cmd, args = [], opts = {}) {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
function defaultDirExists(dir) {
|
|
85
|
+
try {
|
|
86
|
+
return fs.statSync(dir).isDirectory();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Read a text file; `null` for every failure (missing, unreadable, not text). */
|
|
93
|
+
function defaultReadText(file) {
|
|
94
|
+
try {
|
|
95
|
+
return fs.readFileSync(file, 'utf8');
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
/** Run a command for its side effect. Never throws; `{ ok, error, output }`. */
|
|
84
102
|
function defaultRun(cmd, args = [], opts = {}) {
|
|
85
103
|
try {
|
|
@@ -398,6 +416,243 @@ function hasDisplay(options = {}) {
|
|
|
398
416
|
return false;
|
|
399
417
|
}
|
|
400
418
|
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// homeDir / resolvePath
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* This user's home directory, `''` when nothing can say what it is.
|
|
425
|
+
*
|
|
426
|
+
* `USERPROFILE` leads on win32 and `HOME` on posix because that is the variable
|
|
427
|
+
* each OS actually sets; the other is accepted second (a Windows git-bash shell
|
|
428
|
+
* exports `HOME`, and a posix host inheriting `USERPROFILE` is harmless), and
|
|
429
|
+
* `os.homedir()` is the last word. Reading the env BEFORE `os.homedir()` is what
|
|
430
|
+
* lets a test point a whole tool run at a temp directory.
|
|
431
|
+
*/
|
|
432
|
+
function homeDir(options = {}) {
|
|
433
|
+
const { platform, env, o } = seams(options);
|
|
434
|
+
// An explicit `home` is honoured AS GIVEN, including `''`. That is the seam
|
|
435
|
+
// for "this machine has no home directory" — a case the env/os fallbacks below
|
|
436
|
+
// can never produce on a developer's own box, and therefore one that could
|
|
437
|
+
// not be tested at all if an empty string fell through to them.
|
|
438
|
+
if (typeof o.home === 'string') return o.home.trim();
|
|
439
|
+
const keys = platform === 'win32' ? ['USERPROFILE', 'HOME'] : ['HOME', 'USERPROFILE'];
|
|
440
|
+
for (const key of keys) {
|
|
441
|
+
const value = envValue(env, key, platform);
|
|
442
|
+
if (value && String(value).trim()) return String(value).trim();
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
return os.homedir() || '';
|
|
446
|
+
} catch {
|
|
447
|
+
return '';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Do the permission bits in `fs.stat().mode` carry real information here?
|
|
453
|
+
*
|
|
454
|
+
* On Windows they do not. libuv has no NTFS ACL to report, so it SYNTHESISES the
|
|
455
|
+
* mode from a single bit — the read-only attribute — and hands back `0o666` for
|
|
456
|
+
* every writable file and `0o444` for every read-only one, directories included.
|
|
457
|
+
* Two consequences, and both were live bugs:
|
|
458
|
+
*
|
|
459
|
+
* - `(mode & 0o077) !== 0` — "is this readable by other users?" — is therefore
|
|
460
|
+
* ALWAYS TRUE on Windows, for every file, no matter how locked down it is.
|
|
461
|
+
* Code that refuses a secret on that test refuses it unconditionally there
|
|
462
|
+
* (tools/agent-gate.mjs would not accept an agent key at all), and code that
|
|
463
|
+
* reports it as exposure reports the user's whole data directory as exposed
|
|
464
|
+
* with no way to fix it (`/doctor` in cli/src/commands.js).
|
|
465
|
+
* - `chmodSync(f, 0o600)` cannot make that test pass, so a repair loop that
|
|
466
|
+
* re-reads the mode to confirm never converges.
|
|
467
|
+
*
|
|
468
|
+
* Where this returns false the boundary is the DIRECTORY's ACL — a per-user
|
|
469
|
+
* profile directory is already inaccessible to other non-admin accounts — which
|
|
470
|
+
* is what `client/authorship-key.js` has always done for the key file. The right
|
|
471
|
+
* behaviour is to skip the judgement and say so, never to assume the worst.
|
|
472
|
+
*/
|
|
473
|
+
function hasPosixModes(options = {}) {
|
|
474
|
+
const { win32 } = seams(options);
|
|
475
|
+
return !win32;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* This user's real Desktop directory, or `''` when none can be found.
|
|
480
|
+
*
|
|
481
|
+
* `~/Desktop` is a GUESS, and it is wrong on both of the hosts this function was
|
|
482
|
+
* added for:
|
|
483
|
+
*
|
|
484
|
+
* - Windows with OneDrive. Folder redirection moves the live Desktop to
|
|
485
|
+
* `%USERPROFILE%\OneDrive\Desktop` and commonly leaves an empty
|
|
486
|
+
* `%USERPROFILE%\Desktop` behind, so a write to the obvious path lands in a
|
|
487
|
+
* directory the user is not looking at. `%OneDrive%` (set while OneDrive
|
|
488
|
+
* runs) is consulted first for exactly this case.
|
|
489
|
+
* - A localised Linux desktop. XDG translates the directory ITSELF, not just
|
|
490
|
+
* its label: a Swedish session has `~/Skrivbord` and no `~/Desktop` at all.
|
|
491
|
+
* `$XDG_DESKTOP_DIR` answers it, and `~/.config/user-dirs.dirs` is where
|
|
492
|
+
* that variable is defined when the session did not export it.
|
|
493
|
+
*
|
|
494
|
+
* macOS is the one host where `~/Desktop` is simply correct.
|
|
495
|
+
*
|
|
496
|
+
* Seams: `dirExists` (default: a real statSync isDirectory probe) and `readText`
|
|
497
|
+
* (default: a real utf8 read, `null` on any failure), so all three branches are
|
|
498
|
+
* exercised on one host.
|
|
499
|
+
*/
|
|
500
|
+
function desktopDir(options = {}) {
|
|
501
|
+
const { platform, env, win32, o } = seams(options);
|
|
502
|
+
const P = win32 ? path.win32 : path.posix;
|
|
503
|
+
const home = homeDir(options);
|
|
504
|
+
const dirExists = typeof o.dirExists === 'function' ? o.dirExists : defaultDirExists;
|
|
505
|
+
const readText = typeof o.readText === 'function' ? o.readText : defaultReadText;
|
|
506
|
+
|
|
507
|
+
const candidates = [];
|
|
508
|
+
const push = (value) => {
|
|
509
|
+
if (value && String(value).trim()) candidates.push(String(value).trim());
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
if (win32) {
|
|
513
|
+
const oneDrive = envValue(env, 'OneDrive', platform) || envValue(env, 'OneDriveConsumer', platform);
|
|
514
|
+
if (oneDrive) push(P.join(oneDrive, 'Desktop'));
|
|
515
|
+
if (home) push(P.join(home, 'OneDrive', 'Desktop'));
|
|
516
|
+
if (home) push(P.join(home, 'Desktop'));
|
|
517
|
+
} else if (platform === 'darwin') {
|
|
518
|
+
if (home) push(P.join(home, 'Desktop'));
|
|
519
|
+
} else {
|
|
520
|
+
push(envValue(env, 'XDG_DESKTOP_DIR', platform));
|
|
521
|
+
// user-dirs.dirs holds lines like: XDG_DESKTOP_DIR="$HOME/Skrivbord"
|
|
522
|
+
if (home) {
|
|
523
|
+
const conf = readText(P.join(home, '.config', 'user-dirs.dirs'));
|
|
524
|
+
const match = conf && /^\s*XDG_DESKTOP_DIR\s*=\s*"?([^"\n]+)"?\s*$/m.exec(conf);
|
|
525
|
+
if (match) push(match[1].replace(/\$HOME\b|\${HOME}/g, home));
|
|
526
|
+
}
|
|
527
|
+
if (home) push(P.join(home, 'Desktop'));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for (const candidate of candidates) {
|
|
531
|
+
if (dirExists(candidate)) return candidate;
|
|
532
|
+
}
|
|
533
|
+
// Nothing on disk. The conventional path is still a better answer than '' for
|
|
534
|
+
// a machine whose Desktop simply has not been created yet — but only where the
|
|
535
|
+
// name is not localised, which is the one case we would be guessing at.
|
|
536
|
+
if (home && (win32 || platform === 'darwin')) return P.join(home, 'Desktop');
|
|
537
|
+
return '';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** `C:\…` / `C:/…` — a drive-qualified Windows path. */
|
|
541
|
+
const WIN_DRIVE_RE = /^[A-Za-z]:[\\/]/;
|
|
542
|
+
/** `C:` with nothing after it — drive-relative, and never what a model means. */
|
|
543
|
+
const WIN_BARE_DRIVE_RE = /^[A-Za-z]:$/;
|
|
544
|
+
/** `\\server\share` — a UNC path, which IS absolute and stays allowed. */
|
|
545
|
+
const WIN_UNC_RE = /^\\\\[^\\?.]/;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Turn a path a MODEL produced into a path this machine really has.
|
|
549
|
+
*
|
|
550
|
+
* { ok, path, error, expandedHome }
|
|
551
|
+
*
|
|
552
|
+
* Why this exists. The tool layer used to pass `file_path` straight to `fs`,
|
|
553
|
+
* which quietly made two very common model outputs write to the wrong place:
|
|
554
|
+
*
|
|
555
|
+
* - `~/Desktop/notes.txt`. Node does not expand `~`; nothing in the tool
|
|
556
|
+
* layer did either. `fs.mkdirSync(path.dirname(…))` therefore created a
|
|
557
|
+
* directory literally NAMED `~` under the process cwd and wrote inside it —
|
|
558
|
+
* and `writeFile` reported "Wrote 14 bytes to ~/Desktop/notes.txt", so the
|
|
559
|
+
* model told the user it had saved a file to their Desktop that was not
|
|
560
|
+
* there. A wrong write that reports success is worse than a refusal.
|
|
561
|
+
* - `/home/user/Desktop/notes.txt` ON WINDOWS. A leading-slash path there is
|
|
562
|
+
* relative to the current DRIVE, so that write succeeds — at
|
|
563
|
+
* `C:\home\user\Desktop\notes.txt`. Same silent success, same missing file.
|
|
564
|
+
* This is why the bug read as "the tools only work on Linux": on Linux a
|
|
565
|
+
* model's posix guess happens to be right, and off Linux it is accepted
|
|
566
|
+
* anyway. Windows is also where a model reaches for `~` most, because it
|
|
567
|
+
* cannot guess `C:\Users\<name>` and the prompt only names the home dir.
|
|
568
|
+
*
|
|
569
|
+
* So: `~` and `~/…` are expanded (`~other/…` is refused — another user's home
|
|
570
|
+
* is not ours to guess), a path that is absolute for a DIFFERENT OS than this
|
|
571
|
+
* one is refused with the correct form named, a relative path resolves against
|
|
572
|
+
* `cwd`, and anything already native is normalised. A refusal is the useful
|
|
573
|
+
* outcome: the model gets the real home directory back in the error and retries
|
|
574
|
+
* correctly in the next round, where before it got "success" and stopped.
|
|
575
|
+
*
|
|
576
|
+
* Idempotent — the output of one call is unchanged by the next — so a caller
|
|
577
|
+
* may normalise early (before a guard inspects the path) without worrying that
|
|
578
|
+
* a later layer will resolve it a second time.
|
|
579
|
+
*
|
|
580
|
+
* Path SEMANTICS follow the `platform` asked about, not the host, exactly as in
|
|
581
|
+
* `resolveExecutable` above: that is what lets test/platform.test.mjs prove the
|
|
582
|
+
* win32 branch on this Linux CI.
|
|
583
|
+
*/
|
|
584
|
+
function resolvePath(input, options = {}) {
|
|
585
|
+
const { platform, win32, o } = seams(options);
|
|
586
|
+
const P = win32 ? path.win32 : path.posix;
|
|
587
|
+
const refuse = (error) => ({ ok: false, path: null, error, expandedHome: false });
|
|
588
|
+
|
|
589
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
590
|
+
return refuse(`a path is required (got ${JSON.stringify(input)})`);
|
|
591
|
+
}
|
|
592
|
+
const raw = input.trim();
|
|
593
|
+
|
|
594
|
+
// ── the home shorthand ────────────────────────────────────────────────────
|
|
595
|
+
let work = raw;
|
|
596
|
+
let expandedHome = false;
|
|
597
|
+
const tildeRoot = raw === '~' || raw.startsWith('~/') || (win32 && raw.startsWith('~\\'));
|
|
598
|
+
if (tildeRoot) {
|
|
599
|
+
const home = homeDir(options);
|
|
600
|
+
if (!home) {
|
|
601
|
+
return refuse(
|
|
602
|
+
`cannot expand "~" in ${JSON.stringify(raw)}: this machine reports no home directory. ` +
|
|
603
|
+
'Pass a full absolute path instead.',
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
work = raw === '~' ? home : P.join(home, raw.slice(2));
|
|
607
|
+
expandedHome = true;
|
|
608
|
+
} else if (raw.startsWith('~')) {
|
|
609
|
+
return refuse(
|
|
610
|
+
`${JSON.stringify(raw)} names another user's home directory, which cannot be resolved from ` +
|
|
611
|
+
'here. Use "~" for this user, or pass a full absolute path.',
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── absolute-for-the-wrong-OS ─────────────────────────────────────────────
|
|
616
|
+
// Skipped after a `~` expansion: that result is native by construction.
|
|
617
|
+
if (!expandedHome) {
|
|
618
|
+
const home = homeDir(options);
|
|
619
|
+
const hint = home ? ` This machine's home directory is ${home}.` : '';
|
|
620
|
+
if (win32) {
|
|
621
|
+
// A leading slash on Windows is drive-relative, not absolute. Accepted
|
|
622
|
+
// only as a UNC share, which genuinely is absolute.
|
|
623
|
+
if ((work.startsWith('/') || work.startsWith('\\')) && !WIN_UNC_RE.test(work)) {
|
|
624
|
+
return refuse(
|
|
625
|
+
`${JSON.stringify(raw)} is not an absolute path on this machine, which runs ${platform}. ` +
|
|
626
|
+
`A leading slash here means "the root of the current drive", so writing to it would ` +
|
|
627
|
+
`silently land somewhere other than where you meant. Use a drive-qualified path ` +
|
|
628
|
+
`(C:\\Users\\you\\Desktop\\file.txt) or "~/…".${hint}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
} else if (WIN_DRIVE_RE.test(work) || WIN_BARE_DRIVE_RE.test(work) || WIN_UNC_RE.test(work)) {
|
|
632
|
+
return refuse(
|
|
633
|
+
`${JSON.stringify(raw)} is a Windows path, but this machine runs ${platform}. ` +
|
|
634
|
+
`Use a posix path (/home/you/file.txt) or "~/…".${hint}`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ── relative → the working directory ──────────────────────────────────────
|
|
640
|
+
const cwd = typeof o.cwd === 'string' && o.cwd.trim() ? o.cwd.trim() : process.cwd();
|
|
641
|
+
const resolved = P.isAbsolute(work) ? P.resolve(work) : P.resolve(cwd, work);
|
|
642
|
+
|
|
643
|
+
// `P.resolve` falls back to the HOST's cwd when nothing it was given is
|
|
644
|
+
// absolute, which on a foreign-platform query yields a path of the wrong
|
|
645
|
+
// shape. Say so rather than hand back something unusable.
|
|
646
|
+
if (!P.isAbsolute(resolved)) {
|
|
647
|
+
return refuse(
|
|
648
|
+
`${JSON.stringify(raw)} is relative and the working directory (${JSON.stringify(cwd)}) is ` +
|
|
649
|
+
'not absolute, so it cannot be resolved. Pass an absolute path.',
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return { ok: true, path: resolved, error: null, expandedHome };
|
|
654
|
+
}
|
|
655
|
+
|
|
401
656
|
const api = {
|
|
402
657
|
IS_WIN32,
|
|
403
658
|
resolveExecutable,
|
|
@@ -405,6 +660,10 @@ const api = {
|
|
|
405
660
|
killTree,
|
|
406
661
|
isAlive,
|
|
407
662
|
hasDisplay,
|
|
663
|
+
homeDir,
|
|
664
|
+
desktopDir,
|
|
665
|
+
resolvePath,
|
|
666
|
+
hasPosixModes,
|
|
408
667
|
};
|
|
409
668
|
|
|
410
669
|
module.exports = api;
|