@demicodes/host-local 0.17.4 → 0.18.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/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AgentHarness, AgentServer, AgentServerSessionOptions } from "@demicodes/agent";
2
2
  import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
3
- import { BashEnvironmentOptions, Host, HostFileSystem, HostProcess, HostStore } from "@demicodes/shell";
3
+ import { BashEnvironmentOptions, Host, HostFileSystem, HostIdentity, HostProcess, HostStore } from "@demicodes/shell";
4
4
  import { Provider } from "@demicodes/provider";
5
5
  //#region src/command-bridge.d.ts
6
6
  /**
@@ -59,6 +59,7 @@ declare class LocalHost implements Host {
59
59
  readonly fs: HostFileSystem;
60
60
  readonly process: HostProcess;
61
61
  readonly store: HostStore;
62
+ readonly identity: HostIdentity;
62
63
  constructor(defaultCwd: string, options?: LocalHostOptions);
63
64
  }
64
65
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { existsSync, mkdirSync, unlinkSync } from "node:fs";
2
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
1
+ import { constants, existsSync, mkdirSync, unlinkSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
3
3
  import { createServer } from "node:http";
4
4
  import { AgentServer, RunCommandLineCommandNotRegisteredError, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError } from "@demicodes/agent";
5
5
  import { encodeUtf8, errorMessage, isFileNotFoundError, parsePortableJson, stringifyPortableJson } from "@demicodes/utils";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { resolveDemiHome } from "@demicodes/provider/credentials-pool";
8
8
  import { spawn } from "node:child_process";
9
- import { homedir } from "node:os";
10
- import { appendFile, chmod, cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
9
+ import { homedir, hostname, userInfo } from "node:os";
10
+ import { appendFile, chmod, cp, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
11
11
  //#region src/command-bridge.ts
12
12
  /**
13
13
  * Dispatch script shared by every generated command-name symlink.
@@ -337,6 +337,61 @@ function validateHostStoreKey(key) {
337
337
  for (const segment of key.split(/[\\/]+/)) if (segment === "..") throw new Error(`HostStore keys must not contain path traversal: ${key}`);
338
338
  }
339
339
  //#endregion
340
+ //#region src/local-cwd.ts
341
+ const DIR_FLAGS = constants.O_RDONLY | constants.O_DIRECTORY;
342
+ var LocalHostCwd = class LocalHostCwd {
343
+ path;
344
+ handle;
345
+ static async open(path) {
346
+ const cwd = new LocalHostCwd(path);
347
+ cwd.handle = await open(path, DIR_FLAGS);
348
+ return cwd;
349
+ }
350
+ constructor(path) {
351
+ this.path = path;
352
+ }
353
+ spawnPath() {
354
+ return this.fdAnchor() ?? this.path;
355
+ }
356
+ async chdir(path) {
357
+ if (path === ".") return;
358
+ const target = isAbsolute(path) ? path : this.fdAnchor() ? `${this.fdAnchor()}/${path}` : posix.join(this.path, path);
359
+ const next = await open(target, DIR_FLAGS);
360
+ const previous = this.handle;
361
+ this.handle = next;
362
+ this.path = isAbsolute(path) ? path : posix.normalize(posix.join(this.path, path));
363
+ await previous?.close().catch(() => {});
364
+ }
365
+ async snapshot() {
366
+ const anchor = this.fdAnchor();
367
+ if (!this.handle || !anchor) {
368
+ const path = this.path;
369
+ const handle = this.handle;
370
+ return { restore: () => {
371
+ this.path = path;
372
+ this.handle = handle;
373
+ } };
374
+ }
375
+ const dup = await open(anchor, DIR_FLAGS);
376
+ const path = this.path;
377
+ return { restore: () => {
378
+ const abandoned = this.handle;
379
+ this.handle = dup;
380
+ this.path = path;
381
+ if (abandoned && abandoned.fd !== dup.fd) abandoned.close().catch(() => {});
382
+ } };
383
+ }
384
+ async close() {
385
+ await this.handle?.close().catch(() => {});
386
+ this.handle = void 0;
387
+ }
388
+ fdAnchor() {
389
+ if (!this.handle) return void 0;
390
+ if (process.platform === "linux") return `/proc/self/fd/${this.handle.fd}`;
391
+ if (process.platform === "darwin") return `/dev/fd/${this.handle.fd}`;
392
+ }
393
+ };
394
+ //#endregion
340
395
  //#region src/local-host.ts
341
396
  var LocalHost = class {
342
397
  defaultCwd;
@@ -344,6 +399,7 @@ var LocalHost = class {
344
399
  fs;
345
400
  process;
346
401
  store;
402
+ identity;
347
403
  constructor(defaultCwd, options = {}) {
348
404
  this.defaultCwd = resolve(defaultCwd);
349
405
  const storeRoot = options.storeRoot ?? defaultStoreRoot(this.defaultCwd);
@@ -351,6 +407,12 @@ var LocalHost = class {
351
407
  this.fs = new LocalHostFileSystem(this.defaultCwd);
352
408
  this.process = new LocalHostProcess(this.defaultCwd);
353
409
  this.store = options.store ?? new LocalHostStore(storeRoot);
410
+ const info = userInfo();
411
+ this.identity = {
412
+ uid: info.uid,
413
+ gid: info.gid,
414
+ hostname: hostname()
415
+ };
354
416
  }
355
417
  };
356
418
  var LocalHostProcess = class {
@@ -358,13 +420,14 @@ var LocalHostProcess = class {
358
420
  constructor(defaultCwd) {
359
421
  this.defaultCwd = defaultCwd;
360
422
  }
423
+ async openCwd(path) {
424
+ return LocalHostCwd.open(path);
425
+ }
361
426
  async spawn(params) {
427
+ const cwd = params.cwd ?? this.defaultCwd;
362
428
  const child = spawn(params.command, params.args ?? [], {
363
- cwd: params.cwd ?? this.defaultCwd,
364
- env: {
365
- ...process.env,
366
- ...params.env
367
- },
429
+ cwd,
430
+ ...params.env ? { env: definedEnv(params.env) } : {},
368
431
  detached: params.killProcessGroup === true,
369
432
  stdio: [
370
433
  "pipe",
@@ -373,21 +436,26 @@ var LocalHostProcess = class {
373
436
  ]
374
437
  });
375
438
  let settled = false;
376
- const waitPromise = new Promise((resolve) => {
439
+ const waitPromise = new Promise((resolveWait) => {
377
440
  child.once("error", (error) => {
378
441
  if (settled) return;
379
442
  settled = true;
380
- resolve({
381
- exitCode: null,
382
- signal: error.message
443
+ classifySpawnFailure(error, cwd).then((kind) => {
444
+ resolveWait({
445
+ exitCode: null,
446
+ signal: error.message,
447
+ spawnError: { kind }
448
+ });
383
449
  });
384
450
  });
385
451
  child.once("close", (exitCode, signal) => {
386
- if (settled) return;
387
- settled = true;
388
- resolve({
389
- exitCode,
390
- signal: signal ?? void 0
452
+ setImmediate(() => {
453
+ if (settled) return;
454
+ settled = true;
455
+ resolveWait({
456
+ exitCode,
457
+ signal: signal ?? void 0
458
+ });
391
459
  });
392
460
  });
393
461
  });
@@ -565,9 +633,33 @@ function toHostFileStat(value) {
565
633
  isSymbolicLink: value.isSymbolicLink(),
566
634
  mode: value.mode,
567
635
  size: value.size,
568
- mtime: value.mtime
636
+ mtime: value.mtime,
637
+ uid: value.uid,
638
+ gid: value.gid,
639
+ ino: value.ino,
640
+ dev: value.dev,
641
+ nlink: value.nlink,
642
+ isCharacterDevice: value.isCharacterDevice(),
643
+ isFIFO: value.isFIFO()
569
644
  };
570
645
  }
646
+ function definedEnv(env) {
647
+ const defined = {};
648
+ for (const [key, value] of Object.entries(env)) if (value !== void 0) defined[key] = value;
649
+ return defined;
650
+ }
651
+ async function classifySpawnFailure(error, cwd) {
652
+ try {
653
+ if (!(await stat(cwd)).isDirectory()) return "cwd_unusable";
654
+ } catch {
655
+ return "cwd_unusable";
656
+ }
657
+ const code = "code" in error ? String(error.code) : "";
658
+ if (code === "ENOENT") return "executable_not_found";
659
+ if (code === "EACCES" || code === "EPERM") return "permission_denied";
660
+ if (code === "EISDIR") return "is_directory";
661
+ return "other";
662
+ }
571
663
  function toHostDirent(value) {
572
664
  return {
573
665
  name: value.name,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/host-local",
3
3
  "description": "Node LocalHost and open-box local AgentServer assembly (command bridge on by default).",
4
- "version": "0.17.4",
4
+ "version": "0.18.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -11,10 +11,10 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@demicodes/agent": "^0.17.4",
15
- "@demicodes/provider": "^0.17.4",
16
- "@demicodes/shell": "^0.17.4",
17
- "@demicodes/utils": "^0.17.4"
14
+ "@demicodes/agent": "^0.18.0",
15
+ "@demicodes/provider": "^0.18.0",
16
+ "@demicodes/shell": "^0.18.0",
17
+ "@demicodes/utils": "^0.18.0"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "main": "./dist/index.mjs",