hostctl 0.1.41 → 0.1.44

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.
@@ -2,11 +2,13 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import process4 from "process";
5
+ import path7 from "path";
5
6
  import * as cmdr from "commander";
7
+ import "zod";
6
8
 
7
9
  // src/app.ts
8
10
  import process3 from "process";
9
- import * as fs7 from "fs";
11
+ import * as fs8 from "fs";
10
12
  import { homedir as homedir3 } from "os";
11
13
 
12
14
  // src/handlebars.ts
@@ -1098,15 +1100,15 @@ var Equal = class extends Protocol {
1098
1100
 
1099
1101
  // src/flex/path.ts
1100
1102
  var Path = class _Path {
1101
- constructor(path5, isWindowsPath = isWindows()) {
1102
- this.path = path5;
1103
+ constructor(path8, isWindowsPath = isWindows()) {
1104
+ this.path = path8;
1103
1105
  this.isWindowsPath = isWindowsPath;
1104
1106
  }
1105
- static new(path5, isWindowsPath = isWindows()) {
1106
- if (path5 instanceof _Path) {
1107
- return path5;
1107
+ static new(path8, isWindowsPath = isWindows()) {
1108
+ if (path8 instanceof _Path) {
1109
+ return path8;
1108
1110
  }
1109
- return new _Path(path5, isWindowsPath);
1111
+ return new _Path(path8, isWindowsPath);
1110
1112
  }
1111
1113
  static cwd() {
1112
1114
  return _Path.new(process.cwd());
@@ -1146,8 +1148,8 @@ var Path = class _Path {
1146
1148
  return this.build(posix.basename(this.path, suffix));
1147
1149
  }
1148
1150
  }
1149
- build(path5) {
1150
- return new _Path(path5, this.isWindowsPath);
1151
+ build(path8) {
1152
+ return new _Path(path8, this.isWindowsPath);
1151
1153
  }
1152
1154
  // returns the path to the destination on success; null otherwise
1153
1155
  async copy(destPath, mode) {
@@ -1195,7 +1197,7 @@ var Path = class _Path {
1195
1197
  }
1196
1198
  glob(pattern) {
1197
1199
  const cwd = this.absolute().toString();
1198
- return globSync(pattern, { cwd }).map((path5) => this.build(path5));
1200
+ return globSync(pattern, { cwd }).map((path8) => this.build(path8));
1199
1201
  }
1200
1202
  isAbsolute() {
1201
1203
  if (this.isWindowsPath) {
@@ -1234,11 +1236,11 @@ var Path = class _Path {
1234
1236
  }
1235
1237
  }
1236
1238
  parent(count = 1) {
1237
- let path5 = this.absolute();
1239
+ let path8 = this.absolute();
1238
1240
  Range.new(1, count).each((i) => {
1239
- path5 = path5.resolve("..");
1241
+ path8 = path8.resolve("..");
1240
1242
  });
1241
- return path5;
1243
+ return path8;
1242
1244
  }
1243
1245
  // returns an object of the form: { root, dir, base, ext, name }
1244
1246
  //
@@ -1776,8 +1778,8 @@ import process2 from "process";
1776
1778
  import { readFile as readFile2 } from "fs/promises";
1777
1779
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1778
1780
  import { win32 as win322, posix as posix2 } from "path";
1779
- function exists(path5) {
1780
- return existsSync2(path5);
1781
+ function exists(path8) {
1782
+ return existsSync2(path8);
1781
1783
  }
1782
1784
  var File = class {
1783
1785
  static absolutePath(...paths) {
@@ -1789,15 +1791,15 @@ var File = class {
1789
1791
  }
1790
1792
  // basename("c:\\foo\\bar\\baz.txt") => "baz.txt"
1791
1793
  // basename("/tmp/myfile.html") => "myfile.html"
1792
- static basename(path5, suffix) {
1794
+ static basename(path8, suffix) {
1793
1795
  if (isWindows()) {
1794
- return win322.basename(path5, suffix);
1796
+ return win322.basename(path8, suffix);
1795
1797
  } else {
1796
- return posix2.basename(path5, suffix);
1798
+ return posix2.basename(path8, suffix);
1797
1799
  }
1798
1800
  }
1799
- static exists(path5) {
1800
- return exists(path5);
1801
+ static exists(path8) {
1802
+ return exists(path8);
1801
1803
  }
1802
1804
  static join(...paths) {
1803
1805
  if (isWindows()) {
@@ -1806,13 +1808,13 @@ var File = class {
1806
1808
  return posix2.join(...paths);
1807
1809
  }
1808
1810
  }
1809
- static readSync(path5) {
1810
- return readFileSync2(path5, {
1811
+ static readSync(path8) {
1812
+ return readFileSync2(path8, {
1811
1813
  encoding: "utf8"
1812
1814
  });
1813
1815
  }
1814
- static async readAsync(path5) {
1815
- return await readFile2(path5, {
1816
+ static async readAsync(path8) {
1817
+ return await readFile2(path8, {
1816
1818
  encoding: "utf8"
1817
1819
  });
1818
1820
  }
@@ -1822,8 +1824,8 @@ var File = class {
1822
1824
  var TmpFileRegistry = class _TmpFileRegistry {
1823
1825
  static _instance;
1824
1826
  static get instance() {
1825
- const path5 = File.join(osHomeDir(), ".hostctl", "tmpstatic");
1826
- this._instance ??= new _TmpFileRegistry(path5);
1827
+ const path8 = File.join(osHomeDir(), ".hostctl", "tmpstatic");
1828
+ this._instance ??= new _TmpFileRegistry(path8);
1827
1829
  return this._instance;
1828
1830
  }
1829
1831
  rootPath;
@@ -1842,33 +1844,33 @@ var TmpFileRegistry = class _TmpFileRegistry {
1842
1844
  }
1843
1845
  // this directory will be automatically cleaned up at program exit
1844
1846
  createNamedTmpDir(subDirName) {
1845
- const path5 = this.tmpPath(subDirName);
1846
- fs.mkdirSync(path5.toString(), { recursive: true });
1847
- this.registerTempFileOrDir(path5.toString());
1848
- return path5;
1847
+ const path8 = this.tmpPath(subDirName);
1848
+ fs.mkdirSync(path8.toString(), { recursive: true });
1849
+ this.registerTempFileOrDir(path8.toString());
1850
+ return path8;
1849
1851
  }
1850
1852
  // this file will be automatically cleaned up at program exit
1851
1853
  writeTmpFile(fileContent) {
1852
- const path5 = this.tmpPath();
1853
- fs.writeFileSync(path5.toString(), fileContent);
1854
- this.registerTempFileOrDir(path5.toString());
1855
- return path5;
1854
+ const path8 = this.tmpPath();
1855
+ fs.writeFileSync(path8.toString(), fileContent);
1856
+ this.registerTempFileOrDir(path8.toString());
1857
+ return path8;
1856
1858
  }
1857
1859
  exitCallback() {
1858
1860
  this.cleanupTempFiles();
1859
1861
  }
1860
- registerTempFileOrDir(path5) {
1861
- this.tempFilePaths.push(path5);
1862
+ registerTempFileOrDir(path8) {
1863
+ this.tempFilePaths.push(path8);
1862
1864
  }
1863
1865
  cleanupTempFiles() {
1864
- this.tempFilePaths.forEach((path5) => {
1865
- this.rmFile(path5);
1866
+ this.tempFilePaths.forEach((path8) => {
1867
+ this.rmFile(path8);
1866
1868
  });
1867
1869
  this.tempFilePaths = [];
1868
1870
  }
1869
- rmFile(path5) {
1871
+ rmFile(path8) {
1870
1872
  try {
1871
- fs.rmSync(path5, { force: true, recursive: true });
1873
+ fs.rmSync(path8, { force: true, recursive: true });
1872
1874
  } catch (e) {
1873
1875
  }
1874
1876
  }
@@ -2023,12 +2025,12 @@ import * as age from "age-encryption";
2023
2025
  import spawnAsync from "@expo/spawn-async";
2024
2026
 
2025
2027
  // src/age-encryption.ts
2026
- function readIdentityStringFromFile(path5) {
2027
- const contents = fs2.readFileSync(path5, {
2028
+ function readIdentityStringFromFile(path8) {
2029
+ const contents = fs2.readFileSync(path8, {
2028
2030
  encoding: "utf8"
2029
2031
  });
2030
2032
  const identityString = contents.split(/\r?\n|\r|\n/g).map((line) => line.trim()).find((line) => line.startsWith("AGE-SECRET-KEY-1"));
2031
- if (!identityString) throw new Error(`Unable to read identity from file: ${path5}`);
2033
+ if (!identityString) throw new Error(`Unable to read identity from file: ${path8}`);
2032
2034
  return identityString;
2033
2035
  }
2034
2036
  var LibraryDriver = class {
@@ -2044,13 +2046,13 @@ var LibraryDriver = class {
2044
2046
  }
2045
2047
  const d = new age.Decrypter();
2046
2048
  let identitiesAdded = 0;
2047
- for (const path5 of privateKeyFilePaths) {
2049
+ for (const path8 of privateKeyFilePaths) {
2048
2050
  try {
2049
- const identityString = readIdentityStringFromFile(path5);
2051
+ const identityString = readIdentityStringFromFile(path8);
2050
2052
  d.addIdentity(identityString);
2051
2053
  identitiesAdded++;
2052
2054
  } catch (err) {
2053
- console.warn(`Failed to read or parse identity file ${path5}, skipping: ${err.message}`);
2055
+ console.warn(`Failed to read or parse identity file ${path8}, skipping: ${err.message}`);
2054
2056
  }
2055
2057
  }
2056
2058
  if (identitiesAdded === 0) {
@@ -2069,13 +2071,13 @@ var Identity = class {
2069
2071
  identityFilePath;
2070
2072
  identity;
2071
2073
  // either the path to an identity file or an identity string must be supplied
2072
- constructor({ path: path5, identity: identity2 }) {
2074
+ constructor({ path: path8, identity: identity2 }) {
2073
2075
  if (identity2) {
2074
2076
  this.identity = identity2;
2075
2077
  this.identityFilePath = this.writeTmpIdentityFile(identity2);
2076
- } else if (path5) {
2077
- this.identity = this.readIdentityFromFile(path5);
2078
- this.identityFilePath = path5;
2078
+ } else if (path8) {
2079
+ this.identity = this.readIdentityFromFile(path8);
2080
+ this.identityFilePath = path8;
2079
2081
  } else {
2080
2082
  throw "Either an identity string or an identity file path must be supplied to create an Age Encryption identity";
2081
2083
  }
@@ -2087,12 +2089,12 @@ var Identity = class {
2087
2089
  writeTmpIdentityFile(identity2) {
2088
2090
  return writeTmpFile(identity2).toString();
2089
2091
  }
2090
- readIdentityFromFile(path5) {
2091
- const contents = fs2.readFileSync(path5, {
2092
+ readIdentityFromFile(path8) {
2093
+ const contents = fs2.readFileSync(path8, {
2092
2094
  encoding: "utf8"
2093
2095
  });
2094
2096
  const identityString = contents.split(/\r?\n|\r|\n/g).map((line) => line.trim()).find((line) => line.startsWith("AGE-SECRET-KEY-1"));
2095
- if (!identityString) throw new Error(`Unable to read identity file: ${path5}`);
2097
+ if (!identityString) throw new Error(`Unable to read identity file: ${path8}`);
2096
2098
  return identityString;
2097
2099
  }
2098
2100
  get privateKey() {
@@ -2242,8 +2244,8 @@ var SecretRefYamlType = new yaml.Type("!secret", {
2242
2244
  });
2243
2245
  var HOSTCTL_CONFIG_SCHEMA = yaml.DEFAULT_SCHEMA.extend([SecretRefYamlType]);
2244
2246
  var ConfigFile2 = class {
2245
- constructor(path5) {
2246
- this.path = path5;
2247
+ constructor(path8) {
2248
+ this.path = path8;
2247
2249
  this._hosts = /* @__PURE__ */ new Map();
2248
2250
  this._ids = /* @__PURE__ */ new Map();
2249
2251
  this._secrets = /* @__PURE__ */ new Map();
@@ -2387,7 +2389,7 @@ var ConfigFile2 = class {
2387
2389
  }
2388
2390
  if (ageIds) {
2389
2391
  const paths = globSync2(ageIds);
2390
- const ids = paths.map((path5) => new Identity({ path: path5 }));
2392
+ const ids = paths.map((path8) => new Identity({ path: path8 }));
2391
2393
  return ids;
2392
2394
  }
2393
2395
  return [];
@@ -2416,47 +2418,182 @@ var ConfigFile2 = class {
2416
2418
  }
2417
2419
  };
2418
2420
 
2419
- // src/config-url.ts
2420
- var ConfigUrl = class {
2421
- constructor(url) {
2422
- this.url = url;
2423
- this._hosts = /* @__PURE__ */ new Map();
2424
- this._secrets = /* @__PURE__ */ new Map();
2425
- this._ids = /* @__PURE__ */ new Map();
2421
+ // src/config-provider/provider-config.ts
2422
+ var ProviderRecipientGroup = class {
2423
+ constructor(keys3) {
2424
+ this.keys = keys3;
2425
+ }
2426
+ recipients() {
2427
+ return this.keys;
2428
+ }
2429
+ };
2430
+ var ProviderSecret = class {
2431
+ constructor(nameValue, value, recipientKeys) {
2432
+ this.nameValue = nameValue;
2433
+ this.value = value;
2434
+ this.recipientKeys = recipientKeys;
2435
+ }
2436
+ get name() {
2437
+ return this.nameValue;
2438
+ }
2439
+ recipients() {
2440
+ return this.recipientKeys;
2441
+ }
2442
+ async ciphertext() {
2443
+ return this.value;
2444
+ }
2445
+ async plaintext() {
2446
+ return this.value;
2447
+ }
2448
+ };
2449
+ var ProviderConfig = class _ProviderConfig {
2450
+ hostsCache;
2451
+ secretsCache;
2452
+ idsCache;
2453
+ constructor(hosts, secrets, ids) {
2454
+ this.hostsCache = hosts;
2455
+ this.secretsCache = secrets;
2456
+ this.idsCache = ids;
2457
+ }
2458
+ static async load(provider) {
2459
+ const hostInputs = await provider.hosts();
2460
+ const secretsInput = await provider.secrets();
2461
+ const idsInput = await provider.ids();
2462
+ const ids = new Map(
2463
+ idsInput.map((i) => [i.name, new ProviderRecipientGroup([...i.recipients ?? [], ...i.groups ?? []])])
2464
+ );
2465
+ const secrets = new Map(
2466
+ secretsInput.map((s) => [s.name, new ProviderSecret(s.name, s.value, s.ids)])
2467
+ );
2468
+ const config = new _ProviderConfig([], secrets, ids);
2469
+ const hosts = hostInputs.map((h) => config.hostFromInput(h));
2470
+ config.hostsCache = hosts;
2471
+ return config;
2472
+ }
2473
+ hostFromInput(input) {
2474
+ return new Host(this, {
2475
+ hostname: input.hostname,
2476
+ alias: input.name,
2477
+ port: input.port,
2478
+ user: input.user,
2479
+ password: input.password,
2480
+ sshKey: input.sshKey,
2481
+ tags: input.tags
2482
+ });
2426
2483
  }
2427
- _hosts;
2428
- _secrets;
2429
- _ids;
2430
- // Config interface
2431
2484
  hosts() {
2432
- return [];
2485
+ return this.hostsCache;
2486
+ }
2487
+ secrets() {
2488
+ return this.secretsCache;
2489
+ }
2490
+ ids() {
2491
+ return this.idsCache;
2433
2492
  }
2434
2493
  getSecret(name) {
2435
- return void 0;
2494
+ return this.secretsCache.get(name);
2436
2495
  }
2437
2496
  getRecipientGroups(idRefs) {
2438
- return [];
2497
+ return idRefs.map((id) => this.idsCache.get(id)).filter((v) => Boolean(v));
2439
2498
  }
2440
- secrets() {
2441
- return this._secrets;
2499
+ };
2500
+
2501
+ // src/config-provider/file-config-provider.ts
2502
+ var FileConfigProvider = class {
2503
+ constructor(path8) {
2504
+ this.path = path8;
2505
+ }
2506
+ configFile;
2507
+ async getConfigFile() {
2508
+ if (!this.configFile) {
2509
+ const cfg = new ConfigFile2(this.path);
2510
+ await cfg.load();
2511
+ this.configFile = cfg;
2512
+ }
2513
+ return this.configFile;
2514
+ }
2515
+ async hosts() {
2516
+ const cfg = await this.getConfigFile();
2517
+ return cfg.hosts().map((h) => ({
2518
+ name: h.alias || h.hostname,
2519
+ hostname: h.hostname,
2520
+ user: h.user,
2521
+ port: h.port,
2522
+ password: h.password,
2523
+ sshKey: h.sshKey,
2524
+ tags: h.tags
2525
+ }));
2526
+ }
2527
+ async secrets() {
2528
+ const cfg = await this.getConfigFile();
2529
+ return Array.from(cfg.secrets().entries()).map(([name, secret]) => ({
2530
+ name,
2531
+ value: secret.value.toYAML(),
2532
+ ids: secret.ids.toYAML().filter((v) => typeof v === "string")
2533
+ }));
2534
+ }
2535
+ async ids() {
2536
+ const cfg = await this.getConfigFile();
2537
+ return Array.from(cfg.ids().entries()).map(([name, rg]) => ({
2538
+ name,
2539
+ recipients: rg.publicKeys,
2540
+ groups: rg.idRefs
2541
+ }));
2442
2542
  }
2443
- ids() {
2444
- return this._ids;
2543
+ async getSecret(name) {
2544
+ const cfg = await this.getConfigFile();
2545
+ return cfg.getSecret(name);
2546
+ }
2547
+ async getRecipientGroups(idRefs) {
2548
+ const cfg = await this.getConfigFile();
2549
+ return cfg.getRecipientGroups(idRefs);
2445
2550
  }
2446
2551
  };
2447
2552
 
2448
- // src/config.ts
2449
- async function load(configRef) {
2450
- if (configRef && fs4.existsSync(configRef)) {
2451
- const configFile = new ConfigFile2(configRef);
2452
- await configFile.load();
2453
- return configFile;
2454
- } else if (configRef) {
2455
- return new ConfigUrl(configRef);
2456
- } else {
2457
- throw new Error("No config specified.");
2553
+ // src/config-provider/http-config-provider.ts
2554
+ import axios from "axios";
2555
+ import yaml2 from "js-yaml";
2556
+ var HttpConfigProvider = class {
2557
+ constructor(opts) {
2558
+ this.opts = opts;
2559
+ }
2560
+ cache;
2561
+ async fetchData() {
2562
+ const now = Date.now();
2563
+ if (this.cache && this.cache.expiresAt > now) {
2564
+ return this.cache.data;
2565
+ }
2566
+ const headers = { ...this.opts.headers || {} };
2567
+ if (this.opts.auth) {
2568
+ const authHeaders = await this.opts.auth();
2569
+ Object.assign(headers, authHeaders.headers);
2570
+ }
2571
+ const axiosOpts = { headers };
2572
+ const resp = await axios.get(this.opts.url, axiosOpts);
2573
+ const format = this.opts.format ?? "json";
2574
+ const payload = format === "yaml" ? yaml2.load(resp.data) : typeof resp.data === "string" ? JSON.parse(resp.data) : resp.data;
2575
+ const data = Array.isArray(payload) ? payload : payload?.hosts ?? [];
2576
+ const hosts = Array.isArray(data) ? data : [];
2577
+ const ttl = this.opts.cacheTtlMs ?? 0;
2578
+ this.cache = { expiresAt: ttl > 0 ? now + ttl : 0, data: hosts };
2579
+ return hosts;
2458
2580
  }
2459
- }
2581
+ async hosts() {
2582
+ return await this.fetchData();
2583
+ }
2584
+ async secrets() {
2585
+ return [];
2586
+ }
2587
+ async ids() {
2588
+ return [];
2589
+ }
2590
+ async getSecret(_name) {
2591
+ return void 0;
2592
+ }
2593
+ async getRecipientGroups(_idRefs) {
2594
+ return [];
2595
+ }
2596
+ };
2460
2597
 
2461
2598
  // src/interaction-handler.ts
2462
2599
  import { Readable } from "stream";
@@ -2762,19 +2899,29 @@ import chalk from "chalk";
2762
2899
 
2763
2900
  // src/task.model.ts
2764
2901
  var Task = class {
2765
- constructor(runFn, taskModuleAbsolutePath, description, name) {
2902
+ constructor(runFn, taskModuleAbsolutePath, description, name, inputSchema, outputSchema) {
2766
2903
  this.runFn = runFn;
2767
2904
  this.taskModuleAbsolutePath = taskModuleAbsolutePath;
2768
2905
  this.description = description;
2769
2906
  this.name = name;
2907
+ this.inputSchema = inputSchema;
2908
+ this.outputSchema = outputSchema;
2770
2909
  }
2771
2910
  };
2772
2911
 
2773
2912
  // src/runtime-base.ts
2774
2913
  function task(runFn, options) {
2775
2914
  const moduleThatTaskIsDefinedIn = Path.new(callstack().items[2].file).absolute().toString();
2776
- const taskName = options?.name || "anonymous_task";
2777
- const taskInstance = new Task(runFn, moduleThatTaskIsDefinedIn, options?.description, taskName);
2915
+ const inferredName = Path.new(moduleThatTaskIsDefinedIn).basename().toString().replace(/\.[^.]+$/, "");
2916
+ const taskName = options?.name || inferredName || "anonymous_task";
2917
+ const taskInstance = new Task(
2918
+ runFn,
2919
+ moduleThatTaskIsDefinedIn,
2920
+ options?.description,
2921
+ taskName,
2922
+ options?.inputSchema,
2923
+ options?.outputSchema
2924
+ );
2778
2925
  const taskFnObject = function(params) {
2779
2926
  return function(parentInvocation) {
2780
2927
  return parentInvocation.invokeChildTask(taskFnObject, params ?? {});
@@ -2898,8 +3045,20 @@ var SSHSession = class {
2898
3045
  constructor() {
2899
3046
  this.ssh = new NodeSSH();
2900
3047
  }
2901
- connect(sshConnectOpts) {
2902
- return this.ssh.connect(sshConnectOpts);
3048
+ async connect(sshConnectOpts) {
3049
+ const session = await this.ssh.connect(sshConnectOpts);
3050
+ const connection = this.ssh.connection;
3051
+ if (connection && typeof connection.on === "function" && connection.listenerCount?.("error") === 0) {
3052
+ connection.on("error", (err) => {
3053
+ const code = String(err?.code ?? "").toUpperCase();
3054
+ if (code === "ECONNRESET") {
3055
+ console.warn(`SSH connection reset: ${err?.message ?? err}`);
3056
+ return;
3057
+ }
3058
+ console.warn(`SSH connection error: ${err?.message ?? err}`);
3059
+ });
3060
+ }
3061
+ return session;
2903
3062
  }
2904
3063
  async deleteFile(remotePath) {
2905
3064
  const self = this;
@@ -2938,7 +3097,10 @@ var SSHSession = class {
2938
3097
  pty: options.pty
2939
3098
  };
2940
3099
  }
2941
- const result = await this.ssh.execCommand(finalCommand, nodeSshCmdOptions);
3100
+ const result = await this.ssh.execCommand(
3101
+ finalCommand,
3102
+ nodeSshCmdOptions
3103
+ );
2942
3104
  const exitCode = result.code === null ? -1 : result.code;
2943
3105
  const signalString = result.signal;
2944
3106
  const signalObject = signalString ? signalsByName[signalString] : void 0;
@@ -3343,7 +3505,8 @@ var RemoteRuntime = class {
3343
3505
  `RemoteRuntime: Executing command on ${this.host.uri} via sshSession.execCommand (no PTY/default handler)`
3344
3506
  );
3345
3507
  const commandObj = await this.sshSession.execCommand(command, {
3346
- stdin: options?.stdin
3508
+ stdin: options?.stdin,
3509
+ pty: false
3347
3510
  });
3348
3511
  cmdOrErr = commandObj;
3349
3512
  }
@@ -3400,7 +3563,13 @@ var RusPtyCommand = class _RusPtyCommand extends Command {
3400
3563
  */
3401
3564
  static build(command, options = {}) {
3402
3565
  const commandObj = Command.build(command, options);
3403
- return new _RusPtyCommand({ cmd: commandObj.cmd, args: commandObj.args, cwd: commandObj.cwd, env: commandObj.env, sudo: commandObj.sudo });
3566
+ return new _RusPtyCommand({
3567
+ cmd: commandObj.cmd,
3568
+ args: commandObj.args,
3569
+ cwd: commandObj.cwd,
3570
+ env: commandObj.env,
3571
+ sudo: commandObj.sudo
3572
+ });
3404
3573
  }
3405
3574
  process;
3406
3575
  // Definite assignment assertion
@@ -3461,8 +3630,8 @@ var LocalInvocation = class _LocalInvocation extends Invocation {
3461
3630
  )(params);
3462
3631
  this.config = this.runtime.app.config;
3463
3632
  this.file = {
3464
- read: async (path5) => fs5.promises.readFile(path5, "utf-8"),
3465
- write: async (path5, content, options) => {
3633
+ read: async (path8) => fs5.promises.readFile(path8, "utf-8"),
3634
+ write: async (path8, content, options) => {
3466
3635
  const mode = normalizeMode(options?.mode);
3467
3636
  const writeOptions = {
3468
3637
  flag: options?.flag ?? "w"
@@ -3470,20 +3639,20 @@ var LocalInvocation = class _LocalInvocation extends Invocation {
3470
3639
  if (mode !== void 0) {
3471
3640
  writeOptions.mode = mode;
3472
3641
  }
3473
- await fs5.promises.writeFile(path5, content, writeOptions);
3642
+ await fs5.promises.writeFile(path8, content, writeOptions);
3474
3643
  },
3475
- exists: async (path5) => {
3644
+ exists: async (path8) => {
3476
3645
  try {
3477
- await fs5.promises.access(path5);
3646
+ await fs5.promises.access(path8);
3478
3647
  return true;
3479
3648
  } catch {
3480
3649
  return false;
3481
3650
  }
3482
3651
  },
3483
- mkdir: async (path5, options) => {
3484
- await fs5.promises.mkdir(path5, options);
3652
+ mkdir: async (path8, options) => {
3653
+ await fs5.promises.mkdir(path8, options);
3485
3654
  },
3486
- rm: async (path5, options) => fs5.promises.rm(path5, options)
3655
+ rm: async (path8, options) => fs5.promises.rm(path8, options)
3487
3656
  };
3488
3657
  }
3489
3658
  config;
@@ -3495,13 +3664,19 @@ var LocalInvocation = class _LocalInvocation extends Invocation {
3495
3664
  options = options || {};
3496
3665
  const interactionHandler = this.runtime.interactionHandler.clone();
3497
3666
  const stdinForCommand = options.stdin;
3667
+ let hostPassword;
3498
3668
  if (options.input && typeof options.input === "object") {
3499
3669
  O(options.input).each(([pattern, value]) => {
3500
3670
  interactionHandler.map(pattern, value);
3501
3671
  });
3502
3672
  }
3503
3673
  if (options.sudo) {
3504
- const hostPassword = await this.runtime.getPassword();
3674
+ hostPassword = await this.runtime.getPassword();
3675
+ if (!this.runtime.app.isInteractive() && !hostPassword) {
3676
+ throw new Error(
3677
+ "sudo requested but no password was provided in non-interactive mode. Pass a password to App.loadApiMode or avoid sudo for API calls."
3678
+ );
3679
+ }
3505
3680
  interactionHandler.mapInput(withSudo(hostPassword));
3506
3681
  }
3507
3682
  const cwd = options.cwd ?? process.cwd();
@@ -3549,11 +3724,7 @@ var LocalInvocation = class _LocalInvocation extends Invocation {
3549
3724
  async runRemoteTaskOnHost(host, remoteTaskFn) {
3550
3725
  this.debug(`Run function on: ${chalk3.yellow(V.inspect(host.toObject()))}`);
3551
3726
  const interactionHandler = new InteractionHandler(this.runtime.app.getSecretsForHost(host.hostname));
3552
- const remoteRuntime = new RemoteRuntime(
3553
- this.runtime.app,
3554
- host,
3555
- interactionHandler
3556
- );
3727
+ const remoteRuntime = new RemoteRuntime(this.runtime.app, host, interactionHandler);
3557
3728
  try {
3558
3729
  const connected = await remoteRuntime.connect();
3559
3730
  if (!connected) {
@@ -3640,17 +3811,7 @@ var LocalRuntime = class {
3640
3811
  this.localBundlePath = localBundlePath;
3641
3812
  this.interactionHandler = interactionHandler;
3642
3813
  const appConfigInstance = this.app.config;
3643
- if (appConfigInstance instanceof ConfigFile2) {
3644
- this.host = new Host(appConfigInstance, { hostname: "localhost", alias: "localhost" });
3645
- } else {
3646
- const configType = appConfigInstance?.constructor?.name || typeof appConfigInstance;
3647
- this.app.error(
3648
- `CRITICAL ERROR: LocalRuntime could not initialize its 'localhost' Host object. The application's configuration (type: ${configType}) is not a file-based configuration (expected ConfigFile).`
3649
- );
3650
- throw new Error(
3651
- `LocalRuntime init failed: Expected app.config to be an instance of ConfigFile for 'localhost' Host, but got ${configType}.`
3652
- );
3653
- }
3814
+ this.host = new Host(appConfigInstance, { hostname: "localhost", alias: "localhost" });
3654
3815
  this.config = {
3655
3816
  cwd: process.cwd(),
3656
3817
  configFile: appConfigInstance instanceof ConfigFile2 ? appConfigInstance : void 0
@@ -3667,7 +3828,7 @@ var LocalRuntime = class {
3667
3828
  if (this.memoizedPassword) {
3668
3829
  return this.memoizedPassword;
3669
3830
  }
3670
- this.memoizedPassword = await this.app.promptPassword("Enter local sudo password:");
3831
+ this.memoizedPassword = await this.app.promptPasswordInteractively("Enter local sudo password:");
3671
3832
  return this.memoizedPassword;
3672
3833
  }
3673
3834
  async getSecret(name) {
@@ -3727,7 +3888,7 @@ var LocalRuntime = class {
3727
3888
 
3728
3889
  // src/node-runtime.ts
3729
3890
  import os3 from "os";
3730
- import axios from "axios";
3891
+ import axios2 from "axios";
3731
3892
  import * as cheerio from "cheerio";
3732
3893
  import { match as match3 } from "ts-pattern";
3733
3894
  import which from "which";
@@ -3819,7 +3980,7 @@ async function decompressZip(inputPath, outputPath, dropRootDir = 1) {
3819
3980
  var NodeRuntime = class _NodeRuntime {
3820
3981
  constructor(tmpDir) {
3821
3982
  this.tmpDir = tmpDir;
3822
- this.client = axios.create({
3983
+ this.client = axios2.create({
3823
3984
  baseURL: "https://nodejs.org/"
3824
3985
  });
3825
3986
  this.alreadyInstalled = false;
@@ -3904,9 +4065,9 @@ var NodeRuntime = class _NodeRuntime {
3904
4065
  throw new Error(`Unable to download node for ${os3}/${arch} OS/architecture`);
3905
4066
  }
3906
4067
  const filename = File.basename(url);
3907
- const path5 = this.tmpDir.join(filename);
3908
- if (path5.exists()) return path5.toString();
3909
- return await downloadFile(url, path5.toString());
4068
+ const path8 = this.tmpDir.join(filename);
4069
+ if (path8.exists()) return path8.toString();
4070
+ return await downloadFile(url, path8.toString());
3910
4071
  }
3911
4072
  // returns the path to the unzipped package directory
3912
4073
  async unzipPackage(packagePath) {
@@ -4039,12 +4200,18 @@ var ParamMap = class _ParamMap {
4039
4200
  }
4040
4201
  return match4(str).when(matches(/,/), (s) => {
4041
4202
  return VP(s.split(",")).map((v) => v.trim()).compact([""]).map((v) => this.stringToJsonObj(v)).value;
4042
- }).when(isNumeric, (s) => parseFloat(s)).when((s) => s.trim() === "", () => "").otherwise(() => str);
4203
+ }).when(isNumeric, (s) => parseFloat(s)).when(
4204
+ (s) => s.trim() === "",
4205
+ () => ""
4206
+ ).otherwise(() => str);
4043
4207
  }
4044
4208
  };
4045
4209
 
4210
+ // src/runtime.ts
4211
+ import * as z from "zod";
4212
+
4046
4213
  // src/version.ts
4047
- var version = "0.1.41";
4214
+ var version = "0.1.44";
4048
4215
 
4049
4216
  // src/app.ts
4050
4217
  import { retryUntilDefined } from "ts-retry";
@@ -4052,6 +4219,13 @@ import { Mutex as Mutex3 } from "async-mutex";
4052
4219
  import { match as match5 } from "ts-pattern";
4053
4220
 
4054
4221
  // src/core/remote/runAllRemote.ts
4222
+ var RunParamsSchema = z.object({
4223
+ taskFn: z.custom((value) => typeof value === "function", {
4224
+ message: "taskFn must be a function"
4225
+ }),
4226
+ params: z.any()
4227
+ });
4228
+ var RunResultSchema = z.record(z.string(), z.any());
4055
4229
  function serializeError(value) {
4056
4230
  if (value instanceof Error) {
4057
4231
  const err = value;
@@ -4097,7 +4271,9 @@ async function run(context) {
4097
4271
  return Object.fromEntries(normalizedEntries);
4098
4272
  }
4099
4273
  var runAllRemote_default = task(run, {
4100
- description: "run a task on all selected hosts"
4274
+ description: "run a task on all selected hosts",
4275
+ inputSchema: RunParamsSchema,
4276
+ outputSchema: RunResultSchema
4101
4277
  });
4102
4278
 
4103
4279
  // src/commands/pkg/package-manager.ts
@@ -4127,6 +4303,19 @@ var PackageManager = class {
4127
4303
  async saveManifest() {
4128
4304
  await fs6.writeFile(this.manifestPath.toString(), JSON.stringify(this.manifest, null, 2));
4129
4305
  }
4306
+ isLocalPath(source) {
4307
+ if (source.startsWith("file:")) {
4308
+ return true;
4309
+ }
4310
+ if (source.startsWith("./") || source.startsWith("../") || source.startsWith("~")) {
4311
+ return true;
4312
+ }
4313
+ if (/[a-zA-Z]:\\/.test(source) || source.includes("\\")) {
4314
+ return true;
4315
+ }
4316
+ const path8 = Path.new(source);
4317
+ return path8.isAbsolute() || path8.exists();
4318
+ }
4130
4319
  // Normalize git URLs to npm-compatible format
4131
4320
  normalizeSource(source) {
4132
4321
  if ((source.includes("github.com") || source.includes("gitlab.com") || source.includes("bitbucket.org")) && source.startsWith("http://") && !source.startsWith("git@") && !source.startsWith("git+ssh://") && !source.startsWith("git+https://")) {
@@ -4206,15 +4395,20 @@ var PackageManager = class {
4206
4395
  if (packageInfo) {
4207
4396
  const packagePath = Path.new(packageInfo.path);
4208
4397
  if (taskName) {
4209
- const taskPath = await this.findTaskInPackage(packagePath, taskName);
4210
- if (taskPath) {
4211
- return { packagePath: packagePath.toString(), taskPath };
4398
+ const resolved = await this.findTaskInPackage(packagePath, taskName);
4399
+ if (resolved) {
4400
+ return { packagePath: packagePath.toString(), taskPath: resolved.taskPath };
4212
4401
  }
4213
4402
  } else {
4214
4403
  const defaultTaskPath = await this.findDefaultTask(packagePath);
4215
4404
  if (defaultTaskPath) {
4216
4405
  return { packagePath: packagePath.toString(), taskPath: defaultTaskPath };
4217
4406
  }
4407
+ const discovered = await this.discoverTasks(packagePath);
4408
+ if (discovered.length === 1) {
4409
+ const onlyTask = discovered[0];
4410
+ return { packagePath: packagePath.toString(), taskPath: onlyTask.path };
4411
+ }
4218
4412
  }
4219
4413
  }
4220
4414
  return null;
@@ -4325,18 +4519,15 @@ var PackageManager = class {
4325
4519
  * Find a specific task in a package
4326
4520
  */
4327
4521
  async findTaskInPackage(packagePath, taskName) {
4328
- const packageInfo = this.manifest.packages.find((pkg) => pkg.path === packagePath.toString());
4329
- if (packageInfo?.tasks) {
4330
- const task2 = packageInfo.tasks.find((t) => t.name === taskName);
4331
- if (task2) {
4332
- return task2.path;
4333
- }
4522
+ const taskFiles = [`${taskName}.ts`, `${taskName}.js`];
4523
+ if (taskName.includes(".")) {
4524
+ const dottedPath = taskName.replace(/\./g, "/");
4525
+ taskFiles.push(`${dottedPath}.ts`, `${dottedPath}.js`);
4334
4526
  }
4335
- const taskFiles = ["index.ts", "index.js", `${taskName}.ts`, `${taskName}.js`];
4336
4527
  for (const file of taskFiles) {
4337
4528
  const taskPath = packagePath.join(file);
4338
4529
  if (await taskPath.exists()) {
4339
- return taskPath.toString();
4530
+ return { taskPath: taskPath.toString() };
4340
4531
  }
4341
4532
  }
4342
4533
  return null;
@@ -4344,6 +4535,11 @@ var PackageManager = class {
4344
4535
  async installPackage(source) {
4345
4536
  try {
4346
4537
  await this.loadManifest();
4538
+ if (this.isLocalPath(source)) {
4539
+ throw new Error(
4540
+ `Local directories and files are not installable. Run them directly with 'hostctl run ${source} ...' or publish to npm/git and install that package.`
4541
+ );
4542
+ }
4347
4543
  const normalizedSource = this.normalizeSource(source);
4348
4544
  const packagesDir = this.app.packagesDir();
4349
4545
  await fs6.mkdir(packagesDir.toString(), { recursive: true });
@@ -4363,12 +4559,11 @@ var PackageManager = class {
4363
4559
  return installResult;
4364
4560
  }
4365
4561
  const packagePath = Path.new(installResult.installPath);
4366
- const { packageInfo, tasks } = await this.getPackageInfoAndTasks(packagePath, installResult.packageInfo.name);
4562
+ const packageInfo = await this.getPackageInfo(packagePath, installResult.packageInfo.name);
4367
4563
  const finalPackageInfo = {
4368
4564
  ...installResult.packageInfo,
4369
4565
  ...packageInfo || {},
4370
- source: normalizedSource,
4371
- tasks
4566
+ source: normalizedSource
4372
4567
  };
4373
4568
  await this.handleDuplicateNames(finalPackageInfo);
4374
4569
  this.manifest.packages.push(finalPackageInfo);
@@ -4405,7 +4600,10 @@ var PackageManager = class {
4405
4600
  installPath: packagesDir.join(_packageName).toString()
4406
4601
  };
4407
4602
  }
4408
- const { path: actualPackagePath, name: realPackageName } = await this.findRealInstalledNpmPackagePath(packagesDir, source);
4603
+ const { path: actualPackagePath, name: realPackageName } = await this.findRealInstalledNpmPackagePath(
4604
+ packagesDir,
4605
+ source
4606
+ );
4409
4607
  if (!actualPackagePath || !realPackageName) {
4410
4608
  return {
4411
4609
  success: false,
@@ -4482,11 +4680,6 @@ var PackageManager = class {
4482
4680
  }
4483
4681
  return { path: null, name: null };
4484
4682
  }
4485
- async getPackageInfoAndTasks(packagePath, fallbackName) {
4486
- const packageInfo = await this.getPackageInfo(packagePath, fallbackName);
4487
- const tasks = await this.discoverTasks(packagePath);
4488
- return { packageInfo, tasks };
4489
- }
4490
4683
  async handleDuplicateNames(finalPackageInfo) {
4491
4684
  const actualPackageName = finalPackageInfo.name;
4492
4685
  if (this.hasPackageWithName(actualPackageName)) {
@@ -4531,6 +4724,89 @@ var PackageManager = class {
4531
4724
  }
4532
4725
  };
4533
4726
 
4727
+ // src/task-registry-loader.ts
4728
+ import { promises as fs7 } from "fs";
4729
+ import path4 from "path";
4730
+ import { pathToFileURL } from "url";
4731
+ function resolveExportsEntry(exportsField) {
4732
+ if (!exportsField) return void 0;
4733
+ if (typeof exportsField === "string") return exportsField;
4734
+ if (typeof exportsField !== "object") return void 0;
4735
+ const root = exportsField["."] ?? exportsField;
4736
+ if (typeof root === "string") return root;
4737
+ if (typeof root !== "object" || !root) return void 0;
4738
+ const rootRecord = root;
4739
+ if (typeof rootRecord.import === "string") return rootRecord.import;
4740
+ if (typeof rootRecord.default === "string") return rootRecord.default;
4741
+ return void 0;
4742
+ }
4743
+ async function readPackageJson(packagePath) {
4744
+ const packageJsonPath = path4.join(packagePath, "package.json");
4745
+ try {
4746
+ const raw = await fs7.readFile(packageJsonPath, "utf8");
4747
+ return JSON.parse(raw);
4748
+ } catch {
4749
+ return null;
4750
+ }
4751
+ }
4752
+ async function fileExists(filePath) {
4753
+ try {
4754
+ const stat = await fs7.stat(filePath);
4755
+ return stat.isFile();
4756
+ } catch {
4757
+ return false;
4758
+ }
4759
+ }
4760
+ async function resolvePackageEntries(packagePath) {
4761
+ const pkg = await readPackageJson(packagePath);
4762
+ if (!pkg) return [];
4763
+ const candidates = [
4764
+ resolveExportsEntry(pkg.exports),
4765
+ pkg.module,
4766
+ pkg.main,
4767
+ "src/index.ts",
4768
+ "src/index.js",
4769
+ "src/index.mjs",
4770
+ "src/index.cjs",
4771
+ "index.ts",
4772
+ "index.js",
4773
+ "index.mjs",
4774
+ "index.cjs"
4775
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
4776
+ const entries = [];
4777
+ const seen = /* @__PURE__ */ new Set();
4778
+ for (const candidate of candidates) {
4779
+ const resolved = path4.isAbsolute(candidate) ? candidate : path4.resolve(packagePath, candidate);
4780
+ if (seen.has(resolved)) continue;
4781
+ seen.add(resolved);
4782
+ if (await fileExists(resolved)) {
4783
+ entries.push(resolved);
4784
+ }
4785
+ }
4786
+ return entries;
4787
+ }
4788
+ function isRegistryLike(candidate) {
4789
+ return !!candidate && typeof candidate === "object" && typeof candidate.get === "function" && typeof candidate.tasks === "function" && typeof candidate.register === "function";
4790
+ }
4791
+ async function loadRegistryFromPackage(packagePath) {
4792
+ const entries = await resolvePackageEntries(packagePath);
4793
+ if (entries.length === 0) {
4794
+ return null;
4795
+ }
4796
+ for (const entry of entries) {
4797
+ try {
4798
+ const mod = await import(pathToFileURL(entry).href);
4799
+ const registry = mod.registry;
4800
+ if (isRegistryLike(registry)) {
4801
+ return registry;
4802
+ }
4803
+ } catch {
4804
+ continue;
4805
+ }
4806
+ }
4807
+ return null;
4808
+ }
4809
+
4534
4810
  // src/cli/resolve-task-and-args.ts
4535
4811
  async function resolveTaskPathAndArgs(app, pkgTaskArgs) {
4536
4812
  app.debug("resolveTaskPathAndArgs", pkgTaskArgs);
@@ -4562,6 +4838,13 @@ async function resolveTaskPathAndArgs(app, pkgTaskArgs) {
4562
4838
  if (localTaskResult) {
4563
4839
  return localTaskResult;
4564
4840
  }
4841
+ if (looksLikeLocalPath(packageRef)) {
4842
+ const missingPath = Path.new(packageRef);
4843
+ const localReason = missingPath.exists() ? `local path is not a supported task entry: ${missingPath.toString()}` : `local path not found: ${missingPath.toString()}`;
4844
+ throw new Error(
4845
+ `Could not resolve task: ${localReason}. Local directories are run directly (no install); double-check the path or publish the package to npm/git.`
4846
+ );
4847
+ }
4565
4848
  const packageManager = new PackageManager(app);
4566
4849
  const remoteTaskResult = await resolveRemoteTask(app, packageManager, packageRef, scriptRef, scriptArgs);
4567
4850
  if (remoteTaskResult) {
@@ -4570,33 +4853,44 @@ async function resolveTaskPathAndArgs(app, pkgTaskArgs) {
4570
4853
  throw new Error(`Could not resolve task: ${packageRef}${scriptRef ? `:${scriptRef}` : ""}`);
4571
4854
  }
4572
4855
  async function resolveCoreTask(packageRef, scriptRef, scriptArgs) {
4573
- if (packageRef.startsWith("core.")) {
4574
- const taskPath = packageRef.replace(/^core\./, "").replace(/\./g, "/");
4575
- const scriptPath = `src/core/${taskPath}.ts`;
4856
+ const prefixMap = [
4857
+ { prefix: "core.", buildPath: (tail) => `src/core/${tail}.ts` },
4858
+ { prefix: "host.", buildPath: (tail) => `src/core/host/${tail}.ts` }
4859
+ ];
4860
+ for (const entry of prefixMap) {
4861
+ if (!packageRef.startsWith(entry.prefix)) {
4862
+ continue;
4863
+ }
4864
+ const tail = packageRef.slice(entry.prefix.length).replace(/\./g, "/");
4865
+ const scriptPath = entry.buildPath(tail);
4576
4866
  const taskFile = Path.new(scriptPath);
4577
4867
  if (!taskFile.exists()) {
4578
- throw new Error("Core task script not found");
4868
+ throw new Error(`Could not resolve task: Core task script not found (${packageRef}).`);
4579
4869
  }
4580
- return { scriptRef: scriptPath, scriptArgs };
4870
+ return { kind: "script", scriptRef: scriptPath, scriptArgs };
4581
4871
  }
4582
4872
  return null;
4583
4873
  }
4584
- async function resolveLocalTask(app, path5, scriptRef, scriptArgs) {
4585
- if (isGitUrl(path5) || isNpmPackageName(path5)) {
4874
+ async function resolveLocalTask(app, path8, scriptRef, scriptArgs) {
4875
+ if (isGitUrl(path8) || isNpmPackageName(path8)) {
4586
4876
  return null;
4587
4877
  }
4588
- const pathObj = Path.new(path5);
4878
+ const pathObj = Path.new(path8);
4589
4879
  if (!pathObj.exists()) {
4590
4880
  return null;
4591
4881
  }
4592
4882
  if (pathObj.isDirectory()) {
4883
+ const registryTask = await resolveRegistryTask(pathObj.absolute().toString(), scriptRef);
4884
+ if (registryTask) {
4885
+ return { kind: "registry", task: registryTask, scriptArgs };
4886
+ }
4593
4887
  return await handleDirectory(pathObj, scriptRef, scriptArgs);
4594
4888
  }
4595
4889
  if (pathObj.isFile() && (pathObj.ext() === ".ts" || pathObj.ext() === ".js")) {
4596
4890
  if (scriptRef) {
4597
4891
  A2(scriptArgs).prepend(scriptRef);
4598
4892
  }
4599
- return { scriptRef: pathObj.toString(), scriptArgs };
4893
+ return { kind: "script", scriptRef: pathObj.toString(), scriptArgs };
4600
4894
  }
4601
4895
  return null;
4602
4896
  }
@@ -4618,19 +4912,47 @@ function isNpmPackageName(str) {
4618
4912
  }
4619
4913
  return true;
4620
4914
  }
4915
+ function looksLikeLocalPath(ref) {
4916
+ if (ref.startsWith("file:")) {
4917
+ return true;
4918
+ }
4919
+ if (ref === "." || ref === "..") {
4920
+ return true;
4921
+ }
4922
+ if (ref.startsWith("./") || ref.startsWith("../") || ref.startsWith("~") || ref.startsWith("/")) {
4923
+ return true;
4924
+ }
4925
+ if (/[a-zA-Z]:\\/.test(ref) || ref.includes("\\")) {
4926
+ return true;
4927
+ }
4928
+ return false;
4929
+ }
4621
4930
  async function resolveRemoteTask(app, packageManager, packageRef, scriptRef, scriptArgs) {
4622
4931
  const isInstalled = await packageManager.isPackageInstalled(packageRef);
4623
4932
  if (!isInstalled) {
4624
4933
  const installResult = await packageManager.installPackage(packageRef);
4625
4934
  if (!installResult.success) {
4626
- return null;
4935
+ const installError = installResult.error ? ` (${installResult.error})` : "";
4936
+ throw new Error(
4937
+ `Could not resolve task: failed to install npm/git package '${packageRef}'. Local directories are run directly; otherwise provide a valid npm package name or git URL${installError}.`
4938
+ );
4627
4939
  }
4628
4940
  }
4941
+ const installed = await packageManager.getPackageByIdentifier(packageRef);
4942
+ if (!installed) {
4943
+ throw new Error(`Could not resolve task: installed package '${packageRef}' not found in manifest.`);
4944
+ }
4945
+ const registryTask = await resolveRegistryTask(installed.path, scriptRef);
4946
+ if (registryTask) {
4947
+ return { kind: "registry", task: registryTask, scriptArgs };
4948
+ }
4629
4949
  const result = await packageManager.resolveTaskPath(packageRef, scriptRef);
4630
4950
  if (result) {
4631
- return { scriptRef: result.taskPath, scriptArgs };
4951
+ return { kind: "script", scriptRef: result.taskPath, scriptArgs };
4632
4952
  }
4633
- return null;
4953
+ throw new Error(
4954
+ `Could not resolve task: installed package '${packageRef}' but could not find ${scriptRef ? `task '${scriptRef}'` : "a default task"}.`
4955
+ );
4634
4956
  }
4635
4957
  async function handleDirectory(dirPath, scriptRef, scriptArgs) {
4636
4958
  if (scriptRef) {
@@ -4640,23 +4962,62 @@ async function handleDirectory(dirPath, scriptRef, scriptArgs) {
4640
4962
  const tsPath = taskDir ? dirPath.join(taskDir, `${taskFileName}.ts`) : dirPath.join(`${taskFileName}.ts`);
4641
4963
  const jsPath = taskDir ? dirPath.join(taskDir, `${taskFileName}.js`) : dirPath.join(`${taskFileName}.js`);
4642
4964
  if (tsPath.exists()) {
4643
- return { scriptRef: tsPath.toString(), scriptArgs };
4965
+ return { kind: "script", scriptRef: tsPath.toString(), scriptArgs };
4644
4966
  }
4645
4967
  if (jsPath.exists()) {
4646
- return { scriptRef: jsPath.toString(), scriptArgs };
4968
+ return { kind: "script", scriptRef: jsPath.toString(), scriptArgs };
4647
4969
  }
4648
4970
  const resolvedPath = dirPath.resolve(scriptRef);
4649
- return { scriptRef: resolvedPath.toString(), scriptArgs };
4971
+ return { kind: "script", scriptRef: resolvedPath.toString(), scriptArgs };
4650
4972
  }
4651
4973
  const defaultFiles = ["index.ts", "index.js", "main.ts", "main.js"];
4652
4974
  for (const file of defaultFiles) {
4653
4975
  const filePath = dirPath.join(file);
4654
4976
  if (filePath.exists()) {
4655
- return { scriptRef: filePath.toString(), scriptArgs };
4977
+ return { kind: "script", scriptRef: filePath.toString(), scriptArgs };
4656
4978
  }
4657
4979
  }
4658
4980
  throw new Error(`No default entry point found in directory: ${dirPath}`);
4659
4981
  }
4982
+ async function resolveRegistryTask(packagePath, taskName) {
4983
+ const registry = await loadRegistryFromPackage(packagePath);
4984
+ if (!registry) return null;
4985
+ if (taskName) {
4986
+ const task2 = registry.get(taskName);
4987
+ if (!task2) {
4988
+ const names2 = registryNames(registry);
4989
+ const hint2 = names2.length ? ` Available tasks: ${names2.join(", ")}` : "";
4990
+ throw new Error(`Task '${taskName}' not found in registry.${hint2}`);
4991
+ }
4992
+ if (task2.task) {
4993
+ task2.task.name = taskName;
4994
+ }
4995
+ return task2;
4996
+ }
4997
+ const tasks = registry.tasks();
4998
+ if (tasks.length === 1) {
4999
+ const [name, task2] = tasks[0];
5000
+ if (task2.task) {
5001
+ task2.task.name = name;
5002
+ }
5003
+ return task2;
5004
+ }
5005
+ const names = registryNames(registry);
5006
+ const hint = names.length ? `: ${names.join(", ")}` : "";
5007
+ throw new Error(`Package exports ${registrySize(registry)} tasks; specify one${hint}.`);
5008
+ }
5009
+ function registryNames(registry) {
5010
+ if (typeof registry.names === "function") {
5011
+ return registry.names();
5012
+ }
5013
+ return registry.tasks().map(([name]) => name);
5014
+ }
5015
+ function registrySize(registry) {
5016
+ if (typeof registry.size === "function") {
5017
+ return registry.size();
5018
+ }
5019
+ return registry.tasks().length;
5020
+ }
4660
5021
 
4661
5022
  // src/app.ts
4662
5023
  var TaskTree = class {
@@ -4729,29 +5090,42 @@ var TaskTree = class {
4729
5090
  };
4730
5091
  var App3 = class _App {
4731
5092
  static async loadApiMode(options) {
4732
- return _App.load({ ...options, outputStyle: "api" });
5093
+ return _App.load({ ...options, outputStyle: "api", interactive: options.interactive ?? false });
4733
5094
  }
4734
5095
  static async load(options) {
4735
5096
  const app = new _App();
4736
- await app.loadConfig(options.config);
5097
+ app.setInteractive(options.interactive ?? true);
5098
+ if (options.password !== void 0) {
5099
+ const provider2 = typeof options.password === "function" ? options.password : async () => options.password;
5100
+ app.setPasswordProvider(provider2);
5101
+ }
5102
+ const provider = options.configProvider ?? await app.resolveConfigProvider(options.config, {
5103
+ token: options.configToken,
5104
+ headers: options.configHeaders
5105
+ });
5106
+ await app.loadConfigFromProvider(provider);
4737
5107
  app.setVerbosity(options.verbosity ?? Verbosity.WARN);
4738
5108
  app.setOutputStyle(options.outputStyle ?? "plain");
4739
5109
  return app;
4740
5110
  }
4741
5111
  configRef;
5112
+ configProvider;
4742
5113
  _config;
4743
- selectedTags;
5114
+ hostSelector;
4744
5115
  outputStyle;
4745
5116
  _tmpDir;
4746
5117
  tmpFileRegistry;
4747
5118
  taskTree;
4748
5119
  verbosity = Verbosity.ERROR;
5120
+ passwordProvider;
5121
+ providedPassword;
5122
+ interactive = true;
4749
5123
  constructor() {
4750
5124
  this.taskTree = new TaskTree();
4751
- this.selectedTags = /* @__PURE__ */ new Set([]);
4752
5125
  this.outputStyle = "plain";
4753
5126
  this.tmpFileRegistry = new TmpFileRegistry(this.hostctlTmpDir());
4754
5127
  this.configRef = void 0;
5128
+ this.hostSelector = void 0;
4755
5129
  process3.on("exit", (code) => this.appExitCallback());
4756
5130
  }
4757
5131
  appExitCallback() {
@@ -4764,16 +5138,25 @@ var App3 = class _App {
4764
5138
  }
4765
5139
  get tmpDir() {
4766
5140
  if (!this._tmpDir) {
4767
- if (!fs7.existsSync(this.hostctlDir().toString())) {
4768
- fs7.mkdirSync(this.hostctlDir().toString(), { recursive: true });
5141
+ if (!fs8.existsSync(this.hostctlDir().toString())) {
5142
+ fs8.mkdirSync(this.hostctlDir().toString(), { recursive: true });
4769
5143
  }
4770
5144
  this._tmpDir = this.createNamedTmpDir(version);
4771
5145
  }
4772
5146
  return this._tmpDir;
4773
5147
  }
4774
- async loadConfig(configRef) {
4775
- this.configRef = this.deriveConfigRef(configRef);
4776
- this._config = await load(this.configRef);
5148
+ async loadConfig(configRef, auth) {
5149
+ const provider = await this.resolveConfigProvider(configRef, auth);
5150
+ await this.loadConfigFromProvider(provider);
5151
+ }
5152
+ async loadConfigFromProvider(provider) {
5153
+ this.configProvider = provider;
5154
+ if (provider instanceof FileConfigProvider) {
5155
+ this.configRef = provider.path;
5156
+ } else {
5157
+ this.configRef = "provider";
5158
+ }
5159
+ this._config = await ProviderConfig.load(provider);
4777
5160
  }
4778
5161
  isValidUrl(url) {
4779
5162
  try {
@@ -4795,7 +5178,7 @@ var App3 = class _App {
4795
5178
  if (homeConfigPath.isFile()) {
4796
5179
  return homeConfigPath.toString();
4797
5180
  }
4798
- if (configRef && this.isValidUrl(configRef)) {
5181
+ if (configRef) {
4799
5182
  return configRef;
4800
5183
  }
4801
5184
  throw new Error(
@@ -4807,6 +5190,40 @@ var App3 = class _App {
4807
5190
  console.log(...args);
4808
5191
  }
4809
5192
  }
5193
+ async resolveConfigProvider(configRef, auth) {
5194
+ if (configRef && Path.new(configRef).isFile()) {
5195
+ return new FileConfigProvider(Path.new(configRef).toString());
5196
+ }
5197
+ const derived = this.deriveConfigRef(configRef);
5198
+ if (Path.new(derived).isFile()) {
5199
+ return new FileConfigProvider(Path.new(derived).toString());
5200
+ }
5201
+ if (this.isValidUrl(derived)) {
5202
+ const headers = this.buildHttpConfigHeaders(auth);
5203
+ return new HttpConfigProvider({
5204
+ url: derived,
5205
+ format: derived.endsWith(".yaml") ? "yaml" : "json",
5206
+ headers
5207
+ });
5208
+ }
5209
+ throw new Error(`Could not resolve a configuration provider for reference: ${configRef ?? "(none)"}`);
5210
+ }
5211
+ buildHttpConfigHeaders(auth) {
5212
+ if (!auth?.headers && !auth?.token) return void 0;
5213
+ const headers = {};
5214
+ const token = auth?.token;
5215
+ const rawHeaders = auth?.headers ?? {};
5216
+ for (const [key, value] of Object.entries(rawHeaders)) {
5217
+ if (token && key.toLowerCase() === "authorization") {
5218
+ continue;
5219
+ }
5220
+ headers[key] = value;
5221
+ }
5222
+ if (token) {
5223
+ headers.Authorization = `Bearer ${token}`;
5224
+ }
5225
+ return headers;
5226
+ }
4810
5227
  debug(...args) {
4811
5228
  this.log(Verbosity.DEBUG, ...args);
4812
5229
  }
@@ -4852,7 +5269,7 @@ var App3 = class _App {
4852
5269
  }
4853
5270
  shouldRunRemote() {
4854
5271
  const selectedHosts = this.selectedInventory();
4855
- if (selectedHosts.length === 0 && this.selectedTags.size === 0) return false;
5272
+ if (selectedHosts.length === 0 && !this.hostSelector) return false;
4856
5273
  return selectedHosts.some((h) => !h.isLocal());
4857
5274
  }
4858
5275
  logHostCommandResult(host, command, cmdOrErr, isErrorResult = false) {
@@ -4951,21 +5368,23 @@ ${cmdRes.stderr.trim()}`));
4951
5368
  }
4952
5369
  }
4953
5370
  /**
4954
- * Builds the hosts record from host result quads
4955
- */
5371
+ * Builds the hosts record from host result quads
5372
+ */
4956
5373
  buildHostsRecord(hostResultQuads) {
4957
5374
  const hosts = {};
4958
- A2(hostResultQuads).each(([host, success, stdout, stderr, exitCode, signal]) => {
4959
- const key = host.alias || `${host.hostname}:${host.port}`;
4960
- hosts[key] = {
4961
- alias: host.alias || "",
4962
- success,
4963
- stdout,
4964
- stderr,
4965
- exitCode,
4966
- signal
4967
- };
4968
- });
5375
+ A2(hostResultQuads).each(
5376
+ ([host, success, stdout, stderr, exitCode, signal]) => {
5377
+ const key = host.alias || `${host.hostname}:${host.port}`;
5378
+ hosts[key] = {
5379
+ alias: host.alias || "",
5380
+ success,
5381
+ stdout,
5382
+ stderr,
5383
+ exitCode,
5384
+ signal
5385
+ };
5386
+ }
5387
+ );
4969
5388
  return hosts;
4970
5389
  }
4971
5390
  /**
@@ -5063,8 +5482,31 @@ ${cmdRes.stderr.trim()}`));
5063
5482
  file: invocation.file
5064
5483
  };
5065
5484
  }
5485
+ get selectedTags() {
5486
+ return new Set(this.hostSelector?.tags ?? []);
5487
+ }
5066
5488
  setSelectedTags(selectedTags) {
5067
- this.selectedTags = new Set(selectedTags);
5489
+ this.setHostSelector({ tags: selectedTags });
5490
+ }
5491
+ setHostSelector(selector) {
5492
+ if (!selector) {
5493
+ this.hostSelector = void 0;
5494
+ return;
5495
+ }
5496
+ const normalized = {
5497
+ names: selector.names ? [...selector.names] : void 0,
5498
+ tags: selector.tags ? [...selector.tags] : void 0
5499
+ };
5500
+ this.hostSelector = normalized;
5501
+ }
5502
+ getHostSelector() {
5503
+ if (!this.hostSelector) {
5504
+ return void 0;
5505
+ }
5506
+ return {
5507
+ names: this.hostSelector.names ? [...this.hostSelector.names] : void 0,
5508
+ tags: this.hostSelector.tags ? [...this.hostSelector.tags] : void 0
5509
+ };
5068
5510
  }
5069
5511
  setOutputStyle(outputStyle) {
5070
5512
  this.outputStyle = outputStyle;
@@ -5073,6 +5515,17 @@ ${cmdRes.stderr.trim()}`));
5073
5515
  this.verbosity = level;
5074
5516
  return this.verbosity;
5075
5517
  }
5518
+ setPasswordProvider(provider) {
5519
+ this.passwordProvider = provider;
5520
+ this.providedPassword = void 0;
5521
+ }
5522
+ setInteractive(interactive) {
5523
+ this.interactive = interactive;
5524
+ return this.interactive;
5525
+ }
5526
+ isInteractive() {
5527
+ return this.interactive;
5528
+ }
5076
5529
  outputApiMode() {
5077
5530
  return this.outputStyle == "api";
5078
5531
  }
@@ -5085,8 +5538,17 @@ ${cmdRes.stderr.trim()}`));
5085
5538
  querySelectedInventory(tags = /* @__PURE__ */ new Set()) {
5086
5539
  return this.selectInventory(this.selectedInventory(), tags);
5087
5540
  }
5088
- selectedInventory() {
5089
- return this.queryInventory(this.selectedTags);
5541
+ selectedInventory(selector) {
5542
+ if (Array.isArray(selector)) {
5543
+ return this.queryInventory(new Set(selector));
5544
+ }
5545
+ if (selector && typeof selector === "object") {
5546
+ return this.queryInventoryWithSelector(selector);
5547
+ }
5548
+ if (this.hostSelector) {
5549
+ return this.queryInventoryWithSelector(this.hostSelector);
5550
+ }
5551
+ return this.queryInventory(/* @__PURE__ */ new Set());
5090
5552
  }
5091
5553
  // returns hosts that have *all* of the given tags
5092
5554
  // each tag is a string of the form:
@@ -5104,6 +5566,25 @@ ${cmdRes.stderr.trim()}`));
5104
5566
  }
5105
5567
  return this.selectInventory(allHosts, new Set(tags));
5106
5568
  }
5569
+ queryInventoryWithSelector(selector) {
5570
+ const tags = new Set(selector.tags ?? []);
5571
+ const names = new Set((selector.names ?? []).map((n) => n.toLowerCase()));
5572
+ const allHosts = this._config?.hosts() ?? [];
5573
+ if (names.size === 0 && tags.size === 0) {
5574
+ return allHosts;
5575
+ }
5576
+ return allHosts.filter((host) => {
5577
+ const matchesName = names.size > 0 && (names.has(host.hostname.toLowerCase()) || names.has(host.alias.toLowerCase()));
5578
+ const matchesTags = tags.size === 0 ? false : host.hasAnyTag(tags);
5579
+ if (names.size > 0 && tags.size > 0) {
5580
+ return matchesName || matchesTags;
5581
+ }
5582
+ if (names.size > 0) {
5583
+ return matchesName;
5584
+ }
5585
+ return matchesTags;
5586
+ });
5587
+ }
5107
5588
  selectInventory(hosts, tags = /* @__PURE__ */ new Set()) {
5108
5589
  if (S(tags).isEmpty()) {
5109
5590
  return hosts;
@@ -5140,7 +5621,7 @@ ${cmdRes.stderr.trim()}`));
5140
5621
  allSecrets.forEach((secret, name) => {
5141
5622
  output += ` - ${name}:
5142
5623
  `;
5143
- output += ` encrypted: ${secret.value.isEncrypted()}
5624
+ output += ` encrypted: ${Boolean(secret.ciphertext)}
5144
5625
  `;
5145
5626
  });
5146
5627
  } else {
@@ -5151,7 +5632,7 @@ ${cmdRes.stderr.trim()}`));
5151
5632
  allIds.forEach((idGroup, name) => {
5152
5633
  output += ` - ${name}:
5153
5634
  `;
5154
- output += ` publicKeys: ${idGroup.publicKeys.join(", ")}
5635
+ output += ` publicKeys: ${idGroup.recipients().join(", ")}
5155
5636
  `;
5156
5637
  });
5157
5638
  } else {
@@ -5167,16 +5648,15 @@ ${cmdRes.stderr.trim()}`));
5167
5648
  allSecrets.forEach((secret, name) => {
5168
5649
  jsonData.secrets[name] = {
5169
5650
  name: secret.name,
5170
- encrypted: secret.value.isEncrypted()
5651
+ encrypted: false
5171
5652
  // Consider if ids/recipients should be exposed here
5172
5653
  };
5173
5654
  });
5174
5655
  allIds.forEach((idGroup, name) => {
5175
5656
  jsonData.identities[name] = {
5176
- name: idGroup.name || name,
5177
- // Assuming NamedRecipientGroup has a name property
5178
- publicKeys: idGroup.publicKeys,
5179
- idRefs: idGroup.idRefs
5657
+ name,
5658
+ publicKeys: idGroup.recipients(),
5659
+ idRefs: []
5180
5660
  };
5181
5661
  });
5182
5662
  console.log(JSON.stringify(jsonData, null, 2));
@@ -5184,24 +5664,20 @@ ${cmdRes.stderr.trim()}`));
5184
5664
  }
5185
5665
  // entrypoint for the cli: AGE_IDS=~/.secrets/age/test-david.priv tsx bin/hostctl.ts inventory encrypt
5186
5666
  async encryptInventoryFile() {
5187
- if (!this.configRef) {
5188
- throw new Error("Cannot encrypt inventory: Configuration file reference is not set.");
5667
+ if (!(this.configProvider instanceof FileConfigProvider)) {
5668
+ throw new Error("Encrypt is only supported for file-based configuration.");
5189
5669
  }
5190
- const currentConfigRef = this.configRef;
5191
- this.warn(`Encrypting inventory file: ${currentConfigRef}`);
5192
- const configFile = new ConfigFile2(currentConfigRef);
5193
- await configFile.load();
5194
- await configFile.encryptAll();
5195
- await configFile.save(currentConfigRef);
5670
+ const cfg = await this.configProvider.getConfigFile();
5671
+ this.warn(`Encrypting inventory file: ${this.configProvider.path}`);
5672
+ await cfg.encryptAll();
5673
+ await cfg.save(this.configProvider.path);
5196
5674
  }
5197
5675
  // entrypoint for the cli: AGE_IDS=~/.secrets/age/david.priv tsx bin/hostctl.ts inventory decrypt
5198
5676
  async decryptInventoryFile() {
5199
- if (!this.configRef) {
5200
- throw new Error("Cannot decrypt inventory: Configuration file reference is not set.");
5677
+ if (!(this.configProvider instanceof FileConfigProvider)) {
5678
+ throw new Error("Decrypt is only supported for file-based configuration.");
5201
5679
  }
5202
- const currentConfigRef = this.configRef;
5203
- const configFile = new ConfigFile2(currentConfigRef);
5204
- await configFile.load();
5680
+ const configFile = await this.configProvider.getConfigFile();
5205
5681
  let hasEncryptedSecrets = false;
5206
5682
  for (const secret of configFile._secrets.values()) {
5207
5683
  if (secret.value.isEncrypted()) {
@@ -5213,12 +5689,12 @@ ${cmdRes.stderr.trim()}`));
5213
5689
  this.info("No encrypted secrets found to decrypt. Inventory is already decrypted.");
5214
5690
  return;
5215
5691
  }
5216
- this.warn(`Decrypting inventory file: ${currentConfigRef}`);
5692
+ this.warn(`Decrypting inventory file: ${this.configProvider.path}`);
5217
5693
  try {
5218
5694
  await configFile.decryptAllIfPossible();
5219
5695
  const successfullyUsedIdentityPaths = configFile.loadPrivateKeys().filter((identity2) => {
5220
5696
  try {
5221
- return fs7.existsSync(identity2.identityFilePath);
5697
+ return fs8.existsSync(identity2.identityFilePath);
5222
5698
  } catch (e) {
5223
5699
  return false;
5224
5700
  }
@@ -5229,7 +5705,8 @@ ${successfullyUsedIdentityPaths}`);
5229
5705
  } else if (this.verbosity >= Verbosity.INFO && hasEncryptedSecrets && successfullyUsedIdentityPaths.length === 0) {
5230
5706
  this.warn("Encrypted secrets found, but no specified AGE identities were successfully used or found.");
5231
5707
  }
5232
- await configFile.save(currentConfigRef);
5708
+ const targetPath = this.configProvider instanceof FileConfigProvider ? this.configProvider.path : this.configRef || "hostctl.yaml";
5709
+ await configFile.save(targetPath);
5233
5710
  } catch (error) {
5234
5711
  if (error.message?.includes("Unable to read identity file")) {
5235
5712
  throw new Error("Decryption failed: no identity matched or failed to decrypt due to key issue.");
@@ -5257,7 +5734,7 @@ ${successfullyUsedIdentityPaths}`);
5257
5734
  );
5258
5735
  return;
5259
5736
  }
5260
- const taskFn = mod.default;
5737
+ const taskFn = this.resolveTaskFnFromModule(mod, absoluteScriptRef.toString());
5261
5738
  const interactionHandler = new InteractionHandler();
5262
5739
  const localRuntime = new LocalRuntime(this, absoluteScriptRef, interactionHandler);
5263
5740
  if (this.outputPlain()) {
@@ -5305,7 +5782,7 @@ ${successfullyUsedIdentityPaths}`);
5305
5782
  );
5306
5783
  return;
5307
5784
  }
5308
- const taskFn = mod.default;
5785
+ const taskFn = this.resolveTaskFnFromModule(mod, absoluteScriptRef.toString());
5309
5786
  const interactionHandler = new InteractionHandler();
5310
5787
  const localRuntime = new LocalRuntime(this, absoluteScriptRef, interactionHandler);
5311
5788
  this.info(`run: ${chalk4.yellow(absoluteScriptRef.toString())} ${chalk4.cyan(util2.inspect(params))}`);
@@ -5339,6 +5816,58 @@ ${successfullyUsedIdentityPaths}`);
5339
5816
  }
5340
5817
  return scriptResult;
5341
5818
  }
5819
+ async runTaskDefinition(taskFn, params, options) {
5820
+ const selectedHosts = this.selectedInventory();
5821
+ this.info(`Selected hosts: ${selectedHosts.length}`);
5822
+ const modulePath = taskFn.task.taskModuleAbsolutePath || taskFn.task.name || process3.cwd();
5823
+ const absoluteScriptRef = Path.new(modulePath).absolute();
5824
+ const packageFileDir = this.pathOfPackageJsonFile(absoluteScriptRef.toString());
5825
+ if (!packageFileDir) {
5826
+ console.error(
5827
+ chalk4.red(`Bundle failure. "${absoluteScriptRef}" nor any ancestor directory contains a package.json file.`)
5828
+ );
5829
+ return void 0;
5830
+ }
5831
+ const interactionHandler = new InteractionHandler();
5832
+ const localRuntime = new LocalRuntime(this, absoluteScriptRef, interactionHandler);
5833
+ const description = options?.remote ? `Running ${taskFn.task.name || absoluteScriptRef.toString()} on selected hosts` : `Running ${taskFn.task.name || absoluteScriptRef.toString()}`;
5834
+ let invocation;
5835
+ let scriptResult;
5836
+ try {
5837
+ if (options?.remote) {
5838
+ const remoteParams = {
5839
+ taskFn,
5840
+ params
5841
+ };
5842
+ invocation = await localRuntime.invokeRootTask(
5843
+ runAllRemote_default,
5844
+ remoteParams,
5845
+ description
5846
+ );
5847
+ } else {
5848
+ invocation = await localRuntime.invokeRootTask(taskFn, params, description);
5849
+ }
5850
+ if (!invocation) {
5851
+ this.error(`Failed to invoke task: ${taskFn.task.name || absoluteScriptRef.toString()}`);
5852
+ scriptResult = new Error(`Failed to invoke task: ${taskFn.task.name || absoluteScriptRef.toString()}`);
5853
+ } else {
5854
+ scriptResult = await invocation.result;
5855
+ this.reportScriptResult(invocation, scriptResult);
5856
+ }
5857
+ } catch (e) {
5858
+ scriptResult = e;
5859
+ if (this.outputPlain()) {
5860
+ this.error(`Error running task ${taskFn.task.name || absoluteScriptRef.toString()}: ${scriptResult.message}`);
5861
+ if (this.verbosity >= Verbosity.DEBUG && scriptResult.stack) {
5862
+ this.error(scriptResult.stack);
5863
+ }
5864
+ }
5865
+ if (invocation) {
5866
+ this.reportScriptResult(invocation, scriptResult);
5867
+ }
5868
+ }
5869
+ return scriptResult;
5870
+ }
5342
5871
  async walkInvocationTreePreorder(invocation, visitFn, visited = /* @__PURE__ */ new Set(), depth = 0) {
5343
5872
  if (visited.has(invocation)) return;
5344
5873
  visited.add(invocation);
@@ -5421,13 +5950,60 @@ ${successfullyUsedIdentityPaths}`);
5421
5950
  const mod = await import(scriptRef);
5422
5951
  return mod;
5423
5952
  }
5953
+ isTaskInstanceLike(candidate) {
5954
+ return !!candidate && typeof candidate === "object" && "runFn" in candidate && typeof candidate.runFn === "function";
5955
+ }
5956
+ wrapTaskInstance(taskInstance) {
5957
+ const taskFnObject = function(params) {
5958
+ return function(parentInvocation) {
5959
+ return parentInvocation.invokeChildTask(taskFnObject, params ?? {});
5960
+ };
5961
+ };
5962
+ Object.assign(taskFnObject, { task: taskInstance });
5963
+ return taskFnObject;
5964
+ }
5965
+ isTaskFnLike(candidate) {
5966
+ if (typeof candidate !== "function") {
5967
+ return false;
5968
+ }
5969
+ const task2 = candidate.task;
5970
+ return typeof task2 === "object" && task2 !== null;
5971
+ }
5972
+ isTaskExport(candidate) {
5973
+ return this.isTaskFnLike(candidate) || candidate instanceof Task || this.isTaskInstanceLike(candidate);
5974
+ }
5975
+ resolveTaskExport(candidate) {
5976
+ if (this.isTaskFnLike(candidate)) {
5977
+ return candidate;
5978
+ }
5979
+ if (candidate instanceof Task || this.isTaskInstanceLike(candidate)) {
5980
+ return this.wrapTaskInstance(candidate);
5981
+ }
5982
+ return void 0;
5983
+ }
5984
+ resolveTaskFnFromModule(mod, modulePath) {
5985
+ const exports = Object.entries(mod ?? {});
5986
+ const taskExports = exports.filter(([, exported]) => this.isTaskExport(exported));
5987
+ const defaultTask = this.resolveTaskExport(mod.default);
5988
+ if (!defaultTask) {
5989
+ const namedTasks = taskExports.filter(([key]) => key !== "default").map(([key]) => key);
5990
+ const hint = namedTasks.length ? ` Found named task exports: ${namedTasks.join(", ")}.` : "";
5991
+ throw new Error(`Task module ${modulePath} must export a default task.${hint}`);
5992
+ }
5993
+ const namedTaskExports = taskExports.filter(([key]) => key !== "default");
5994
+ if (namedTaskExports.length > 0) {
5995
+ const names = namedTaskExports.map(([key]) => key).join(", ");
5996
+ this.warn(`Task module ${modulePath} exports multiple tasks (${names}). Only the default export is used.`);
5997
+ }
5998
+ return defaultTask;
5999
+ }
5424
6000
  parseParams(scriptArgs) {
5425
6001
  return ParamMap.parse(scriptArgs).toObject();
5426
6002
  }
5427
6003
  // walks the directory tree that contains the given path from leaf to root searching for the deepest directory
5428
6004
  // containing a package.json file and returns the absolute path of that directory
5429
- pathOfPackageJsonFile(path5) {
5430
- let p = Path.new(path5);
6005
+ pathOfPackageJsonFile(path8) {
6006
+ let p = Path.new(path8);
5431
6007
  while (true) {
5432
6008
  if (p.dirContains("package.json")) {
5433
6009
  return p.absolute();
@@ -5447,8 +6023,23 @@ ${successfullyUsedIdentityPaths}`);
5447
6023
  };
5448
6024
  this.info(reportObj);
5449
6025
  }
5450
- async promptPassword(message = "Enter your password") {
5451
- return await promptPassword({ message });
6026
+ async promptPasswordInteractively(message = "Enter your password") {
6027
+ if (this.providedPassword !== void 0) {
6028
+ return this.providedPassword;
6029
+ }
6030
+ if (this.passwordProvider) {
6031
+ this.providedPassword = await this.passwordProvider();
6032
+ if (this.providedPassword !== void 0) {
6033
+ return this.providedPassword;
6034
+ }
6035
+ }
6036
+ if (!this.interactive) {
6037
+ throw new Error(
6038
+ "Password requested but App is in non-interactive mode. Provide LoadAppOptions.password or allow prompts by setting interactive:true."
6039
+ );
6040
+ }
6041
+ this.providedPassword = await promptPassword({ message });
6042
+ return this.providedPassword;
5452
6043
  }
5453
6044
  async installRuntime() {
5454
6045
  this.info(`creating node runtime with ${this.tmpDir.toString()}`);
@@ -5482,197 +6073,374 @@ ${successfullyUsedIdentityPaths}`);
5482
6073
  if (options.task) {
5483
6074
  pkgTaskArgs.push(options.task);
5484
6075
  }
5485
- const { scriptRef } = await resolveTaskPathAndArgs(this, pkgTaskArgs);
6076
+ const resolved = await resolveTaskPathAndArgs(this, pkgTaskArgs);
5486
6077
  if (options.hostTags) {
5487
6078
  this.setSelectedTags(options.hostTags);
5488
6079
  }
6080
+ if (resolved.kind === "registry") {
6081
+ return await this.runTaskDefinition(resolved.task, options.params, { remote: options.remote });
6082
+ }
5489
6083
  if (options.remote) {
5490
- return await this.runScriptRemote(scriptRef, options.params);
5491
- } else {
5492
- return await this.runScript(scriptRef, options.params);
6084
+ return await this.runScriptRemote(resolved.scriptRef, options.params);
5493
6085
  }
6086
+ return await this.runScript(resolved.scriptRef, options.params);
5494
6087
  }
5495
6088
  };
5496
6089
 
5497
6090
  // src/commands/pkg/create.ts
5498
- import { promises as fs8 } from "fs";
5499
- import path4 from "path";
6091
+ import { promises as fs9 } from "fs";
6092
+ import os4 from "os";
6093
+ import path5 from "path";
6094
+ function expandTildePath(input) {
6095
+ if (!input.startsWith("~")) return input;
6096
+ if (input === "~") return os4.homedir();
6097
+ if (input.startsWith("~/") || input.startsWith("~\\")) {
6098
+ return path5.join(os4.homedir(), input.slice(2));
6099
+ }
6100
+ return input;
6101
+ }
5500
6102
  var packageJsonTsTemplate = (packageName) => `{
5501
6103
  "name": "${packageName}",
5502
- "version": "1.0.0",
5503
- "description": "A hostctl task",
6104
+ "version": "0.1.0",
6105
+ "description": "hostctl task package",
5504
6106
  "type": "module",
5505
- "main": "dist/index.js",
6107
+ "main": "./dist/index.js",
6108
+ "module": "./dist/index.js",
6109
+ "types": "./dist/index.d.ts",
6110
+ "exports": {
6111
+ ".": {
6112
+ "import": "./dist/index.js",
6113
+ "types": "./dist/index.d.ts",
6114
+ "default": "./dist/index.js"
6115
+ }
6116
+ },
6117
+ "files": ["dist", "src", "README.md", "LICENSE"],
5506
6118
  "scripts": {
5507
- "build": "tsc"
6119
+ "build": "tsc -p tsconfig.json"
5508
6120
  },
5509
6121
  "keywords": ["hostctl"],
5510
6122
  "author": "",
5511
6123
  "license": "MIT",
5512
6124
  "dependencies": {
5513
- "hostctl": "latest"
6125
+ "hostctl": "^0.1.42",
6126
+ "zod": "^4.1.13"
5514
6127
  },
5515
6128
  "devDependencies": {
5516
- "typescript": "^5.0.0",
5517
- "@types/node": "latest"
6129
+ "typescript": "^5.8.3",
6130
+ "@types/node": "^24.0.0"
6131
+ },
6132
+ "engines": {
6133
+ "node": ">=24"
6134
+ },
6135
+ "publishConfig": {
6136
+ "access": "public"
5518
6137
  }
5519
6138
  }
5520
6139
  `;
5521
6140
  var packageJsonJsTemplate = (packageName) => `{
5522
6141
  "name": "${packageName}",
5523
- "version": "1.0.0",
5524
- "description": "A hostctl task",
6142
+ "version": "0.1.0",
6143
+ "description": "hostctl task package",
5525
6144
  "type": "module",
5526
- "main": "index.js",
6145
+ "main": "./src/index.js",
6146
+ "module": "./src/index.js",
6147
+ "exports": {
6148
+ ".": {
6149
+ "import": "./src/index.js",
6150
+ "default": "./src/index.js"
6151
+ }
6152
+ },
6153
+ "files": ["src", "README.md", "LICENSE"],
5527
6154
  "scripts": {},
5528
6155
  "keywords": ["hostctl"],
5529
6156
  "author": "",
5530
6157
  "license": "MIT",
5531
6158
  "dependencies": {
5532
- "hostctl": "latest"
6159
+ "hostctl": "^0.1.42",
6160
+ "zod": "^4.1.13"
6161
+ },
6162
+ "engines": {
6163
+ "node": ">=24"
6164
+ },
6165
+ "publishConfig": {
6166
+ "access": "public"
5533
6167
  }
5534
6168
  }
5535
6169
  `;
5536
6170
  var tsconfigTemplate = `{
5537
6171
  "compilerOptions": {
5538
- "target": "ES2020",
5539
- "module": "ESNext",
5540
- "moduleResolution": "node",
6172
+ "target": "ES2022",
6173
+ "module": "NodeNext",
6174
+ "moduleResolution": "NodeNext",
6175
+ "rootDir": "src",
6176
+ "outDir": "dist",
6177
+ "declaration": true,
6178
+ "declarationMap": true,
6179
+ "sourceMap": true,
5541
6180
  "strict": true,
5542
6181
  "esModuleInterop": true,
5543
6182
  "skipLibCheck": true,
5544
- "forceConsistentCasingInFileNames": true,
5545
- "outDir": "./dist"
6183
+ "forceConsistentCasingInFileNames": true
5546
6184
  },
5547
- "include": ["*.ts"],
6185
+ "include": ["src/**/*.ts"],
5548
6186
  "exclude": ["node_modules", "dist"]
5549
6187
  }
5550
6188
  `;
5551
- var indexTsTemplate = (packageName) => {
5552
- const sanitizedName = packageName.replace(/[^a-zA-Z0-9]/g, "").replace(/^[0-9]/, "_$&");
5553
- const interfaceName = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
5554
- return `import { task, type TaskContext, Verbosity, type LogLevel } from 'hostctl';
5555
-
5556
- export interface ${interfaceName}Params {
5557
- // Define your parameters here
5558
- message?: string;
6189
+ function registryPrefixFromPackageName(packageName) {
6190
+ const withoutScope = packageName.replace(/^@[^/]+\//, "");
6191
+ const withoutPrefix = withoutScope.replace(/^hostctl[-_]?/, "");
6192
+ const normalized = withoutPrefix.replace(/[^a-zA-Z0-9]+/g, ".").replace(/^\.|\.$/g, "");
6193
+ return normalized || "example";
5559
6194
  }
6195
+ var indexTsTemplate = (registryPrefix) => `import { createRegistry } from './registry.js';
6196
+ import hello from './tasks/hello.js';
5560
6197
 
5561
- export interface ${interfaceName}Result {
5562
- success: boolean;
5563
- message: string;
5564
- }
5565
-
5566
- async function run(context: TaskContext<${interfaceName}Params>): Promise<${interfaceName}Result> {
5567
- const { params, info } = context;
6198
+ export { hello };
5568
6199
 
5569
- const message = params.message || 'Hello from your new hostctl task!';
5570
- info(message);
6200
+ export const registry = createRegistry().register('${registryPrefix}.hello', hello);
6201
+ `;
6202
+ var indexJsTemplate = (registryPrefix) => `import { createRegistry } from './registry.js';
6203
+ import hello from './tasks/hello.js';
5571
6204
 
5572
- return {
5573
- success: true,
5574
- message: message
5575
- };
5576
- }
6205
+ export { hello };
5577
6206
 
5578
- export default task(run, { name: '${packageName}', description: 'A sample hostctl task' });
6207
+ export const registry = createRegistry().register('${registryPrefix}.hello', hello);
5579
6208
  `;
6209
+ var registryTsTemplate = `import type { TaskFn } from 'hostctl';
6210
+
6211
+ export type TaskRegistry = {
6212
+ register: (name: string, task: TaskFn) => TaskRegistry;
6213
+ get: (name: string) => TaskFn | undefined;
6214
+ tasks: () => Array<[string, TaskFn]>;
6215
+ has: (name: string) => boolean;
6216
+ names: () => string[];
6217
+ size: () => number;
5580
6218
  };
5581
- var indexJsTemplate = `import { task, Verbosity } from 'hostctl';
5582
6219
 
5583
- async function run(context) {
5584
- const { params, info } = context;
6220
+ function isTaskFnLike(candidate: unknown): candidate is TaskFn {
6221
+ return (
6222
+ typeof candidate === 'function' &&
6223
+ !!candidate &&
6224
+ 'task' in (candidate as Record<string, unknown>) &&
6225
+ typeof (candidate as { task?: unknown }).task === 'object'
6226
+ );
6227
+ }
5585
6228
 
5586
- const message = params.message || 'Hello from your new hostctl task!';
5587
- info(message);
6229
+ export function createRegistry(): TaskRegistry {
6230
+ const entries = new Map<string, TaskFn>();
5588
6231
 
5589
- return {
5590
- success: true,
5591
- message: message
6232
+ const registry: TaskRegistry = {
6233
+ register(name: string, task: TaskFn) {
6234
+ if (!name || typeof name !== 'string') {
6235
+ throw new Error('Registry task name must be a non-empty string.');
6236
+ }
6237
+ if (!isTaskFnLike(task)) {
6238
+ throw new Error(\`Registry task '\${name}' must be a TaskFn.\`);
6239
+ }
6240
+ if (entries.has(name)) {
6241
+ throw new Error(\`Registry already has a task named '\${name}'.\`);
6242
+ }
6243
+ if (task.task) {
6244
+ task.task.name = name;
6245
+ }
6246
+ entries.set(name, task);
6247
+ return registry;
6248
+ },
6249
+ get(name: string) {
6250
+ return entries.get(name);
6251
+ },
6252
+ tasks() {
6253
+ return Array.from(entries.entries()).sort(([a], [b]) => a.localeCompare(b));
6254
+ },
6255
+ has(name: string) {
6256
+ return entries.has(name);
6257
+ },
6258
+ names() {
6259
+ return Array.from(entries.keys()).sort((a, b) => a.localeCompare(b));
6260
+ },
6261
+ size() {
6262
+ return entries.size;
6263
+ },
5592
6264
  };
6265
+
6266
+ return registry;
5593
6267
  }
6268
+ `;
6269
+ var registryJsTemplate = `export function createRegistry() {
6270
+ const entries = new Map();
5594
6271
 
5595
- export default task(run, { name: 'sample-task', description: 'A sample hostctl task' });
6272
+ const registry = {
6273
+ register(name, task) {
6274
+ if (!name || typeof name !== 'string') {
6275
+ throw new Error('Registry task name must be a non-empty string.');
6276
+ }
6277
+ if (!task || typeof task !== 'function' || !task.task) {
6278
+ throw new Error(\`Registry task '\${name}' must be a TaskFn.\`);
6279
+ }
6280
+ if (entries.has(name)) {
6281
+ throw new Error(\`Registry already has a task named '\${name}'.\`);
6282
+ }
6283
+ if (task.task) {
6284
+ task.task.name = name;
6285
+ }
6286
+ entries.set(name, task);
6287
+ return registry;
6288
+ },
6289
+ get(name) {
6290
+ return entries.get(name);
6291
+ },
6292
+ tasks() {
6293
+ return Array.from(entries.entries()).sort(([a], [b]) => a.localeCompare(b));
6294
+ },
6295
+ has(name) {
6296
+ return entries.has(name);
6297
+ },
6298
+ names() {
6299
+ return Array.from(entries.keys()).sort((a, b) => a.localeCompare(b));
6300
+ },
6301
+ size() {
6302
+ return entries.size;
6303
+ },
6304
+ };
6305
+
6306
+ return registry;
6307
+ }
5596
6308
  `;
5597
- var sampleTaskTsTemplate = (packageName) => {
5598
- const sanitizedName = packageName.replace(/[^a-zA-Z0-9]/g, "").replace(/^[0-9]/, "_$&");
5599
- const taskName = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
5600
- return `import { task, type TaskContext, Verbosity, type LogLevel } from 'hostctl';
6309
+ var taskWrapperTsTemplate = `import { task as baseTask, type RunFn, type RunFnReturnValue, type TaskFn, type TaskOptions, type TaskParams } from 'hostctl';
6310
+ import type { ZodTypeAny } from 'zod';
5601
6311
 
5602
- export interface ${taskName}SampleParams {
5603
- name: string;
5604
- greeting?: string;
6312
+ export type TaskOptionsWithSchema = TaskOptions & {
6313
+ inputSchema?: ZodTypeAny;
6314
+ outputSchema?: ZodTypeAny;
6315
+ };
6316
+
6317
+ export function task<TParams extends TaskParams, TReturn extends RunFnReturnValue>(
6318
+ runFn: RunFn<TParams, TReturn>,
6319
+ options?: TaskOptionsWithSchema,
6320
+ ): TaskFn<TParams, TReturn> {
6321
+ const taskFn = baseTask(runFn, options as TaskOptions);
6322
+ if (options?.inputSchema) {
6323
+ (taskFn.task as any).inputSchema = options.inputSchema;
6324
+ }
6325
+ if (options?.outputSchema) {
6326
+ (taskFn.task as any).outputSchema = options.outputSchema;
6327
+ }
6328
+ return taskFn;
5605
6329
  }
6330
+ `;
6331
+ var taskWrapperJsTemplate = `import { task as baseTask } from 'hostctl';
5606
6332
 
5607
- export interface ${taskName}SampleResult {
5608
- success: boolean;
5609
- greeting: string;
6333
+ export function task(runFn, options) {
6334
+ const taskFn = baseTask(runFn, options);
6335
+ if (options?.inputSchema) {
6336
+ taskFn.task.inputSchema = options.inputSchema;
6337
+ }
6338
+ if (options?.outputSchema) {
6339
+ taskFn.task.outputSchema = options.outputSchema;
6340
+ }
6341
+ return taskFn;
5610
6342
  }
6343
+ `;
6344
+ var sampleTaskTsTemplate = `import { task, type TaskContext } from '../task.js';
6345
+ import { z } from 'zod';
5611
6346
 
5612
- async function run(context: TaskContext<${taskName}SampleParams>): Promise<${taskName}SampleResult> {
5613
- const { params, info } = context;
6347
+ const HelloInputSchema = z.object({
6348
+ name: z.string().optional(),
6349
+ excited: z.boolean().optional(),
6350
+ });
5614
6351
 
5615
- const greeting = params.greeting || 'Hello';
5616
- const message = \`\${greeting}, \${params.name}!\`;
6352
+ type HelloParams = z.infer<typeof HelloInputSchema>;
5617
6353
 
5618
- info(message);
6354
+ const HelloOutputSchema = z.object({
6355
+ success: z.boolean(),
6356
+ message: z.string(),
6357
+ });
5619
6358
 
5620
- return {
5621
- success: true,
5622
- greeting: message
5623
- };
6359
+ type HelloResult = z.infer<typeof HelloOutputSchema>;
6360
+
6361
+ async function run(context: TaskContext<HelloParams>): Promise<HelloResult> {
6362
+ const { params, info } = context;
6363
+ const name = params.name ?? 'world';
6364
+ const suffix = params.excited ? '!' : '.';
6365
+ const message = \`Hello, \${name}\${suffix}\`;
6366
+
6367
+ info(message);
6368
+ return { success: true, message };
5624
6369
  }
5625
6370
 
5626
- export default task(run, { name: 'sample-task', description: 'Greets {{name}} with {{greeting}}' });
6371
+ export default task(run, {
6372
+ name: 'hello',
6373
+ description: 'Prints a greeting.',
6374
+ inputSchema: HelloInputSchema,
6375
+ outputSchema: HelloOutputSchema,
6376
+ });
5627
6377
  `;
5628
- };
5629
- var sampleTaskJsTemplate = `import { task, Verbosity } from 'hostctl';
6378
+ var sampleTaskJsTemplate = `import { task } from '../task.js';
6379
+ import { z } from 'zod';
6380
+
6381
+ const HelloInputSchema = z.object({
6382
+ name: z.string().optional(),
6383
+ excited: z.boolean().optional(),
6384
+ });
6385
+
6386
+ const HelloOutputSchema = z.object({
6387
+ success: z.boolean(),
6388
+ message: z.string(),
6389
+ });
5630
6390
 
5631
6391
  async function run(context) {
5632
6392
  const { params, info } = context;
5633
-
5634
- const greeting = params.greeting || 'Hello';
5635
- const message = \`\${greeting}, \${params.name}!\`;
6393
+ const name = params.name ?? 'world';
6394
+ const suffix = params.excited ? '!' : '.';
6395
+ const message = \`Hello, \${name}\${suffix}\`;
5636
6396
 
5637
6397
  info(message);
5638
-
5639
- return {
5640
- success: true,
5641
- greeting: message
5642
- };
6398
+ return { success: true, message };
5643
6399
  }
5644
6400
 
5645
- export default task(run, { name: 'sample-task', description: 'Greets {{name}} with {{greeting}}' });
6401
+ export default task(run, {
6402
+ name: 'hello',
6403
+ description: 'Prints a greeting.',
6404
+ inputSchema: HelloInputSchema,
6405
+ outputSchema: HelloOutputSchema,
6406
+ });
5646
6407
  `;
5647
- var readmeTemplate = (packageName) => `# ${packageName}
6408
+ var readmeTemplate = (packageName, registryPrefix, usesBuild) => `# ${packageName}
5648
6409
 
5649
6410
  This is a hostctl task package.
5650
6411
 
5651
- ## Usage
6412
+ ## Quick start
5652
6413
 
5653
- ### Run without installing
5654
-
5655
- \`\`\`
5656
- \u276F npx hostctl run https://github.com/yourusername/${packageName} message:Hello
6414
+ \`\`\`bash
6415
+ npm install
6416
+ ${usesBuild ? "npm run build\n" : ""}hostctl tasks .
6417
+ hostctl run . ${registryPrefix}.hello name:Hostctl excited:true
5657
6418
  \`\`\`
5658
6419
 
5659
- ### Publish to npm and run
6420
+ ## Conventions
5660
6421
 
5661
- \`\`\`
5662
- \u276F npm publish
5663
- \u276F npx hostctl run ${packageName} message:Hello
5664
- \`\`\`
6422
+ - Tasks live under \`src/\` and export a default \`task(...)\`.
6423
+ - \`src/index.(ts|js)\` re-exports tasks and publishes a registry for discovery.
6424
+ - Task names are unqualified; the registry assigns qualified names.
6425
+ - Schema helpers come from \`zod\` and are attached to tasks for discovery.
5665
6426
 
5666
- ### Install locally and run
6427
+ ## Publish
5667
6428
 
6429
+ \`\`\`bash
6430
+ npm login
6431
+ ${usesBuild ? "npm run build\n" : ""}npm publish --access public
5668
6432
  \`\`\`
5669
- \u276F npx hostctl install https://github.com/yourusername/${packageName}
5670
- \u276F npx hostctl run ${packageName} message:Hello
6433
+
6434
+ Then run from the registry:
6435
+
6436
+ \`\`\`bash
6437
+ npx hostctl run ${packageName} ${registryPrefix}.hello name:Hostctl
5671
6438
  \`\`\`
5672
6439
 
5673
6440
  ## About
5674
6441
 
5675
- This is a hostctl task package that demonstrates the basic structure for creating reusable tasks.
6442
+ This package was scaffolded by \`hostctl pkg create\`. See
6443
+ \`docs/task-package-authoring.md\` in the hostctl repository for full guidance.
5676
6444
  `;
5677
6445
  var gitignoreTemplate = `node_modules/
5678
6446
  dist/
@@ -5681,33 +6449,50 @@ dist/
5681
6449
  .env
5682
6450
  `;
5683
6451
  async function createPackage(packageName, options) {
5684
- const packageDir = path4.join(process.cwd(), packageName);
5685
- await fs8.mkdir(packageDir, { recursive: true });
6452
+ const resolvedName = expandTildePath(packageName);
6453
+ const packageDir = path5.isAbsolute(resolvedName) ? resolvedName : path5.join(process.cwd(), resolvedName);
6454
+ const packageSlug = path5.basename(resolvedName);
6455
+ const packageJsonName = packageName.startsWith("@") ? packageName : packageSlug;
6456
+ const registryPrefix = registryPrefixFromPackageName(packageSlug);
6457
+ await fs9.mkdir(packageDir, { recursive: true });
5686
6458
  if (options.lang === "typescript") {
5687
- await fs8.writeFile(path4.join(packageDir, "package.json"), packageJsonTsTemplate(packageName));
5688
- await fs8.writeFile(path4.join(packageDir, "tsconfig.json"), tsconfigTemplate);
5689
- await fs8.writeFile(path4.join(packageDir, "index.ts"), indexTsTemplate(packageName));
5690
- await fs8.writeFile(path4.join(packageDir, "sample-task.ts"), sampleTaskTsTemplate(packageName));
5691
- await fs8.writeFile(path4.join(packageDir, "README.md"), readmeTemplate(packageName));
5692
- await fs8.writeFile(path4.join(packageDir, ".gitignore"), gitignoreTemplate);
6459
+ await fs9.writeFile(path5.join(packageDir, "package.json"), packageJsonTsTemplate(packageJsonName));
6460
+ await fs9.writeFile(path5.join(packageDir, "tsconfig.json"), tsconfigTemplate);
6461
+ await fs9.mkdir(path5.join(packageDir, "src", "tasks"), { recursive: true });
6462
+ await fs9.writeFile(path5.join(packageDir, "src", "index.ts"), indexTsTemplate(registryPrefix));
6463
+ await fs9.writeFile(path5.join(packageDir, "src", "registry.ts"), registryTsTemplate);
6464
+ await fs9.writeFile(path5.join(packageDir, "src", "task.ts"), taskWrapperTsTemplate);
6465
+ await fs9.writeFile(path5.join(packageDir, "src", "tasks", "hello.ts"), sampleTaskTsTemplate);
6466
+ await fs9.writeFile(path5.join(packageDir, "README.md"), readmeTemplate(packageJsonName, registryPrefix, true));
6467
+ await fs9.writeFile(path5.join(packageDir, ".gitignore"), gitignoreTemplate);
5693
6468
  } else {
5694
- await fs8.writeFile(path4.join(packageDir, "package.json"), packageJsonJsTemplate(packageName));
5695
- await fs8.writeFile(path4.join(packageDir, "index.js"), indexJsTemplate);
5696
- await fs8.writeFile(path4.join(packageDir, "sample-task.js"), sampleTaskJsTemplate);
5697
- await fs8.writeFile(path4.join(packageDir, "README.md"), readmeTemplate(packageName));
5698
- await fs8.writeFile(path4.join(packageDir, ".gitignore"), gitignoreTemplate);
5699
- }
5700
- console.log(`Created new hostctl package '${packageName}' at ${packageDir}`);
6469
+ await fs9.writeFile(path5.join(packageDir, "package.json"), packageJsonJsTemplate(packageJsonName));
6470
+ await fs9.mkdir(path5.join(packageDir, "src", "tasks"), { recursive: true });
6471
+ await fs9.writeFile(path5.join(packageDir, "src", "index.js"), indexJsTemplate(registryPrefix));
6472
+ await fs9.writeFile(path5.join(packageDir, "src", "registry.js"), registryJsTemplate);
6473
+ await fs9.writeFile(path5.join(packageDir, "src", "task.js"), taskWrapperJsTemplate);
6474
+ await fs9.writeFile(path5.join(packageDir, "src", "tasks", "hello.js"), sampleTaskJsTemplate);
6475
+ await fs9.writeFile(path5.join(packageDir, "README.md"), readmeTemplate(packageJsonName, registryPrefix, false));
6476
+ await fs9.writeFile(path5.join(packageDir, ".gitignore"), gitignoreTemplate);
6477
+ }
6478
+ console.log(`Created new hostctl package '${resolvedName}' at ${packageDir}`);
5701
6479
  console.log(`
5702
6480
  Next steps:`);
5703
- console.log(`1. cd ${packageName}`);
6481
+ console.log(`1. cd ${packageDir}`);
5704
6482
  console.log(`2. npm install`);
6483
+ let step = 3;
5705
6484
  if (options.lang === "typescript") {
5706
- console.log(`3. Edit index.ts and sample-task.ts to implement your tasks`);
6485
+ console.log(`${step}. Edit src/index.ts and src/tasks/hello.ts to implement your tasks`);
6486
+ step += 1;
6487
+ console.log(`${step}. npm run build`);
6488
+ step += 1;
5707
6489
  } else {
5708
- console.log(`3. Edit index.js and sample-task.js to implement your tasks`);
6490
+ console.log(`${step}. Edit src/index.js and src/tasks/hello.js to implement your tasks`);
6491
+ step += 1;
5709
6492
  }
5710
- console.log(`4. Test your package with: npx hostctl run .`);
6493
+ console.log(`${step}. Test your package with: hostctl tasks .`);
6494
+ step += 1;
6495
+ console.log(`${step}. Run it with: hostctl run . ${registryPrefix}.hello`);
5711
6496
  console.log(`
5712
6497
  Note: The package includes 'hostctl' as a dependency for local development.`);
5713
6498
  console.log(`For production, you may want to add it as a peer dependency instead.`);
@@ -5771,13 +6556,7 @@ async function listPackages(app) {
5771
6556
  }
5772
6557
  }
5773
6558
  console.log(output);
5774
- if (pkg.tasks && pkg.tasks.length > 0) {
5775
- pkg.tasks.forEach((task2) => {
5776
- console.log(` \u2514\u2500 ${task2.name}`);
5777
- });
5778
- } else {
5779
- console.log(` \u2514\u2500 (no tasks discovered)`);
5780
- }
6559
+ console.log(` \u2514\u2500 (run "hostctl tasks ${pkg.name}" to list tasks)`);
5781
6560
  console.log("");
5782
6561
  });
5783
6562
  const duplicateNames = Array.from(packagesByName.entries()).filter(([_, pkgs]) => pkgs.length > 1).map(([name, _]) => name);
@@ -5809,6 +6588,192 @@ async function removePackage(packageIdentifier, app) {
5809
6588
 
5810
6589
  // src/cli.ts
5811
6590
  import JSON5 from "json5";
6591
+
6592
+ // src/task-discovery.ts
6593
+ import { promises as fs10 } from "fs";
6594
+ import path6 from "path";
6595
+ import { pathToFileURL as pathToFileURL2 } from "url";
6596
+ import { glob as glob3 } from "glob";
6597
+ var metaCache = /* @__PURE__ */ new Map();
6598
+ async function readPackageJson2(pkgPath) {
6599
+ try {
6600
+ const pkgJson = await fs10.readFile(path6.join(pkgPath, "package.json"), "utf8");
6601
+ const parsed = JSON.parse(pkgJson);
6602
+ return { name: parsed.name, version: parsed.version };
6603
+ } catch {
6604
+ return {};
6605
+ }
6606
+ }
6607
+ function cacheKey(packagePath, includeSchemas) {
6608
+ return `${packagePath}::schemas=${includeSchemas ? "1" : "0"}`;
6609
+ }
6610
+ function deriveNameFromPath(filePath, pkgPath) {
6611
+ const relative = path6.relative(pkgPath, filePath);
6612
+ const withoutExt = relative.replace(/\.[^.]+$/, "");
6613
+ return withoutExt.replace(new RegExp(`\\${path6.sep}`, "g"), ".");
6614
+ }
6615
+ function isTaskInstanceLike(candidate) {
6616
+ return !!candidate && typeof candidate === "object" && "runFn" in candidate && typeof candidate.runFn === "function";
6617
+ }
6618
+ function resolveTaskInstance(candidate) {
6619
+ if (!candidate) return void 0;
6620
+ if (candidate instanceof Task) {
6621
+ return candidate;
6622
+ }
6623
+ if (isTaskInstanceLike(candidate)) {
6624
+ return candidate;
6625
+ }
6626
+ if (typeof candidate === "function") {
6627
+ const taskInstance = candidate.task;
6628
+ if (taskInstance instanceof Task || isTaskInstanceLike(taskInstance)) {
6629
+ return taskInstance;
6630
+ }
6631
+ }
6632
+ return void 0;
6633
+ }
6634
+ function isTaskFnLike(candidate) {
6635
+ if (typeof candidate !== "function") {
6636
+ return false;
6637
+ }
6638
+ const task2 = candidate.task;
6639
+ return typeof task2 === "object" && task2 !== null;
6640
+ }
6641
+ function buildMetaFromTaskFn(taskName, taskFn, packagePath, pkgInfo) {
6642
+ const taskInstance = taskFn.task;
6643
+ return {
6644
+ name: taskName,
6645
+ description: taskInstance.description,
6646
+ modulePath: void 0,
6647
+ package: { ...pkgInfo, path: packagePath },
6648
+ inputSchema: taskInstance.inputSchema,
6649
+ outputSchema: taskInstance.outputSchema,
6650
+ schemaSource: taskInstance.inputSchema || taskInstance.outputSchema ? "present" : "absent"
6651
+ };
6652
+ }
6653
+ function buildMetaFromTaskInstance(taskName, taskInstance, packagePath, pkgInfo) {
6654
+ return {
6655
+ name: taskName,
6656
+ description: taskInstance.description,
6657
+ modulePath: taskInstance.taskModuleAbsolutePath ?? packagePath,
6658
+ package: { ...pkgInfo, path: packagePath },
6659
+ inputSchema: taskInstance.inputSchema,
6660
+ outputSchema: taskInstance.outputSchema,
6661
+ schemaSource: taskInstance.inputSchema || taskInstance.outputSchema ? "present" : "absent"
6662
+ };
6663
+ }
6664
+ async function collectTaskMeta(packagePath, opts = {}) {
6665
+ const absPackagePath = path6.resolve(packagePath);
6666
+ const includeSchemas = opts.includeSchemas ?? false;
6667
+ const cacheId = cacheKey(absPackagePath, includeSchemas);
6668
+ if (opts.cache !== false && metaCache.has(cacheId)) {
6669
+ return metaCache.get(cacheId);
6670
+ }
6671
+ const pkgInfo = await readPackageJson2(absPackagePath);
6672
+ const registry = await loadRegistryFromPackage(absPackagePath);
6673
+ if (registry) {
6674
+ const metas2 = [];
6675
+ for (const [name, taskFn] of registry.tasks()) {
6676
+ if (!isTaskFnLike(taskFn)) {
6677
+ continue;
6678
+ }
6679
+ metas2.push(buildMetaFromTaskFn(name, taskFn, absPackagePath, pkgInfo));
6680
+ }
6681
+ metas2.sort((a, b) => a.name.localeCompare(b.name));
6682
+ if (opts.cache !== false) {
6683
+ metaCache.set(cacheId, metas2);
6684
+ }
6685
+ return metas2;
6686
+ }
6687
+ const ignore = opts.ignore ?? [];
6688
+ const candidates = await glob3("**/*.{ts,js,mjs,cjs}", {
6689
+ cwd: absPackagePath,
6690
+ absolute: true,
6691
+ nodir: true,
6692
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/coverage/**", "**/.turbo/**", ...ignore]
6693
+ });
6694
+ const metas = [];
6695
+ const seenTasks = /* @__PURE__ */ new Set();
6696
+ const seenKeys = /* @__PURE__ */ new Set();
6697
+ for (const file of candidates) {
6698
+ try {
6699
+ const mod = await import(pathToFileURL2(file).href);
6700
+ const exports = Object.entries(mod ?? {});
6701
+ const taskExports = exports.map(([key, exported]) => ({ key, task: resolveTaskInstance(exported) })).filter((entry) => entry.task);
6702
+ const defaultTask = resolveTaskInstance(mod.default);
6703
+ if (!defaultTask) {
6704
+ if (taskExports.length > 0) {
6705
+ const namedTasks = taskExports.filter((entry) => entry.key !== "default").map((entry) => entry.key);
6706
+ const hint = namedTasks.length ? ` Named exports: ${namedTasks.join(", ")}.` : "";
6707
+ console.warn(`Task module ${file} has task exports but no default task.${hint}`);
6708
+ }
6709
+ continue;
6710
+ }
6711
+ const namedTaskExports = taskExports.filter((entry) => entry.key !== "default");
6712
+ if (namedTaskExports.length > 0) {
6713
+ const names = namedTaskExports.map((entry) => entry.key).join(", ");
6714
+ console.warn(`Task module ${file} exports multiple tasks (${names}). Only the default export is used.`);
6715
+ }
6716
+ if (seenTasks.has(defaultTask)) {
6717
+ continue;
6718
+ }
6719
+ const taskFile = defaultTask.taskModuleAbsolutePath ?? file;
6720
+ if (seenKeys.has(taskFile)) {
6721
+ continue;
6722
+ }
6723
+ seenTasks.add(defaultTask);
6724
+ seenKeys.add(taskFile);
6725
+ metas.push(
6726
+ buildMetaFromTaskInstance(deriveNameFromPath(taskFile, absPackagePath), defaultTask, absPackagePath, pkgInfo)
6727
+ );
6728
+ } catch (err) {
6729
+ continue;
6730
+ }
6731
+ }
6732
+ metas.sort((a, b) => a.name.localeCompare(b.name));
6733
+ if (opts.cache !== false) {
6734
+ metaCache.set(cacheId, metas);
6735
+ }
6736
+ return metas;
6737
+ }
6738
+
6739
+ // src/config-provider/memory-config-provider.ts
6740
+ var MemoryConfigProvider = class {
6741
+ hostsData;
6742
+ secretsData;
6743
+ idsData;
6744
+ resolver;
6745
+ constructor(opts = {}) {
6746
+ this.hostsData = opts.hosts ?? [];
6747
+ this.secretsData = opts.secrets ?? [];
6748
+ this.idsData = opts.ids ?? [];
6749
+ this.resolver = opts.secretResolver;
6750
+ }
6751
+ async hosts() {
6752
+ return this.hostsData;
6753
+ }
6754
+ async secrets() {
6755
+ return this.secretsData;
6756
+ }
6757
+ async ids() {
6758
+ return this.idsData;
6759
+ }
6760
+ async getSecret(name) {
6761
+ const direct = this.secretsData.find((s) => s.name === name);
6762
+ if (direct) {
6763
+ return direct.value;
6764
+ }
6765
+ if (this.resolver) {
6766
+ return this.resolver(name);
6767
+ }
6768
+ return void 0;
6769
+ }
6770
+ async getRecipientGroups(idRefs) {
6771
+ const groups = this.idsData.filter((g) => idRefs.includes(g.name));
6772
+ return groups.map((g) => ({ name: g.name, recipients: g.recipients ?? [], groups: g.groups ?? [] }));
6773
+ }
6774
+ };
6775
+
6776
+ // src/cli.ts
5812
6777
  var isError2 = (value) => !!value && typeof value === "object" && "message" in value && typeof value.message === "string" && "stack" in value && typeof value.stack === "string";
5813
6778
  var logError = (message, error) => {
5814
6779
  console.error(`Error: ${message}`);
@@ -5833,6 +6798,60 @@ function isValidUrl(url) {
5833
6798
  return false;
5834
6799
  }
5835
6800
  }
6801
+ function shouldSkipConfig(envValue) {
6802
+ if (!envValue) return false;
6803
+ const normalized = envValue.trim().toLowerCase();
6804
+ return normalized === "1" || normalized === "true";
6805
+ }
6806
+ function collectConfigHeader(value, previous) {
6807
+ return previous.concat(value);
6808
+ }
6809
+ function parseHeaderEntry(entry) {
6810
+ const index = entry.indexOf(":");
6811
+ if (index === -1) {
6812
+ throw new Error(`Invalid config header "${entry}". Expected "Header-Name: value".`);
6813
+ }
6814
+ const name = entry.slice(0, index).trim();
6815
+ const value = entry.slice(index + 1).trim();
6816
+ if (!name) {
6817
+ throw new Error(`Invalid config header "${entry}". Header name is required.`);
6818
+ }
6819
+ return [name, value];
6820
+ }
6821
+ function parseHeaderMap(raw) {
6822
+ if (!raw) return {};
6823
+ const parsed = JSON5.parse(raw);
6824
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6825
+ throw new Error("HOSTCTL_CONFIG_HEADERS must be a JSON object map of header names to values.");
6826
+ }
6827
+ return Object.entries(parsed).reduce((acc, [key, value]) => {
6828
+ if (typeof value === "string") {
6829
+ acc[key] = value;
6830
+ return acc;
6831
+ }
6832
+ acc[key] = String(value);
6833
+ return acc;
6834
+ }, {});
6835
+ }
6836
+ function buildConfigAuthOptions(options) {
6837
+ const token = options.configToken ?? process4.env.HOSTCTL_CONFIG_TOKEN;
6838
+ const envHeaders = parseHeaderMap(process4.env.HOSTCTL_CONFIG_HEADERS);
6839
+ const headerEntries = options.configHeader ?? [];
6840
+ const headerList = Array.isArray(headerEntries) ? headerEntries : [headerEntries];
6841
+ const optionHeaders = headerList.reduce((acc, entry) => {
6842
+ if (typeof entry !== "string") {
6843
+ return acc;
6844
+ }
6845
+ const [name, value] = parseHeaderEntry(entry);
6846
+ acc[name] = value;
6847
+ return acc;
6848
+ }, {});
6849
+ const headers = { ...envHeaders, ...optionHeaders };
6850
+ return {
6851
+ token,
6852
+ headers: Object.keys(headers).length ? headers : void 0
6853
+ };
6854
+ }
5836
6855
  function increaseVerbosity(dummyValue, previous) {
5837
6856
  return previous + 1;
5838
6857
  }
@@ -5850,7 +6869,12 @@ var Cli = class {
5850
6869
  "verbose; may be specified multiple times for increased verbosity",
5851
6870
  increaseVerbosity,
5852
6871
  Verbosity.WARN
5853
- ).option("-c, --config <file path>", "config file path or http endpoint").option("--json", "output should be json formatted").option("-p, --password", "should prompt for sudo password?", false).option("-t, --tag <tags...>", "specify a tag (repeat for multiple tags)");
6872
+ ).option("-c, --config <file path>", "config file path or http endpoint").option("--config-token <token>", "bearer token for HTTP config requests").option(
6873
+ "--config-header <header>",
6874
+ 'additional HTTP config header (repeatable, format: "Header-Name: value")',
6875
+ collectConfigHeader,
6876
+ []
6877
+ ).option("--json", "output should be json formatted").option("-p, --password", "should prompt for sudo password?", false).option("-t, --tag <tags...>", "specify a tag (repeat for multiple tags)");
5854
6878
  this.program.command("exec").alias("e").argument(
5855
6879
  "<command...>",
5856
6880
  `the command string to run, with optional arguments (e.g. hostctl exec sudo sh -c 'echo "$(whoami)"')`
@@ -5859,9 +6883,9 @@ var Cli = class {
5859
6883
  inventoryCmd.command("report", { isDefault: true }).description("print out an inventory report (default)").action(this.handleInventory.bind(this));
5860
6884
  inventoryCmd.command("encrypt").alias("e").description("encrypt the inventory file").action(this.handleInventoryEncrypt.bind(this));
5861
6885
  inventoryCmd.command("decrypt").alias("d").description("decrypt the inventory file").action(this.handleInventoryDecrypt.bind(this));
5862
- inventoryCmd.command("list").alias("ls").description("list the hosts in the inventory file").action(this.handleInventoryList.bind(this));
6886
+ inventoryCmd.command("list").alias("ls").alias("l").description("list the hosts in the inventory file").action(this.handleInventoryList.bind(this));
5863
6887
  const pkgCmd = this.program.command("pkg").description("manage hostctl packages");
5864
- pkgCmd.command("create <package-name>").description("create a new hostctl package").option("--lang <language>", "the language for the package (typescript or javascript)", "typescript").hook("preAction", (thisCommand, actionCommand) => {
6888
+ pkgCmd.command("create <package-path>").description("create a new hostctl package (path is relative to cwd unless absolute)").option("--lang <language>", "the language for the package (typescript or javascript)", "typescript").hook("preAction", (thisCommand, actionCommand) => {
5865
6889
  const options = actionCommand.opts();
5866
6890
  const supportedLanguages = ["typescript", "javascript"];
5867
6891
  if (!supportedLanguages.includes(options.lang)) {
@@ -5891,6 +6915,7 @@ var Cli = class {
5891
6915
  ).option("-r, --remote", "run the script on remote hosts specified by tags via SSH orchestration").action(this.handleRun.bind(this));
5892
6916
  const runtimeCmd = this.program.command("runtime").alias("rt").description("print out a report of the runtime environment").action(this.handleRuntime.bind(this));
5893
6917
  runtimeCmd.command("install").alias("i").description("install a temporary runtime environment on the local host if needed").action(this.handleRuntimeInstall.bind(this));
6918
+ this.program.command("tasks").description("list tasks available in a package or path").argument("[package-or-path...]", "npm package spec, installed package name, or local path").option("--task <name>", "filter to a specific task name").option("--json", "output should be json formatted").action(this.handleTasksList.bind(this));
5894
6919
  }
5895
6920
  async handleInventory(options, cmd) {
5896
6921
  const opts = cmd.optsWithGlobals();
@@ -5963,9 +6988,10 @@ var Cli = class {
5963
6988
  process4.exitCode = 1;
5964
6989
  return;
5965
6990
  }
5966
- throw error;
6991
+ console.error(error instanceof Error ? error.message : String(error));
6992
+ process4.exitCode = 1;
6993
+ return;
5967
6994
  }
5968
- const scriptRef = resolved.scriptRef;
5969
6995
  const scriptArgs = resolved.scriptArgs;
5970
6996
  let params = {};
5971
6997
  try {
@@ -5984,16 +7010,23 @@ var Cli = class {
5984
7010
  );
5985
7011
  throw new Error(`Failed to parse params as JSON5: ${err.message}`);
5986
7012
  }
5987
- if (!scriptRef) {
7013
+ if (resolved.kind === "script" && !resolved.scriptRef) {
5988
7014
  this.program.help();
5989
7015
  }
5990
- this.app.debug(`Resolved script: ${scriptRef}`);
7016
+ if (resolved.kind === "registry") {
7017
+ const taskName = resolved.task.task?.name ?? "unknown";
7018
+ this.app.debug(`Resolved registry task: ${taskName}`);
7019
+ } else {
7020
+ this.app.debug(`Resolved script: ${resolved.scriptRef}`);
7021
+ }
5991
7022
  this.app.debug(`Script params: ${JSON.stringify(params)}`);
5992
7023
  let result;
5993
- if (opts.remote) {
5994
- result = await this.app.runScriptRemote(scriptRef, params);
7024
+ if (resolved.kind === "registry") {
7025
+ result = await this.app.runTaskDefinition(resolved.task, params, { remote: opts.remote });
7026
+ } else if (opts.remote) {
7027
+ result = await this.app.runScriptRemote(resolved.scriptRef, params);
5995
7028
  } else {
5996
- result = await this.app.runScript(scriptRef, params);
7029
+ result = await this.app.runScript(resolved.scriptRef, params);
5997
7030
  }
5998
7031
  if (result instanceof Error) {
5999
7032
  process4.exitCode = 1;
@@ -6031,7 +7064,14 @@ var Cli = class {
6031
7064
  );
6032
7065
  }
6033
7066
  async loadApp(opts) {
6034
- await this.app.loadConfig(opts.config);
7067
+ const configAuth = buildConfigAuthOptions(opts);
7068
+ if (opts.config) {
7069
+ await this.app.loadConfig(opts.config, configAuth);
7070
+ } else if (shouldSkipConfig(process4.env.HOSTCTL_E2E_SKIP_CONFIG)) {
7071
+ await this.app.loadConfigFromProvider(new MemoryConfigProvider());
7072
+ } else {
7073
+ await this.app.loadConfig(void 0, configAuth);
7074
+ }
6035
7075
  const verbosity = opts.quiet + opts.verbose;
6036
7076
  this.app.setVerbosity(verbosity);
6037
7077
  if (opts.json) {
@@ -6086,7 +7126,166 @@ var Cli = class {
6086
7126
  await this.loadApp(this.program.opts());
6087
7127
  await removePackage(packageIdentifier, this.app);
6088
7128
  }
7129
+ async handleTasksList(specParts, options, cmd) {
7130
+ const opts = cmd.optsWithGlobals();
7131
+ await this.loadApp(opts);
7132
+ const packageManager = new PackageManager(this.app);
7133
+ let spec;
7134
+ let taskFilter = options.task;
7135
+ if (Array.isArray(specParts)) {
7136
+ if (specParts.length === 0) {
7137
+ spec = ".";
7138
+ } else if (specParts[0] === "list" && specParts.length >= 2) {
7139
+ spec = specParts[1];
7140
+ } else {
7141
+ spec = specParts[0];
7142
+ }
7143
+ } else {
7144
+ spec = specParts || ".";
7145
+ }
7146
+ if (!taskFilter && spec && spec.includes(".") && !Path.new(spec).exists()) {
7147
+ taskFilter = spec;
7148
+ spec = ".";
7149
+ }
7150
+ const specPath = Path.new(spec);
7151
+ let packagePath = null;
7152
+ if (specPath.exists()) {
7153
+ packagePath = specPath.isFile() ? specPath.parent().absolute().toString() : specPath.absolute().toString();
7154
+ } else {
7155
+ const installed = await packageManager.getPackageByIdentifier(spec);
7156
+ if (installed) {
7157
+ packagePath = Path.new(installed.path).absolute().toString();
7158
+ } else {
7159
+ const installResult = await packageManager.installPackage(spec);
7160
+ if (!installResult.success) {
7161
+ console.error(`Failed to resolve package '${spec}': ${installResult.error}`);
7162
+ process4.exitCode = 1;
7163
+ return;
7164
+ }
7165
+ packagePath = Path.new(installResult.packageInfo.path).absolute().toString();
7166
+ }
7167
+ }
7168
+ if (!packagePath) {
7169
+ console.error(`Could not resolve package or path: ${spec}`);
7170
+ process4.exitCode = 1;
7171
+ return;
7172
+ }
7173
+ const metas = await collectTaskMeta(packagePath);
7174
+ const rows = metas.filter((m) => taskFilter ? m.name === taskFilter : true).map((m) => {
7175
+ const moduleRel = m.modulePath ? path7.relative(packagePath, m.modulePath) || path7.basename(m.modulePath) : void 0;
7176
+ return {
7177
+ name: m.name,
7178
+ description: m.description ?? "",
7179
+ module: moduleRel,
7180
+ inputSchema: m.inputSchema,
7181
+ outputSchema: m.outputSchema,
7182
+ schemaSource: m.schemaSource
7183
+ };
7184
+ });
7185
+ if (opts.json) {
7186
+ const sanitized = rows.map((r) => ({
7187
+ name: r.name,
7188
+ description: r.description,
7189
+ module: r.module,
7190
+ inputSchema: Boolean(r.inputSchema),
7191
+ outputSchema: Boolean(r.outputSchema),
7192
+ schemaSource: r.schemaSource
7193
+ }));
7194
+ console.log(JSON.stringify(sanitized, null, 2));
7195
+ return;
7196
+ }
7197
+ if (rows.length === 0) {
7198
+ const target = taskFilter ? `${taskFilter} in ${packagePath}` : packagePath;
7199
+ console.log(`No tasks found in ${target}`);
7200
+ return;
7201
+ }
7202
+ console.log(`Tasks in ${packagePath}${taskFilter ? ` (filtered to ${taskFilter})` : ""}:`);
7203
+ rows.forEach((row) => {
7204
+ console.log(`- ${row.name}${row.module ? ` (${row.module})` : ""}`);
7205
+ console.log(` description: ${row.description || "no description"}`);
7206
+ if (row.inputSchema) {
7207
+ const lines = formatSchemaLines(row.inputSchema);
7208
+ if (lines.length) {
7209
+ console.log(" input:");
7210
+ lines.forEach((line) => console.log(` ${line}`));
7211
+ }
7212
+ }
7213
+ if (row.outputSchema) {
7214
+ const lines = formatSchemaLines(row.outputSchema);
7215
+ if (lines.length) {
7216
+ console.log(" output:");
7217
+ lines.forEach((line) => console.log(` ${line}`));
7218
+ }
7219
+ }
7220
+ });
7221
+ }
6089
7222
  };
7223
+ function summarizeType(val) {
7224
+ const def = val?._def ?? val?.def;
7225
+ const tn = def?.typeName || def?.type;
7226
+ if (!tn) return {};
7227
+ const desc = def?.description;
7228
+ switch (tn) {
7229
+ case "ZodOptional":
7230
+ case "optional":
7231
+ case "ZodNullable":
7232
+ case "nullable":
7233
+ return summarizeType(def.innerType || def.type);
7234
+ case "ZodEffects":
7235
+ return summarizeType(def.schema);
7236
+ case "ZodString":
7237
+ case "string":
7238
+ return { type: "string", desc };
7239
+ case "ZodNumber":
7240
+ case "number":
7241
+ return { type: "number", desc };
7242
+ case "ZodBoolean":
7243
+ case "boolean":
7244
+ return { type: "boolean", desc };
7245
+ case "ZodArray":
7246
+ case "array": {
7247
+ const innerSource = def.element ?? def.type;
7248
+ const inner = summarizeType(innerSource);
7249
+ return inner.type ? { type: `array<${inner.type}>`, desc: desc || inner.desc } : {};
7250
+ }
7251
+ case "ZodUnion":
7252
+ case "union": {
7253
+ const options = def.options || [];
7254
+ const types = options.map((o) => summarizeType(o).type).filter(Boolean);
7255
+ return types.length ? { type: types.join(" | "), desc } : {};
7256
+ }
7257
+ case "ZodLiteral":
7258
+ case "literal":
7259
+ return { type: JSON.stringify(def.value), desc };
7260
+ case "ZodObject":
7261
+ case "object":
7262
+ return { type: "object", desc };
7263
+ default:
7264
+ return { type: String(tn).replace(/^Zod/, "").toLowerCase(), desc };
7265
+ }
7266
+ }
7267
+ function formatSchemaLines(schema) {
7268
+ if (!schema) return ["none"];
7269
+ const def = schema._def ?? schema.def;
7270
+ const shapeSource = def?.shape || def?.shape_;
7271
+ const shape = shapeSource ? typeof shapeSource === "function" ? shapeSource.call(def) : shapeSource : void 0;
7272
+ if (shape && typeof shape === "object") {
7273
+ const lines = Object.entries(shape).map(([key, val]) => {
7274
+ const summary2 = summarizeType(val);
7275
+ const anyVal = val;
7276
+ const tn = anyVal?._def?.typeName || anyVal?.def?.type;
7277
+ const typeString = summary2.type || (tn ? String(tn).replace(/^Zod/, "").toLowerCase() : void 0);
7278
+ const desc = summary2.desc;
7279
+ const optional = tn === "ZodOptional" || tn === "ZodNullable";
7280
+ const typePart = typeString ? `: ${typeString}` : "";
7281
+ const descPart = desc ? ` - ${desc}` : "";
7282
+ return `- ${key}${optional ? "?" : ""}${typePart}${descPart}`;
7283
+ }).filter(Boolean);
7284
+ return lines;
7285
+ }
7286
+ const summary = summarizeType(schema);
7287
+ return summary.type ? [`- ${summary.type}${summary.desc ? ` - ${summary.desc}` : ""}`] : [];
7288
+ }
6090
7289
 
6091
7290
  // src/bin/hostctl.ts
6092
7291
  var cli = new Cli();