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