hostctl 0.1.55 → 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.55";
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",
@@ -31924,8 +31934,8 @@ function renderTaskRows(rows, {
31924
31934
  name: r.name,
31925
31935
  description: r.description,
31926
31936
  module: r.module,
31927
- inputSchema: Boolean(r.inputSchema),
31928
- outputSchema: Boolean(r.outputSchema),
31937
+ inputSchema: schemaToDescriptor(r.inputSchema),
31938
+ outputSchema: schemaToDescriptor(r.outputSchema),
31929
31939
  schemaSource: r.schemaSource
31930
31940
  }));
31931
31941
  console.log(JSON.stringify(sanitized, null, 2));
@@ -31966,9 +31976,12 @@ function summarizeType(val) {
31966
31976
  case "optional":
31967
31977
  case "ZodNullable":
31968
31978
  case "nullable":
31979
+ case "ZodDefault":
31980
+ case "default":
31969
31981
  return summarizeType(def.innerType || def.type);
31970
31982
  case "ZodEffects":
31971
- return summarizeType(def.schema);
31983
+ case "pipe":
31984
+ return summarizeType(def.schema || def.in);
31972
31985
  case "ZodString":
31973
31986
  case "string":
31974
31987
  return { type: "string", desc };
@@ -31991,8 +32004,11 @@ function summarizeType(val) {
31991
32004
  return types.length ? { type: types.join(" | "), desc } : {};
31992
32005
  }
31993
32006
  case "ZodLiteral":
31994
- case "literal":
31995
- return { type: JSON.stringify(def.value), desc };
32007
+ case "literal": {
32008
+ const values3 = Array.isArray(def?.values) ? def.values : def?.value !== void 0 ? [def.value] : [];
32009
+ const value = values3.length ? values3[0] : void 0;
32010
+ return { type: value !== void 0 ? JSON.stringify(value) : void 0, desc };
32011
+ }
31996
32012
  case "ZodObject":
31997
32013
  case "object":
31998
32014
  return { type: "object", desc };
@@ -32000,6 +32016,140 @@ function summarizeType(val) {
32000
32016
  return { type: String(tn).replace(/^Zod/, "").toLowerCase(), desc };
32001
32017
  }
32002
32018
  }
