hostctl 0.1.57 → 0.1.58

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/dist/index.d.ts CHANGED
@@ -397,6 +397,24 @@ declare class InteractionHandler {
397
397
  end(): void;
398
398
  }
399
399
 
400
+ type CacheScope = 'global' | 'host' | 'invocation';
401
+ type CacheMode = 'use' | 'refresh' | 'bypass';
402
+ interface CacheOptions {
403
+ scope?: CacheScope;
404
+ key?: string;
405
+ ttlMs?: number;
406
+ mode?: CacheMode;
407
+ }
408
+ type CacheConfig = boolean | CacheScope | CacheOptions;
409
+ interface TaskRunOptions {
410
+ cache?: CacheConfig;
411
+ }
412
+ interface MemoizeOptions {
413
+ scope?: CacheScope;
414
+ ttlMs?: number;
415
+ mode?: CacheMode;
416
+ }
417
+
400
418
  /**
401
419
  * Generic object type for parameters and return values.
402
420
  */
@@ -471,7 +489,11 @@ interface TaskContext<TParams extends TaskParams = TaskParams> {
471
489
  pty?: boolean;
472
490
  }) => Promise<CommandResult>;
473
491
  ssh: <TRemoteParams extends TaskParams = {}, TRemoteReturn extends RunFnReturnValue = RunFnReturnValue>(tags: string[], remoteTaskFn: (remoteContext: TaskContext<TRemoteParams>) => Promise<TRemoteReturn>) => Promise<Record<string, TRemoteReturn | Error>>;
474
- run: <TRunReturn extends RunFnReturnValue>(taskPartialFn: TaskPartialFn<TRunReturn>) => Promise<TRunReturn>;
492
+ run: {
493
+ <TRunReturn extends RunFnReturnValue>(taskPartialFn: TaskPartialFn<TRunReturn>, options?: TaskRunOptions): Promise<TRunReturn>;
494
+ <TParams extends TaskParams, TRunReturn extends RunFnReturnValue>(taskFn: TaskFn<TParams, TRunReturn>, params?: TParams, options?: TaskRunOptions): Promise<TRunReturn>;
495
+ };
496
+ memoize: <TReturn>(key: string, valueOrFactory: TReturn | Promise<TReturn> | (() => TReturn | Promise<TReturn>), options?: MemoizeOptions) => Promise<TReturn>;
475
497
  getPassword: () => Promise<string | undefined>;
476
498
  getSecret: (name: string) => Promise<string | undefined>;
477
499
  exit: (exitCode: number, message?: string) => void;
@@ -479,7 +501,10 @@ interface TaskContext<TParams extends TaskParams = TaskParams> {
479
501
  selectedInventory: (tags?: string[]) => Host[];
480
502
  file: FileSystemOperations$1;
481
503
  }
