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.
- package/README.md +201 -189
- package/dist/bin/hostctl.js +960 -250
- package/dist/bin/hostctl.js.map +1 -1
- package/dist/index.d.ts +158 -72
- package/dist/index.js +4865 -3254
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/hostctl.js
CHANGED
|
@@ -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(
|
|
1100
|
-
this.path =
|
|
1099
|
+
constructor(path4, isWindowsPath = isWindows()) {
|
|
1100
|
+
this.path = path4;
|
|
1101
1101
|
this.isWindowsPath = isWindowsPath;
|
|
1102
1102
|
}
|
|
1103
|
-
static new(
|
|
1104
|
-
if (
|
|
1105
|
-
return
|
|
1103
|
+
static new(path4, isWindowsPath = isWindows()) {
|
|
1104
|
+
if (path4 instanceof _Path) {
|
|
1105
|
+
return path4;
|
|
1106
1106
|
}
|
|
1107
|
-
return new _Path(
|
|
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(
|
|
1148
|
-
return new _Path(
|
|
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((
|
|
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
|
|
1235
|
+
let path4 = this.absolute();
|
|
1236
1236
|
Range.new(1, count).each((i) => {
|
|
1237
|
-
|
|
1237
|
+
path4 = path4.resolve("..");
|
|
1238
1238
|
});
|
|
1239
|
-
return
|
|
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(
|
|
1778
|
-
return existsSync2(
|
|
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(
|
|
1790
|
+
static basename(path4, suffix) {
|
|
1791
1791
|
if (isWindows()) {
|
|
1792
|
-
return win322.basename(
|
|
1792
|
+
return win322.basename(path4, suffix);
|
|
1793
1793
|
} else {
|
|
1794
|
-
return posix2.basename(
|
|
1794
|
+
return posix2.basename(path4, suffix);
|
|
1795
1795
|
}
|
|
1796
1796
|
}
|
|
1797
|
-
static exists(
|
|
1798
|
-
return exists(
|
|
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(
|
|
1808
|
-
return readFileSync2(
|
|
1807
|
+
static readSync(path4) {
|
|
1808
|
+
return readFileSync2(path4, {
|
|
1809
1809
|
encoding: "utf8"
|
|
1810
1810
|
});
|
|
1811
1811
|
}
|
|
1812
|
-
static async readAsync(
|
|
1813
|
-
return await readFile2(
|
|
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
|
|
1824
|
-
this._instance ??= new _TmpFileRegistry(
|
|
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
|
|
1844
|
-
fs.mkdirSync(
|
|
1845
|
-
this.registerTempFileOrDir(
|
|
1846
|
-
return
|
|
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
|
|
1851
|
-
fs.writeFileSync(
|
|
1852
|
-
this.registerTempFileOrDir(
|
|
1853
|
-
return
|
|
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(
|
|
1859
|
-
this.tempFilePaths.push(
|
|
1858
|
+
registerTempFileOrDir(path4) {
|
|
1859
|
+
this.tempFilePaths.push(path4);
|
|
1860
1860
|
}
|
|
1861
1861
|
cleanupTempFiles() {
|
|
1862
|
-
this.tempFilePaths.forEach((
|
|
1863
|
-
this.rmFile(
|
|
1862
|
+
this.tempFilePaths.forEach((path4) => {
|
|
1863
|
+
this.rmFile(path4);
|
|
1864
1864
|
});
|
|
1865
1865
|
this.tempFilePaths = [];
|
|
1866
1866
|
}
|
|
1867
|
-
rmFile(
|
|
1867
|
+
rmFile(path4) {
|
|
1868
1868
|
try {
|
|
1869
|
-
fs.rmSync(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
2019
|
-
const contents = fs2.readFileSync(
|
|
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: ${
|
|
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
|
|
2041
|
+
for (const path4 of privateKeyFilePaths) {
|
|
2040
2042
|
try {
|
|
2041
|
-
const identityString = readIdentityStringFromFile(
|
|
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 ${
|
|
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:
|
|
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 (
|
|
2069
|
-
this.identity = this.readIdentityFromFile(
|
|
2070
|
-
this.identityFilePath =
|
|
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(
|
|
2083
|
-
const contents = fs2.readFileSync(
|
|
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: ${
|
|
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(
|
|
2238
|
-
this.path =
|
|
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, [
|
|
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
|
-
|
|
2287
|
+
alias,
|
|
2288
|
+
// Use the key as the alias
|
|
2285
2289
|
new Host(this, {
|
|
2286
|
-
|
|
2287
|
-
// Use
|
|
2288
|
-
|
|
2289
|
-
// The
|
|
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((
|
|
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 (
|
|
2874
|
-
write: async (
|
|
2875
|
-
exists: async (
|
|
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(
|
|
2888
|
+
await fs5.promises.access(path4);
|
|
2878
2889
|
return true;
|
|
2879
2890
|
} catch {
|
|
2880
2891
|
return false;
|
|
2881
2892
|
}
|
|
2882
2893
|
},
|
|
2883
|
-
mkdir: async (
|
|
2884
|
-
await fs5.promises.mkdir(
|
|
2894
|
+
mkdir: async (path4, options) => {
|
|
2895
|
+
await fs5.promises.mkdir(path4, options);
|
|
2885
2896
|
},
|
|
2886
|
-
rm: async (
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3779
|
-
if (
|
|
3780
|
-
return await downloadFile(url,
|
|
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(
|
|
3768
|
+
async npmInstall(options = {}) {
|
|
3769
|
+
const { omitDev = true, cwd, installedPackagesDir } = options;
|
|
3770
|
+
const args = ["install"];
|
|
3790
3771
|
if (omitDev) {
|
|
3791
|
-
|
|
3792
|
-
}
|
|
3793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
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(
|
|
4626
|
-
let p = Path.new(
|
|
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
|
|
4664
|
-
import
|
|
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
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
if (!
|
|
4675
|
-
throw new Error(
|
|
4676
|
-
}
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
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
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
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
|
|
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
|
-
"[
|
|
4782
|
-
`the package
|
|
4783
|
-
The package
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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(
|
|
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
|
-
|
|
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 &&
|
|
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
|