32019
+ function schemaToDescriptor(schema) {
32020
+ if (!schema) return null;
32021
+ const { schema: base, nullable } = unwrapSchema(schema);
32022
+ const def = base?._def ?? base?.def;
32023
+ const typeName = def?.typeName || def?.type;
32024
+ const description = def?.description || base?.description;
32025
+ let descriptor = {};
32026
+ switch (typeName) {
32027
+ case "ZodString":
32028
+ case "string":
32029
+ descriptor.type = "string";
32030
+ break;
32031
+ case "ZodNumber":
32032
+ case "number":
32033
+ descriptor.type = "number";
32034
+ break;
32035
+ case "ZodBoolean":
32036
+ case "boolean":
32037
+ descriptor.type = "boolean";
32038
+ break;
32039
+ case "ZodArray":
32040
+ case "array": {
32041
+ const inner = def?.element ?? def?.type;
32042
+ descriptor.type = "array";
32043
+ descriptor.items = schemaToDescriptor(inner);
32044
+ break;
32045
+ }
32046
+ case "ZodObject":
32047
+ case "object": {
32048
+ const shapeSource = def?.shape || def?.shape_;
32049
+ const shape = shapeSource && typeof shapeSource === "function" ? shapeSource.call(def) : shapeSource;
32050
+ const properties = {};
32051
+ const required = [];
32052
+ if (shape && typeof shape === "object") {
32053
+ for (const [key, value] of Object.entries(shape)) {
32054
+ const { schema: fieldSchema, optional, nullable: fieldNullable } = unwrapSchema(value);
32055
+ const fieldDescriptor = schemaToDescriptor(fieldSchema) ?? {};
32056
+ if (fieldNullable) {
32057
+ fieldDescriptor.nullable = true;
32058
+ }
32059
+ properties[key] = fieldDescriptor;
32060
+ if (!optional) {
32061
+ required.push(key);
32062
+ }
32063
+ }
32064
+ }
32065
+ descriptor.type = "object";
32066
+ descriptor.properties = properties;
32067
+ descriptor.required = required.length ? required : void 0;
32068
+ break;
32069
+ }
32070
+ case "ZodUnion":
32071
+ case "union": {
32072
+ const options = def?.options ?? [];
32073
+ descriptor.anyOf = options.map((option) => schemaToDescriptor(option)).filter(Boolean);
32074
+ break;
32075
+ }
32076
+ case "ZodLiteral":
32077
+ case "literal": {
32078
+ const values3 = Array.isArray(def?.values) ? def.values : def?.value !== void 0 ? [def.value] : [];
32079
+ const sample = values3.length ? values3[0] : void 0;
32080
+ descriptor.type = sample === null ? "null" : typeof sample;
32081
+ descriptor.enum = values3;
32082
+ break;
32083
+ }
32084
+ case "ZodEnum":
32085
+ case "enum":
32086
+ const enumValues = Array.isArray(def?.values) ? def.values : Object.values(def?.entries ?? {});
32087
+ descriptor.enum = enumValues;
32088
+ if (enumValues.length) {
32089
+ const hasNumber = enumValues.some((value) => typeof value === "number");
32090
+ const hasString = enumValues.some((value) => typeof value === "string");
32091
+ descriptor.type = hasNumber && !hasString ? "number" : "string";
32092
+ } else {
32093
+ descriptor.type = "string";
32094
+ }
32095
+ break;
32096
+ case "ZodNativeEnum":
32097
+ case "nativeenum": {
32098
+ const values3 = Array.isArray(def?.values) ? def.values : Object.values(def?.entries ?? def?.values ?? {});
32099
+ descriptor.type = values3.every((value) => typeof value === "number") ? "number" : "string";
32100
+ descriptor.enum = values3;
32101
+ break;
32102
+ }
32103
+ default:
32104
+ descriptor.type = typeName ? String(typeName).replace(/^Zod/, "").toLowerCase() : "unknown";
32105
+ break;
32106
+ }
32107
+ if (description) {
32108
+ descriptor.description = description;
32109
+ }
32110
+ if (nullable) {
32111
+ descriptor.nullable = true;
32112
+ }
32113
+ return descriptor;
32114
+ }
32115
+ function unwrapSchema(schema) {
32116
+ let current = schema;
32117
+ let optional = false;
32118
+ let nullable = false;
32119
+ while (current) {
32120
+ const def = current?._def ?? current?.def;
32121
+ const typeName = def?.typeName || def?.type;
32122
+ if (typeName === "ZodOptional" || typeName === "optional") {
32123
+ optional = true;
32124
+ current = def?.innerType ?? def?.type;
32125
+ continue;
32126
+ }
32127
+ if (typeName === "ZodNullable" || typeName === "nullable") {
32128
+ nullable = true;
32129
+ current = def?.innerType ?? def?.type;
32130
+ continue;
32131
+ }
32132
+ if (typeName === "ZodDefault" || typeName === "default") {
32133
+ optional = true;
32134
+ current = def?.innerType ?? def?.type;
32135
+ continue;
32136
+ }
32137
+ if (typeName === "ZodEffects" || typeName === "effects") {
32138
+ current = def?.schema;
32139
+ continue;
32140
+ }
32141
+ if (typeName === "pipe") {
32142
+ current = def?.in ?? def?.schema;
32143
+ continue;
32144
+ }
32145
+ if (typeName === "transform") {
32146
+ current = def?.schema ?? def?.in;
32147
+ continue;
32148
+ }
32149
+ break;
32150
+ }
32151
+ return { schema: current, optional, nullable };
32152
+ }
32003
32153
  function formatSchemaLines(schema) {
32004
32154
  if (!schema) return ["none"];
32005
32155
  const def = schema._def ?? schema.def;
@@ -32012,7 +32162,7 @@ function formatSchemaLines(schema) {
32012
32162
  const tn = anyVal?._def?.typeName || anyVal?.def?.type;
32013
32163
  const typeString = summary2.type || (tn ? String(tn).replace(/^Zod/, "").toLowerCase() : void 0);
32014
32164
  const desc = summary2.desc;
32015
- const optional = tn === "ZodOptional" || tn === "ZodNullable";
32165
+ const optional = ["ZodOptional", "optional", "ZodNullable", "nullable", "ZodDefault", "default"].includes(tn);
32016
32166
  const typePart = typeString ? `: ${typeString}` : "";
32017
32167
  const descPart = desc ? ` - ${desc}` : "";
32018
32168
  return `- ${key}${optional ? "?" : ""}${typePart}${descPart}`;
@@ -32792,6 +32942,9 @@ import AdmZip from "adm-zip";
32792
32942
 
32793
32943
  // src/hash.ts
32794
32944
  import { createHash } from "crypto";
32945
+ function sha256(str) {
32946
+ return createHash("sha256").update(str).digest("hex");
32947
+ }
32795
32948
 
32796
32949
  // src/param-map.ts
32797
32950
  import { match as match5 } from "ts-pattern";
@@ -32935,6 +33088,154 @@ var runAllRemote_default = task(run252, {
32935
33088
  outputSchema: RunResultSchema
32936
33089
  });
32937
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
+
32938
33239
  // src/app.ts
32939
33240
  var TaskTree = class {
32940
33241
  // private taskEventBus: Emittery<{ newTask: NewTaskEvent; taskComplete: TaskCompleteEvent }>;
@@ -33031,6 +33332,7 @@ var App6 = class _App {
33031
33332
  outputStyle;
33032
33333
  _tmpDir;
33033
33334
  tmpFileRegistry;
33335
+ taskCache;
33034
33336
  taskTree;
33035
33337
  verbosity = Verbosity.ERROR;
33036
33338
  passwordProvider;
@@ -33040,6 +33342,7 @@ var App6 = class _App {
33040
33342
  this.taskTree = new TaskTree();
33041
33343
  this.outputStyle = "plain";
33042
33344
  this.tmpFileRegistry = new TmpFileRegistry(this.hostctlTmpDir());
33345
+ this.taskCache = new TaskCacheStore();
33043
33346
  this.configRef = void 0;
33044
33347
  this.hostSelector = void 0;
33045
33348
  process4.on("exit", (code) => this.appExitCallback());
@@ -33355,6 +33658,27 @@ ${cmdRes.stderr.trim()}`));
33355
33658
  }
33356
33659
  taskContextForRunFn(invocation, params, hostForContext) {
33357
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
+ };
33358
33682
  return {
33359
33683
  // Properties from TaskContext
33360
33684
  params,
@@ -33384,8 +33708,58 @@ ${cmdRes.stderr.trim()}`));
33384
33708
  ssh: async (tags, remoteTaskFn) => {
33385
33709
  return await invocation.ssh(tags, remoteTaskFn);
33386
33710
  },
33387
- run: async (taskPartialFn) => {
33388
- 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
+ });
33389
33763
  },
33390
33764
  getPassword: async () => {
33391
33765
  return await invocation.getPassword();