482
- type TaskPartialFn<TReturn extends RunFnReturnValue = RunFnReturnValue> = (parentInvocation: IInvocation) => Promise<TReturn>;
504
+ type TaskPartialFn<TReturn extends RunFnReturnValue = RunFnReturnValue> = ((parentInvocation: IInvocation) => Promise<TReturn>) & {
505
+ taskFn?: TaskFn<any, RunFnReturnValue>;
506
+ params?: TaskParams;
507
+ };
483
508
  interface TaskFn<TParams extends TaskParams = TaskParams, TReturn extends RunFnReturnValue = RunFnReturnValue> {
484
509
  (params?: TParams): TaskPartialFn<TReturn>;
485
510
  task: Task<TParams, TReturn>;
@@ -748,6 +773,7 @@ declare class App {
748
773
  private outputStyle;
749
774
  private _tmpDir?;
750
775
  private tmpFileRegistry;
776
+ private taskCache;
751
777
  taskTree: TaskTree;
752
778
  verbosity: VerbosityLevel;
753
779
  private passwordProvider?;
package/dist/index.js CHANGED
@@ -2719,7 +2719,7 @@ var Verbosity = {
2719
2719
  };
2720
2720
 
2721
2721
  // src/version.ts
2722
- var version = "0.1.57";
2722
+ var version = "0.1.58";
2723
2723
 
2724
2724
  // src/commands/pkg/create.ts
2725
2725
  import { promises as fs5 } from "fs";
@@ -4150,9 +4150,13 @@ function task(runFn4, options) {
4150
4150
  options?.outputSchema
4151
4151
  );
4152
4152
  const taskFnObject = function(params) {
4153
- return function(parentInvocation) {
4154
- return parentInvocation.invokeChildTask(taskFnObject, params ?? {});
4153
+ const normalizedParams = params ?? {};
4154
+ const taskPartialFn = function(parentInvocation) {
4155
+ return parentInvocation.invokeChildTask(taskFnObject, normalizedParams);
4155
4156
  };
4157
+ taskPartialFn.taskFn = taskFnObject;
4158
+ taskPartialFn.params = normalizedParams;
4159
+ return taskPartialFn;
4156
4160
  };
4157
4161
  Object.assign(taskFnObject, { task: taskInstance });
4158
4162
  return taskFnObject;
@@ -6154,58 +6158,64 @@ async function detectRockyLinux(exec) {
6154
6158
  }
6155
6159
  var os_default = task(
6156
6160
  async function run26(context) {
6157
- try {
6158
- const { exec } = context;
6159
- const {
6160
- success: ostypeSuccess,
6161
- stdout: ostypeOutput,
6162
- stderr: ostypeStderr
6163
- } = await exec(["bash", "-c", "echo $OSTYPE"]);
6164
- if (!ostypeSuccess) {
6165
- throw new Error(`Failed to get OSTYPE: ${ostypeStderr}`);
6166
- }
6167
- const family = await match3(ostypeOutput.trim().toLowerCase()).with(P2.string.startsWith("solaris"), () => "solaris").with(P2.string.startsWith("darwin"), () => "darwin").with(P2.string.startsWith("linux"), () => "linux").with(P2.string.startsWith("bsd"), () => "bsd").with(P2.string.startsWith("freebsd"), () => "bsd").with(P2.string.startsWith("msys"), () => "windows").with(P2.string.startsWith("cygwin"), () => "windows").with(P2.string.startsWith("mingw"), () => "windows").otherwise(async () => {
6168
- const { stdout: unameOutput } = await exec(["uname"]);
6169
- const unameFamily = match3(unameOutput.trim().toLowerCase()).with(P2.string.startsWith("sunos"), () => "solaris").with(P2.string.startsWith("darwin"), () => "darwin").with(P2.string.startsWith("linux"), () => "linux").with(P2.string.startsWith("freebsd"), () => "bsd").with(P2.string.startsWith("openbsd"), () => "bsd").with(P2.string.startsWith("netbsd"), () => "bsd").otherwise(() => "unknown");
6170
- return unameFamily;
6171
- });
6172
- const [osIdLike, osId, osVersion] = await match3(family).with("bsd", async () => {
6173
- const { stdout: unameROutput } = await exec(["uname", "-r"]);
6174
- return [family, family, unameROutput.trim()];
6175
- }).with("darwin", async () => {
6176
- const { stdout: swVersOutput } = await exec(["sw_vers", "-productVersion"]);
6177
- return [family, family, swVersOutput.trim()];
6178
- }).with("linux", async () => {
6179
- const { idLike, id, version: version2 } = await getOsReleaseInfo(exec);
6180
- return [idLike, id, version2];
6181
- }).with("solaris", async () => {
6182
- const { stdout: unameROutput } = await exec(["uname", "-r"]);
6183
- return ["solaris", "solaris", unameROutput.trim()];
6184
- }).with("windows", () => ["windows", "windows", "unknown"]).otherwise(() => ["unknown", "unknown", "unknown"]);
6185
- let variant = osId.toLowerCase();
6186
- if (family === "linux" && !variant.includes("rocky") && (osIdLike.toLowerCase().includes("rocky") || osId.toLowerCase().includes("rhel"))) {
6187
- const isRocky = await detectRockyLinux(exec);
6188
- if (isRocky) {
6189
- variant = "rocky";
6161
+ return await context.memoize(
6162
+ "core.host.os:v1",
6163
+ async () => {
6164
+ try {
6165
+ const { exec } = context;
6166
+ const {
6167
+ success: ostypeSuccess,
6168
+ stdout: ostypeOutput,
6169
+ stderr: ostypeStderr
6170
+ } = await exec(["bash", "-c", "echo $OSTYPE"]);
6171
+ if (!ostypeSuccess) {
6172
+ throw new Error(`Failed to get OSTYPE: ${ostypeStderr}`);
6173
+ }
6174
+ const family = await match3(ostypeOutput.trim().toLowerCase()).with(P2.string.startsWith("solaris"), () => "solaris").with(P2.string.startsWith("darwin"), () => "darwin").with(P2.string.startsWith("linux"), () => "linux").with(P2.string.startsWith("bsd"), () => "bsd").with(P2.string.startsWith("freebsd"), () => "bsd").with(P2.string.startsWith("msys"), () => "windows").with(P2.string.startsWith("cygwin"), () => "windows").with(P2.string.startsWith("mingw"), () => "windows").otherwise(async () => {
6175
+ const { stdout: unameOutput } = await exec(["uname"]);
6176
+ const unameFamily = match3(unameOutput.trim().toLowerCase()).with(P2.string.startsWith("sunos"), () => "solaris").with(P2.string.startsWith("darwin"), () => "darwin").with(P2.string.startsWith("linux"), () => "linux").with(P2.string.startsWith("freebsd"), () => "bsd").with(P2.string.startsWith("openbsd"), () => "bsd").with(P2.string.startsWith("netbsd"), () => "bsd").otherwise(() => "unknown");
6177
+ return unameFamily;
6178
+ });
6179
+ const [osIdLike, osId, osVersion] = await match3(family).with("bsd", async () => {
6180
+ const { stdout: unameROutput } = await exec(["uname", "-r"]);
6181
+ return [family, family, unameROutput.trim()];
6182
+ }).with("darwin", async () => {
6183
+ const { stdout: swVersOutput } = await exec(["sw_vers", "-productVersion"]);
6184
+ return [family, family, swVersOutput.trim()];
6185
+ }).with("linux", async () => {
6186
+ const { idLike, id, version: version2 } = await getOsReleaseInfo(exec);
6187
+ return [idLike, id, version2];
6188
+ }).with("solaris", async () => {
6189
+ const { stdout: unameROutput } = await exec(["uname", "-r"]);
6190
+ return ["solaris", "solaris", unameROutput.trim()];
6191
+ }).with("windows", () => ["windows", "windows", "unknown"]).otherwise(() => ["unknown", "unknown", "unknown"]);
6192
+ let variant = osId.toLowerCase();
6193
+ if (family === "linux" && !variant.includes("rocky") && (osIdLike.toLowerCase().includes("rocky") || osId.toLowerCase().includes("rhel"))) {
6194
+ const isRocky = await detectRockyLinux(exec);
6195
+ if (isRocky) {
6196
+ variant = "rocky";
6197
+ }
6198
+ }
6199
+ return {
6200
+ success: true,
6201
+ family,
6202
+ os: osIdLike.toLowerCase(),
6203
+ variant,
6204
+ version: osVersion
6205
+ };
6206
+ } catch (error) {
6207
+ return {
6208
+ success: false,
6209
+ error: error.message,
6210
+ family: "unknown",
6211
+ os: "unknown",
6212
+ variant: "unknown",
6213
+ version: "unknown"
6214
+ };
6190
6215
  }
6191
- }
6192
- return {
6193
- success: true,
6194
- family,
6195
- os: osIdLike.toLowerCase(),
6196
- variant,
6197
- version: osVersion
6198
- };
6199
- } catch (error) {
6200
- return {
6201
- success: false,
6202
- error: error.message,
6203
- family: "unknown",
6204
- os: "unknown",
6205
- variant: "unknown",
6206
- version: "unknown"
6207
- };
6208
- }
6216
+ },
6217
+ { scope: "host" }
6218
+ );
6209
6219
  },
6210
6220
  {
6211
6221
  name: "os",
@@ -32932,6 +32942,9 @@ import AdmZip from "adm-zip";
32932
32942
 
32933
32943
  // src/hash.ts
32934
32944
  import { createHash } from "crypto";
32945
+ function sha256(str) {
32946
+ return createHash("sha256").update(str).digest("hex");
32947
+ }
32935
32948
 
32936
32949
  // src/param-map.ts
32937
32950
  import { match as match5 } from "ts-pattern";
@@ -33075,6 +33088,154 @@ var runAllRemote_default = task(run252, {
33075
33088
  outputSchema: RunResultSchema
33076
33089
  });
33077
33090
 
33091
+ // src/task-cache.ts
33092
+ var DEFAULT_CACHE_MODE = "use";
33093
+ var LOCAL_HOST_SCOPE_KEY = "host:local";
33094
+ function normalizeValue(value) {
33095
+ if (value === void 0) {
33096
+ return void 0;
33097
+ }
33098
+ if (value === null) {
33099
+ return null;
33100
+ }
33101
+ if (typeof value === "bigint") {
33102
+ return value.toString();
33103
+ }
33104
+ if (value instanceof Date) {
33105
+ return value.toISOString();
33106
+ }
33107
+ if (Array.isArray(value)) {
33108
+ return value.map((item) => {
33109
+ const normalized = normalizeValue(item);
33110
+ return normalized === void 0 ? null : normalized;
33111
+ });
33112
+ }
33113
+ if (typeof value === "object") {
33114
+ const obj = value;
33115
+ const sortedKeys = Object.keys(obj).sort();
33116
+ const result = {};
33117
+ for (const key of sortedKeys) {
33118
+ const normalized = normalizeValue(obj[key]);
33119
+ if (normalized !== void 0) {
33120
+ result[key] = normalized;
33121
+ }
33122
+ }
33123
+ return result;
33124
+ }
33125
+ return value;
33126
+ }
33127
+ function stableJson(value) {
33128
+ const normalized = normalizeValue(value);
33129
+ return JSON.stringify(normalized ?? null);
33130
+ }
33131
+ function normalizeCacheConfig(cache) {
33132
+ if (!cache) {
33133
+ return { enabled: false, mode: DEFAULT_CACHE_MODE };
33134
+ }
33135
+ if (cache === true) {
33136
+ return { enabled: true, mode: DEFAULT_CACHE_MODE };
33137
+ }
33138
+ if (typeof cache === "string") {
33139
+ return { enabled: true, scope: cache, mode: DEFAULT_CACHE_MODE };
33140
+ }
33141
+ return {
33142
+ enabled: true,
33143
+ scope: cache.scope,
33144
+ key: cache.key,
33145
+ ttlMs: cache.ttlMs,
33146
+ mode: cache.mode ?? DEFAULT_CACHE_MODE
33147
+ };
33148
+ }
33149
+ function buildTaskCacheKey(taskIdentity, params) {
33150
+ const taskName = taskIdentity?.task?.name;
33151
+ const modulePath = taskIdentity?.task?.taskModuleAbsolutePath;
33152
+ const identity2 = taskName ?? (modulePath ? Path.new(modulePath).absolute().toString() : "unknown-task");
33153
+ const paramsHash = sha256(stableJson(params ?? {}));
33154
+ return `${identity2}:${paramsHash}`;
33155
+ }
33156
+ function buildHostScopeKey(host, configRef) {
33157
+ if (!host) {
33158
+ return LOCAL_HOST_SCOPE_KEY;
33159
+ }
33160
+ const identity2 = {
33161
+ alias: host.alias ?? "",
33162
+ hostname: host.hostname ?? "",
33163
+ user: host.user ?? "",
33164
+ port: host.port ?? 22,
33165
+ config: configRef ?? ""
33166
+ };
33167
+ return `host:${sha256(stableJson(identity2))}`;
33168
+ }
33169
+ function buildInvocationScopeKey(rootInvocationId) {
33170
+ return `invocation:${rootInvocationId}`;
33171
+ }
33172
+ var TaskCacheStore = class {
33173
+ entries = /* @__PURE__ */ new Map();
33174
+ runId;
33175
+ constructor(runId) {
33176
+ this.runId = runId ?? crypto.randomUUID();
33177
+ }
33178
+ globalScopeKey() {
33179
+ return `global:${this.runId}`;
33180
+ }
33181
+ get(scopeKey, cacheKey2) {
33182
+ const scope = this.entries.get(scopeKey);
33183
+ if (!scope) {
33184
+ return void 0;
33185
+ }
33186
+ const entry = scope.get(cacheKey2);
33187
+ if (!entry) {
33188
+ return void 0;
33189
+ }
33190
+ if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) {
33191
+ scope.delete(cacheKey2);
33192
+ if (scope.size === 0) {
33193
+ this.entries.delete(scopeKey);
33194
+ }
33195
+ return void 0;
33196
+ }
33197
+ return entry;
33198
+ }
33199
+ set(scopeKey, cacheKey2, value, ttlMs) {
33200
+ const scope = this.entries.get(scopeKey) ?? /* @__PURE__ */ new Map();
33201
+ const expiresAt = ttlMs && ttlMs > 0 ? Date.now() + ttlMs : 0;
33202
+ const entry = { value, expiresAt };
33203
+ scope.set(cacheKey2, entry);
33204
+ this.entries.set(scopeKey, scope);
33205
+ return entry;
33206
+ }
33207
+ delete(scopeKey, cacheKey2) {
33208
+ const scope = this.entries.get(scopeKey);
33209
+ if (!scope) {
33210
+ return;
33211
+ }
33212
+ scope.delete(cacheKey2);
33213
+ if (scope.size === 0) {
33214
+ this.entries.delete(scopeKey);
33215
+ }
33216
+ }
33217
+ async resolve(scopeKey, cacheKey2, compute, options = {}) {
33218
+ const mode = options.mode ?? DEFAULT_CACHE_MODE;
33219
+ if (mode === "bypass") {
33220
+ return await compute();
33221
+ }
33222
+ if (mode === "use") {
33223
+ const cached = this.get(scopeKey, cacheKey2);
33224
+ if (cached) {
33225
+ return await cached.value;
33226
+ }
33227
+ }
33228
+ const promise = Promise.resolve().then(compute);
33229
+ this.set(scopeKey, cacheKey2, promise, options.ttlMs);
33230
+ try {
33231
+ return await promise;
33232
+ } catch (error) {
33233
+ this.delete(scopeKey, cacheKey2);
33234
+ throw error;
33235
+ }
33236
+ }
33237
+ };
33238
+
33078
33239
  // src/app.ts
33079
33240
  var TaskTree = class {
33080
33241
  // private taskEventBus: Emittery<{ newTask: NewTaskEvent; taskComplete: TaskCompleteEvent }>;
@@ -33171,6 +33332,7 @@ var App6 = class _App {
33171
33332
  outputStyle;
33172
33333
  _tmpDir;
33173
33334
  tmpFileRegistry;
33335
+ taskCache;
33174
33336
  taskTree;
33175
33337
  verbosity = Verbosity.ERROR;
33176
33338
  passwordProvider;
@@ -33180,6 +33342,7 @@ var App6 = class _App {
33180
33342
  this.taskTree = new TaskTree();
33181
33343
  this.outputStyle = "plain";
33182
33344
  this.tmpFileRegistry = new TmpFileRegistry(this.hostctlTmpDir());
33345
+ this.taskCache = new TaskCacheStore();
33183
33346
  this.configRef = void 0;
33184
33347
  this.hostSelector = void 0;
33185
33348
  process4.on("exit", (code) => this.appExitCallback());
@@ -33495,6 +33658,27 @@ ${cmdRes.stderr.trim()}`));
33495
33658
  }
33496
33659
  taskContextForRunFn(invocation, params, hostForContext) {
33497
33660
  const effectiveHost = hostForContext || invocation.host;
33661
+ const rootInvocationId = (() => {
33662
+ let current = invocation;
33663
+ while (current.parent) {
33664
+ current = current.parent;
33665
+ }
33666
+ return current.id;
33667
+ })();
33668
+ const defaultScope = () => effectiveHost ? "host" : "global";
33669
+ const scopeKeyFor = (scope) => {
33670
+ switch (scope) {
33671
+ case "global":
33672
+ return this.taskCache.globalScopeKey();
33673
+ case "host":
33674
+ return buildHostScopeKey(effectiveHost, this.configRef);
33675
+ case "invocation":
33676
+ return buildInvocationScopeKey(rootInvocationId);
33677
+ }
33678
+ };
33679
+ const isTaskFn = (candidate) => {
33680
+ return typeof candidate === "function" && !!candidate.task;
33681
+ };
33498
33682
  return {
33499
33683
  // Properties from TaskContext
33500
33684
  params,
@@ -33524,8 +33708,58 @@ ${cmdRes.stderr.trim()}`));
33524
33708
  ssh: async (tags, remoteTaskFn) => {
33525
33709
  return await invocation.ssh(tags, remoteTaskFn);
33526
33710
  },
33527
- run: async (taskPartialFn) => {
33528
- return await invocation.run(taskPartialFn);
33711
+ run: async (...args) => {
33712
+ const [firstArg, secondArg, thirdArg] = args;
33713
+ let taskPartialFn;
33714
+ let taskIdentity;
33715
+ let paramsForKey;
33716
+ let options;
33717
+ if (isTaskFn(firstArg)) {
33718
+ taskIdentity = firstArg;
33719
+ paramsForKey = secondArg ?? {};
33720
+ options = thirdArg;
33721
+ taskPartialFn = taskIdentity(paramsForKey);
33722
+ } else {
33723
+ taskPartialFn = firstArg;
33724
+ options = secondArg;
33725
+ const meta = taskPartialFn;
33726
+ taskIdentity = meta.taskFn;
33727
+ paramsForKey = meta.params;
33728
+ }
33729
+ const cacheDecision = normalizeCacheConfig(options?.cache);
33730
+ if (!cacheDecision.enabled || cacheDecision.mode === "bypass") {
33731
+ return await invocation.run(taskPartialFn);
33732
+ }
33733
+ const scope = cacheDecision.scope ?? defaultScope();
33734
+ const cacheKey2 = cacheDecision.key ?? buildTaskCacheKey(taskIdentity, paramsForKey ?? {});
33735
+ const scopeKey = scopeKeyFor(scope);
33736
+ return await this.taskCache.resolve(scopeKey, cacheKey2, () => invocation.run(taskPartialFn), {
33737
+ ttlMs: cacheDecision.ttlMs,
33738
+ mode: cacheDecision.mode
33739
+ });
33740
+ },
33741
+ memoize: async (key, valueOrFactory, options) => {
33742
+ const cacheDecision = normalizeCacheConfig({
33743
+ scope: options?.scope,
33744
+ ttlMs: options?.ttlMs,
33745
+ mode: options?.mode,
33746
+ key
33747
+ });
33748
+ const scope = cacheDecision.scope ?? defaultScope();
33749
+ const scopeKey = scopeKeyFor(scope);
33750
+ const compute = async () => {
33751
+ if (typeof valueOrFactory === "function") {
33752
+ return await valueOrFactory();
33753
+ }
33754
+ return await valueOrFactory;
33755
+ };
33756
+ if (cacheDecision.mode === "bypass") {
33757
+ return await compute();
33758
+ }
33759
+ return await this.taskCache.resolve(scopeKey, key, compute, {
33760
+ ttlMs: cacheDecision.ttlMs,
33761
+ mode: cacheDecision.mode
33762
+ });
33529
33763
  },
33530
33764
  getPassword: async () => {
33531
33765
  return await invocation.getPassword();