hostctl 0.1.45 → 0.1.48
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 +2 -1
- package/dist/bin/hostctl.js +181 -339
- package/dist/bin/hostctl.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +44 -200
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,12 +93,14 @@ npx hostctl run core.echo message:hello
|
|
|
93
93
|
```
|
|
94
94
|
Local directories are executed directly and are not installable via `hostctl pkg install`.
|
|
95
95
|
- **Remote orchestration**
|
|
96
|
+
|
|
96
97
|
```bash
|
|
97
98
|
hostctl run -r -t ubuntu core.net.interfaces --json
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
- `-r/--remote` targets hosts selected by tags via SSH.
|
|
101
102
|
- `-t/--tag` is greedy; use `--` before positional args when needed.
|
|
103
|
+
|
|
102
104
|
- **From npm or git**
|
|
103
105
|
```bash
|
|
104
106
|
hostctl run hostctl-hello greet name:Phil
|
|
@@ -204,7 +206,6 @@ Key `TaskContext` capabilities (see [`docs/task-api.md`](docs/task-api.md) for d
|
|
|
204
206
|
- **Version mismatch**: keep `package.json`, `src/version.ts`, and `jsr.json` in sync before releasing.
|
|
205
207
|
- **SSH failures**: verify inventory entries (hostname, user, auth) and only pass `-r` when you intend remote execution.
|
|
206
208
|
- **Secrets**: when inventories are encrypted, ensure `AGE_IDS` includes the private keys so `hostctl` can decrypt secrets.
|
|
207
|
-
- **Environment drift**: run `hostctl runtime` to inspect prerequisites or `hostctl runtime install` to bootstrap them.
|
|
208
209
|
|
|
209
210
|
## Developer workflow
|
|
210
211
|
|
package/dist/bin/hostctl.js
CHANGED
|
@@ -3886,11 +3886,168 @@ var LocalRuntime = class {
|
|
|
3886
3886
|
}
|
|
3887
3887
|
};
|
|
3888
3888
|
|
|
3889
|
+
// src/zip.ts
|
|
3890
|
+
import AdmZip from "adm-zip";
|
|
3891
|
+
|
|
3892
|
+
// src/hash.ts
|
|
3893
|
+
import { createHash } from "crypto";
|
|
3894
|
+
|
|
3895
|
+
// src/param-map.ts
|
|
3896
|
+
import { match as match2 } from "ts-pattern";
|
|
3897
|
+
var ParamMap = class _ParamMap {
|
|
3898
|
+
/**
|
|
3899
|
+
* Parses CLI arguments of the form key:value, only supporting top-level keys.
|
|
3900
|
+
* Only the first colon is treated as a key/value separator; all subsequent colons are part of the value.
|
|
3901
|
+
* This means nested keys are not supported, but values can contain colons (e.g., URLs, JSON, etc.).
|
|
3902
|
+
*/
|
|
3903
|
+
static parse(cliArguments) {
|
|
3904
|
+
const root = new _ParamMap();
|
|
3905
|
+
A2(cliArguments).each((arg) => {
|
|
3906
|
+
const idx = arg.indexOf(":");
|
|
3907
|
+
if (idx === -1) {
|
|
3908
|
+
root.set([arg], "true");
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
const key = arg.slice(0, idx);
|
|
3912
|
+
const value = arg.slice(idx + 1);
|
|
3913
|
+
root.set([key], value);
|
|
3914
|
+
});
|
|
3915
|
+
return root;
|
|
3916
|
+
}
|
|
3917
|
+
map;
|
|
3918
|
+
constructor() {
|
|
3919
|
+
this.map = /* @__PURE__ */ new Map();
|
|
3920
|
+
}
|
|
3921
|
+
get(keyPath) {
|
|
3922
|
+
const keys3 = keyPath.split(":");
|
|
3923
|
+
let node = this;
|
|
3924
|
+
for (let i = 0; i < keys3.length; i++) {
|
|
3925
|
+
if (!(node instanceof _ParamMap)) return void 0;
|
|
3926
|
+
node = node.map.get(keys3[i]);
|
|
3927
|
+
if (node === void 0) return void 0;
|
|
3928
|
+
}
|
|
3929
|
+
if (node instanceof _ParamMap) {
|
|
3930
|
+
return node.toObject();
|
|
3931
|
+
}
|
|
3932
|
+
return this.stringToJsonObj(node);
|
|
3933
|
+
}
|
|
3934
|
+
set(keyPath, value) {
|
|
3935
|
+
const keyParts = Array.isArray(keyPath) ? keyPath : keyPath.split(":");
|
|
3936
|
+
let node = this;
|
|
3937
|
+
for (let i = 0; i < keyParts.length - 1; i++) {
|
|
3938
|
+
const key = keyParts[i];
|
|
3939
|
+
let nextNode = node.map.get(key);
|
|
3940
|
+
if (!(nextNode instanceof _ParamMap)) {
|
|
3941
|
+
nextNode = new _ParamMap();
|
|
3942
|
+
node.map.set(key, nextNode);
|
|
3943
|
+
}
|
|
3944
|
+
node = nextNode;
|
|
3945
|
+
}
|
|
3946
|
+
const lastKey = keyParts[keyParts.length - 1];
|
|
3947
|
+
node.map.set(lastKey, value);
|
|
3948
|
+
}
|
|
3949
|
+
toObject() {
|
|
3950
|
+
let obj = {};
|
|
3951
|
+
M(this.map).each(([k, v]) => {
|
|
3952
|
+
obj[k] = v instanceof _ParamMap ? v.toObject() : this.stringToJsonObj(v);
|
|
3953
|
+
});
|
|
3954
|
+
return obj;
|
|
3955
|
+
}
|
|
3956
|
+
stringToJsonObj(str) {
|
|
3957
|
+
if (typeof str !== "string") return str;
|
|
3958
|
+
const trimmed = str.trim();
|
|
3959
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
3960
|
+
try {
|
|
3961
|
+
return JSON.parse(trimmed);
|
|
3962
|
+
} catch {
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
return match2(str).when(matches(/,/), (s) => {
|
|
3966
|
+
return VP(s.split(",")).map((v) => v.trim()).compact([""]).map((v) => this.stringToJsonObj(v)).value;
|
|
3967
|
+
}).when(isNumeric, (s) => parseFloat(s)).when(
|
|
3968
|
+
(s) => s.trim() === "",
|
|
3969
|
+
() => ""
|
|
3970
|
+
).otherwise(() => str);
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3973
|
+
|
|
3974
|
+
// src/runtime.ts
|
|
3975
|
+
import * as z from "zod";
|
|
3976
|
+
|
|
3977
|
+
// src/version.ts
|
|
3978
|
+
var version = "0.1.48";
|
|
3979
|
+
|
|
3980
|
+
// src/app.ts
|
|
3981
|
+
import { retryUntilDefined } from "ts-retry";
|
|
3982
|
+
import { Mutex as Mutex3 } from "async-mutex";
|
|
3983
|
+
import { match as match5 } from "ts-pattern";
|
|
3984
|
+
|
|
3985
|
+
// src/core/remote/runAllRemote.ts
|
|
3986
|
+
var RunParamsSchema = z.object({
|
|
3987
|
+
taskFn: z.custom((value) => typeof value === "function", {
|
|
3988
|
+
message: "taskFn must be a function"
|
|
3989
|
+
}),
|
|
3990
|
+
params: z.any()
|
|
3991
|
+
});
|
|
3992
|
+
var RunResultSchema = z.record(z.string(), z.any());
|
|
3993
|
+
function serializeError(value) {
|
|
3994
|
+
if (value instanceof Error) {
|
|
3995
|
+
const err = value;
|
|
3996
|
+
return {
|
|
3997
|
+
error: err.message,
|
|
3998
|
+
name: err.name,
|
|
3999
|
+
code: err.code,
|
|
4000
|
+
stack: err.stack
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
if (value && typeof value === "object") {
|
|
4004
|
+
return { error: JSON.stringify(value) };
|
|
4005
|
+
}
|
|
4006
|
+
return { error: String(value) };
|
|
4007
|
+
}
|
|
4008
|
+
async function run(context) {
|
|
4009
|
+
const { params, ssh } = context;
|
|
4010
|
+
const { taskFn, params: taskParams } = params;
|
|
4011
|
+
const remoteResults = await ssh([], async (remoteContext) => {
|
|
4012
|
+
const hostIdentifier = remoteContext.host?.alias || remoteContext.host?.shortName || "unknown_remote_host";
|
|
4013
|
+
remoteContext.debug(`[${hostIdentifier}] Connected via SSH.`);
|
|
4014
|
+
try {
|
|
4015
|
+
const result = await remoteContext.run(taskFn(taskParams));
|
|
4016
|
+
remoteContext.debug(`[${hostIdentifier}] Remote task result:`, JSON.stringify(result));
|
|
4017
|
+
if (result instanceof Error) {
|
|
4018
|
+
remoteContext.warn?.(
|
|
4019
|
+
`[${hostIdentifier}] Remote task returned an Error instance. Converting to serializable object.`
|
|
4020
|
+
);
|
|
4021
|
+
return serializeError(result);
|
|
4022
|
+
}
|
|
4023
|
+
return result;
|
|
4024
|
+
} catch (e) {
|
|
4025
|
+
remoteContext.debug(`[${hostIdentifier}] Failed to run remote task:`, e.message);
|
|
4026
|
+
return serializeError(e);
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
const normalizedEntries = Object.entries(remoteResults ?? {}).map(([hostAlias, value]) => {
|
|
4030
|
+
if (value instanceof Error) {
|
|
4031
|
+
return [hostAlias, serializeError(value)];
|
|
4032
|
+
}
|
|
4033
|
+
return [hostAlias, value];
|
|
4034
|
+
});
|
|
4035
|
+
return Object.fromEntries(normalizedEntries);
|
|
4036
|
+
}
|
|
4037
|
+
var runAllRemote_default = task(run, {
|
|
4038
|
+
description: "run a task on all selected hosts",
|
|
4039
|
+
inputSchema: RunParamsSchema,
|
|
4040
|
+
outputSchema: RunResultSchema
|
|
4041
|
+
});
|
|
4042
|
+
|
|
4043
|
+
// src/commands/pkg/package-manager.ts
|
|
4044
|
+
import { promises as fs6 } from "fs";
|
|
4045
|
+
|
|
3889
4046
|
// src/node-runtime.ts
|
|
3890
4047
|
import os3 from "os";
|
|
3891
4048
|
import axios2 from "axios";
|
|
3892
4049
|
import * as cheerio from "cheerio";
|
|
3893
|
-
import { match as
|
|
4050
|
+
import { match as match4 } from "ts-pattern";
|
|
3894
4051
|
import which from "which";
|
|
3895
4052
|
|
|
3896
4053
|
// src/http.ts
|
|
@@ -3935,14 +4092,14 @@ async function downloadFile(url, dest) {
|
|
|
3935
4092
|
import decompress from "decompress";
|
|
3936
4093
|
import decompressTarGzPlugin from "decompress-targz";
|
|
3937
4094
|
import decompressZipPlugin from "decompress-unzip";
|
|
3938
|
-
import { match as
|
|
4095
|
+
import { match as match3, P as P2 } from "ts-pattern";
|
|
3939
4096
|
import decompressTarPlugin from "decompress-tar";
|
|
3940
4097
|
import { fileTypeFromBuffer } from "file-type";
|
|
3941
4098
|
import { getStreamAsBuffer } from "get-stream";
|
|
3942
4099
|
import xzdecompress from "xz-decompress";
|
|
3943
4100
|
async function unarchive(inputPath, outputPath) {
|
|
3944
4101
|
const filename = File.basename(inputPath);
|
|
3945
|
-
return await
|
|
4102
|
+
return await match3(filename).with(P2.string.regex(/.tar.xz$/), () => decompressTarXz(inputPath, outputPath)).with(P2.string.regex(/.tar.gz$/), () => decompressTarGz(inputPath, outputPath)).with(P2.string.regex(/.zip$/), () => decompressZip(inputPath, outputPath)).otherwise(() => {
|
|
3946
4103
|
throw new Error(`unable to decompress unknown file type: ${inputPath}`);
|
|
3947
4104
|
});
|
|
3948
4105
|
}
|
|
@@ -4057,8 +4214,8 @@ var NodeRuntime = class _NodeRuntime {
|
|
|
4057
4214
|
}
|
|
4058
4215
|
// returns the path to the downloaded zip file
|
|
4059
4216
|
async downloadNodePackage(platform2, arch) {
|
|
4060
|
-
const platformInFilename =
|
|
4061
|
-
const archInFilename =
|
|
4217
|
+
const platformInFilename = match4(platform2).with("linux", () => "linux").with("win32", () => "win").with("darwin", () => "darwin").otherwise(() => "unknown-platform");
|
|
4218
|
+
const archInFilename = match4(arch).with("x64", () => "x64").with("x86", () => "x86").with("arm", () => "armv7l").with("arm64", () => "arm64").otherwise(() => "unknown-arch");
|
|
4062
4219
|
const packages = await this.listPackages();
|
|
4063
4220
|
const url = A2(packages).find((url2) => url2.match(`node-v.*-${platformInFilename}-${archInFilename}`));
|
|
4064
4221
|
if (V.isAbsent(url)) {
|
|
@@ -4122,162 +4279,7 @@ var NodeRuntime = class _NodeRuntime {
|
|
|
4122
4279
|
}
|
|
4123
4280
|
};
|
|
4124
4281
|
|
|
4125
|
-
// src/zip.ts
|
|
4126
|
-
import AdmZip from "adm-zip";
|
|
4127
|
-
|
|
4128
|
-
// src/hash.ts
|
|
4129
|
-
import { createHash } from "crypto";
|
|
4130
|
-
|
|
4131
|
-
// src/param-map.ts
|
|
4132
|
-
import { match as match4 } from "ts-pattern";
|
|
4133
|
-
var ParamMap = class _ParamMap {
|
|
4134
|
-
/**
|
|
4135
|
-
* Parses CLI arguments of the form key:value, only supporting top-level keys.
|
|
4136
|
-
* Only the first colon is treated as a key/value separator; all subsequent colons are part of the value.
|
|
4137
|
-
* This means nested keys are not supported, but values can contain colons (e.g., URLs, JSON, etc.).
|
|
4138
|
-
*/
|
|
4139
|
-
static parse(cliArguments) {
|
|
4140
|
-
const root = new _ParamMap();
|
|
4141
|
-
A2(cliArguments).each((arg) => {
|
|
4142
|
-
const idx = arg.indexOf(":");
|
|
4143
|
-
if (idx === -1) {
|
|
4144
|
-
root.set([arg], "true");
|
|
4145
|
-
return;
|
|
4146
|
-
}
|
|
4147
|
-
const key = arg.slice(0, idx);
|
|
4148
|
-
const value = arg.slice(idx + 1);
|
|
4149
|
-
root.set([key], value);
|
|
4150
|
-
});
|
|
4151
|
-
return root;
|
|
4152
|
-
}
|
|
4153
|
-
map;
|
|
4154
|
-
constructor() {
|
|
4155
|
-
this.map = /* @__PURE__ */ new Map();
|
|
4156
|
-
}
|
|
4157
|
-
get(keyPath) {
|
|
4158
|
-
const keys3 = keyPath.split(":");
|
|
4159
|
-
let node = this;
|
|
4160
|
-
for (let i = 0; i < keys3.length; i++) {
|
|
4161
|
-
if (!(node instanceof _ParamMap)) return void 0;
|
|
4162
|
-
node = node.map.get(keys3[i]);
|
|
4163
|
-
if (node === void 0) return void 0;
|
|
4164
|
-
}
|
|
4165
|
-
if (node instanceof _ParamMap) {
|
|
4166
|
-
return node.toObject();
|
|
4167
|
-
}
|
|
4168
|
-
return this.stringToJsonObj(node);
|
|
4169
|
-
}
|
|
4170
|
-
set(keyPath, value) {
|
|
4171
|
-
const keyParts = Array.isArray(keyPath) ? keyPath : keyPath.split(":");
|
|
4172
|
-
let node = this;
|
|
4173
|
-
for (let i = 0; i < keyParts.length - 1; i++) {
|
|
4174
|
-
const key = keyParts[i];
|
|
4175
|
-
let nextNode = node.map.get(key);
|
|
4176
|
-
if (!(nextNode instanceof _ParamMap)) {
|
|
4177
|
-
nextNode = new _ParamMap();
|
|
4178
|
-
node.map.set(key, nextNode);
|
|
4179
|
-
}
|
|
4180
|
-
node = nextNode;
|
|
4181
|
-
}
|
|
4182
|
-
const lastKey = keyParts[keyParts.length - 1];
|
|
4183
|
-
node.map.set(lastKey, value);
|
|
4184
|
-
}
|
|
4185
|
-
toObject() {
|
|
4186
|
-
let obj = {};
|
|
4187
|
-
M(this.map).each(([k, v]) => {
|
|
4188
|
-
obj[k] = v instanceof _ParamMap ? v.toObject() : this.stringToJsonObj(v);
|
|
4189
|
-
});
|
|
4190
|
-
return obj;
|
|
4191
|
-
}
|
|
4192
|
-
stringToJsonObj(str) {
|
|
4193
|
-
if (typeof str !== "string") return str;
|
|
4194
|
-
const trimmed = str.trim();
|
|
4195
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
4196
|
-
try {
|
|
4197
|
-
return JSON.parse(trimmed);
|
|
4198
|
-
} catch {
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
|
-
return match4(str).when(matches(/,/), (s) => {
|
|
4202
|
-
return VP(s.split(",")).map((v) => v.trim()).compact([""]).map((v) => this.stringToJsonObj(v)).value;
|
|
4203
|
-
}).when(isNumeric, (s) => parseFloat(s)).when(
|
|
4204
|
-
(s) => s.trim() === "",
|
|
4205
|
-
() => ""
|
|
4206
|
-
).otherwise(() => str);
|
|
4207
|
-
}
|
|
4208
|
-
};
|
|
4209
|
-
|
|
4210
|
-
// src/runtime.ts
|
|
4211
|
-
import * as z from "zod";
|
|
4212
|
-
|
|
4213
|
-
// src/version.ts
|
|
4214
|
-
var version = "0.1.45";
|
|
4215
|
-
|
|
4216
|
-
// src/app.ts
|
|
4217
|
-
import { retryUntilDefined } from "ts-retry";
|
|
4218
|
-
import { Mutex as Mutex3 } from "async-mutex";
|
|
4219
|
-
import { match as match5 } from "ts-pattern";
|
|
4220
|
-
|
|
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());
|
|
4229
|
-
function serializeError(value) {
|
|
4230
|
-
if (value instanceof Error) {
|
|
4231
|
-
const err = value;
|
|
4232
|
-
return {
|
|
4233
|
-
error: err.message,
|
|
4234
|
-
name: err.name,
|
|
4235
|
-
code: err.code,
|
|
4236
|
-
stack: err.stack
|
|
4237
|
-
};
|
|
4238
|
-
}
|
|
4239
|
-
if (value && typeof value === "object") {
|
|
4240
|
-
return { error: JSON.stringify(value) };
|
|
4241
|
-
}
|
|
4242
|
-
return { error: String(value) };
|
|
4243
|
-
}
|
|
4244
|
-
async function run(context) {
|
|
4245
|
-
const { params, ssh } = context;
|
|
4246
|
-
const { taskFn, params: taskParams } = params;
|
|
4247
|
-
const remoteResults = await ssh([], async (remoteContext) => {
|
|
4248
|
-
const hostIdentifier = remoteContext.host?.alias || remoteContext.host?.shortName || "unknown_remote_host";
|
|
4249
|
-
remoteContext.debug(`[${hostIdentifier}] Connected via SSH.`);
|
|
4250
|
-
try {
|
|
4251
|
-
const result = await remoteContext.run(taskFn(taskParams));
|
|
4252
|
-
remoteContext.debug(`[${hostIdentifier}] Remote task result:`, JSON.stringify(result));
|
|
4253
|
-
if (result instanceof Error) {
|
|
4254
|
-
remoteContext.warn?.(
|
|
4255
|
-
`[${hostIdentifier}] Remote task returned an Error instance. Converting to serializable object.`
|
|
4256
|
-
);
|
|
4257
|
-
return serializeError(result);
|
|
4258
|
-
}
|
|
4259
|
-
return result;
|
|
4260
|
-
} catch (e) {
|
|
4261
|
-
remoteContext.debug(`[${hostIdentifier}] Failed to run remote task:`, e.message);
|
|
4262
|
-
return serializeError(e);
|
|
4263
|
-
}
|
|
4264
|
-
});
|
|
4265
|
-
const normalizedEntries = Object.entries(remoteResults ?? {}).map(([hostAlias, value]) => {
|
|
4266
|
-
if (value instanceof Error) {
|
|
4267
|
-
return [hostAlias, serializeError(value)];
|
|
4268
|
-
}
|
|
4269
|
-
return [hostAlias, value];
|
|
4270
|
-
});
|
|
4271
|
-
return Object.fromEntries(normalizedEntries);
|
|
4272
|
-
}
|
|
4273
|
-
var runAllRemote_default = task(run, {
|
|
4274
|
-
description: "run a task on all selected hosts",
|
|
4275
|
-
inputSchema: RunParamsSchema,
|
|
4276
|
-
outputSchema: RunResultSchema
|
|
4277
|
-
});
|
|
4278
|
-
|
|
4279
4282
|
// src/commands/pkg/package-manager.ts
|
|
4280
|
-
import { promises as fs6 } from "fs";
|
|
4281
4283
|
import filenamify from "filenamify";
|
|
4282
4284
|
var PackageManager = class {
|
|
4283
4285
|
constructor(app) {
|
|
@@ -4628,15 +4630,24 @@ var PackageManager = class {
|
|
|
4628
4630
|
}
|
|
4629
4631
|
async ensurePackagesDirHasPackageJson(packagesDir) {
|
|
4630
4632
|
const packageJsonPath = packagesDir.join("package.json");
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
+
const hostctlOverride = process.env.HOSTCTL_PKG_HOSTCTL_OVERRIDE;
|
|
4634
|
+
const overrideValue = hostctlOverride ? hostctlOverride.startsWith("file:") ? hostctlOverride : `file:${Path.new(hostctlOverride).absolute().toString()}` : void 0;
|
|
4635
|
+
let packageJson;
|
|
4636
|
+
if (await packageJsonPath.exists()) {
|
|
4637
|
+
const raw = await fs6.readFile(packageJsonPath.toString(), "utf-8");
|
|
4638
|
+
packageJson = JSON.parse(raw);
|
|
4639
|
+
} else {
|
|
4640
|
+
packageJson = {
|
|
4633
4641
|
name: "hostctl-packages",
|
|
4634
4642
|
version: "1.0.0",
|
|
4635
4643
|
description: "Hostctl package management directory",
|
|
4636
4644
|
private: true
|
|
4637
4645
|
};
|
|
4638
|
-
await fs6.writeFile(packageJsonPath.toString(), JSON.stringify(packageJson, null, 2));
|
|
4639
4646
|
}
|
|
4647
|
+
if (overrideValue) {
|
|
4648
|
+
packageJson.overrides = { ...packageJson.overrides ?? {}, hostctl: overrideValue };
|
|
4649
|
+
}
|
|
4650
|
+
await fs6.writeFile(packageJsonPath.toString(), JSON.stringify(packageJson, null, 2));
|
|
4640
4651
|
}
|
|
4641
4652
|
// Scan node_modules for the real installed package (by name or repo match)
|
|
4642
4653
|
async findRealInstalledNpmPackagePath(packagesDir, source) {
|
|
@@ -6014,15 +6025,6 @@ ${successfullyUsedIdentityPaths}`);
|
|
|
6014
6025
|
p = p.parent();
|
|
6015
6026
|
}
|
|
6016
6027
|
}
|
|
6017
|
-
async printRuntimeReport() {
|
|
6018
|
-
const nodeRuntime = new NodeRuntime(this.tmpDir);
|
|
6019
|
-
const reportObj = {
|
|
6020
|
-
nodePath: await nodeRuntime.nodePath(),
|
|
6021
|
-
npmPath: await nodeRuntime.npmPath()
|
|
6022
|
-
// summary: report?.getReport(),
|
|
6023
|
-
};
|
|
6024
|
-
this.info(reportObj);
|
|
6025
|
-
}
|
|
6026
6028
|
async promptPasswordInteractively(message = "Enter your password") {
|
|
6027
6029
|
if (this.providedPassword !== void 0) {
|
|
6028
6030
|
return this.providedPassword;
|
|
@@ -6041,11 +6043,6 @@ ${successfullyUsedIdentityPaths}`);
|
|
|
6041
6043
|
this.providedPassword = await promptPassword({ message });
|
|
6042
6044
|
return this.providedPassword;
|
|
6043
6045
|
}
|
|
6044
|
-
async installRuntime() {
|
|
6045
|
-
this.info(`creating node runtime with ${this.tmpDir.toString()}`);
|
|
6046
|
-
const nodeRuntime = new NodeRuntime(this.tmpDir);
|
|
6047
|
-
await nodeRuntime.installIfNeeded();
|
|
6048
|
-
}
|
|
6049
6046
|
/**
|
|
6050
6047
|
* Executes a command on the selected hosts
|
|
6051
6048
|
*
|
|
@@ -6122,8 +6119,7 @@ var packageJsonTsTemplate = (packageName) => `{
|
|
|
6122
6119
|
"author": "",
|
|
6123
6120
|
"license": "MIT",
|
|
6124
6121
|
"dependencies": {
|
|
6125
|
-
"hostctl": "^0.1.42"
|
|
6126
|
-
"zod": "^4.1.13"
|
|
6122
|
+
"hostctl": "^0.1.42"
|
|
6127
6123
|
},
|
|
6128
6124
|
"devDependencies": {
|
|
6129
6125
|
"typescript": "^5.8.3",
|
|
@@ -6156,8 +6152,7 @@ var packageJsonJsTemplate = (packageName) => `{
|
|
|
6156
6152
|
"author": "",
|
|
6157
6153
|
"license": "MIT",
|
|
6158
6154
|
"dependencies": {
|
|
6159
|
-
"hostctl": "^0.1.42"
|
|
6160
|
-
"zod": "^4.1.13"
|
|
6155
|
+
"hostctl": "^0.1.42"
|
|
6161
6156
|
},
|
|
6162
6157
|
"engines": {
|
|
6163
6158
|
"node": ">=24"
|
|
@@ -6192,157 +6187,21 @@ function registryPrefixFromPackageName(packageName) {
|
|
|
6192
6187
|
const normalized = withoutPrefix.replace(/[^a-zA-Z0-9]+/g, ".").replace(/^\.|\.$/g, "");
|
|
6193
6188
|
return normalized || "example";
|
|
6194
6189
|
}
|
|
6195
|
-
var indexTsTemplate = (registryPrefix) => `import { createRegistry } from '
|
|
6190
|
+
var indexTsTemplate = (registryPrefix) => `import { createRegistry } from 'hostctl';
|
|
6196
6191
|
import hello from './tasks/hello.js';
|
|
6197
6192
|
|
|
6198
6193
|
export { hello };
|
|
6199
6194
|
|
|
6200
6195
|
export const registry = createRegistry().register('${registryPrefix}.hello', hello);
|
|
6201
6196
|
`;
|
|
6202
|
-
var indexJsTemplate = (registryPrefix) => `import { createRegistry } from '
|
|
6197
|
+
var indexJsTemplate = (registryPrefix) => `import { createRegistry } from 'hostctl';
|
|
6203
6198
|
import hello from './tasks/hello.js';
|
|
6204
6199
|
|
|
6205
6200
|
export { hello };
|
|
6206
6201
|
|
|
6207
6202
|
export const registry = createRegistry().register('${registryPrefix}.hello', hello);
|
|
6208
6203
|
`;
|
|
6209
|
-
var
|
|
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;
|
|
6218
|
-
};
|
|
6219
|
-
|
|
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
|
-
}
|
|
6228
|
-
|
|
6229
|
-
export function createRegistry(): TaskRegistry {
|
|
6230
|
-
const entries = new Map<string, TaskFn>();
|
|
6231
|
-
|
|
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
|
-
},
|
|
6264
|
-
};
|
|
6265
|
-
|
|
6266
|
-
return registry;
|
|
6267
|
-
}
|
|
6268
|
-
`;
|
|
6269
|
-
var registryJsTemplate = `export function createRegistry() {
|
|
6270
|
-
const entries = new Map();
|
|
6271
|
-
|
|
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
|
-
}
|
|
6308
|
-
`;
|
|
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';
|
|
6311
|
-
|
|
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;
|
|
6329
|
-
}
|
|
6330
|
-
`;
|
|
6331
|
-
var taskWrapperJsTemplate = `import { task as baseTask } from 'hostctl';
|
|
6332
|
-
|
|
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;
|
|
6342
|
-
}
|
|
6343
|
-
`;
|
|
6344
|
-
var sampleTaskTsTemplate = `import { task, type TaskContext } from '../task.js';
|
|
6345
|
-
import { z } from 'zod';
|
|
6204
|
+
var sampleTaskTsTemplate = `import { task, type TaskContext, z } from 'hostctl';
|
|
6346
6205
|
|
|
6347
6206
|
const HelloInputSchema = z.object({
|
|
6348
6207
|
name: z.string().optional(),
|
|
@@ -6375,8 +6234,7 @@ export default task(run, {
|
|
|
6375
6234
|
outputSchema: HelloOutputSchema,
|
|
6376
6235
|
});
|
|
6377
6236
|
`;
|
|
6378
|
-
var sampleTaskJsTemplate = `import { task } from '
|
|
6379
|
-
import { z } from 'zod';
|
|
6237
|
+
var sampleTaskJsTemplate = `import { task, z } from 'hostctl';
|
|
6380
6238
|
|
|
6381
6239
|
const HelloInputSchema = z.object({
|
|
6382
6240
|
name: z.string().optional(),
|
|
@@ -6422,7 +6280,7 @@ hostctl run . ${registryPrefix}.hello name:Hostctl excited:true
|
|
|
6422
6280
|
- Tasks live under \`src/\` and export a default \`task(...)\`.
|
|
6423
6281
|
- \`src/index.(ts|js)\` re-exports tasks and publishes a registry for discovery.
|
|
6424
6282
|
- Task names are unqualified; the registry assigns qualified names.
|
|
6425
|
-
- Schema helpers come from \`zod\` and are attached to tasks for discovery.
|
|
6283
|
+
- Schema helpers come from \`hostctl\` (re-exported \`zod\`) and are attached to tasks for discovery.
|
|
6426
6284
|
|
|
6427
6285
|
## Publish
|
|
6428
6286
|
|
|
@@ -6460,8 +6318,6 @@ async function createPackage(packageName, options) {
|
|
|
6460
6318
|
await fs9.writeFile(path5.join(packageDir, "tsconfig.json"), tsconfigTemplate);
|
|
6461
6319
|
await fs9.mkdir(path5.join(packageDir, "src", "tasks"), { recursive: true });
|
|
6462
6320
|
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
6321
|
await fs9.writeFile(path5.join(packageDir, "src", "tasks", "hello.ts"), sampleTaskTsTemplate);
|
|
6466
6322
|
await fs9.writeFile(path5.join(packageDir, "README.md"), readmeTemplate(packageJsonName, registryPrefix, true));
|
|
6467
6323
|
await fs9.writeFile(path5.join(packageDir, ".gitignore"), gitignoreTemplate);
|
|
@@ -6469,8 +6325,6 @@ async function createPackage(packageName, options) {
|
|
|
6469
6325
|
await fs9.writeFile(path5.join(packageDir, "package.json"), packageJsonJsTemplate(packageJsonName));
|
|
6470
6326
|
await fs9.mkdir(path5.join(packageDir, "src", "tasks"), { recursive: true });
|
|
6471
6327
|
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
6328
|
await fs9.writeFile(path5.join(packageDir, "src", "tasks", "hello.js"), sampleTaskJsTemplate);
|
|
6475
6329
|
await fs9.writeFile(path5.join(packageDir, "README.md"), readmeTemplate(packageJsonName, registryPrefix, false));
|
|
6476
6330
|
await fs9.writeFile(path5.join(packageDir, ".gitignore"), gitignoreTemplate);
|
|
@@ -6913,8 +6767,6 @@ var Cli = class {
|
|
|
6913
6767
|
'-p, --params "<json object>"',
|
|
6914
6768
|
"runtime parameters supplied as a string to be parsed as a json object representing params to supply to the script"
|
|
6915
6769
|
).option("-r, --remote", "run the script on remote hosts specified by tags via SSH orchestration").action(this.handleRun.bind(this));
|
|
6916
|
-
const runtimeCmd = this.program.command("runtime").alias("rt").description("print out a report of the runtime environment").action(this.handleRuntime.bind(this));
|
|
6917
|
-
runtimeCmd.command("install").alias("i").description("install a temporary runtime environment on the local host if needed").action(this.handleRuntimeInstall.bind(this));
|
|
6918
6770
|
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));
|
|
6919
6771
|
}
|
|
6920
6772
|
async handleInventory(options, cmd) {
|
|
@@ -7094,16 +6946,6 @@ var Cli = class {
|
|
|
7094
6946
|
logError("hostctl error:", e);
|
|
7095
6947
|
}
|
|
7096
6948
|
}
|
|
7097
|
-
async handleRuntime(options, cmd) {
|
|
7098
|
-
const opts = cmd.optsWithGlobals();
|
|
7099
|
-
await this.loadApp(opts);
|
|
7100
|
-
await this.app.printRuntimeReport();
|
|
7101
|
-
}
|
|
7102
|
-
async handleRuntimeInstall(options, cmd) {
|
|
7103
|
-
const opts = cmd.optsWithGlobals();
|
|
7104
|
-
await this.loadApp(opts);
|
|
7105
|
-
await this.app.installRuntime();
|
|
7106
|
-
}
|
|
7107
6949
|
async handlePkgCreate(packageName, options) {
|
|
7108
6950
|
await createPackage(packageName, options);
|
|
7109
6951
|
}
|