hostctl 0.1.36 → 0.1.39

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.
@@ -1096,15 +1096,15 @@ var Equal = class extends Protocol {
1096
1096
 
1097
1097
  // src/flex/path.ts
1098
1098
  var Path = class _Path {
1099
- constructor(path3, isWindowsPath = isWindows()) {
1100
- this.path = path3;
1099
+ constructor(path4, isWindowsPath = isWindows()) {
1100
+ this.path = path4;
1101
1101
  this.isWindowsPath = isWindowsPath;
1102
1102
  }
1103
- static new(path3, isWindowsPath = isWindows()) {
1104
- if (path3 instanceof _Path) {
1105
- return path3;
1103
+ static new(path4, isWindowsPath = isWindows()) {
1104
+ if (path4 instanceof _Path) {
1105
+ return path4;
1106
1106
  }
1107
- return new _Path(path3, isWindowsPath);
1107
+ return new _Path(path4, isWindowsPath);
1108
1108
  }
1109
1109
  static cwd() {
1110
1110
  return _Path.new(process.cwd());
@@ -1144,8 +1144,8 @@ var Path = class _Path {
1144
1144
  return this.build(posix.basename(this.path, suffix));
1145
1145
  }
1146
1146
  }
1147
- build(path3) {
1148
- return new _Path(path3, this.isWindowsPath);
1147
+ build(path4) {
1148
+ return new _Path(path4, this.isWindowsPath);
1149
1149
  }
1150
1150
  // returns the path to the destination on success; null otherwise
1151
1151
  async copy(destPath, mode) {
@@ -1193,7 +1193,7 @@ var Path = class _Path {
1193
1193
  }
1194
1194
  glob(pattern) {
1195
1195
  const cwd = this.absolute().toString();
1196
- return globSync(pattern, { cwd }).map((path3) => this.build(path3));
1196
+ return globSync(pattern, { cwd }).map((path4) => this.build(path4));
1197
1197
  }
1198
1198
  isAbsolute() {
1199
1199
  if (this.isWindowsPath) {
@@ -1232,11 +1232,11 @@ var Path = class _Path {
1232
1232
  }
1233
1233
  }
1234
1234
  parent(count = 1) {
1235
- let path3 = this.absolute();
1235
+ let path4 = this.absolute();
1236
1236
  Range.new(1, count).each((i) => {
1237
- path3 = path3.resolve("..");
1237
+ path4 = path4.resolve("..");
1238
1238
  });
1239
- return path3;
1239
+ return path4;
1240
1240
  }
1241
1241
  // returns an object of the form: { root, dir, base, ext, name }
1242
1242
  //
@@ -1774,8 +1774,8 @@ import process2 from "process";
1774
1774
  import { readFile as readFile2 } from "fs/promises";
1775
1775
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1776
1776
  import { win32 as win322, posix as posix2 } from "path";
1777
- function exists(path3) {
1778
- return existsSync2(path3);
1777
+ function exists(path4) {
1778
+ return existsSync2(path4);
1779
1779
  }
1780
1780
  var File = class {
1781
1781
  static absolutePath(...paths) {
@@ -1787,15 +1787,15 @@ var File = class {
1787
1787
  }
1788
1788
  // basename("c:\\foo\\bar\\baz.txt") => "baz.txt"
1789
1789
  // basename("/tmp/myfile.html") => "myfile.html"
1790
- static basename(path3, suffix) {
1790
+ static basename(path4, suffix) {
1791
1791
  if (isWindows()) {
1792
- return win322.basename(path3, suffix);
1792
+ return win322.basename(path4, suffix);
1793
1793
  } else {
1794
- return posix2.basename(path3, suffix);
1794
+ return posix2.basename(path4, suffix);
1795
1795
  }
1796
1796
  }
1797
- static exists(path3) {
1798
- return exists(path3);
1797
+ static exists(path4) {
1798
+ return exists(path4);
1799
1799
  }
1800
1800
  static join(...paths) {
1801
1801
  if (isWindows()) {
@@ -1804,13 +1804,13 @@ var File = class {
1804
1804
  return posix2.join(...paths);
1805
1805
  }
1806
1806
  }
1807
- static readSync(path3) {
1808
- return readFileSync2(path3, {
1807
+ static readSync(path4) {
1808
+ return readFileSync2(path4, {
1809
1809
  encoding: "utf8"
1810
1810
  });
1811
1811
  }
1812
- static async readAsync(path3) {
1813
- return await readFile2(path3, {
1812
+ static async readAsync(path4) {
1813
+ return await readFile2(path4, {
1814
1814
  encoding: "utf8"
1815
1815
  });
1816
1816
  }
@@ -1820,8 +1820,8 @@ var File = class {
1820
1820
  var TmpFileRegistry = class _TmpFileRegistry {
1821
1821
  static _instance;
1822
1822
  static get instance() {
1823
- const path3 = File.join(osHomeDir(), ".hostctl", "tmpstatic");
1824
- this._instance ??= new _TmpFileRegistry(path3);
1823
+ const path4 = File.join(osHomeDir(), ".hostctl", "tmpstatic");
1824
+ this._instance ??= new _TmpFileRegistry(path4);
1825
1825
  return this._instance;
1826
1826
  }
1827
1827
  rootPath;
@@ -1840,33 +1840,33 @@ var TmpFileRegistry = class _TmpFileRegistry {
1840
1840
  }
1841
1841
  // this directory will be automatically cleaned up at program exit
1842
1842
  createNamedTmpDir(subDirName) {
1843
- const path3 = this.tmpPath(subDirName);
1844
- fs.mkdirSync(path3.toString(), { recursive: true });
1845
- this.registerTempFileOrDir(path3.toString());
1846
- return path3;
1843
+ const path4 = this.tmpPath(subDirName);
1844
+ fs.mkdirSync(path4.toString(), { recursive: true });
1845
+ this.registerTempFileOrDir(path4.toString());
1846
+ return path4;
1847
1847
  }
1848
1848
  // this file will be automatically cleaned up at program exit
1849
1849
  writeTmpFile(fileContent) {
1850
- const path3 = this.tmpPath();
1851
- fs.writeFileSync(path3.toString(), fileContent);
1852
- this.registerTempFileOrDir(path3.toString());
1853
- return path3;
1850
+ const path4 = this.tmpPath();
1851
+ fs.writeFileSync(path4.toString(), fileContent);
1852
+ this.registerTempFileOrDir(path4.toString());
1853
+ return path4;
1854
1854
  }
1855
1855
  exitCallback() {
1856
1856
  this.cleanupTempFiles();
1857
1857
  }
1858
- registerTempFileOrDir(path3) {
1859
- this.tempFilePaths.push(path3);
1858
+ registerTempFileOrDir(path4) {
1859
+ this.tempFilePaths.push(path4);
1860
1860
  }
1861
1861
  cleanupTempFiles() {
1862
- this.tempFilePaths.forEach((path3) => {
1863
- this.rmFile(path3);
1862
+ this.tempFilePaths.forEach((path4) => {
1863
+ this.rmFile(path4);
1864
1864
  });
1865
1865
  this.tempFilePaths = [];
1866
1866
  }
1867
- rmFile(path3) {
1867
+ rmFile(path4) {
1868
1868
  try {
1869
- fs.rmSync(path3, { force: true, recursive: true });
1869
+ fs.rmSync(path4, { force: true, recursive: true });
1870
1870
  } catch (e) {
1871
1871
  }
1872
1872
  }
@@ -1892,8 +1892,9 @@ var Host = class {
1892
1892
  this.tagSet = new Set(this.tags);
1893
1893
  }
1894
1894
  hostname;
1895
- // The network hostname or IP address, from the YAML hosts map key
1895
+ // The network hostname or IP address, from the YAML hosts map host field
1896
1896
  alias;
1897
+ // Required alias for the host
1897
1898
  port;
1898
1899
  user;
1899
1900
  password;
@@ -1927,7 +1928,8 @@ var Host = class {
1927
1928
  sshKeyForYaml = this.sshKey.toYAML();
1928
1929
  }
1929
1930
  return {
1930
- alias: this.alias,
1931
+ host: this.hostname,
1932
+ // Always include the host field
1931
1933
  user: this.user,
1932
1934
  port: this.port === 22 ? void 0 : this.port,
1933
1935
  // Only include port if not default
@@ -1938,13 +1940,13 @@ var Host = class {
1938
1940
  }
1939
1941
  // domain logic
1940
1942
  get shortName() {
1941
- return this.alias || this.hostname;
1943
+ return this.alias;
1942
1944
  }
1943
1945
  isLocal() {
1944
1946
  return this.hostname === "localhost" || this.hostname === "127.0.0.1";
1945
1947
  }
1946
1948
  get uri() {
1947
- return this.hostname;
1949
+ return this.port === 22 ? this.hostname : `${this.hostname}:${this.port}`;
1948
1950
  }
1949
1951
  // returns the temporary path to the decrypted ssh key
1950
1952
  async writeTmpDecryptedSshKey() {
@@ -2015,12 +2017,12 @@ import * as age from "age-encryption";
2015
2017
  import spawnAsync from "@expo/spawn-async";
2016
2018
 
2017
2019
  // src/age-encryption.ts
2018
- function readIdentityStringFromFile(path3) {
2019
- const contents = fs2.readFileSync(path3, {
2020
+ function readIdentityStringFromFile(path4) {
2021
+ const contents = fs2.readFileSync(path4, {
2020
2022
  encoding: "utf8"
2021
2023
  });
2022
2024
  const identityString = contents.split(/\r?\n|\r|\n/g).map((line) => line.trim()).find((line) => line.startsWith("AGE-SECRET-KEY-1"));
2023
- if (!identityString) throw new Error(`Unable to read identity from file: ${path3}`);
2025
+ if (!identityString) throw new Error(`Unable to read identity from file: ${path4}`);
2024
2026
  return identityString;
2025
2027
  }
2026
2028
  var LibraryDriver = class {
@@ -2036,13 +2038,13 @@ var LibraryDriver = class {
2036
2038
  }
2037
2039
  const d = new age.Decrypter();
2038
2040
  let identitiesAdded = 0;
2039
- for (const path3 of privateKeyFilePaths) {
2041
+ for (const path4 of privateKeyFilePaths) {
2040
2042
  try {
2041
- const identityString = readIdentityStringFromFile(path3);
2043
+ const identityString = readIdentityStringFromFile(path4);
2042
2044
  d.addIdentity(identityString);
2043
2045
  identitiesAdded++;
2044
2046
  } catch (err) {
2045
- console.warn(`Failed to read or parse identity file ${path3}, skipping: ${err.message}`);
2047
+ console.warn(`Failed to read or parse identity file ${path4}, skipping: ${err.message}`);
2046
2048
  }
2047
2049
  }
2048
2050
  if (identitiesAdded === 0) {
@@ -2061,13 +2063,13 @@ var Identity = class {
2061
2063
  identityFilePath;
2062
2064
  identity;
2063
2065
  // either the path to an identity file or an identity string must be supplied
2064
- constructor({ path: path3, identity: identity2 }) {
2066
+ constructor({ path: path4, identity: identity2 }) {
2065
2067
  if (identity2) {
2066
2068
  this.identity = identity2;
2067
2069
  this.identityFilePath = this.writeTmpIdentityFile(identity2);
2068
- } else if (path3) {
2069
- this.identity = this.readIdentityFromFile(path3);
2070
- this.identityFilePath = path3;
2070
+ } else if (path4) {
2071
+ this.identity = this.readIdentityFromFile(path4);
2072
+ this.identityFilePath = path4;
2071
2073
  } else {
2072
2074
  throw "Either an identity string or an identity file path must be supplied to create an Age Encryption identity";
2073
2075
  }
@@ -2079,12 +2081,12 @@ var Identity = class {
2079
2081
  writeTmpIdentityFile(identity2) {
2080
2082
  return writeTmpFile(identity2).toString();
2081
2083
  }
2082
- readIdentityFromFile(path3) {
2083
- const contents = fs2.readFileSync(path3, {
2084
+ readIdentityFromFile(path4) {
2085
+ const contents = fs2.readFileSync(path4, {
2084
2086
  encoding: "utf8"
2085
2087
  });
2086
2088
  const identityString = contents.split(/\r?\n|\r|\n/g).map((line) => line.trim()).find((line) => line.startsWith("AGE-SECRET-KEY-1"));
2087
- if (!identityString) throw new Error(`Unable to read identity file: ${path3}`);
2089
+ if (!identityString) throw new Error(`Unable to read identity file: ${path4}`);
2088
2090
  return identityString;
2089
2091
  }
2090
2092
  get privateKey() {
@@ -2234,8 +2236,8 @@ var SecretRefYamlType = new yaml.Type("!secret", {
2234
2236
  });
2235
2237
  var HOSTCTL_CONFIG_SCHEMA = yaml.DEFAULT_SCHEMA.extend([SecretRefYamlType]);
2236
2238
  var ConfigFile2 = class {
2237
- constructor(path3) {
2238
- this.path = path3;
2239
+ constructor(path4) {
2240
+ this.path = path4;
2239
2241
  this._hosts = /* @__PURE__ */ new Map();
2240
2242
  this._ids = /* @__PURE__ */ new Map();
2241
2243
  this._secrets = /* @__PURE__ */ new Map();
@@ -2276,19 +2278,25 @@ var ConfigFile2 = class {
2276
2278
  }
2277
2279
  // yamlHosts is an object
2278
2280
  parseHosts(yamlHosts) {
2279
- const hosts = Object.entries(yamlHosts).reduce((hostMap, [hostUri, hostObj]) => {
2281
+ const hosts = Object.entries(yamlHosts).reduce((hostMap, [alias, hostObj]) => {
2280
2282
  hostObj ||= {};
2281
2283
  const password = this.parseSecretValue(hostObj.password);
2282
2284
  const sshKey = this.parseSecretValue(hostObj["ssh-key"]);
2285
+ const hostname = hostObj.host || alias;
2283
2286
  hostMap.set(
2284
- hostUri,
2287
+ alias,
2288
+ // Use the key as the alias
2285
2289
  new Host(this, {
2286
- ...hostObj,
2287
- // Use 'as any' to allow spread of hostObj properties
2288
- hostname: hostUri,
2289
- // The YAML map key is always the hostname
2290
+ hostname,
2291
+ // Use the resolved hostname
2292
+ alias,
2293
+ // The key becomes the alias
2294
+ port: hostObj.port,
2295
+ // SSH port (defaults to 22 in Host constructor)
2296
+ user: hostObj.user,
2290
2297
  password,
2291
- sshKey
2298
+ sshKey,
2299
+ tags: hostObj.tags
2292
2300
  })
2293
2301
  );
2294
2302
  return hostMap;
@@ -2367,7 +2375,7 @@ var ConfigFile2 = class {
2367
2375
  const ageIds = process.env.AGE_IDS;
2368
2376
  if (ageIds) {
2369
2377
  const paths = globSync2(ageIds);
2370
- const ids = paths.map((path3) => new Identity({ path: path3 }));
2378
+ const ids = paths.map((path4) => new Identity({ path: path4 }));
2371
2379
  return ids;
2372
2380
  }
2373
2381
  return [];
@@ -2569,6 +2577,9 @@ import { Mutex as Mutex2 } from "async-mutex";
2569
2577
  // src/command.ts
2570
2578
  import "shell-quote";
2571
2579
  import Shellwords from "shellwords-ts";
2580
+ function processEnvVars() {
2581
+ return O(process.env).map(([key, value]) => [key, String(value)]);
2582
+ }
2572
2583
  var CommandResult = class {
2573
2584
  constructor(stdout, stderr, exitCode, signal) {
2574
2585
  this.stdout = stdout;
@@ -2870,20 +2881,20 @@ var LocalInvocation = class _LocalInvocation extends Invocation {
2870
2881
  )(params);
2871
2882
  this.config = this.runtime.app.config;
2872
2883
  this.file = {
2873
- read: async (path3) => fs5.promises.readFile(path3, "utf-8"),
2874
- write: async (path3, content, options) => fs5.promises.writeFile(path3, content, { mode: options?.mode }),
2875
- exists: async (path3) => {
2884
+ read: async (path4) => fs5.promises.readFile(path4, "utf-8"),
2885
+ write: async (path4, content, options) => fs5.promises.writeFile(path4, content, { mode: options?.mode }),
2886
+ exists: async (path4) => {
2876
2887
  try {
2877
- await fs5.promises.access(path3);
2888
+ await fs5.promises.access(path4);
2878
2889
  return true;
2879
2890
  } catch {
2880
2891
  return false;
2881
2892
  }
2882
2893
  },
2883
- mkdir: async (path3, options) => {
2884
- await fs5.promises.mkdir(path3, options);
2894
+ mkdir: async (path4, options) => {
2895
+ await fs5.promises.mkdir(path4, options);
2885
2896
  },
2886
- rm: async (path3, options) => fs5.promises.rm(path3, options)
2897
+ rm: async (path4, options) => fs5.promises.rm(path4, options)
2887
2898
  };
2888
2899
  }
2889
2900
  config;
@@ -2942,7 +2953,7 @@ var LocalInvocation = class _LocalInvocation extends Invocation {
2942
2953
  }
2943
2954
  const entryPromises = VP(targetHosts).map(async (host) => {
2944
2955
  const result = await this.runRemoteTaskOnHost(host, remoteTaskFn);
2945
- return [host.hostname, result];
2956
+ return [host.alias, result];
2946
2957
  }).value;
2947
2958
  const entries = await Promise.all(entryPromises);
2948
2959
  const record = Object.fromEntries(entries);
@@ -3048,7 +3059,7 @@ var LocalRuntime = class {
3048
3059
  this.interactionHandler = interactionHandler;
3049
3060
  const appConfigInstance = this.app.config;
3050
3061
  if (appConfigInstance instanceof ConfigFile2) {
3051
- this.host = new Host(appConfigInstance, { hostname: "localhost" });
3062
+ this.host = new Host(appConfigInstance, { hostname: "localhost", alias: "localhost" });
3052
3063
  } else {
3053
3064
  const configType = appConfigInstance?.constructor?.name || typeof appConfigInstance;
3054
3065
  this.app.error(
@@ -3609,38 +3620,6 @@ async function downloadFile(url, dest) {
3609
3620
  });
3610
3621
  }
3611
3622
 
3612
- // src/shell-command.ts
3613
- import spawnAsync2 from "@expo/spawn-async";
3614
- import { signalsByName as signalsByName2 } from "human-signals";
3615
- var ShellCommand = class _ShellCommand extends Command {
3616
- static fromString(command, env, cwd) {
3617
- const { cmd, args } = this.parse(command, env);
3618
- return new _ShellCommand({ cmd, args, cwd, env });
3619
- }
3620
- async run() {
3621
- try {
3622
- const resultPromise = spawnAsync2(this.cmd, this.args, {
3623
- cwd: this.cwd,
3624
- env: this.env
3625
- // shell: true
3626
- });
3627
- let { pid, stdout, stderr, status, signal } = await resultPromise;
3628
- const signalObj = signal && signalsByName2[signal] || void 0;
3629
- const commandResult = new CommandResult(stdout || "", stderr || "", status || 0, signalObj);
3630
- this.result = commandResult;
3631
- } catch (e) {
3632
- const error = e;
3633
- if (error.message) console.error(error.message);
3634
- if (error.stack) console.error(error.stack);
3635
- let { pid, stdout, stderr, status, signal } = error;
3636
- const signalObj = signal && signalsByName2[signal] || void 0;
3637
- const commandResult = new CommandResult(stdout || "", stderr || "", status || 1, signalObj);
3638
- this.result = commandResult;
3639
- }
3640
- return this.result;
3641
- }
3642
- };
3643
-
3644
3623
  // src/unarchive.ts
3645
3624
  import decompress from "decompress";
3646
3625
  import decompressTarGzPlugin from "decompress-targz";
@@ -3703,19 +3682,19 @@ var NodeRuntime = class _NodeRuntime {
3703
3682
  localNode;
3704
3683
  localNpm;
3705
3684
  async isNodeInstalledGlobally() {
3706
- const result = await ShellCommand.fromString(`node --version`).run();
3685
+ const result = await RusPtyCommand.fromString(`node --version`, processEnvVars()).run();
3707
3686
  return result.success;
3708
3687
  }
3709
3688
  async isNodeInstalledLocally() {
3710
- const result = await ShellCommand.fromString(`${this.localNode} --version`).run();
3689
+ const result = await RusPtyCommand.fromString(`${this.localNode} --version`, processEnvVars()).run();
3711
3690
  return result.success;
3712
3691
  }
3713
3692
  async isNpmInstalledGlobally() {
3714
- const result = await ShellCommand.fromString(`npm --version`).run();
3693
+ const result = await RusPtyCommand.fromString(`npm --version`, processEnvVars()).run();
3715
3694
  return result.success;
3716
3695
  }
3717
3696
  async isNpmInstalledLocally() {
3718
- const result = await ShellCommand.fromString(`${this.localNpm} --version`).run();
3697
+ const result = await RusPtyCommand.fromString(`${this.localNpm} --version`, processEnvVars()).run();
3719
3698
  return result.success;
3720
3699
  }
3721
3700
  async nodeCmd() {
@@ -3775,9 +3754,9 @@ var NodeRuntime = class _NodeRuntime {
3775
3754
  throw new Error(`Unable to download node for ${os2}/${arch} OS/architecture`);
3776
3755
  }
3777
3756
  const filename = File.basename(url);
3778
- const path3 = this.tmpDir.join(filename);
3779
- if (path3.exists()) return path3.toString();
3780
- return await downloadFile(url, path3.toString());
3757
+ const path4 = this.tmpDir.join(filename);
3758
+ if (path4.exists()) return path4.toString();
3759
+ return await downloadFile(url, path4.toString());
3781
3760
  }
3782
3761
  // returns the path to the unzipped package directory
3783
3762
  async unzipPackage(packagePath) {
@@ -3786,33 +3765,29 @@ var NodeRuntime = class _NodeRuntime {
3786
3765
  await unarchive(packagePath, dir.toString());
3787
3766
  return dir.toString();
3788
3767
  }
3789
- async npmInstall(omitDev = true, cwd) {
3768
+ async npmInstall(options = {}) {
3769
+ const { omitDev = true, cwd, installedPackagesDir } = options;
3770
+ const args = ["install"];
3790
3771
  if (omitDev) {
3791
- return this.npm("install --omit=dev", cwd);
3792
- } else {
3793
- return this.npm("install", cwd);
3772
+ args.push("--omit=dev");
3773
+ }
3774
+ if (installedPackagesDir) {
3775
+ args.push("--prefix", installedPackagesDir);
3794
3776
  }
3777
+ return this.npm(args.join(" "), cwd);
3795
3778
  }
3796
3779
  async npm(npmArgs, cwd) {
3797
3780
  const npmCmd = await this.npmCmd();
3798
- const env = O(process.env).select(([_, v]) => !!v);
3799
- return ShellCommand.fromString(`${npmCmd} ${npmArgs}`.trim(), env, cwd).run();
3781
+ return RusPtyCommand.fromString(`${npmCmd} ${npmArgs}`.trim(), processEnvVars(), cwd).run();
3800
3782
  }
3801
3783
  async node(nodeArgs, cwd) {
3802
3784
  const nodeCmd = await this.nodeCmd();
3803
- const env = O(process.env).select(([_, v]) => !!v);
3804
- return ShellCommand.fromString(`${nodeCmd} ${nodeArgs}`.trim(), env, cwd).run();
3785
+ return RusPtyCommand.fromString(`${nodeCmd} ${nodeArgs}`.trim(), processEnvVars(), cwd).run();
3805
3786
  }
3806
3787
  };
3807
3788
 
3808
3789
  // src/zip.ts
3809
3790
  import AdmZip from "adm-zip";
3810
- async function zipDirectory(sourceDir, outputFilePath) {
3811
- const zip2 = new AdmZip();
3812
- zip2.addLocalFolder(sourceDir);
3813
- await zip2.writeZipPromise(outputFilePath);
3814
- return outputFilePath;
3815
- }
3816
3791
  async function unzipDirectory(inputFilePath, outputDirectory) {
3817
3792
  const zip2 = new AdmZip(inputFilePath);
3818
3793
  return new Promise((resolve, reject) => {
@@ -3828,9 +3803,6 @@ async function unzipDirectory(inputFilePath, outputDirectory) {
3828
3803
 
3829
3804
  // src/hash.ts
3830
3805
  import { createHash } from "crypto";
3831
- function sha256(str) {
3832
- return createHash("sha256").update(str).digest("hex");
3833
- }
3834
3806
 
3835
3807
  // src/param-map.ts
3836
3808
  import { match as match4 } from "ts-pattern";
@@ -3892,7 +3864,7 @@ var ParamMap = class _ParamMap {
3892
3864
  };
3893
3865
 
3894
3866
  // src/version.ts
3895
- var version = "0.1.36";
3867
+ var version = "0.1.39";
3896
3868
 
3897
3869
  // src/app.ts
3898
3870
  import { retryUntilDefined } from "ts-retry";
@@ -3953,7 +3925,7 @@ var TaskTree = class {
3953
3925
  this.nodes.set(id, newNode);
3954
3926
  this.listr.add({
3955
3927
  title: newNode.name,
3956
- task: async (ctx, task3) => {
3928
+ task: async (ctx, task2) => {
3957
3929
  await result;
3958
3930
  }
3959
3931
  });
@@ -4060,6 +4032,13 @@ var App = class {
4060
4032
  hostctlTmpDir() {
4061
4033
  return this.hostctlDir().join("tmp");
4062
4034
  }
4035
+ packagesDir() {
4036
+ const envPkgDir = process3.env.HOSTCTL_PKG_DIR;
4037
+ if (envPkgDir) {
4038
+ return Path.new(envPkgDir);
4039
+ }
4040
+ return this.hostctlDir().join("packages");
4041
+ }
4063
4042
  randName() {
4064
4043
  return Math.random().toString(36).slice(-5) + Math.random().toString(36).slice(-5);
4065
4044
  }
@@ -4150,7 +4129,8 @@ ${cmdRes.stderr.trim()}`));
4150
4129
  try {
4151
4130
  const hostPassword = await host.decryptedPassword();
4152
4131
  const sshConnection = {
4153
- host: host.uri,
4132
+ host: host.hostname,
4133
+ port: host.port,
4154
4134
  username: host.user,
4155
4135
  password: hostPassword,
4156
4136
  privateKeyPath: await host.plaintextSshKeyPath()
@@ -4582,48 +4562,10 @@ ${successfullyUsedIdentityPaths}`);
4582
4562
  parseParams(scriptArgs) {
4583
4563
  return ParamMap.parse(scriptArgs).toObject();
4584
4564
  }
4585
- // this function creates a bundle of a given directory
4586
- async bundleProject(entrypointPath) {
4587
- const nodeRuntime = new NodeRuntime(this.tmpDir);
4588
- const entrypointDir = this.pathOfPackageJsonFile(entrypointPath);
4589
- if (!entrypointDir) {
4590
- console.error(
4591
- chalk4.red(`Bundle failure. "${entrypointPath}" nor any ancestor directory contains a package.json file.`)
4592
- );
4593
- return;
4594
- }
4595
- const tasks = new Listr([], {
4596
- exitOnError: false,
4597
- concurrent: true
4598
- });
4599
- tasks.add([
4600
- {
4601
- title: `Bundling ${entrypointDir.toString()}`,
4602
- task: async (ctx) => {
4603
- await this.generateBundle(nodeRuntime, entrypointDir);
4604
- }
4605
- }
4606
- ]);
4607
- try {
4608
- await tasks.run();
4609
- } catch (e) {
4610
- console.error(e);
4611
- }
4612
- }
4613
- async generateBundle(nodeRuntime, packageFileDir) {
4614
- await nodeRuntime.installIfNeeded();
4615
- const result = await nodeRuntime.npmInstall(true, packageFileDir.toString());
4616
- if (result.failure) throw new Error(result.err);
4617
- const absoluteDirPath = packageFileDir.toString();
4618
- const dirNameHash = sha256(absoluteDirPath).slice(0, 10);
4619
- const bundleZipFile = this.tmpDir.join(`bundle${dirNameHash}.zip`);
4620
- const zipPath = await zipDirectory(packageFileDir.toString(), bundleZipFile.toString());
4621
- return Path.new(zipPath);
4622
- }
4623
4565
  // walks the directory tree that contains the given path from leaf to root searching for the deepest directory
4624
4566
  // containing a package.json file and returns the absolute path of that directory
4625
- pathOfPackageJsonFile(path3) {
4626
- let p = Path.new(path3);
4567
+ pathOfPackageJsonFile(path4) {
4568
+ let p = Path.new(path4);
4627
4569
  while (true) {
4628
4570
  if (p.dirContains("package.json")) {
4629
4571
  return p.absolute();
@@ -4655,73 +4597,805 @@ ${successfullyUsedIdentityPaths}`);
4655
4597
  }
4656
4598
  };
4657
4599
 
4600
+ // src/commands/pkg/create.ts
4601
+ import { promises as fs7 } from "fs";
4602
+ import path3 from "path";
4603
+ var packageJsonTsTemplate = (packageName) => `{
4604
+ "name": "${packageName}",
4605
+ "version": "1.0.0",
4606
+ "description": "A hostctl task",
4607
+ "main": "index.ts",
4608
+ "scripts": {
4609
+ "build": "tsc"
4610
+ },
4611
+ "keywords": ["hostctl-task"],
4612
+ "author": "",
4613
+ "license": "ISC",
4614
+ "dependencies": {
4615
+ "hostctl": "latest"
4616
+ },
4617
+ "devDependencies": {
4618
+ "typescript": "^5.0.0",
4619
+ "@types/node": "latest"
4620
+ }
4621
+ }
4622
+ `;
4623
+ var packageJsonJsTemplate = (packageName) => `{
4624
+ "name": "${packageName}",
4625
+ "version": "1.0.0",
4626
+ "description": "A hostctl task",
4627
+ "type": "module",
4628
+ "main": "index.js",
4629
+ "scripts": {},
4630
+ "keywords": ["hostctl-task"],
4631
+ "author": "",
4632
+ "license": "ISC",
4633
+ "dependencies": {
4634
+ "hostctl": "latest"
4635
+ }
4636
+ }
4637
+ `;
4638
+ var tsconfigTemplate = `{
4639
+ "compilerOptions": {
4640
+ "target": "es2020",
4641
+ "module": "commonjs",
4642
+ "strict": true,
4643
+ "esModuleInterop": true,
4644
+ "skipLibCheck": true,
4645
+ "forceConsistentCasingInFileNames": true,
4646
+ "outDir": "./dist"
4647
+ },
4648
+ "include": ["*.ts"],
4649
+ "exclude": ["node_modules", "dist"]
4650
+ }
4651
+ `;
4652
+ var indexTsTemplate = (packageName) => {
4653
+ const sanitizedName = packageName.replace(/[^a-zA-Z0-9]/g, "").replace(/^[0-9]/, "_$&");
4654
+ const interfaceName = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
4655
+ return `import { task, type TaskContext } from 'hostctl';
4656
+
4657
+ export interface ${interfaceName}Params {
4658
+ // Define your parameters here
4659
+ message?: string;
4660
+ }
4661
+
4662
+ export interface ${interfaceName}Result {
4663
+ success: boolean;
4664
+ message: string;
4665
+ }
4666
+
4667
+ async function run(context: TaskContext<${interfaceName}Params>): Promise<${interfaceName}Result> {
4668
+ const { params, log } = context;
4669
+
4670
+ const message = params.message || 'Hello from your new hostctl task!';
4671
+ log('info', message);
4672
+
4673
+ return {
4674
+ success: true,
4675
+ message: message
4676
+ };
4677
+ }
4678
+
4679
+ export default task(run, 'A sample hostctl task');
4680
+ `;
4681
+ };
4682
+ var indexJsTemplate = `import { task } from 'hostctl';
4683
+
4684
+ async function run(context) {
4685
+ const { params, log } = context;
4686
+
4687
+ const message = params.message || 'Hello from your new hostctl task!';
4688
+ log('info', message);
4689
+
4690
+ return {
4691
+ success: true,
4692
+ message: message
4693
+ };
4694
+ }
4695
+
4696
+ export default task(run, 'A sample hostctl task');
4697
+ `;
4698
+ var sampleTaskTsTemplate = (packageName) => {
4699
+ const sanitizedName = packageName.replace(/[^a-zA-Z0-9]/g, "").replace(/^[0-9]/, "_$&");
4700
+ const taskName = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
4701
+ return `import { task, type TaskContext } from 'hostctl';
4702
+
4703
+ export interface ${taskName}SampleParams {
4704
+ name: string;
4705
+ greeting?: string;
4706
+ }
4707
+
4708
+ export interface ${taskName}SampleResult {
4709
+ success: boolean;
4710
+ greeting: string;
4711
+ }
4712
+
4713
+ async function run(context: TaskContext<${taskName}SampleParams>): Promise<${taskName}SampleResult> {
4714
+ const { params, log } = context;
4715
+
4716
+ const greeting = params.greeting || 'Hello';
4717
+ const message = \`\${greeting}, \${params.name}!\`;
4718
+
4719
+ log('info', message);
4720
+
4721
+ return {
4722
+ success: true,
4723
+ greeting: message
4724
+ };
4725
+ }
4726
+
4727
+ export default task(run, 'Greets {{name}} with {{greeting}}');
4728
+ `;
4729
+ };
4730
+ var sampleTaskJsTemplate = `import { task } from 'hostctl';
4731
+
4732
+ async function run(context) {
4733
+ const { params, log } = context;
4734
+
4735
+ const greeting = params.greeting || 'Hello';
4736
+ const message = \`\${greeting}, \${params.name}!\`;
4737
+
4738
+ log('info', message);
4739
+
4740
+ return {
4741
+ success: true,
4742
+ greeting: message
4743
+ };
4744
+ }
4745
+
4746
+ export default task(run, 'Greets {{name}} with {{greeting}}');
4747
+ `;
4748
+ var readmeTemplate = (packageName) => `# ${packageName}
4749
+
4750
+ This is a hostctl task package.
4751
+
4752
+ ## Usage
4753
+
4754
+ ### Run without installing
4755
+
4756
+ \`\`\`
4757
+ \u276F npx hostctl run https://github.com/yourusername/${packageName} message:Hello
4758
+ \`\`\`
4759
+
4760
+ ### Explicitly install and run
4761
+
4762
+ \`\`\`
4763
+ \u276F npx hostctl install https://github.com/yourusername/${packageName}
4764
+ \u276F npx hostctl run ${packageName} message:Hello
4765
+ \`\`\`
4766
+
4767
+ ## About
4768
+
4769
+ This is a hostctl task package that demonstrates the basic structure for creating reusable tasks.
4770
+ `;
4771
+ var gitignoreTemplate = `node_modules/
4772
+ dist/
4773
+ *.log
4774
+ .DS_Store
4775
+ .env
4776
+ `;
4777
+ async function createPackage(packageName, options) {
4778
+ const packageDir = path3.join(process.cwd(), packageName);
4779
+ await fs7.mkdir(packageDir, { recursive: true });
4780
+ if (options.lang === "typescript") {
4781
+ await fs7.writeFile(path3.join(packageDir, "package.json"), packageJsonTsTemplate(packageName));
4782
+ await fs7.writeFile(path3.join(packageDir, "tsconfig.json"), tsconfigTemplate);
4783
+ await fs7.writeFile(path3.join(packageDir, "index.ts"), indexTsTemplate(packageName));
4784
+ await fs7.writeFile(path3.join(packageDir, "sample-task.ts"), sampleTaskTsTemplate(packageName));
4785
+ await fs7.writeFile(path3.join(packageDir, "README.md"), readmeTemplate(packageName));
4786
+ await fs7.writeFile(path3.join(packageDir, ".gitignore"), gitignoreTemplate);
4787
+ } else {
4788
+ await fs7.writeFile(path3.join(packageDir, "package.json"), packageJsonJsTemplate(packageName));
4789
+ await fs7.writeFile(path3.join(packageDir, "index.js"), indexJsTemplate);
4790
+ await fs7.writeFile(path3.join(packageDir, "sample-task.js"), sampleTaskJsTemplate);
4791
+ await fs7.writeFile(path3.join(packageDir, "README.md"), readmeTemplate(packageName));
4792
+ await fs7.writeFile(path3.join(packageDir, ".gitignore"), gitignoreTemplate);
4793
+ }
4794
+ console.log(`Created new hostctl package '${packageName}' at ${packageDir}`);
4795
+ console.log(`
4796
+ Next steps:`);
4797
+ console.log(`1. cd ${packageName}`);
4798
+ console.log(`2. npm install`);
4799
+ if (options.lang === "typescript") {
4800
+ console.log(`3. Edit index.ts and sample-task.ts to implement your tasks`);
4801
+ } else {
4802
+ console.log(`3. Edit index.js and sample-task.js to implement your tasks`);
4803
+ }
4804
+ console.log(`4. Test your package with: npx hostctl run .`);
4805
+ }
4806
+
4807
+ // src/commands/pkg/package-manager.ts
4808
+ import { promises as fs8 } from "fs";
4809
+ import { simpleGit } from "simple-git";
4810
+ import filenamify from "filenamify";
4811
+ var PackageManager = class {
4812
+ constructor(app) {
4813
+ this.app = app;
4814
+ this.manifestPath = this.app.packagesDir().join("manifest.json");
4815
+ this.manifest = { packages: {}, version: "1.0" };
4816
+ }
4817
+ manifestPath;
4818
+ manifest;
4819
+ async loadManifest() {
4820
+ try {
4821
+ if (await this.manifestPath.exists()) {
4822
+ const content = await fs8.readFile(this.manifestPath.toString(), "utf-8");
4823
+ this.manifest = JSON.parse(content);
4824
+ }
4825
+ } catch (error) {
4826
+ this.manifest = { packages: {}, version: "1.0" };
4827
+ }
4828
+ }
4829
+ async saveManifest() {
4830
+ await fs8.writeFile(this.manifestPath.toString(), JSON.stringify(this.manifest, null, 2));
4831
+ }
4832
+ // Normalize partial git URLs to full https URLs
4833
+ normalizeGitUrl(source) {
4834
+ 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://")) {
4835
+ return source.replace("http://", "https://");
4836
+ }
4837
+ if ((source.includes("github.com") || source.includes("gitlab.com") || source.includes("bitbucket.org")) && !/^\w+:\/\//.test(source) && !source.startsWith("git@") && !source.startsWith("git+ssh://") && !source.startsWith("git+https://")) {
4838
+ return "https://" + source;
4839
+ }
4840
+ return source;
4841
+ }
4842
+ detectSourceType(source) {
4843
+ const normalized = this.normalizeGitUrl(source);
4844
+ if (normalized.startsWith("git@") || normalized.startsWith("http://") || normalized.startsWith("https://") || normalized.startsWith("git+ssh://") || normalized.startsWith("git+https://") || normalized.includes("github.com") || normalized.includes("gitlab.com") || normalized.includes("bitbucket.org")) {
4845
+ return "git";
4846
+ }
4847
+ if (source.startsWith(".") || source.startsWith("/")) {
4848
+ return "local";
4849
+ }
4850
+ return "npm";
4851
+ }
4852
+ async listPackages() {
4853
+ await this.loadManifest();
4854
+ return Object.values(this.manifest.packages);
4855
+ }
4856
+ async discoverTasks(packagePath) {
4857
+ const tasks = [];
4858
+ try {
4859
+ const entries = await fs8.readdir(packagePath.toString(), { withFileTypes: true });
4860
+ for (const entry of entries) {
4861
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
4862
+ if (entry.name === "index.ts" || entry.name === "index.js") {
4863
+ continue;
4864
+ }
4865
+ const taskPath = packagePath.join(entry.name);
4866
+ const taskName = entry.name.replace(/\.(ts|js)$/, "");
4867
+ tasks.push({
4868
+ name: taskName,
4869
+ path: taskPath.toString()
4870
+ });
4871
+ }
4872
+ }
4873
+ const subdirs = ["tasks", "src"];
4874
+ for (const subdir of subdirs) {
4875
+ const subdirPath = packagePath.join(subdir);
4876
+ if (await subdirPath.exists()) {
4877
+ const subEntries = await fs8.readdir(subdirPath.toString(), { withFileTypes: true });
4878
+ for (const entry of subEntries) {
4879
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
4880
+ const taskPath = subdirPath.join(entry.name);
4881
+ const taskName = entry.name.replace(/\.(ts|js)$/, "");
4882
+ tasks.push({
4883
+ name: taskName,
4884
+ path: taskPath.toString()
4885
+ });
4886
+ }
4887
+ }
4888
+ }
4889
+ }
4890
+ } catch (error) {
4891
+ }
4892
+ return tasks.sort((a, b) => a.name.localeCompare(b.name));
4893
+ }
4894
+ /**
4895
+ * Enhanced task resolution that supports dotted notation and npm packages
4896
+ */
4897
+ async resolveTaskPath(packageOrTask, taskName) {
4898
+ await this.loadManifest();
4899
+ const normalizedPackageOrTask = this.normalizeGitUrl(packageOrTask);
4900
+ let packageInfo = this.manifest.packages[packageOrTask] || this.manifest.packages[normalizedPackageOrTask];
4901
+ if (!packageInfo) {
4902
+ const packagesWithName = Object.values(this.manifest.packages).filter(
4903
+ (pkg) => pkg.name === packageOrTask || pkg.name === normalizedPackageOrTask
4904
+ );
4905
+ if (packagesWithName.length === 1) {
4906
+ packageInfo = packagesWithName[0];
4907
+ } else if (packagesWithName.length > 1) {
4908
+ throw new Error(
4909
+ `Multiple packages found with name '${packageOrTask}'. Please use the full source URL to specify which package to run:
4910
+ ` + packagesWithName.map((pkg) => ` - ${pkg.source}`).join("\n")
4911
+ );
4912
+ }
4913
+ }
4914
+ if (packageInfo) {
4915
+ const packagePath = this.app.packagesDir().join(packageInfo.directory);
4916
+ if (taskName) {
4917
+ const taskPath = await this.resolveDottedTask(packagePath, taskName);
4918
+ if (taskPath) {
4919
+ return { packagePath: packagePath.toString(), taskPath };
4920
+ }
4921
+ } else {
4922
+ const defaultTask = await this.findDefaultTask(packagePath);
4923
+ if (defaultTask) {
4924
+ return { packagePath: packagePath.toString(), taskPath: defaultTask };
4925
+ }
4926
+ }
4927
+ }
4928
+ const sourceType = this.detectSourceType(packageOrTask);
4929
+ if (sourceType === "git" || sourceType === "npm") {
4930
+ const packageInfo2 = this.manifest.packages[packageOrTask] || this.manifest.packages[normalizedPackageOrTask];
4931
+ if (packageInfo2) {
4932
+ const packagePath = this.app.packagesDir().join(packageInfo2.directory);
4933
+ if (taskName) {
4934
+ const taskPath = await this.resolveDottedTask(packagePath, taskName);
4935
+ if (taskPath) {
4936
+ return { packagePath: packagePath.toString(), taskPath };
4937
+ }
4938
+ } else {
4939
+ const defaultTask = await this.findDefaultTask(packagePath);
4940
+ if (defaultTask) {
4941
+ return { packagePath: packagePath.toString(), taskPath: defaultTask };
4942
+ }
4943
+ }
4944
+ }
4945
+ }
4946
+ return null;
4947
+ }
4948
+ /**
4949
+ * Resolve a dotted task notation to a file path
4950
+ * e.g., "os.harden.debian" -> "os/harden/debian.ts" or "os/harden/debian.js"
4951
+ */
4952
+ async resolveDottedTask(packagePath, dottedTask) {
4953
+ const taskParts = dottedTask.split(".");
4954
+ const taskFileName = taskParts.pop();
4955
+ const taskDir = taskParts.length > 0 ? taskParts.join("/") : "";
4956
+ const tsPath = taskDir ? packagePath.join(taskDir, `${taskFileName}.ts`) : packagePath.join(`${taskFileName}.ts`);
4957
+ const jsPath = taskDir ? packagePath.join(taskDir, `${taskFileName}.js`) : packagePath.join(`${taskFileName}.js`);
4958
+ if (await tsPath.exists()) {
4959
+ return tsPath.toString();
4960
+ }
4961
+ if (await jsPath.exists()) {
4962
+ return jsPath.toString();
4963
+ }
4964
+ return null;
4965
+ }
4966
+ /**
4967
+ * Check if a package is installed (by name, source, or directory)
4968
+ */
4969
+ async isPackageInstalled(identifier) {
4970
+ await this.loadManifest();
4971
+ const normalizedIdentifier = this.normalizeGitUrl(identifier);
4972
+ return Object.values(this.manifest.packages).some(
4973
+ (pkg) => pkg.name === identifier || pkg.name === normalizedIdentifier || pkg.source === identifier || pkg.source === normalizedIdentifier || pkg.directory === identifier || pkg.directory === normalizedIdentifier
4974
+ );
4975
+ }
4976
+ /**
4977
+ * Get package info by any identifier (name, source, or directory)
4978
+ */
4979
+ async getPackageByIdentifier(identifier) {
4980
+ await this.loadManifest();
4981
+ const normalizedIdentifier = this.normalizeGitUrl(identifier);
4982
+ return Object.values(this.manifest.packages).find(
4983
+ (pkg) => pkg.name === identifier || pkg.name === normalizedIdentifier || pkg.source === identifier || pkg.source === normalizedIdentifier || pkg.directory === identifier || pkg.directory === normalizedIdentifier
4984
+ ) || null;
4985
+ }
4986
+ async findDefaultTask(packagePath) {
4987
+ const defaultFiles = ["index.ts", "index.js", "main.ts", "main.js"];
4988
+ for (const file of defaultFiles) {
4989
+ const filePath = packagePath.join(file);
4990
+ if (await filePath.exists()) {
4991
+ return filePath.toString();
4992
+ }
4993
+ }
4994
+ return null;
4995
+ }
4996
+ async removePackage(packageIdentifier) {
4997
+ try {
4998
+ await this.loadManifest();
4999
+ const normalizedIdentifier = this.normalizeGitUrl(packageIdentifier);
5000
+ const packageToRemove = Object.values(this.manifest.packages).find(
5001
+ (pkg) => pkg.name === packageIdentifier || pkg.name === normalizedIdentifier || pkg.source === packageIdentifier || pkg.source === normalizedIdentifier || pkg.directory === packageIdentifier || pkg.directory === normalizedIdentifier
5002
+ );
5003
+ if (!packageToRemove) {
5004
+ return {
5005
+ success: false,
5006
+ error: `Package '${packageIdentifier}' not found. Use 'hostctl pkg list' to see installed packages.`
5007
+ };
5008
+ }
5009
+ const packagePath = this.app.packagesDir().join(packageToRemove.directory);
5010
+ await fs8.rm(packagePath.toString(), { recursive: true });
5011
+ delete this.manifest.packages[packageToRemove.source];
5012
+ await this.saveManifest();
5013
+ return {
5014
+ success: true,
5015
+ packageInfo: packageToRemove
5016
+ };
5017
+ } catch (error) {
5018
+ return {
5019
+ success: false,
5020
+ error: error instanceof Error ? error.message : String(error)
5021
+ };
5022
+ }
5023
+ }
5024
+ /**
5025
+ * Check if a package name already exists in the manifest
5026
+ */
5027
+ hasPackageWithName(packageName) {
5028
+ return Object.values(this.manifest.packages).some((pkg) => pkg.name === packageName);
5029
+ }
5030
+ /**
5031
+ * Get all packages with the same name
5032
+ */
5033
+ getPackagesWithName(packageName) {
5034
+ return Object.values(this.manifest.packages).filter((pkg) => pkg.name === packageName);
5035
+ }
5036
+ async installPackage(source) {
5037
+ try {
5038
+ await this.loadManifest();
5039
+ const normalizedSource = this.normalizeGitUrl(source);
5040
+ const type = this.detectSourceType(source);
5041
+ const packagesDir = this.app.packagesDir();
5042
+ await fs8.mkdir(packagesDir.toString(), { recursive: true });
5043
+ const packageName = filenamify(normalizedSource, { replacement: "_" });
5044
+ const installDir = packagesDir.join(packageName);
5045
+ const existingPackage = this.manifest.packages[normalizedSource];
5046
+ if (existingPackage) {
5047
+ console.log(`Package '${existingPackage.name}' is already installed from ${normalizedSource}`);
5048
+ return {
5049
+ success: true,
5050
+ packageInfo: existingPackage,
5051
+ installPath: this.app.packagesDir().join(existingPackage.directory).toString()
5052
+ };
5053
+ }
5054
+ switch (type) {
5055
+ case "local": {
5056
+ const sourcePath = Path.new(source);
5057
+ if (!await sourcePath.exists()) {
5058
+ return {
5059
+ success: false,
5060
+ error: `Local path not found: ${source}`,
5061
+ packageInfo: { name: packageName, directory: packageName },
5062
+ installPath: installDir.toString()
5063
+ };
5064
+ }
5065
+ await sourcePath.copy(installDir.toString());
5066
+ break;
5067
+ }
5068
+ case "git": {
5069
+ const git = simpleGit();
5070
+ if (await installDir.exists()) {
5071
+ await fs8.rm(installDir.toString(), { recursive: true, force: true });
5072
+ }
5073
+ await git.clone(normalizedSource, installDir.toString());
5074
+ const nodeRuntime = new NodeRuntime(this.app.tmpDir);
5075
+ await nodeRuntime.npmInstall({ cwd: installDir.toString() });
5076
+ break;
5077
+ }
5078
+ case "npm": {
5079
+ const nodeRuntime = new NodeRuntime(this.app.tmpDir);
5080
+ await nodeRuntime.npm(`install ${source}`, packagesDir.toString());
5081
+ break;
5082
+ }
5083
+ }
5084
+ const packageInfo = await this.getPackageInfo(installDir);
5085
+ const tasks = await this.discoverTasks(installDir);
5086
+ const finalPackageInfo = {
5087
+ ...packageInfo || { name: packageName, directory: packageName },
5088
+ source: normalizedSource,
5089
+ tasks
5090
+ };
5091
+ const actualPackageName = finalPackageInfo.name;
5092
+ if (this.hasPackageWithName(actualPackageName)) {
5093
+ const existingPackages = this.getPackagesWithName(actualPackageName);
5094
+ console.warn(`\u26A0\uFE0F Warning: Package name '${actualPackageName}' already exists.`);
5095
+ console.warn(` Existing packages:`);
5096
+ existingPackages.forEach((pkg) => {
5097
+ console.warn(` - ${pkg.source} (${pkg.directory})`);
5098
+ });
5099
+ console.warn(` You will need to use the full source URL to run tasks from this package.`);
5100
+ }
5101
+ this.manifest.packages[normalizedSource] = finalPackageInfo;
5102
+ await this.saveManifest();
5103
+ return {
5104
+ success: true,
5105
+ packageInfo: finalPackageInfo,
5106
+ installPath: installDir.toString()
5107
+ };
5108
+ } catch (error) {
5109
+ const normalizedSource = this.normalizeGitUrl(source);
5110
+ const packageName = filenamify(normalizedSource, { replacement: "_" });
5111
+ return {
5112
+ success: false,
5113
+ error: error instanceof Error ? error.message : String(error),
5114
+ packageInfo: { name: packageName, directory: packageName },
5115
+ installPath: this.app.packagesDir().join(packageName).toString()
5116
+ };
5117
+ }
5118
+ }
5119
+ async getPackageInfo(packagePath) {
5120
+ try {
5121
+ const packageJsonPath = packagePath.join("package.json");
5122
+ if (await packageJsonPath.exists()) {
5123
+ const packageJsonContent = await fs8.readFile(packageJsonPath.toString(), "utf-8");
5124
+ const packageJson = JSON.parse(packageJsonContent);
5125
+ return {
5126
+ name: packageJson.name || packagePath.basename().toString(),
5127
+ version: packageJson.version,
5128
+ description: packageJson.description,
5129
+ directory: packagePath.basename().toString()
5130
+ };
5131
+ }
5132
+ } catch (error) {
5133
+ }
5134
+ return null;
5135
+ }
5136
+ };
5137
+
5138
+ // src/commands/pkg/install.ts
5139
+ async function installPackage(source, app) {
5140
+ const packageManager = new PackageManager(app);
5141
+ const result = await packageManager.installPackage(source);
5142
+ if (result.success) {
5143
+ console.log(`Installed package '${result.packageInfo.name}' from ${source} to ${result.installPath}`);
5144
+ } else {
5145
+ console.error(`Failed to install package from ${source}: ${result.error}`);
5146
+ throw new Error(result.error);
5147
+ }
5148
+ }
5149
+
5150
+ // src/commands/pkg/list.ts
5151
+ import chalk5 from "chalk";
5152
+ async function listPackages(app) {
5153
+ const packageManager = new PackageManager(app);
5154
+ try {
5155
+ const packages = await packageManager.listPackages();
5156
+ if (packages.length === 0) {
5157
+ console.log("No packages installed.");
5158
+ return;
5159
+ }
5160
+ const packagesByName = /* @__PURE__ */ new Map();
5161
+ packages.forEach((pkg) => {
5162
+ if (!packagesByName.has(pkg.name)) {
5163
+ packagesByName.set(pkg.name, []);
5164
+ }
5165
+ packagesByName.get(pkg.name).push(pkg);
5166
+ });
5167
+ console.log(`Found ${packages.length} package(s):
5168
+ `);
5169
+ packages.forEach((pkg) => {
5170
+ const packagesWithSameName = packagesByName.get(pkg.name);
5171
+ const hasDuplicates = packagesWithSameName.length > 1;
5172
+ let output = `\u2022 ${pkg.name}`;
5173
+ if (pkg.version) {
5174
+ output += ` (v${pkg.version})`;
5175
+ }
5176
+ if (pkg.description) {
5177
+ output += ` - ${pkg.description}`;
5178
+ }
5179
+ if (hasDuplicates) {
5180
+ output = chalk5.yellow(output);
5181
+ output += chalk5.yellow(" \u26A0\uFE0F (duplicate name)");
5182
+ }
5183
+ if (pkg.source) {
5184
+ if (pkg.source.startsWith("http") || pkg.source.startsWith("git@")) {
5185
+ output += `
5186
+ \u{1F4E6} Source: ${pkg.source}`;
5187
+ } else if (!pkg.source.startsWith(".") && !pkg.source.startsWith("/")) {
5188
+ output += `
5189
+ \u{1F4E6} Source: https://www.npmjs.com/package/${pkg.source}`;
5190
+ } else {
5191
+ output += `
5192
+ \u{1F4E6} Source: ${pkg.source}`;
5193
+ }
5194
+ }
5195
+ console.log(output);
5196
+ if (pkg.tasks && pkg.tasks.length > 0) {
5197
+ pkg.tasks.forEach((task2) => {
5198
+ console.log(` \u2514\u2500 ${task2.name}`);
5199
+ });
5200
+ } else {
5201
+ console.log(` \u2514\u2500 (no tasks discovered)`);
5202
+ }
5203
+ console.log("");
5204
+ });
5205
+ const duplicateNames = Array.from(packagesByName.entries()).filter(([_, pkgs]) => pkgs.length > 1).map(([name, _]) => name);
5206
+ if (duplicateNames.length > 0) {
5207
+ console.log(chalk5.yellow("\u26A0\uFE0F Warning: The following package names have duplicates:"));
5208
+ duplicateNames.forEach((name) => {
5209
+ console.log(chalk5.yellow(` - ${name}`));
5210
+ });
5211
+ console.log(chalk5.yellow(" Use the full source URL to run tasks from these packages."));
5212
+ console.log("");
5213
+ }
5214
+ } catch (error) {
5215
+ console.error("Error listing packages:", error);
5216
+ throw error;
5217
+ }
5218
+ }
5219
+
5220
+ // src/commands/pkg/remove.ts
5221
+ async function removePackage(packageIdentifier, app) {
5222
+ const packageManager = new PackageManager(app);
5223
+ const result = await packageManager.removePackage(packageIdentifier);
5224
+ if (result.success && result.packageInfo) {
5225
+ console.log(`Successfully removed package '${result.packageInfo.name}' (${result.packageInfo.directory})`);
5226
+ } else {
5227
+ console.error(`Failed to remove package: ${result.error}`);
5228
+ return;
5229
+ }
5230
+ }
5231
+
4658
5232
  // src/cli.ts
4659
5233
  import JSON5 from "json5";
4660
5234
 
4661
5235
  // src/cli/run-resolver.ts
4662
- import { simpleGit } from "simple-git";
4663
- import filenamify from "filenamify";
4664
- import { match as match6 } from "ts-pattern";
5236
+ import { simpleGit as simpleGit2 } from "simple-git";
5237
+ import filenamify2 from "filenamify";
5238
+ import "ts-pattern";
4665
5239
  import { dirname } from "path";
4666
5240
  import { fileURLToPath } from "url";
4667
- async function resolveScriptPath(app, packageOrBundle, scriptRef, scriptArgs) {
4668
- if (packageOrBundle && packageOrBundle.startsWith("core.") || !packageOrBundle && scriptRef && scriptRef.startsWith("core.")) {
4669
- const name = packageOrBundle?.startsWith("core.") ? packageOrBundle : scriptRef;
4670
- const relPath = name.replace(/^core\./, "").split(".").join(Path.sep());
4671
- const moduleDir = Path.new(dirname(fileURLToPath(import.meta.url)));
4672
- const coreRoot = moduleDir.parent().join("core");
4673
- const candidate = coreRoot.join(`${relPath}.ts`);
4674
- if (!candidate.isFile()) {
4675
- throw new Error(`Core task script not found: ${candidate}`);
4676
- }
4677
- if (packageOrBundle?.startsWith("core.") && scriptRef) {
4678
- scriptArgs = [scriptRef, ...scriptArgs];
4679
- }
4680
- return { scriptRef: candidate.toString(), scriptArgs };
4681
- }
4682
- if (!packageOrBundle) {
4683
- throw new Error("No bundle or package specified.");
4684
- }
4685
- const currentPackageOrBundle = packageOrBundle;
4686
- let resolvedScriptRef = scriptRef ?? "";
4687
- await match6(currentPackageOrBundle).when(
4688
- (path3) => Path.new(path3).isFile() && Path.new(path3).ext() === ".zip",
4689
- async () => {
4690
- const tmpName = filenamify(currentPackageOrBundle);
4691
- const destPath = app.tmpDir.join(tmpName);
4692
- await unzipDirectory(currentPackageOrBundle, destPath.toString());
4693
- resolvedScriptRef = destPath.resolve(resolvedScriptRef).toString();
4694
- }
4695
- ).when(
4696
- (path3) => Path.new(path3).isDirectory(),
4697
- async () => {
4698
- const tmpName = filenamify(currentPackageOrBundle);
4699
- const destPath = app.tmpDir.join(tmpName);
4700
- const pkgPath = Path.new(currentPackageOrBundle);
4701
- await pkgPath.copy(destPath.toString());
4702
- resolvedScriptRef = destPath.resolve(resolvedScriptRef).toString();
4703
- }
4704
- ).when(isValidUrl, async () => {
4705
- const tmpName = filenamify(currentPackageOrBundle);
4706
- const destPath = app.tmpDir.join(tmpName);
4707
- const git = simpleGit();
4708
- await git.clone(currentPackageOrBundle, destPath.toString());
4709
- resolvedScriptRef = destPath.resolve(resolvedScriptRef).toString();
4710
- }).otherwise(async () => {
4711
- if (resolvedScriptRef) {
4712
- A2(scriptArgs).prepend(resolvedScriptRef);
4713
- }
4714
- resolvedScriptRef = currentPackageOrBundle;
4715
- });
4716
- return { scriptRef: resolvedScriptRef, scriptArgs };
5241
+ async function resolveTaskPathAndArgs(app, packageRef, scriptRef, scriptArgs) {
5242
+ app.debug("resolveTaskPathAndArgs", packageRef, scriptRef, scriptArgs);
5243
+ if (scriptRef && scriptRef.includes(":")) {
5244
+ scriptArgs.unshift(scriptRef);
5245
+ scriptRef = void 0;
5246
+ }
5247
+ if (!packageRef) {
5248
+ if (!scriptRef) {
5249
+ throw new Error("No task specified. Please provide a task reference.");
5250
+ }
5251
+ packageRef = scriptRef;
5252
+ scriptRef = void 0;
5253
+ }
5254
+ if (packageRef.startsWith("core.")) {
5255
+ return await resolveCoreTask(packageRef, scriptRef, scriptArgs);
5256
+ }
5257
+ const localResult = await resolveLocalTask(app, packageRef, scriptRef, scriptArgs);
5258
+ if (localResult) {
5259
+ return localResult;
5260
+ }
5261
+ const packageManager = new PackageManager(app);
5262
+ const packageResult = await resolvePackageTask(packageManager, packageRef, scriptRef, scriptArgs);
5263
+ if (packageResult) {
5264
+ return packageResult;
5265
+ }
5266
+ const remoteResult = await resolveRemoteTask(app, packageManager, packageRef, scriptRef, scriptArgs);
5267
+ if (remoteResult) {
5268
+ return remoteResult;
5269
+ }
5270
+ throw new Error(`Could not resolve task: ${packageRef}${scriptRef ? `:${scriptRef}` : ""}`);
4717
5271
  }
4718
- function isValidUrl(url) {
4719
- try {
4720
- new URL(url);
4721
- return true;
4722
- } catch {
5272
+ async function resolveCoreTask(coreTask, scriptRef, scriptArgs) {
5273
+ const relPath = coreTask.replace(/^core\./, "").split(".").join(Path.sep());
5274
+ const moduleDir = Path.new(dirname(fileURLToPath(import.meta.url)));
5275
+ const coreRoot = moduleDir.parent().join("core");
5276
+ const candidate = coreRoot.join(`${relPath}.ts`);
5277
+ if (!candidate.isFile()) {
5278
+ throw new Error(`Core task script not found: ${candidate}`);
5279
+ }
5280
+ if (scriptRef) {
5281
+ A2(scriptArgs).prepend(scriptRef);
5282
+ }
5283
+ return { scriptRef: candidate.toString(), scriptArgs };
5284
+ }
5285
+ async function resolveLocalTask(app, path4, scriptRef, scriptArgs) {
5286
+ if (isGitUrl(path4) || isNpmPackageName(path4)) {
5287
+ return null;
5288
+ }
5289
+ const pathObj = Path.new(path4);
5290
+ if (!pathObj.exists()) {
5291
+ return null;
5292
+ }
5293
+ if (pathObj.isFile() && pathObj.ext() === ".zip") {
5294
+ return await handleZipFile(app, path4, scriptRef, scriptArgs);
5295
+ }
5296
+ if (pathObj.ext() === ".git") {
5297
+ return await handleGitRepo(app, path4, scriptRef, scriptArgs);
5298
+ }
5299
+ if (pathObj.isDirectory()) {
5300
+ return await handleDirectory(pathObj, scriptRef, scriptArgs);
5301
+ }
5302
+ if (pathObj.isFile() && (pathObj.ext() === ".ts" || pathObj.ext() === ".js")) {
5303
+ if (scriptRef) {
5304
+ A2(scriptArgs).prepend(scriptRef);
5305
+ }
5306
+ return { scriptRef: pathObj.toString(), scriptArgs };
5307
+ }
5308
+ return null;
5309
+ }
5310
+ function isGitUrl(str) {
5311
+ return str.startsWith("git@") || str.startsWith("http://") || str.startsWith("https://") || str.includes("github.com") || str.includes("gitlab.com") || str.includes("bitbucket.org");
5312
+ }
5313
+ function isNpmPackageName(str) {
5314
+ if (isGitUrl(str)) {
5315
+ return false;
5316
+ }
5317
+ if (str.startsWith("/") || str.startsWith("\\")) {
5318
+ return false;
5319
+ }
5320
+ if (str.includes("\\")) {
4723
5321
  return false;
4724
5322
  }
5323
+ if (str.includes("/")) {
5324
+ return str.startsWith("@") && str.split("/").length === 2;
5325
+ }
5326
+ return true;
5327
+ }
5328
+ async function resolvePackageTask(packageManager, packageRef, scriptRef, scriptArgs) {
5329
+ const result = await packageManager.resolveTaskPath(packageRef, scriptRef);
5330
+ if (result) {
5331
+ return { scriptRef: result.taskPath, scriptArgs };
5332
+ }
5333
+ return null;
5334
+ }
5335
+ async function resolveRemoteTask(app, packageManager, packageRef, scriptRef, scriptArgs) {
5336
+ const isInstalled = await packageManager.isPackageInstalled(packageRef);
5337
+ if (!isInstalled) {
5338
+ const installResult = await packageManager.installPackage(packageRef);
5339
+ if (!installResult.success) {
5340
+ return null;
5341
+ }
5342
+ }
5343
+ const result = await packageManager.resolveTaskPath(packageRef, scriptRef);
5344
+ if (result) {
5345
+ return { scriptRef: result.taskPath, scriptArgs };
5346
+ }
5347
+ return null;
5348
+ }
5349
+ async function handleZipFile(app, zipPath, scriptRef, scriptArgs) {
5350
+ const tmpName = filenamify2(zipPath, { replacement: "_" });
5351
+ const destPath = app.tmpDir.join(tmpName);
5352
+ await unzipDirectory(zipPath, destPath.toString());
5353
+ if (scriptRef) {
5354
+ const resolvedPath = destPath.resolve(scriptRef);
5355
+ return { scriptRef: resolvedPath.toString(), scriptArgs };
5356
+ }
5357
+ return { scriptRef: destPath.toString(), scriptArgs };
5358
+ }
5359
+ async function handleGitRepo(app, gitUrl, scriptRef, scriptArgs) {
5360
+ const tmpName = filenamify2(gitUrl, { replacement: "_" });
5361
+ const destPath = app.tmpDir.join(tmpName);
5362
+ const git = simpleGit2();
5363
+ await git.clone(gitUrl, destPath.toString());
5364
+ const packageJsonPath = destPath.join("package.json");
5365
+ if (packageJsonPath.exists()) {
5366
+ const nodeRuntime = new NodeRuntime(app.tmpDir);
5367
+ await nodeRuntime.npmInstall({ cwd: destPath.toString() });
5368
+ }
5369
+ if (scriptRef) {
5370
+ const resolvedPath = destPath.resolve(scriptRef);
5371
+ return { scriptRef: resolvedPath.toString(), scriptArgs };
5372
+ }
5373
+ return { scriptRef: destPath.toString(), scriptArgs };
5374
+ }
5375
+ async function handleDirectory(dirPath, scriptRef, scriptArgs) {
5376
+ if (scriptRef) {
5377
+ const taskParts = scriptRef.split(".");
5378
+ const taskFileName = taskParts.pop();
5379
+ const taskDir = taskParts.length > 0 ? taskParts.join("/") : "";
5380
+ const tsPath = taskDir ? dirPath.join(taskDir, `${taskFileName}.ts`) : dirPath.join(`${taskFileName}.ts`);
5381
+ const jsPath = taskDir ? dirPath.join(taskDir, `${taskFileName}.js`) : dirPath.join(`${taskFileName}.js`);
5382
+ if (tsPath.exists()) {
5383
+ return { scriptRef: tsPath.toString(), scriptArgs };
5384
+ }
5385
+ if (jsPath.exists()) {
5386
+ return { scriptRef: jsPath.toString(), scriptArgs };
5387
+ }
5388
+ const resolvedPath = dirPath.resolve(scriptRef);
5389
+ return { scriptRef: resolvedPath.toString(), scriptArgs };
5390
+ }
5391
+ const defaultFiles = ["index.ts", "index.js", "main.ts", "main.js"];
5392
+ for (const file of defaultFiles) {
5393
+ const filePath = dirPath.join(file);
5394
+ if (filePath.exists()) {
5395
+ return { scriptRef: filePath.toString(), scriptArgs };
5396
+ }
5397
+ }
5398
+ throw new Error(`No default entry point found in directory: ${dirPath}`);
4725
5399
  }
4726
5400
 
4727
5401
  // src/cli.ts
@@ -4741,7 +5415,7 @@ var logError = (message, error) => {
4741
5415
  }
4742
5416
  }
4743
5417
  };
4744
- function isValidUrl2(url) {
5418
+ function isValidUrl(url) {
4745
5419
  try {
4746
5420
  new URL(url);
4747
5421
  return true;
@@ -4767,7 +5441,6 @@ var Cli = class {
4767
5441
  increaseVerbosity,
4768
5442
  Verbosity.WARN
4769
5443
  ).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)");
4770
- this.program.command("bundle").alias("b").argument("[path]", `the path to bundle (e.g. hostctl bundle .)`, ".").description("bundle the given path as an npm project").action(this.handleBundle.bind(this));
4771
5444
  this.program.command("exec").alias("e").argument(
4772
5445
  "<command...>",
4773
5446
  `the command string to run, with optional arguments (e.g. hostctl exec sudo sh -c 'echo "$(whoami)"')`
@@ -4777,21 +5450,35 @@ var Cli = class {
4777
5450
  inventoryCmd.command("encrypt").alias("e").description("encrypt the inventory file").action(this.handleInventoryEncrypt.bind(this));
4778
5451
  inventoryCmd.command("decrypt").alias("d").description("decrypt the inventory file").action(this.handleInventoryDecrypt.bind(this));
4779
5452
  inventoryCmd.command("list").alias("ls").description("list the hosts in the inventory file").action(this.handleInventoryList.bind(this));
5453
+ const pkgCmd = this.program.command("pkg").description("manage hostctl packages");
5454
+ 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) => {
5455
+ const options = actionCommand.opts();
5456
+ const supportedLanguages = ["typescript", "javascript"];
5457
+ if (!supportedLanguages.includes(options.lang)) {
5458
+ console.error(
5459
+ `Error: Unsupported language '${options.lang}'. Supported languages are: ${supportedLanguages.join(", ")}`
5460
+ );
5461
+ process4.exit(1);
5462
+ }
5463
+ }).action(this.handlePkgCreate.bind(this));
5464
+ pkgCmd.command("install <package-source>").description("install a hostctl package").action(this.handlePkgInstall.bind(this));
5465
+ pkgCmd.command("list").alias("ls").description("list installed hostctl packages").action(this.handlePkgList.bind(this));
5466
+ pkgCmd.command("remove <package-identifier>").alias("rm").description("remove an installed hostctl package").action(this.handlePkgRemove.bind(this));
4780
5467
  this.program.command("run").alias("r").argument(
4781
- "[package_or_bundle]",
4782
- `the package or bundle to run the specified <script> from.
4783
- The package or bundle may be either:
5468
+ "[package]",
5469
+ `the package to run the specified <script> from.
5470
+ The package may be either:
4784
5471
  - a directory in the local filesystem, e.g. /my/package/foo
4785
5472
  - a git repository, e.g. https://github.com/hostctl/core
4786
- - a zipped package (a bundle), e.g. packagebundle123.zip
5473
+ - an npm package name, e.g. hostctl-hello
4787
5474
  `
4788
5475
  ).argument(
4789
5476
  "[script]",
4790
- `the hostctl script to run (e.g. hostctl run myscript OR hostctl run mypackage myscript).
4791
- The script may be specified as either:
5477
+ `the hostctl task script to run (e.g. hostctl run myscript OR hostctl run mypackage myscript).
5478
+ The task script may be specified as either:
4792
5479
  - a package followed by the script, e.g. hostctl run github.com/hostctl/core echo args:hello,world
4793
- - a script, e.g. hostctl run main.ts
4794
- - a zipped package (a bundle), e.g. hostctl run packagebundle.zip echo args:hello,world`
5480
+ - a task script, e.g. hostctl run main.ts
5481
+ - an npm package followed by a task script, e.g. hostctl run hostctl-hello goodbye name:Phil`
4795
5482
  ).argument("[script arguments...]", "the runtime arguments to the script.").description("run a hostctl script (default)").option(
4796
5483
  "-f, --file <path>",
4797
5484
  "runtime parameters should be read from file at <path> containing json object representing params to supply to the script"
@@ -4802,11 +5489,6 @@ var Cli = class {
4802
5489
  const runtimeCmd = this.program.command("runtime").alias("rt").description("print out a report of the runtime environment").action(this.handleRuntime.bind(this));
4803
5490
  runtimeCmd.command("install").alias("i").description("install a temporary runtime environment on the local host if needed").action(this.handleRuntimeInstall.bind(this));
4804
5491
  }
4805
- async handleBundle(path3, options, cmd) {
4806
- const opts = cmd.optsWithGlobals();
4807
- await this.loadApp(opts);
4808
- this.app.bundleProject(path3);
4809
- }
4810
5492
  async handleInventory(options, cmd) {
4811
5493
  const opts = cmd.optsWithGlobals();
4812
5494
  await this.loadApp(opts);
@@ -4862,11 +5544,24 @@ var Cli = class {
4862
5544
  process4.exitCode = 1;
4863
5545
  }
4864
5546
  }
4865
- async handleRun(packageOrBundle, scriptRef, scriptArgs, options, cmd) {
5547
+ async handleRun(packageRef, scriptRef, scriptArgs, options, cmd) {
4866
5548
  const opts = cmd.optsWithGlobals();
4867
5549
  await this.loadApp(opts);
4868
5550
  this.app.debug(`tags: ${opts.tag}, remote: ${opts.remote}`);
4869
- const resolved = await resolveScriptPath(this.app, packageOrBundle, scriptRef, scriptArgs);
5551
+ let resolved;
5552
+ try {
5553
+ resolved = await resolveTaskPathAndArgs(this.app, packageRef, scriptRef, scriptArgs);
5554
+ } catch (error) {
5555
+ if (error instanceof Error && error.message.includes("Multiple packages found with name")) {
5556
+ console.error("Error:", error.message);
5557
+ console.error("\nTo resolve this, use the full source URL instead of just the package name.");
5558
+ console.error("For example:");
5559
+ console.error(" ./hostctl run <full-source-url> <task-name>");
5560
+ process4.exitCode = 1;
5561
+ return;
5562
+ }
5563
+ throw error;
5564
+ }
4870
5565
  scriptRef = resolved.scriptRef;
4871
5566
  scriptArgs = resolved.scriptArgs;
4872
5567
  let params = {};
@@ -4909,7 +5604,7 @@ var Cli = class {
4909
5604
  if (homeConfigPath.isFile()) {
4910
5605
  return homeConfigPath.toString();
4911
5606
  }
4912
- if (suppliedConfigRef && isValidUrl2(suppliedConfigRef)) {
5607
+ if (suppliedConfigRef && isValidUrl(suppliedConfigRef)) {
4913
5608
  return suppliedConfigRef;
4914
5609
  }
4915
5610
  throw new Error(
@@ -4951,6 +5646,21 @@ var Cli = class {
4951
5646
  await this.loadApp(opts);
4952
5647
  await this.app.installRuntime();
4953
5648
  }
5649
+ async handlePkgCreate(packageName, options) {
5650
+ await createPackage(packageName, options);
5651
+ }
5652
+ async handlePkgInstall(source) {
5653
+ await this.loadApp(this.program.opts());
5654
+ await installPackage(source, this.app);
5655
+ }
5656
+ async handlePkgList() {
5657
+ await this.loadApp(this.program.opts());
5658
+ await listPackages(this.app);
5659
+ }
5660
+ async handlePkgRemove(packageIdentifier) {
5661
+ await this.loadApp(this.program.opts());
5662
+ await removePackage(packageIdentifier, this.app);
5663
+ }
4954
5664
  };
4955
5665
 
4956
5666
  // src/bin/hostctl.ts