@dovocode/workstation 0.1.0 → 0.1.1

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/cli.js CHANGED
@@ -24,12 +24,12 @@ var __copyProps = (to, from, except, desc) => {
24
24
  }
25
25
  return to;
26
26
  };
27
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
27
+ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
28
  // If the importer is in node compatibility mode or this is not an ESM
29
29
  // file that has been converted to a CommonJS file using a Babel-
30
30
  // compatible transform (i.e. "__esModule" has not been set), then set
31
31
  // "default" to the CommonJS "module.exports" for node compatibility.
32
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2,
33
33
  mod
34
34
  ));
35
35
 
@@ -2960,8 +2960,8 @@ var require_jiti = __commonJS({
2960
2960
  }
2961
2961
  const He = /^[/\\]{2}/, ze = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/, Je = /^[A-Za-z]:$/, Ye = /.(\.[^./]+|\.)$/, pathe_M_eThtNZ_normalize = function(e3) {
2962
2962
  if (0 === e3.length) return ".";
2963
- const t3 = (e3 = pathe_M_eThtNZ_normalizeWindowsPath(e3)).match(He), i2 = isAbsolute3(e3), n2 = "/" === e3[e3.length - 1];
2964
- return 0 === (e3 = normalizeString(e3, !i2)).length ? i2 ? "/" : n2 ? "./" : "." : (n2 && (e3 += "/"), Je.test(e3) && (e3 += "/"), t3 ? i2 ? `//${e3}` : `//./${e3}` : i2 && !isAbsolute3(e3) ? `/${e3}` : e3);
2963
+ const t3 = (e3 = pathe_M_eThtNZ_normalizeWindowsPath(e3)).match(He), i2 = isAbsolute4(e3), n2 = "/" === e3[e3.length - 1];
2964
+ return 0 === (e3 = normalizeString(e3, !i2)).length ? i2 ? "/" : n2 ? "./" : "." : (n2 && (e3 += "/"), Je.test(e3) && (e3 += "/"), t3 ? i2 ? `//${e3}` : `//./${e3}` : i2 && !isAbsolute4(e3) ? `/${e3}` : e3);
2965
2965
  }, pathe_M_eThtNZ_join = function(...e3) {
2966
2966
  let t3 = "";
2967
2967
  for (const i2 of e3) if (i2) if (t3.length > 0) {
@@ -2977,9 +2977,9 @@ var require_jiti = __commonJS({
2977
2977
  let t3 = "", i2 = false;
2978
2978
  for (let n2 = (e3 = e3.map((e4) => pathe_M_eThtNZ_normalizeWindowsPath(e4))).length - 1; n2 >= -1 && !i2; n2--) {
2979
2979
  const a2 = n2 >= 0 ? e3[n2] : pathe_M_eThtNZ_cwd();
2980
- a2 && 0 !== a2.length && (t3 = `${a2}/${t3}`, i2 = isAbsolute3(a2));
2980
+ a2 && 0 !== a2.length && (t3 = `${a2}/${t3}`, i2 = isAbsolute4(a2));
2981
2981
  }
2982
- return t3 = normalizeString(t3, !i2), i2 && !isAbsolute3(t3) ? `/${t3}` : t3.length > 0 ? t3 : ".";
2982
+ return t3 = normalizeString(t3, !i2), i2 && !isAbsolute4(t3) ? `/${t3}` : t3.length > 0 ? t3 : ".";
2983
2983
  };
2984
2984
  function normalizeString(e3, t3) {
2985
2985
  let i2 = "", n2 = 0, a2 = -1, c2 = 0, l2 = null;
@@ -3010,7 +3010,7 @@ var require_jiti = __commonJS({
3010
3010
  }
3011
3011
  return i2;
3012
3012
  }
3013
- const isAbsolute3 = function(e3) {
3013
+ const isAbsolute4 = function(e3) {
3014
3014
  return ze.test(e3);
3015
3015
  }, extname = function(e3) {
3016
3016
  if (".." === e3) return "";
@@ -3018,8 +3018,8 @@ var require_jiti = __commonJS({
3018
3018
  return t3 && t3[1] || "";
3019
3019
  }, pathe_M_eThtNZ_dirname = function(e3) {
3020
3020
  const t3 = pathe_M_eThtNZ_normalizeWindowsPath(e3).replace(/\/$/, "").split("/").slice(0, -1);
3021
- return 1 === t3.length && Je.test(t3[0]) && (t3[0] += "/"), t3.join("/") || (isAbsolute3(e3) ? "/" : ".");
3022
- }, basename = function(e3, t3) {
3021
+ return 1 === t3.length && Je.test(t3[0]) && (t3[0] += "/"), t3.join("/") || (isAbsolute4(e3) ? "/" : ".");
3022
+ }, basename2 = function(e3, t3) {
3023
3023
  const i2 = pathe_M_eThtNZ_normalizeWindowsPath(e3).split("/");
3024
3024
  let n2 = "";
3025
3025
  for (let e4 = i2.length - 1; e4 >= 0; e4--) {
@@ -3494,7 +3494,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
3494
3494
  }
3495
3495
  if (/(?:node|data|http|https):/.test(e3)) return e3;
3496
3496
  if (st.has(e3)) return "node:" + e3;
3497
- if (e3.startsWith("file://") && (e3 = fileURLToPath(e3)), isAbsolute3(e3)) try {
3497
+ if (e3.startsWith("file://") && (e3 = fileURLToPath(e3)), isAbsolute4(e3)) try {
3498
3498
  if ((0, $e.statSync)(e3).isFile()) return pathToFileURL(e3);
3499
3499
  } catch (e4) {
3500
3500
  if ("ENOENT" !== e4?.code) throw e4;
@@ -3732,7 +3732,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
3732
3732
  }
3733
3733
  function nativeImportOrRequire(e3, t3, i2) {
3734
3734
  return i2 && e3.nativeImport ? e3.nativeImport((function(e4) {
3735
- return ni && isAbsolute3(e4) ? pathToFileURL(e4) : e4;
3735
+ return ni && isAbsolute4(e4) ? pathToFileURL(e4) : e4;
3736
3736
  })(t3)).then((t4) => jitiInteropDefault(e3, t4)) : jitiInteropDefault(e3, e3.nativeRequire(t3));
3737
3737
  }
3738
3738
  const Ti = "9";
@@ -3740,7 +3740,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
3740
3740
  if (!e3.opts.fsCache || !t3.filename) return i2();
3741
3741
  const n2 = ` /* v${Ti}-${utils_hash(t3.source, 16)} */
3742
3742
  `;
3743
- let a2 = `${basename(pathe_M_eThtNZ_dirname(t3.filename))}-${(function(e4) {
3743
+ let a2 = `${basename2(pathe_M_eThtNZ_dirname(t3.filename))}-${(function(e4) {
3744
3744
  const t4 = e4.split(Wt).pop();
3745
3745
  if (!t4) return;
3746
3746
  const i3 = t4.lastIndexOf(".");
@@ -3786,7 +3786,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
3786
3786
  return i2.startsWith("#!") && (i2 = "// " + i2), i2;
3787
3787
  }
3788
3788
  function eval_evalModule(t3, i2, n2 = {}) {
3789
- const a2 = n2.id || (n2.filename ? basename(n2.filename) : `_jitiEval.${n2.ext || (n2.async ? "mjs" : "js")}`), c2 = n2.filename || jitiResolve(t3, a2, { async: n2.async }), l2 = n2.ext || extname(c2), y2 = n2.cache || t3.parentCache || {}, E2 = /\.[cm]?tsx?$/.test(l2), w2 = ".mjs" === l2 || ".js" === l2 && "module" === (function(e3) {
3789
+ const a2 = n2.id || (n2.filename ? basename2(n2.filename) : `_jitiEval.${n2.ext || (n2.async ? "mjs" : "js")}`), c2 = n2.filename || jitiResolve(t3, a2, { async: n2.async }), l2 = n2.ext || extname(c2), y2 = n2.cache || t3.parentCache || {}, E2 = /\.[cm]?tsx?$/.test(l2), w2 = ".mjs" === l2 || ".js" === l2 && "module" === (function(e3) {
3790
3790
  for (; e3 && "." !== e3 && "/" !== e3; ) {
3791
3791
  e3 = pathe_M_eThtNZ_join(e3, "..");
3792
3792
  try {
@@ -3834,7 +3834,7 @@ Default "index" lookups for the main are deprecated for ES modules.`, "Deprecati
3834
3834
  (0, $e.mkdirSync)(n4, { recursive: true });
3835
3835
  } catch {
3836
3836
  }
3837
- const a4 = pathe_M_eThtNZ_join(n4, `${basename(i6, extname(i6))}-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
3837
+ const a4 = pathe_M_eThtNZ_join(n4, `${basename2(i6, extname(i6))}-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
3838
3838
  return (0, $e.writeFileSync)(a4, t5), a4;
3839
3839
  })(l3, n3), debug(t4, "[esm]", "[tempfile]", c4), a3(pathToFileURL(c4))), E3 = y3 ? a3(y3).catch((e3) => {
3840
3840
  if ("ENAMETOOLONG" !== e3?.code) throw e3;
@@ -4210,7 +4210,7 @@ Did you specify these with the most recent transformation maps first?`);
4210
4210
  for (let e5 = 1; e5 < s; e5++) a += "/" + n[e5];
4211
4211
  (!a || o && !a.endsWith("/..")) && (a += "/"), e4.path = a;
4212
4212
  }
4213
- function resolve10(e4, t3) {
4213
+ function resolve12(e4, t3) {
4214
4214
  if (!e4 && !t3) return "";
4215
4215
  const r3 = parseUrl(e4);
4216
4216
  let n = r3.type;
@@ -4247,7 +4247,7 @@ Did you specify these with the most recent transformation maps first?`);
4247
4247
  return r3.scheme + "//" + r3.user + r3.host + r3.port + r3.path + s;
4248
4248
  }
4249
4249
  }
4250
- return resolve10;
4250
+ return resolve12;
4251
4251
  })();
4252
4252
  }, "./node_modules/.pnpm/@jridgewell+sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.umd.js"(e2, t2, r2) {
4253
4253
  var n;
@@ -7368,7 +7368,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7368
7368
  return e3;
7369
7369
  }, e3;
7370
7370
  }
7371
- Object.defineProperty(t2, "__esModule", { value: true }), t2.access = access2, t2.assertArray = assertArray, t2.assertAssumptions = function(e3, t3) {
7371
+ Object.defineProperty(t2, "__esModule", { value: true }), t2.access = access3, t2.assertArray = assertArray, t2.assertAssumptions = function(e3, t3) {
7372
7372
  if (void 0 === t3) return;
7373
7373
  if ("object" != typeof t3 || null === t3) throw new Error(`${msg(e3)} must be an object or undefined.`);
7374
7374
  let r3 = e3;
@@ -7377,7 +7377,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7377
7377
  } while ("root" !== r3.type);
7378
7378
  const s = "preset" === r3.source;
7379
7379
  for (const r4 of Object.keys(t3)) {
7380
- const i = access2(e3, r4);
7380
+ const i = access3(e3, r4);
7381
7381
  if (!n.assumptionsNames.has(r4)) throw new Error(`${msg(i)} is not a supported assumption.`);
7382
7382
  if ("boolean" != typeof t3[r4]) throw new Error(`${msg(i)} must be a boolean.`);
7383
7383
  if (s && false === t3[r4]) throw new Error(`${msg(i)} cannot be set to 'false' inside presets.`);
@@ -7386,7 +7386,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7386
7386
  }, t2.assertBabelrcSearch = function(e3, t3) {
7387
7387
  if (void 0 === t3 || "boolean" == typeof t3) return t3;
7388
7388
  if (Array.isArray(t3)) t3.forEach((t4, r3) => {
7389
- if (!checkValidTest(t4)) throw new Error(`${msg(access2(e3, r3))} must be a string/Function/RegExp.`);
7389
+ if (!checkValidTest(t4)) throw new Error(`${msg(access3(e3, r3))} must be a string/Function/RegExp.`);
7390
7390
  });
7391
7391
  else if (!checkValidTest(t3)) throw new Error(`${msg(e3)} must be a undefined, a boolean, a string/Function/RegExp or an array of those, got ${JSON.stringify(t3)}`);
7392
7392
  return t3;
@@ -7395,7 +7395,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7395
7395
  if (r3) {
7396
7396
  if ("string" != typeof r3.name) throw new Error(`${msg(e3)} set but does not contain "name" property string`);
7397
7397
  for (const t4 of Object.keys(r3)) {
7398
- const n2 = access2(e3, t4), s = r3[t4];
7398
+ const n2 = access3(e3, t4), s = r3[t4];
7399
7399
  if (null != s && "boolean" != typeof s && "string" != typeof s && "number" != typeof s) throw new Error(`${msg(n2)} must be null, undefined, a boolean, a string, or a number.`);
7400
7400
  }
7401
7401
  }
@@ -7406,7 +7406,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7406
7406
  }, t2.assertConfigApplicableTest = function(e3, t3) {
7407
7407
  if (void 0 === t3) return t3;
7408
7408
  if (Array.isArray(t3)) t3.forEach((t4, r3) => {
7409
- if (!checkValidTest(t4)) throw new Error(`${msg(access2(e3, r3))} must be a string/Function/RegExp.`);
7409
+ if (!checkValidTest(t4)) throw new Error(`${msg(access3(e3, r3))} must be a string/Function/RegExp.`);
7410
7410
  });
7411
7411
  else if (!checkValidTest(t3)) throw new Error(`${msg(e3)} must be a string/Function/RegExp, or an array of those`);
7412
7412
  return t3;
@@ -7421,7 +7421,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7421
7421
  return null == r3 || r3.forEach((t4, r4) => (function(e4, t5) {
7422
7422
  if ("string" != typeof t5 && "function" != typeof t5 && !(t5 instanceof RegExp)) throw new Error(`${msg(e4)} must be an array of string/Function/RegExp values, or undefined`);
7423
7423
  return t5;
7424
- })(access2(e3, r4), t4)), r3;
7424
+ })(access3(e3, r4), t4)), r3;
7425
7425
  }, t2.assertInputSourceMap = function(e3, t3) {
7426
7426
  if (void 0 !== t3 && "boolean" != typeof t3 && ("object" != typeof t3 || !t3)) throw new Error(`${msg(e3)} must be a boolean, object, or undefined`);
7427
7427
  return t3;
@@ -7431,17 +7431,17 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7431
7431
  if (Array.isArray(t5)) {
7432
7432
  if (0 === t5.length) throw new Error(`${msg(e4)} must include an object`);
7433
7433
  if (t5.length > 3) throw new Error(`${msg(e4)} may only be a two-tuple or three-tuple`);
7434
- if (assertPluginTarget(access2(e4, 0), t5[0]), t5.length > 1) {
7434
+ if (assertPluginTarget(access3(e4, 0), t5[0]), t5.length > 1) {
7435
7435
  const r5 = t5[1];
7436
- if (void 0 !== r5 && false !== r5 && ("object" != typeof r5 || Array.isArray(r5) || null === r5)) throw new Error(`${msg(access2(e4, 1))} must be an object, false, or undefined`);
7436
+ if (void 0 !== r5 && false !== r5 && ("object" != typeof r5 || Array.isArray(r5) || null === r5)) throw new Error(`${msg(access3(e4, 1))} must be an object, false, or undefined`);
7437
7437
  }
7438
7438
  if (3 === t5.length) {
7439
7439
  const r5 = t5[2];
7440
- if (void 0 !== r5 && "string" != typeof r5) throw new Error(`${msg(access2(e4, 2))} must be a string, or undefined`);
7440
+ if (void 0 !== r5 && "string" != typeof r5) throw new Error(`${msg(access3(e4, 2))} must be a string, or undefined`);
7441
7441
  }
7442
7442
  } else assertPluginTarget(e4, t5);
7443
7443
  return t5;
7444
- })(access2(e3, r4), t4));
7444
+ })(access3(e3, r4), t4));
7445
7445
  return r3;
7446
7446
  }, t2.assertRootMode = function(e3, t3) {
7447
7447
  if (void 0 !== t3 && "root" !== t3 && "upward" !== t3 && "upward-optional" !== t3) throw new Error(`${msg(e3)} must be a "root", "upward", "upward-optional" or undefined`);
@@ -7458,10 +7458,10 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7458
7458
  }, t2.assertTargets = function(e3, t3) {
7459
7459
  if ((0, _helperCompilationTargets().isBrowsersQueryValid)(t3)) return t3;
7460
7460
  if ("object" != typeof t3 || !t3 || Array.isArray(t3)) throw new Error(`${msg(e3)} must be a string, an array of strings or an object`);
7461
- const r3 = access2(e3, "browsers"), n2 = access2(e3, "esmodules");
7461
+ const r3 = access3(e3, "browsers"), n2 = access3(e3, "esmodules");
7462
7462
  assertBrowsersList(r3, t3.browsers), assertBoolean(n2, t3.esmodules);
7463
7463
  for (const r4 of Object.keys(t3)) {
7464
- const n3 = t3[r4], s = access2(e3, r4);
7464
+ const n3 = t3[r4], s = access3(e3, r4);
7465
7465
  if ("esmodules" === r4) assertBoolean(s, n3);
7466
7466
  else if ("browsers" === r4) assertBrowsersList(s, n3);
7467
7467
  else {
@@ -7491,7 +7491,7 @@ ${yield* s.optionsAndDescriptors(e3.content)}`;
7491
7491
  throw new Error(`Assertion failure: Unknown type ${e3.type}`);
7492
7492
  }
7493
7493
  }
7494
- function access2(e3, t3) {
7494
+ function access3(e3, t3) {
7495
7495
  return { type: "access", name: t3, parent: e3 };
7496
7496
  }
7497
7497
  function assertBoolean(e3, t3) {
@@ -39080,12 +39080,12 @@ var require_parser = __commonJS({
39080
39080
  }
39081
39081
  return prev.splice(i, prev.length);
39082
39082
  }
39083
- function arrayPushArray(target, source) {
39083
+ function arrayPushArray(target2, source) {
39084
39084
  if (source.length < 1e5)
39085
- Array.prototype.push.apply(target, source);
39085
+ Array.prototype.push.apply(target2, source);
39086
39086
  else
39087
39087
  for (let i = 0; i < source.length; ++i)
39088
- target.push(source[i]);
39088
+ target2.push(source[i]);
39089
39089
  }
39090
39090
  function fixFlowSeqItems(fc) {
39091
39091
  if (fc.start.type === "flow-seq-start") {
@@ -40034,14 +40034,15 @@ var require_dist = __commonJS({
40034
40034
 
40035
40035
  // src/config/load.ts
40036
40036
  import { hostname as readHostname, homedir, platform as readPlatform } from "os";
40037
- import { dirname, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
40038
- import { access } from "fs/promises";
40037
+ import { dirname, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
40038
+ import { access as access2 } from "fs/promises";
40039
40039
 
40040
40040
  // src/config/identity.ts
40041
40041
  import { createHash } from "crypto";
40042
40042
  function resourceId(resource) {
40043
40043
  switch (resource.kind) {
40044
40044
  case "package":
40045
+ if (resource.manager === "flatpak") return `package:flatpak:${resource.flatpak?.scope ?? "user"}:${resource.name}:${resource.flatpak?.branch ?? "stable"}`;
40045
40046
  return `package:${resource.manager}:${resource.name}`;
40046
40047
  case "symlink":
40047
40048
  return `file:${resource.target}`;
@@ -40096,6 +40097,20 @@ async function addPathToHash(hash, path, relativePath) {
40096
40097
  }
40097
40098
  }
40098
40099
 
40100
+ // src/config/package-options.ts
40101
+ function isFlatpakOptions(value2) {
40102
+ if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) return false;
40103
+ const options = value2;
40104
+ return Object.keys(options).every((key) => ["scope", "remote", "branch"].includes(key)) && (options.scope === void 0 || options.scope === "user" || options.scope === "system") && [options.remote, options.branch].every((field) => field === void 0 || typeof field === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(field));
40105
+ }
40106
+ function isPackageName(manager, name) {
40107
+ if (typeof name !== "string" || name.length === 0) return false;
40108
+ if (manager === "mas") return /^[1-9]\d*$/.test(name);
40109
+ if (manager === "flatpak") return /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*){2,}$/.test(name);
40110
+ if (manager === "pacman") return /^[A-Za-z0-9@_+][A-Za-z0-9@._+-]*$/.test(name);
40111
+ return true;
40112
+ }
40113
+
40099
40114
  // src/config/validation.ts
40100
40115
  function validateResource(value2) {
40101
40116
  if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) {
@@ -40104,7 +40119,7 @@ function validateResource(value2) {
40104
40119
  const candidate = value2;
40105
40120
  switch (candidate.kind) {
40106
40121
  case "package":
40107
- if (!["mise", "brew", "brew-cask", "apt", "system"].includes(String(candidate.manager)) || typeof candidate.name !== "string" || candidate.name.length === 0 || candidate.version !== void 0 && typeof candidate.version !== "string" || !isBrewCaskUpgradeOptions(candidate.upgrade)) {
40122
+ if (!["mise", "brew", "brew-cask", "apt", "dnf", "yum", "pacman", "flatpak", "mas", "system"].includes(String(candidate.manager)) || !isPackageName(candidate.manager, candidate.name) || candidate.flatpak !== void 0 && (!["flatpak", "system"].includes(String(candidate.manager)) || !isFlatpakOptions(candidate.flatpak)) || candidate.version !== void 0 && typeof candidate.version !== "string" || !isBrewCaskUpgradeOptions(candidate.upgrade)) {
40108
40123
  throw new Error("Invalid package resource");
40109
40124
  }
40110
40125
  return;
@@ -40191,11 +40206,11 @@ function resolveTasks(definition, context) {
40191
40206
  const cwd = value2.cwd?.replace(/^~(?=\/|$)/, context.home) ?? context.configDir;
40192
40207
  tasks[name] = { ...value2, cwd: isAbsolute(cwd) ? cwd : resolve2(context.configDir, cwd) };
40193
40208
  }
40194
- for (const [name, target] of Object.entries(definition.aliases ?? {})) {
40209
+ for (const [name, target2] of Object.entries(definition.aliases ?? {})) {
40195
40210
  validateName(name);
40196
40211
  if (Object.hasOwn(tasks, name)) throw new Error(`Task and alias share a name: ${name}`);
40197
- if (typeof target !== "string") throw new Error(`Invalid alias target: ${name}`);
40198
- aliases[name] = target;
40212
+ if (typeof target2 !== "string") throw new Error(`Invalid alias target: ${name}`);
40213
+ aliases[name] = target2;
40199
40214
  }
40200
40215
  for (const name of Object.keys(aliases)) resolveTaskName(name, tasks, aliases);
40201
40216
  return { tasks, aliases };
@@ -40206,9 +40221,9 @@ function resolveTaskName(name, tasks, aliases) {
40206
40221
  while (Object.hasOwn(aliases, current)) {
40207
40222
  if (visited.has(current)) throw new Error(`Task alias cycle: ${[...visited, current].join(" -> ")}`);
40208
40223
  visited.add(current);
40209
- const target = aliases[current];
40210
- if (target === void 0) throw new Error(`Invalid alias: ${current}`);
40211
- current = target;
40224
+ const target2 = aliases[current];
40225
+ if (target2 === void 0) throw new Error(`Invalid alias: ${current}`);
40226
+ current = target2;
40212
40227
  }
40213
40228
  if (!Object.hasOwn(tasks, current)) throw new Error(`Unknown task: ${current}`);
40214
40229
  return current;
@@ -40217,13 +40232,32 @@ function validateName(name) {
40217
40232
  if (!/^[a-zA-Z0-9][a-zA-Z0-9:_-]*$/.test(name) || name === "help" || name === "init") throw new Error(`Invalid or reserved task name: ${name}`);
40218
40233
  }
40219
40234
 
40235
+ // src/config/system-manager.ts
40236
+ import { access, stat } from "fs/promises";
40237
+ import { constants } from "fs";
40238
+ import { delimiter, resolve as resolve3 } from "path";
40239
+ async function detectLinuxManager(searchPath = process.env.PATH ?? "") {
40240
+ for (const [manager, command] of [["apt", "apt-get"], ["dnf", "dnf"], ["yum", "yum"], ["pacman", "pacman"]]) {
40241
+ for (const directory of searchPath.split(delimiter).filter(Boolean)) {
40242
+ const path = resolve3(directory, command);
40243
+ try {
40244
+ await access(path, constants.X_OK);
40245
+ if ((await stat(path)).isFile()) return manager;
40246
+ } catch (error) {
40247
+ if (!(error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR", "EACCES"].includes(String(error.code)))) throw error;
40248
+ }
40249
+ }
40250
+ }
40251
+ throw new Error("No supported Linux package manager found on PATH (apt-get, dnf, yum, pacman). Install one or set managers.linux explicitly.");
40252
+ }
40253
+
40220
40254
  // src/config/load.ts
40221
40255
  var DEFAULT_CONFIG_FILE = "workstation.config.ts";
40222
40256
  async function findConfig(explicit) {
40223
- if (explicit) return resolve3(explicit);
40224
- const path = resolve3(DEFAULT_CONFIG_FILE);
40257
+ if (explicit) return resolve4(explicit);
40258
+ const path = resolve4(DEFAULT_CONFIG_FILE);
40225
40259
  try {
40226
- await access(path);
40260
+ await access2(path);
40227
40261
  return path;
40228
40262
  } catch {
40229
40263
  throw new Error(`No configuration found (${DEFAULT_CONFIG_FILE})`);
@@ -40269,7 +40303,11 @@ async function loadConfig(configPath, machineOverride) {
40269
40303
  }
40270
40304
  async function resolveResource(resource, definition, context) {
40271
40305
  if (resource.kind === "package") {
40272
- const manager = resolveManager(resource.manager, definition, context);
40306
+ const manager = await resolveManager(resource.manager, definition, context);
40307
+ if (!isPackageName(manager, resource.name)) throw new Error(`Invalid ${manager} package ID: ${resource.name}`);
40308
+ if ((manager === "pacman" || manager === "flatpak") && context.platform !== "linux") throw new Error(`${manager} requires Linux`);
40309
+ if (manager === "mas" && context.platform !== "darwin") throw new Error("mas requires macOS");
40310
+ if (resource.flatpak !== void 0 && manager !== "flatpak") throw new Error(`Flatpak options require the flatpak backend (${resource.name})`);
40273
40311
  if (resource.upgrade !== void 0 && manager !== "brew-cask") {
40274
40312
  throw new Error(`Upgrade options are only supported for Homebrew casks (${resource.name})`);
40275
40313
  }
@@ -40279,7 +40317,11 @@ async function resolveResource(resource, definition, context) {
40279
40317
  if (resource.version !== void 0) {
40280
40318
  throw new Error(`${manager} package ${resource.name} cannot declare a version`);
40281
40319
  }
40282
- return { ...resource, manager };
40320
+ return { ...resource, manager, ...manager === "flatpak" ? { flatpak: {
40321
+ scope: resource.flatpak?.scope ?? "user",
40322
+ remote: resource.flatpak?.remote ?? "flathub",
40323
+ branch: resource.flatpak?.branch ?? "stable"
40324
+ } } : {} };
40283
40325
  }
40284
40326
  if (resource.kind === "symlink") {
40285
40327
  return {
@@ -40303,11 +40345,11 @@ async function resolveResource(resource, definition, context) {
40303
40345
  }
40304
40346
  if (resource.kind === "custom-tool") {
40305
40347
  const source = expandPath(resource.source, context, true);
40306
- const target = expandPath(resource.target, context, false);
40348
+ const target2 = expandPath(resource.target, context, false);
40307
40349
  return {
40308
40350
  ...resource,
40309
40351
  source,
40310
- target,
40352
+ target: target2,
40311
40353
  sourceHash: await hashSource(source),
40312
40354
  build: {
40313
40355
  ...resource.build,
@@ -40325,14 +40367,16 @@ async function resolveResource(resource, definition, context) {
40325
40367
  ...resource.stderrPath ? { stderrPath: expandPath(resource.stderrPath, context, false) } : {}
40326
40368
  };
40327
40369
  }
40328
- function resolveManager(manager, definition, context) {
40370
+ async function resolveManager(manager, definition, context) {
40329
40371
  if (manager !== "system") return manager;
40330
- return definition.managers?.[context.platform] ?? (context.platform === "darwin" ? "brew" : "apt");
40372
+ const configured = definition.managers?.[context.platform];
40373
+ if (configured !== void 0) return configured;
40374
+ return context.platform === "darwin" ? "brew" : await detectLinuxManager();
40331
40375
  }
40332
40376
  function expandPath(path, context, relativeToConfig) {
40333
40377
  const expanded = path === "~" ? context.home : path.replace(/^~\//, `${context.home}/`);
40334
- if (isAbsolute2(expanded)) return resolve3(expanded);
40335
- return resolve3(relativeToConfig ? context.configDir : context.home, expanded);
40378
+ if (isAbsolute2(expanded)) return resolve4(expanded);
40379
+ return resolve4(relativeToConfig ? context.configDir : context.home, expanded);
40336
40380
  }
40337
40381
  function flatten(input) {
40338
40382
  if (!input) return [];
@@ -40370,7 +40414,7 @@ function mergeDefinitions(definitions) {
40370
40414
 
40371
40415
  // src/persistence/manifest.ts
40372
40416
  import { mkdir, readFile as readFile2, rename, writeFile } from "fs/promises";
40373
- import { dirname as dirname2, resolve as resolve4 } from "path";
40417
+ import { dirname as dirname2, resolve as resolve5 } from "path";
40374
40418
 
40375
40419
  // node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
40376
40420
  var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
@@ -41169,7 +41213,7 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
41169
41213
  // src/persistence/manifest.ts
41170
41214
  var MANIFEST_VERSION = 1;
41171
41215
  function manifestPath(config) {
41172
- return resolve4(dirname2(config.stateFile), "config.toml");
41216
+ return resolve5(dirname2(config.stateFile), "config.toml");
41173
41217
  }
41174
41218
  async function writeManifest(path, config) {
41175
41219
  const document2 = {
@@ -41211,6 +41255,11 @@ function toTomlResource(resource) {
41211
41255
  name: resource.name,
41212
41256
  ...resource.version ? { version: resource.version } : {},
41213
41257
  ...resource.lockedVersion ? { locked_version: resource.lockedVersion } : {},
41258
+ ...resource.flatpak ? { flatpak: {
41259
+ ...resource.flatpak.scope ? { scope: resource.flatpak.scope } : {},
41260
+ ...resource.flatpak.remote ? { remote: resource.flatpak.remote } : {},
41261
+ ...resource.flatpak.branch ? { branch: resource.flatpak.branch } : {}
41262
+ } } : {},
41214
41263
  ...resource.upgrade ? {
41215
41264
  upgrade: {
41216
41265
  ...resource.upgrade.greedy !== void 0 ? { greedy: resource.upgrade.greedy } : {},
@@ -41361,6 +41410,9 @@ function parsePackage(value2) {
41361
41410
  if (!isPackageManager(manager)) {
41362
41411
  throw new Error(`Invalid manifest package manager: ${manager}`);
41363
41412
  }
41413
+ if (!isPackageName(manager, value2.name)) throw new Error(`Invalid ${manager} package ID`);
41414
+ if (value2.flatpak !== void 0 && (manager !== "flatpak" || !isFlatpakOptions(value2.flatpak))) throw new Error("Invalid Flatpak options");
41415
+ if (manager === "mas" && value2.locked_version !== void 0) throw new Error("mas does not support version pins");
41364
41416
  const upgrade = value2.upgrade === void 0 ? void 0 : requireTable(value2.upgrade, "package.upgrade");
41365
41417
  if (value2.version !== void 0 && manager !== "mise") {
41366
41418
  throw new Error(`Manifest ${manager} package cannot declare a version`);
@@ -41372,6 +41424,7 @@ function parsePackage(value2) {
41372
41424
  kind: "package",
41373
41425
  manager,
41374
41426
  name: requireString(value2.name, "package.name"),
41427
+ ...isFlatpakOptions(value2.flatpak) ? { flatpak: value2.flatpak } : {},
41375
41428
  ...value2.version !== void 0 ? { version: requireString(value2.version, "package.version") } : {},
41376
41429
  ...value2.locked_version !== void 0 ? { lockedVersion: requireString(value2.locked_version, "package.locked_version") } : {},
41377
41430
  ...upgrade ? {
@@ -41391,13 +41444,13 @@ function parseSymlink(value2) {
41391
41444
  }
41392
41445
  function parseLaunchAgent(value2) {
41393
41446
  const args = value2.args === void 0 ? void 0 : requireStringArray(value2.args, "launch-agent.args");
41394
- const environment = value2.environment === void 0 ? void 0 : requireStringTable(value2.environment, "launch-agent.environment");
41447
+ const environment3 = value2.environment === void 0 ? void 0 : requireStringTable(value2.environment, "launch-agent.environment");
41395
41448
  return {
41396
41449
  kind: "launch-agent",
41397
41450
  label: requireString(value2.label, "launch-agent.label"),
41398
41451
  program: requireString(value2.program, "launch-agent.program"),
41399
41452
  ...args ? { args } : {},
41400
- ...environment ? { environment } : {},
41453
+ ...environment3 ? { environment: environment3 } : {},
41401
41454
  ...value2.run_at_load !== void 0 ? { runAtLoad: requireBoolean(value2.run_at_load, "launch-agent.run_at_load") } : {},
41402
41455
  ...value2.keep_alive !== void 0 ? { keepAlive: requireBoolean(value2.keep_alive, "launch-agent.keep_alive") } : {},
41403
41456
  ...value2.stdout_path !== void 0 ? { stdoutPath: requireString(value2.stdout_path, "launch-agent.stdout_path") } : {},
@@ -41434,7 +41487,7 @@ function requireStringTable(value2, field) {
41434
41487
  return result;
41435
41488
  }
41436
41489
  function isPackageManager(value2) {
41437
- return value2 === "mise" || value2 === "brew" || value2 === "brew-cask" || value2 === "apt";
41490
+ return value2 === "mise" || value2 === "brew" || value2 === "brew-cask" || value2 === "apt" || value2 === "dnf" || value2 === "yum" || value2 === "pacman" || value2 === "flatpak" || value2 === "mas";
41438
41491
  }
41439
41492
  function isConfigValue2(value2) {
41440
41493
  if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean" || typeof value2 === "number" && Number.isFinite(value2)) {
@@ -41445,7 +41498,7 @@ function isConfigValue2(value2) {
41445
41498
  }
41446
41499
 
41447
41500
  // src/reconciliation/plan.ts
41448
- import { lstat as lstat5 } from "fs/promises";
41501
+ import { lstat as lstat6 } from "fs/promises";
41449
41502
 
41450
41503
  // src/resources/generated-file.ts
41451
41504
  import { lstat as lstat2, mkdir as mkdir2, readFile as readFile3, readlink as readlink2, rename as rename3, symlink, unlink } from "fs/promises";
@@ -41637,29 +41690,325 @@ function isRecord2(value2) {
41637
41690
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
41638
41691
  }
41639
41692
 
41640
- // src/resources/package.ts
41641
- async function reconcilePackage(resource, runner) {
41642
- if ((resource.manager === "brew" || resource.manager === "brew-cask") && resource.lockedVersion !== void 0) {
41643
- const before = await inspectPackage(resource, runner);
41644
- if (!before.present) {
41645
- await installPackage(resource, runner);
41646
- } else if (!before.matches) {
41647
- await upgradeLockedBrewPackage(resource, runner);
41693
+ // src/resources/mise-cask.ts
41694
+ import { lstat as lstat3, mkdtemp, readFile as readFile4, readdir as readdir2, realpath, rename as rename4, readlink as readlink3, symlink as symlink2 } from "fs/promises";
41695
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute3, join, resolve as resolve6 } from "path";
41696
+ async function inspectMiseCask(resource, runner) {
41697
+ const result = await requireSuccess(runner, "brew", ["--caskroom", resource.name]);
41698
+ const path = result.stdout.trim();
41699
+ if (!isAbsolute3(path) || basename(path) !== resource.name.split("/").at(-1) || dirname4(path) === path) {
41700
+ throw new Error(`Homebrew returned an invalid cask directory for ${resource.name}`);
41701
+ }
41702
+ try {
41703
+ const stat2 = await lstat3(path);
41704
+ if (!stat2.isDirectory() || stat2.isSymbolicLink()) return void 0;
41705
+ const entries = await readdir2(path, { withFileTypes: true });
41706
+ if (entries.length !== 1 || !entries[0]?.isDirectory()) return void 0;
41707
+ const version = entries[0].name;
41708
+ const receiptPath = join(path, version, ".mise-cask.toml");
41709
+ if (!(await lstat3(receiptPath)).isFile()) return void 0;
41710
+ const receipt = parse(await readFile4(receiptPath, "utf8"));
41711
+ if (receipt.schema_version !== 3 || receipt.version !== version || !Array.isArray(receipt.apps) || !Array.isArray(receipt.fonts) || !Array.isArray(receipt.binaries) || receipt.apps.length + receipt.fonts.length + receipt.binaries.length === 0 || !["flight_directories", "generic", "pkg_ids"].every((key) => Array.isArray(receipt[key]) && receipt[key].length === 0)) return void 0;
41712
+ const apps = [];
41713
+ for (const app of receipt.apps) {
41714
+ if (typeof app !== "string" || !isAbsolute3(app) || !app.endsWith(".app") || resolve6(app) !== app) return void 0;
41715
+ const staged = join(path, version, basename(app));
41716
+ if (!(await lstat3(staged)).isSymbolicLink() || await realpath(staged) !== await realpath(app) || !(await lstat3(app)).isDirectory() || app.startsWith(`${dirname4(path)}/`)) return void 0;
41717
+ apps.push(app);
41718
+ }
41719
+ const fonts = [];
41720
+ for (const font of receipt.fonts) {
41721
+ if (typeof font !== "string" || !isAbsolute3(font) || resolve6(font) !== font || !/\.(ttf|otf|ttc|dfont)$/i.test(font) || !dirname4(font).endsWith("/Library/Fonts") || font.startsWith(`${dirname4(path)}/`) || !(await lstat3(font)).isFile()) return void 0;
41722
+ const staged = join(path, version, basename(font));
41723
+ if (!(await lstat3(staged)).isFile() || !(await readFile4(staged)).equals(await readFile4(font))) return void 0;
41724
+ fonts.push(font);
41648
41725
  }
41649
- } else if (resource.manager === "brew-cask" && resource.upgrade !== void 0) {
41650
- const before = await inspectPackage(resource, runner);
41651
- if (before.present && !before.matches) {
41652
- await upgradePackage(resource, runner);
41653
- } else if (!before.present) {
41654
- await installPackage(resource, runner);
41726
+ const links = [];
41727
+ const linkRoots = [await realpath(join(path, version)), ...await Promise.all(apps.map((app) => realpath(app)))];
41728
+ if (!Array.isArray(receipt.completions)) return void 0;
41729
+ for (const completion of [...receipt.binaries, ...receipt.completions]) {
41730
+ if (typeof completion !== "string" || !isAbsolute3(completion) || resolve6(completion) !== completion || completion.startsWith(`${path}/`) || !(await lstat3(completion)).isSymbolicLink()) return void 0;
41731
+ const target2 = await readlink3(completion);
41732
+ const destination = await realpath(completion);
41733
+ if (!resolve6(dirname4(completion), target2).startsWith(`${path}/${version}/`) || !linkRoots.some((root) => destination.startsWith(`${root}/`))) return void 0;
41734
+ links.push({ path: completion, target: target2 });
41655
41735
  }
41736
+ return { path, version, apps: [...new Set(apps)], fonts: [...new Set(fonts)], links };
41737
+ } catch (error) {
41738
+ if (isMissingFile(error)) return void 0;
41739
+ throw error;
41740
+ }
41741
+ }
41742
+ async function migrateMiseCask(resource, mise, runner) {
41743
+ const backup = await mkdtemp(join(dirname4(dirname4(mise.path)), `.workstation-mise-${basename(mise.path)}-`));
41744
+ runner.report?.(`Migrating ${resource.name} from mise to Homebrew; backups: ${backup}`);
41745
+ for (const [index, app] of mise.apps.entries()) {
41746
+ await requireSuccess(runner, "/usr/bin/ditto", [app, join(backup, `app-${index}.app`)], { streamOutput: true });
41747
+ }
41748
+ for (const [index, font] of mise.fonts.entries()) {
41749
+ await requireSuccess(runner, "/usr/bin/ditto", [font, join(backup, `font-${index}`)], { streamOutput: true });
41750
+ }
41751
+ await rename4(mise.path, join(backup, "original-cask"));
41752
+ try {
41753
+ await requireSuccess(runner, "brew", [
41754
+ "install",
41755
+ "--cask",
41756
+ resource.lockedVersion === mise.version ? "--adopt" : "--force",
41757
+ resource.name
41758
+ ], {
41759
+ streamOutput: true,
41760
+ environment: { HOMEBREW_NO_AUTO_UPDATE: "1" }
41761
+ });
41762
+ const installed = await readInstalledBrewVersion(resource, runner);
41763
+ if (!installed || resource.lockedVersion !== void 0 && installed !== resource.lockedVersion) {
41764
+ throw new Error(`Homebrew migration expected ${resource.lockedVersion ?? "an installed version"}; reported ${installed ?? "no installed version"}`);
41765
+ }
41766
+ } catch (error) {
41767
+ try {
41768
+ try {
41769
+ await rename4(mise.path, join(backup, "failed-cask"));
41770
+ } catch (moveError) {
41771
+ if (!isMissingFile(moveError)) throw moveError;
41772
+ }
41773
+ await rename4(join(backup, "original-cask"), mise.path);
41774
+ for (const [index, link] of mise.links.entries()) {
41775
+ try {
41776
+ const stat2 = await lstat3(link.path);
41777
+ if (stat2.isSymbolicLink()) {
41778
+ const current = await readlink3(link.path);
41779
+ const destination = resolve6(dirname4(link.path), current);
41780
+ if (current !== link.target && [mise.path, ...mise.apps].some((root) => destination.startsWith(`${root}/`))) {
41781
+ await rename4(link.path, join(backup, `failed-link-${index}`));
41782
+ await symlink2(link.target, link.path);
41783
+ }
41784
+ }
41785
+ } catch (linkError) {
41786
+ if (!isMissingFile(linkError)) throw linkError;
41787
+ await symlink2(link.target, link.path);
41788
+ }
41789
+ }
41790
+ for (const [index, app] of mise.apps.entries()) {
41791
+ try {
41792
+ await lstat3(app);
41793
+ } catch (appError) {
41794
+ if (!isMissingFile(appError)) throw appError;
41795
+ await requireSuccess(runner, "/usr/bin/ditto", [join(backup, `app-${index}.app`), app], { streamOutput: true });
41796
+ }
41797
+ }
41798
+ for (const [index, font] of mise.fonts.entries()) {
41799
+ try {
41800
+ await lstat3(font);
41801
+ } catch (fontError) {
41802
+ if (!isMissingFile(fontError)) throw fontError;
41803
+ await requireSuccess(runner, "/usr/bin/ditto", [join(backup, `font-${index}`), font], { streamOutput: true });
41804
+ }
41805
+ }
41806
+ } catch (restoreError) {
41807
+ throw new AggregateError([error, restoreError], `Migration and recovery failed for ${resource.name}. Backups: ${backup}`, { cause: restoreError });
41808
+ }
41809
+ throw new Error(`Migration failed for ${resource.name}: ${error instanceof Error ? error.message : String(error)}. Mise staging restored; artifact backups: ${backup}`, { cause: error });
41810
+ }
41811
+ runner.report?.(`Migrated ${resource.name} from mise to Homebrew. Backups: ${backup}. Remove brew-cask:${resource.name} from your mise configuration; do not run mise uninstall for this cask.`);
41812
+ }
41813
+
41814
+ // src/resources/package-command.ts
41815
+ async function runPackageCommand(mutation, runner) {
41816
+ if (!mutation) return;
41817
+ await requireSuccess(runner, mutation.command, [...mutation.args, ...mutation.targets], { ...mutation.options, streamOutput: true });
41818
+ }
41819
+ function packageCommandKey(mutation) {
41820
+ return mutation.single ? void 0 : JSON.stringify([mutation.command, mutation.args, mutation.options]);
41821
+ }
41822
+
41823
+ // src/resources/rpm.ts
41824
+ var environment = { LC_ALL: "C" };
41825
+ var versionPattern = /^\d+:[A-Za-z0-9._+~^]+-[A-Za-z0-9._+~^]+\.[A-Za-z0-9_]+$/;
41826
+ async function resolveRpmVersion(resource, runner) {
41827
+ const architecture = (await requireSuccess(runner, "rpm", ["--eval", "%{_arch}"], { environment })).stdout.trim();
41828
+ if (!/^[A-Za-z0-9_]+$/.test(architecture)) throw new Error("RPM did not report a valid native architecture");
41829
+ const result = await requireSuccess(runner, resource.manager, ["--quiet", "--color=never", "list", resource.name], { environment });
41830
+ const candidates = [];
41831
+ let available = false;
41832
+ const lines = result.stdout.replace(/^(\S+\.\S+)\n[ \t]+(?=\S+\s+\S+)/gm, "$1 ").split("\n");
41833
+ for (const line of lines) {
41834
+ if (/^Available packages\s*$/i.test(line.trim())) {
41835
+ available = true;
41836
+ continue;
41837
+ }
41838
+ if (/^Installed packages\s*$/i.test(line.trim())) {
41839
+ available = false;
41840
+ continue;
41841
+ }
41842
+ const [nameArch, evr, repository] = line.trim().split(/\s+/);
41843
+ if (!nameArch || !evr || !repository) continue;
41844
+ const dot = nameArch.lastIndexOf(".");
41845
+ const name = nameArch.slice(0, dot);
41846
+ const arch = nameArch.slice(dot + 1);
41847
+ if (nameArch !== resource.name && (name !== resource.name || arch !== architecture && arch !== "noarch")) continue;
41848
+ const version = `${evr.includes(":") ? evr : `0:${evr}`}.${arch}`;
41849
+ if (!versionPattern.test(version)) throw new Error(`${resource.manager} reported an invalid RPM version for ${resource.name}: ${version}`);
41850
+ candidates.push({ version, native: arch === architecture, available });
41851
+ }
41852
+ candidates.sort((left, right) => Number(right.available) - Number(left.available) || Number(right.native) - Number(left.native));
41853
+ const candidate = candidates[0];
41854
+ if (!candidate) throw new Error(`${resource.manager} has no installation candidate for ${resource.name}`);
41855
+ return candidate.version;
41856
+ }
41857
+ async function inspectRpmPackage(resource, runner) {
41858
+ const result = await runner.run("rpm", ["-q", "--qf", "%{EPOCHNUM}:%{VERSION}-%{RELEASE}.%{ARCH}\n", resource.name], { environment });
41859
+ if (result.exitCode !== 0) {
41860
+ if (result.exitCode === 1 && result.stdout.trim() === `package ${resource.name} is not installed` && !result.stderr.trim()) {
41861
+ return { present: false, matches: false };
41862
+ }
41863
+ throw new Error(`rpm query failed for ${resource.name} (${result.exitCode}): ${(result.stderr || result.stdout).trim()}`);
41864
+ }
41865
+ const versions = result.stdout.trim().split("\n");
41866
+ if (!versions.every((version) => versionPattern.test(version))) throw new Error(`RPM returned invalid installed versions for ${resource.name}`);
41867
+ const requestedArch = resource.lockedVersion?.slice(resource.lockedVersion.lastIndexOf(".") + 1);
41868
+ const installedVersion = versions.find((version) => version === resource.lockedVersion) ?? versions.find((version) => requestedArch !== void 0 && version.endsWith(`.${requestedArch}`)) ?? versions[0];
41869
+ if (!installedVersion) throw new Error(`RPM reported no installed version for ${resource.name}`);
41870
+ return { present: true, matches: resource.lockedVersion === void 0 || installedVersion === resource.lockedVersion, installedVersion };
41871
+ }
41872
+ async function prepareRpmPackage(resource, runner, inspection) {
41873
+ const before = inspection ?? await inspectRpmPackage(resource, runner);
41874
+ if (before.matches) return;
41875
+ let operation = "install";
41876
+ let spec = resource.name;
41877
+ if (resource.lockedVersion !== void 0) {
41878
+ const desired = resource.lockedVersion;
41879
+ if (!versionPattern.test(desired)) throw new Error(`Invalid RPM lock version for ${resource.name}: ${desired}`);
41880
+ const arch = desired.slice(desired.lastIndexOf(".") + 1);
41881
+ const name = resource.name.endsWith(`.${arch}`) ? resource.name.slice(0, -arch.length - 1) : resource.name;
41882
+ spec = `${name}-${desired}`;
41883
+ if (before.installedVersion?.endsWith(`.${arch}`)) {
41884
+ const current = before.installedVersion.slice(0, before.installedVersion.lastIndexOf("."));
41885
+ const target2 = desired.slice(0, desired.lastIndexOf("."));
41886
+ const comparison = (await requireSuccess(runner, "rpm", ["--eval", `%{lua:print(rpm.vercmp("${current}", "${target2}"))}`], { environment })).stdout.trim();
41887
+ if (!["-1", "0", "1"].includes(comparison)) throw new Error(`RPM could not compare versions for ${resource.name}`);
41888
+ if (comparison === "1") operation = "downgrade";
41889
+ }
41890
+ }
41891
+ return { command: "sudo", args: [resource.manager, operation, "-y"], targets: [spec] };
41892
+ }
41893
+
41894
+ // src/resources/pacman.ts
41895
+ var environment2 = { LC_ALL: "C" };
41896
+ async function resolvePacmanVersion(resource, runner) {
41897
+ const result = await requireSuccess(runner, "pacman", ["-Sp", "--print-format", "%n %v", resource.name], { environment: environment2 });
41898
+ const versions = result.stdout.trim().split("\n").flatMap((line) => {
41899
+ const [name, version, extra] = line.trim().split(/\s+/);
41900
+ return name === resource.name && version && !extra ? [version] : [];
41901
+ });
41902
+ if (versions.length !== 1 || !versions[0]) throw new Error(`pacman did not report a unique version for ${resource.name}`);
41903
+ return versions[0];
41904
+ }
41905
+ async function inspectPacman(resource, runner) {
41906
+ const result = await runner.run("pacman", ["-Q", resource.name], { environment: environment2 });
41907
+ if (result.exitCode !== 0) {
41908
+ if (result.exitCode === 1 && !result.stdout.trim() && result.stderr.trim() === `error: package '${resource.name}' was not found`) {
41909
+ return { present: false, matches: false };
41910
+ }
41911
+ throw new Error(`pacman query failed for ${resource.name} (${result.exitCode}): ${(result.stderr || result.stdout).trim()}`);
41912
+ }
41913
+ const [name, installedVersion, extra] = result.stdout.trim().split(/\s+/);
41914
+ if (name !== resource.name || !installedVersion || extra) throw new Error(`pacman returned invalid installed information for ${resource.name}`);
41915
+ return { present: true, matches: resource.lockedVersion === void 0 || resource.lockedVersion === installedVersion, installedVersion };
41916
+ }
41917
+ async function preparePacman(resource, runner, inspection) {
41918
+ if ((inspection ?? await inspectPacman(resource, runner)).matches) return;
41919
+ const available = await resolvePacmanVersion(resource, runner);
41920
+ if (resource.lockedVersion !== void 0 && available !== resource.lockedVersion) {
41921
+ throw new Error(`pacman offers ${resource.name} ${available}, but workstation.lock pins ${resource.lockedVersion}. Refresh the pin after your normal full system upgrade; Workstation does not fetch archived packages.`);
41922
+ }
41923
+ return { command: "sudo", args: ["pacman", "-S", "--needed", "--noconfirm"], targets: [resource.name] };
41924
+ }
41925
+
41926
+ // src/resources/flatpak.ts
41927
+ function target(resource) {
41928
+ const scope = resource.flatpak?.scope ?? "user";
41929
+ const branch = resource.flatpak?.branch ?? "stable";
41930
+ return { scope, branch, flag: `--${scope}`, remote: resource.flatpak?.remote ?? "flathub", ref: `app/${resource.name}//${branch}` };
41931
+ }
41932
+ function commit(value2) {
41933
+ const hash = value2.trim();
41934
+ if (!/^[a-f0-9]{64}$/.test(hash)) throw new Error(`Flatpak did not report a full commit ID: ${hash}`);
41935
+ return hash;
41936
+ }
41937
+ async function resolveFlatpakVersion(resource, runner) {
41938
+ const { flag, remote, ref } = target(resource);
41939
+ return commit((await requireSuccess(runner, "flatpak", ["remote-info", flag, "--show-commit", remote, ref])).stdout);
41940
+ }
41941
+ async function inspectFlatpak(resource, runner) {
41942
+ const { flag, remote, ref, branch } = target(resource);
41943
+ const result = await requireSuccess(runner, "flatpak", ["list", flag, "--app", "--columns=application,branch,origin"]);
41944
+ const rows = result.stdout.trim() === "" ? [] : result.stdout.trim().split("\n").map((line) => line.split(" "));
41945
+ if (rows.some((row) => row.length !== 3 || row.some((value2) => value2 === ""))) throw new Error("Flatpak returned an invalid application list");
41946
+ const installed = rows.find(([id, installedBranch]) => id === resource.name && installedBranch === branch);
41947
+ if (!installed) return { present: false, matches: false };
41948
+ if (installed[2] !== remote) return { present: true, matches: false, conflict: `${resource.name} is installed from ${installed[2]}, but the configuration requests ${remote}. Migrate its remote explicitly.` };
41949
+ const installedVersion = commit((await requireSuccess(runner, "flatpak", ["info", flag, "--show-commit", ref])).stdout);
41950
+ return { present: true, matches: resource.lockedVersion === void 0 || installedVersion === resource.lockedVersion, installedVersion };
41951
+ }
41952
+ async function prepareFlatpak(resource, runner, inspection) {
41953
+ const before = inspection ?? await inspectFlatpak(resource, runner);
41954
+ if (before.conflict) throw new Error(before.conflict);
41955
+ if (before.matches) return;
41956
+ const { flag, scope, remote, ref } = target(resource);
41957
+ if (resource.lockedVersion !== void 0) commit(resource.lockedVersion);
41958
+ if (!before.present && resource.lockedVersion !== void 0 && await resolveFlatpakVersion(resource, runner) !== resource.lockedVersion) {
41959
+ throw new Error(`Flatpak's current ${resource.name} commit differs from workstation.lock. Refresh its pin before a fresh install; existing installations can update to retained historical commits.`);
41960
+ }
41961
+ const args = before.present ? ["update", flag, "--noninteractive", "--assumeyes", ...resource.lockedVersion ? [`--commit=${resource.lockedVersion}`] : []] : ["install", flag, "--noninteractive", "--assumeyes", "--app", remote];
41962
+ return {
41963
+ command: scope === "system" ? "sudo" : "flatpak",
41964
+ args: scope === "system" ? ["flatpak", ...args] : args,
41965
+ targets: [ref],
41966
+ ...before.present && resource.lockedVersion ? { single: true } : {}
41967
+ };
41968
+ }
41969
+ function prepareFlatpakRemoval(resource) {
41970
+ const { flag, scope, ref } = target(resource);
41971
+ const args = ["uninstall", flag, "--noninteractive", "--assumeyes", "--app"];
41972
+ return { command: scope === "system" ? "sudo" : "flatpak", args: scope === "system" ? ["flatpak", ...args] : args, targets: [ref] };
41973
+ }
41974
+
41975
+ // src/resources/mas.ts
41976
+ function appRows(output) {
41977
+ return output.trim() === "" ? [] : output.trim().split("\n").map((line) => {
41978
+ const match = /^\s*(\d+)\s+.+\s+\(([^()]+)\)\s*$/.exec(line);
41979
+ if (!match?.[1] || !match[2]) throw new Error(`mas returned an unrecognized app row: ${line}`);
41980
+ return { id: match[1], version: match[2] };
41981
+ });
41982
+ }
41983
+ async function inspectMas(resource, runner) {
41984
+ if (resource.lockedVersion !== void 0) throw new Error("mas does not support version pins");
41985
+ const installed = appRows((await requireSuccess(runner, "mas", ["list"])).stdout).find((app) => app.id === resource.name);
41986
+ if (!installed) return { present: false, matches: false };
41987
+ const outdated = appRows((await requireSuccess(runner, "mas", ["outdated"])).stdout);
41988
+ return { present: true, matches: !outdated.some((app) => app.id === resource.name), installedVersion: installed.version };
41989
+ }
41990
+ async function prepareMas(resource, runner, inspection) {
41991
+ const before = inspection ?? await inspectMas(resource, runner);
41992
+ if (before.matches) return;
41993
+ return { command: "mas", args: [before.present ? "upgrade" : "install"], targets: [resource.name] };
41994
+ }
41995
+
41996
+ // src/resources/package.ts
41997
+ async function reconcilePackage(resource, runner) {
41998
+ const before = await inspectPackage(resource, runner);
41999
+ if (before.conflict) throw new Error(before.conflict);
42000
+ if (before.migration === "mise-cask") {
42001
+ await assertBrewVersionAvailable(resource, runner);
42002
+ const mise = await inspectMiseCask(resource, runner);
42003
+ if (!mise) throw new Error(`Mise installation changed during inspection: ${resource.name}`);
42004
+ await migrateMiseCask(resource, mise, runner);
41656
42005
  } else {
41657
- await installPackage(resource, runner);
42006
+ await runPackageCommand(await preparePackageInstallation(resource, runner, before), runner);
41658
42007
  }
41659
42008
  const inspection = await inspectPackage(resource, runner);
41660
42009
  if (!inspection.matches) {
41661
42010
  throw new Error(
41662
- inspection.conflict ?? `${resource.manager} reported success but ${resource.name} does not match the requested version`
42011
+ inspection.conflict ?? `${resource.manager} reported success but ${resource.name} did not converge: expected ${resource.lockedVersion ?? resource.version ?? "no available upgrades"}; installed ${inspection.installedVersion ?? (inspection.present ? "version unknown" : "not present")}. Check the package manager output above for skipped upgrades or warnings.`
41663
42012
  );
41664
42013
  }
41665
42014
  return inspection;
@@ -41685,10 +42034,17 @@ async function inspectPackage(resource, runner) {
41685
42034
  const installed = result.exitCode === 0;
41686
42035
  if (installed && resource.lockedVersion !== void 0) {
41687
42036
  const installedVersion = await readInstalledBrewVersion(resource, runner);
42037
+ if (installedVersion === void 0 && resource.manager === "brew-cask") {
42038
+ const mise = await inspectMiseCask(resource, runner);
42039
+ if (mise) return { present: true, matches: false, installedVersion: mise.version, migration: "mise-cask" };
42040
+ }
41688
42041
  return {
41689
42042
  present: true,
41690
42043
  matches: installedVersion === resource.lockedVersion,
41691
- ...installedVersion ? { installedVersion } : {}
42044
+ ...installedVersion ? { installedVersion } : {},
42045
+ ...installedVersion === void 0 ? {
42046
+ conflict: `Homebrew lists ${resource.name} but reports no installed version. Its installation metadata may be incomplete; inspect brew info --json=v2 ${flag} ${resource.name} and repair the Homebrew installation before retrying.`
42047
+ } : {}
41692
42048
  };
41693
42049
  }
41694
42050
  if (!installed || resource.manager !== "brew-cask" || resource.upgrade === void 0) {
@@ -41716,52 +42072,54 @@ async function inspectPackage(resource, runner) {
41716
42072
  ...installedVersion ? { installedVersion } : {}
41717
42073
  };
41718
42074
  }
42075
+ case "dnf":
42076
+ case "yum":
42077
+ return await inspectRpmPackage(resource, runner);
42078
+ case "pacman":
42079
+ return await inspectPacman(resource, runner);
42080
+ case "flatpak":
42081
+ return await inspectFlatpak(resource, runner);
42082
+ case "mas":
42083
+ return await inspectMas(resource, runner);
41719
42084
  case "system":
41720
42085
  throw new Error("System package manager must be resolved before inspection");
41721
42086
  }
41722
42087
  }
41723
- async function upgradePackage(resource, runner) {
41724
- if (resource.manager !== "brew-cask" || resource.upgrade === void 0) {
41725
- throw new Error(`Upgrade policy is not configured for ${resource.name}`);
41726
- }
41727
- await assertBrewVersionAvailable(resource, runner);
41728
- const args = ["upgrade", "--cask", "--no-ask"];
41729
- if (resource.upgrade.greedy) args.push("--greedy");
41730
- if (resource.upgrade.force) args.push("--force");
41731
- args.push(resource.name);
41732
- await requireSuccess(runner, "brew", args);
41733
- }
41734
- async function upgradeLockedBrewPackage(resource, runner) {
41735
- if (resource.manager !== "brew" && resource.manager !== "brew-cask") {
41736
- throw new Error(`Cannot upgrade non-Homebrew package ${resource.name}`);
41737
- }
41738
- await assertBrewVersionAvailable(resource, runner);
41739
- const args = ["upgrade"];
41740
- if (resource.manager === "brew-cask") {
41741
- args.push("--cask", "--no-ask");
41742
- if (resource.upgrade?.greedy) args.push("--greedy");
41743
- if (resource.upgrade?.force) args.push("--force");
41744
- }
41745
- args.push(resource.name);
41746
- await requireSuccess(runner, "brew", args);
41747
- }
41748
- async function installPackage(resource, runner) {
42088
+ async function preparePackageInstallation(resource, runner, before) {
42089
+ if (before.conflict) throw new Error(before.conflict);
42090
+ if (before.migration) throw new Error(`Cask migration must run separately: ${resource.name}`);
42091
+ if (before.matches) return;
41749
42092
  let command;
41750
42093
  let args;
42094
+ let spec = resource.name;
41751
42095
  switch (resource.manager) {
41752
42096
  case "mise":
41753
42097
  command = "mise";
41754
- args = ["install", `${resource.name}@${resource.lockedVersion ?? resource.version ?? "latest"}`];
42098
+ args = ["install"];
42099
+ spec = `${resource.name}@${resource.lockedVersion ?? resource.version ?? "latest"}`;
41755
42100
  break;
42101
+ case "dnf":
42102
+ case "yum":
42103
+ return await prepareRpmPackage(resource, runner, before);
42104
+ case "pacman":
42105
+ return await preparePacman(resource, runner, before);
42106
+ case "flatpak":
42107
+ return await prepareFlatpak(resource, runner, before);
42108
+ case "mas":
42109
+ return await prepareMas(resource, runner, before);
41756
42110
  case "brew":
41757
- await assertBrewVersionAvailable(resource, runner);
41758
- command = "brew";
41759
- args = ["install", resource.name];
41760
- break;
41761
42111
  case "brew-cask":
41762
42112
  await assertBrewVersionAvailable(resource, runner);
41763
42113
  command = "brew";
41764
- args = ["install", "--cask", resource.name];
42114
+ args = [before.present ? "upgrade" : "install"];
42115
+ if (resource.manager === "brew-cask") {
42116
+ args.push("--cask");
42117
+ if (before.present) {
42118
+ args.push("--no-ask");
42119
+ if (resource.lockedVersion || resource.upgrade?.greedy) args.push("--greedy");
42120
+ if (resource.upgrade?.force) args.push("--force");
42121
+ }
42122
+ }
41765
42123
  break;
41766
42124
  case "apt":
41767
42125
  command = "sudo";
@@ -41769,14 +42127,20 @@ async function installPackage(resource, runner) {
41769
42127
  "apt-get",
41770
42128
  "install",
41771
42129
  "-y",
41772
- ...resource.lockedVersion ? ["--allow-downgrades"] : [],
41773
- resource.lockedVersion ? `${resource.name}=${resource.lockedVersion}` : resource.name
42130
+ ...resource.lockedVersion ? ["--allow-downgrades"] : []
41774
42131
  ];
42132
+ spec = resource.lockedVersion ? `${resource.name}=${resource.lockedVersion}` : resource.name;
41775
42133
  break;
41776
42134
  case "system":
41777
42135
  throw new Error("System package manager must be resolved before installation");
41778
42136
  }
41779
- await requireSuccess(runner, command, args);
42137
+ return { command, args, targets: [spec], options: brewMutationOptions(resource) };
42138
+ }
42139
+ function brewMutationOptions(resource) {
42140
+ return {
42141
+ streamOutput: true,
42142
+ ...(resource.manager === "brew" || resource.manager === "brew-cask") && resource.lockedVersion ? { environment: { HOMEBREW_NO_AUTO_UPDATE: "1" } } : {}
42143
+ };
41780
42144
  }
41781
42145
  async function assertBrewVersionAvailable(resource, runner) {
41782
42146
  if (!resource.lockedVersion) return;
@@ -41788,41 +42152,60 @@ async function assertBrewVersionAvailable(resource, runner) {
41788
42152
  }
41789
42153
  }
41790
42154
  async function removePackage(resource, runner, installedVersion) {
42155
+ await runPackageCommand(preparePackageRemoval(resource, installedVersion), runner);
42156
+ }
42157
+ function preparePackageRemoval(resource, installedVersion) {
41791
42158
  let command;
41792
42159
  let args;
42160
+ let spec = resource.name;
41793
42161
  switch (resource.manager) {
41794
42162
  case "mise":
41795
42163
  command = "mise";
41796
42164
  args = [
41797
42165
  "uninstall",
41798
- "--yes",
41799
- `${resource.name}@${installedVersion ?? resource.lockedVersion ?? resource.version ?? "latest"}`
42166
+ "--yes"
41800
42167
  ];
42168
+ spec = `${resource.name}@${installedVersion ?? resource.lockedVersion ?? resource.version ?? "latest"}`;
41801
42169
  break;
41802
42170
  case "brew":
41803
42171
  command = "brew";
41804
- args = ["uninstall", resource.name];
42172
+ args = ["uninstall"];
41805
42173
  break;
41806
42174
  case "brew-cask":
41807
42175
  command = "brew";
41808
- args = ["uninstall", "--cask", resource.name];
42176
+ args = ["uninstall", "--cask"];
41809
42177
  break;
41810
42178
  case "apt":
41811
42179
  command = "sudo";
41812
- args = ["apt-get", "remove", "-y", resource.name];
42180
+ args = ["apt-get", "remove", "-y"];
42181
+ break;
42182
+ case "dnf":
42183
+ case "yum":
42184
+ command = "sudo";
42185
+ args = [resource.manager, "remove", "-y"];
42186
+ break;
42187
+ case "pacman":
42188
+ command = "sudo";
42189
+ args = ["pacman", "-R", "--noconfirm"];
42190
+ break;
42191
+ case "flatpak":
42192
+ return prepareFlatpakRemoval(resource);
42193
+ case "mas":
42194
+ command = "mas";
42195
+ args = ["uninstall"];
41813
42196
  break;
41814
42197
  case "system":
41815
42198
  throw new Error("System package manager must be resolved before removal");
41816
42199
  }
41817
- await requireSuccess(runner, command, args);
42200
+ return { command, args, targets: [spec] };
41818
42201
  }
41819
42202
 
41820
42203
  // src/resources/symlink.ts
41821
- import { lstat as lstat3, mkdir as mkdir3, readlink as readlink3, symlink as symlink2, unlink as unlink2 } from "fs/promises";
41822
- import { dirname as dirname4, resolve as resolve5 } from "path";
42204
+ import { lstat as lstat4, mkdir as mkdir3, readlink as readlink4, symlink as symlink3, unlink as unlink2 } from "fs/promises";
42205
+ import { dirname as dirname5, resolve as resolve7 } from "path";
41823
42206
  async function inspectSymlink(resource) {
41824
42207
  try {
41825
- const stats = await lstat3(resource.target);
42208
+ const stats = await lstat4(resource.target);
41826
42209
  if (!stats.isSymbolicLink()) {
41827
42210
  return {
41828
42211
  present: true,
@@ -41830,8 +42213,8 @@ async function inspectSymlink(resource) {
41830
42213
  conflict: `${resource.target} exists and is not a symbolic link`
41831
42214
  };
41832
42215
  }
41833
- const target = await readlink3(resource.target);
41834
- const actual = resolve5(dirname4(resource.target), target);
42216
+ const target2 = await readlink4(resource.target);
42217
+ const actual = resolve7(dirname5(resource.target), target2);
41835
42218
  const matches = actual === resource.source;
41836
42219
  return {
41837
42220
  present: true,
@@ -41847,12 +42230,12 @@ async function installSymlink(resource) {
41847
42230
  const inspection = await inspectSymlink(resource);
41848
42231
  if (inspection.conflict) throw new Error(inspection.conflict);
41849
42232
  if (inspection.matches) return;
41850
- await lstat3(resource.source).catch((error) => {
42233
+ await lstat4(resource.source).catch((error) => {
41851
42234
  if (isMissingFile(error)) throw new Error(`Symlink source does not exist: ${resource.source}`);
41852
42235
  throw error;
41853
42236
  });
41854
- await mkdir3(dirname4(resource.target), { recursive: true });
41855
- await symlink2(resource.source, resource.target);
42237
+ await mkdir3(dirname5(resource.target), { recursive: true });
42238
+ await symlink3(resource.source, resource.target);
41856
42239
  }
41857
42240
  async function removeSymlink(resource) {
41858
42241
  const inspection = await inspectSymlink(resource);
@@ -41864,13 +42247,13 @@ async function removeSymlink(resource) {
41864
42247
  }
41865
42248
 
41866
42249
  // src/resources/launch-agent.ts
41867
- import { mkdir as mkdir4, readFile as readFile4, unlink as unlink3 } from "fs/promises";
41868
- import { dirname as dirname5, resolve as resolve6 } from "path";
42250
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink3 } from "fs/promises";
42251
+ import { dirname as dirname6, resolve as resolve8 } from "path";
41869
42252
 
41870
42253
  // src/rendering/plist.ts
41871
42254
  function renderLaunchAgent(resource) {
41872
42255
  const args = resource.args ?? [];
41873
- const environment = Object.entries(resource.environment ?? {}).sort(
42256
+ const environment3 = Object.entries(resource.environment ?? {}).sort(
41874
42257
  ([left], [right]) => left.localeCompare(right)
41875
42258
  );
41876
42259
  const lines = [
@@ -41888,9 +42271,9 @@ function renderLaunchAgent(resource) {
41888
42271
  ];
41889
42272
  if (resource.runAtLoad ?? true) lines.push(" <key>RunAtLoad</key>", " <true/>");
41890
42273
  if (resource.keepAlive ?? false) lines.push(" <key>KeepAlive</key>", " <true/>");
41891
- if (environment.length > 0) {
42274
+ if (environment3.length > 0) {
41892
42275
  lines.push(" <key>EnvironmentVariables</key>", " <dict>");
41893
- for (const [key, value2] of environment) {
42276
+ for (const [key, value2] of environment3) {
41894
42277
  lines.push(` <key>${escapeXml(key)}</key>`, ` <string>${escapeXml(value2)}</string>`);
41895
42278
  }
41896
42279
  lines.push(" </dict>");
@@ -41918,7 +42301,7 @@ function escapeXml(value2) {
41918
42301
  async function inspectLaunchAgent(resource) {
41919
42302
  const path = launchAgentPath(resource);
41920
42303
  try {
41921
- const current = await readFile4(path, "utf8");
42304
+ const current = await readFile5(path, "utf8");
41922
42305
  const matches = current === renderLaunchAgent(resource);
41923
42306
  return {
41924
42307
  present: true,
@@ -41935,7 +42318,7 @@ async function installLaunchAgent(resource, runner) {
41935
42318
  if (inspection.present && !inspection.matches) throw new Error(inspection.conflict);
41936
42319
  if (!inspection.present) {
41937
42320
  const path = launchAgentPath(resource);
41938
- await mkdir4(dirname5(path), { recursive: true });
42321
+ await mkdir4(dirname6(path), { recursive: true });
41939
42322
  await atomicWrite(path, renderLaunchAgent(resource));
41940
42323
  }
41941
42324
  const domain = `gui/${process.getuid?.() ?? 0}`;
@@ -41959,13 +42342,13 @@ async function removeLaunchAgent(resource, runner) {
41959
42342
  function launchAgentPath(resource) {
41960
42343
  const home = process.env.HOME;
41961
42344
  if (!home) throw new Error("HOME is required to manage LaunchAgents");
41962
- return resolve6(home, "Library", "LaunchAgents", `${resource.label}.plist`);
42345
+ return resolve8(home, "Library", "LaunchAgents", `${resource.label}.plist`);
41963
42346
  }
41964
42347
 
41965
42348
  // src/resources/systemd-service.ts
41966
- import { mkdir as mkdir5, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
42349
+ import { mkdir as mkdir5, readFile as readFile6, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
41967
42350
  import { tmpdir } from "os";
41968
- import { dirname as dirname6, resolve as resolve7 } from "path";
42351
+ import { dirname as dirname7, resolve as resolve9 } from "path";
41969
42352
 
41970
42353
  // src/rendering/systemd.ts
41971
42354
  function renderSystemdService(resource) {
@@ -41978,7 +42361,7 @@ function renderSystemdService(resource) {
41978
42361
  if (!/^[A-Za-z0-9_.@-]+\.(?:target|service)$/.test(wantedBy)) {
41979
42362
  throw new Error(`Invalid systemd WantedBy unit: ${wantedBy}`);
41980
42363
  }
41981
- const environment = Object.entries(resource.environment ?? {}).map(
42364
+ const environment3 = Object.entries(resource.environment ?? {}).map(
41982
42365
  ([key, value2]) => {
41983
42366
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
41984
42367
  throw new Error(`Invalid systemd environment variable: ${key}`);
@@ -41992,7 +42375,7 @@ function renderSystemdService(resource) {
41992
42375
  "",
41993
42376
  "[Service]",
41994
42377
  `ExecStart=${[resource.program, ...resource.args ?? []].map(quote).join(" ")}`,
41995
- ...environment,
42378
+ ...environment3,
41996
42379
  `Restart=${restart}`,
41997
42380
  "",
41998
42381
  "[Install]",
@@ -42008,7 +42391,7 @@ function quote(value2) {
42008
42391
  async function inspectSystemdService(resource) {
42009
42392
  const path = systemdServicePath(resource);
42010
42393
  try {
42011
- const current = await readFile5(path, "utf8");
42394
+ const current = await readFile6(path, "utf8");
42012
42395
  const matches = current === renderSystemdService(resource);
42013
42396
  return {
42014
42397
  present: true,
@@ -42046,11 +42429,11 @@ async function writeSystemdService(resource, runner) {
42046
42429
  const path = systemdServicePath(resource);
42047
42430
  const content = renderSystemdService(resource);
42048
42431
  if (resource.scope === "user") {
42049
- await mkdir5(dirname6(path), { recursive: true });
42432
+ await mkdir5(dirname7(path), { recursive: true });
42050
42433
  await atomicWrite(path, content, 420);
42051
42434
  return;
42052
42435
  }
42053
- const temporary = resolve7(tmpdir(), `workstation-${process.pid}-${resource.name}`);
42436
+ const temporary = resolve9(tmpdir(), `workstation-${process.pid}-${resource.name}`);
42054
42437
  await writeFile3(temporary, content, { mode: 384 });
42055
42438
  try {
42056
42439
  await requireSuccess(runner, "sudo", ["install", "-m", "0644", temporary, path]);
@@ -42066,26 +42449,26 @@ async function systemctl(resource, runner, args) {
42066
42449
  }
42067
42450
  }
42068
42451
  function systemdServicePath(resource) {
42069
- if (resource.scope === "system") return resolve7("/etc/systemd/system", resource.name);
42452
+ if (resource.scope === "system") return resolve9("/etc/systemd/system", resource.name);
42070
42453
  const home = process.env.HOME;
42071
42454
  if (!home) throw new Error("HOME is required to manage user systemd services");
42072
- return resolve7(home, ".config", "systemd", "user", resource.name);
42455
+ return resolve9(home, ".config", "systemd", "user", resource.name);
42073
42456
  }
42074
42457
 
42075
42458
  // src/resources/custom-tool.ts
42076
- import { chmod as chmod2, lstat as lstat4, mkdir as mkdir6, readFile as readFile6, rename as rename4, unlink as unlink5 } from "fs/promises";
42077
- import { dirname as dirname7 } from "path";
42459
+ import { chmod as chmod2, lstat as lstat5, mkdir as mkdir6, readFile as readFile7, rename as rename5, unlink as unlink5 } from "fs/promises";
42460
+ import { dirname as dirname8 } from "path";
42078
42461
  import { createHash as createHash3 } from "crypto";
42079
42462
  async function inspectCustomTool(resource) {
42080
42463
  try {
42081
- const stats = await lstat4(resource.target);
42464
+ const stats = await lstat5(resource.target);
42082
42465
  if (!stats.isFile()) {
42083
42466
  return { present: true, matches: false, conflict: `${resource.target} is not a regular file` };
42084
42467
  }
42085
42468
  return {
42086
42469
  present: true,
42087
42470
  matches: true,
42088
- installedHash: createHash3("sha256").update(await readFile6(resource.target)).digest("hex")
42471
+ installedHash: createHash3("sha256").update(await readFile7(resource.target)).digest("hex")
42089
42472
  };
42090
42473
  } catch (error) {
42091
42474
  if (isMissingFile(error)) return { present: false, matches: false };
@@ -42093,7 +42476,7 @@ async function inspectCustomTool(resource) {
42093
42476
  }
42094
42477
  }
42095
42478
  async function installCustomTool(resource, runner) {
42096
- await mkdir6(dirname7(resource.target), { recursive: true });
42479
+ await mkdir6(dirname8(resource.target), { recursive: true });
42097
42480
  const output = `${resource.target}.${process.pid}.workstation.tmp`;
42098
42481
  await unlink5(output).catch((error) => {
42099
42482
  if (!isMissingFile(error)) throw error;
@@ -42106,11 +42489,12 @@ async function installCustomTool(resource, runner) {
42106
42489
  resource.build.command,
42107
42490
  (resource.build.args ?? []).map(replace),
42108
42491
  {
42492
+ streamOutput: true,
42109
42493
  ...resource.build.cwd ? { cwd: replace(resource.build.cwd) } : {},
42110
42494
  ...resource.build.environment ? { environment: Object.fromEntries(Object.entries(resource.build.environment).map(([key, value2]) => [key, replace(value2)])) } : {}
42111
42495
  }
42112
42496
  );
42113
- const stats = await lstat4(output).catch((error) => {
42497
+ const stats = await lstat5(output).catch((error) => {
42114
42498
  if (isMissingFile(error)) {
42115
42499
  throw new Error(`Custom tool build did not create {output}: ${resource.name}`);
42116
42500
  }
@@ -42118,7 +42502,7 @@ async function installCustomTool(resource, runner) {
42118
42502
  });
42119
42503
  if (!stats.isFile()) throw new Error(`Custom tool output is not a regular file: ${output}`);
42120
42504
  await chmod2(output, 493);
42121
- await rename4(output, resource.target);
42505
+ await rename5(output, resource.target);
42122
42506
  } finally {
42123
42507
  await unlink5(output).catch(() => void 0);
42124
42508
  }
@@ -42200,11 +42584,11 @@ async function removeResource(resource, runner, installedVersion, originalFile,
42200
42584
  }
42201
42585
 
42202
42586
  // src/persistence/state.ts
42203
- import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile4 } from "fs/promises";
42204
- import { dirname as dirname8 } from "path";
42587
+ import { mkdir as mkdir7, readFile as readFile8, rename as rename6, writeFile as writeFile4 } from "fs/promises";
42588
+ import { dirname as dirname9 } from "path";
42205
42589
  async function readState(path, machine) {
42206
42590
  try {
42207
- const parsed = JSON.parse(await readFile7(path, "utf8"));
42591
+ const parsed = JSON.parse(await readFile8(path, "utf8"));
42208
42592
  if (!isState(parsed)) {
42209
42593
  throw new Error(`Unsupported or invalid state file: ${path}`);
42210
42594
  }
@@ -42222,11 +42606,11 @@ async function readState(path, machine) {
42222
42606
  }
42223
42607
  }
42224
42608
  async function writeState(path, state) {
42225
- await mkdir7(dirname8(path), { recursive: true, mode: 448 });
42609
+ await mkdir7(dirname9(path), { recursive: true, mode: 448 });
42226
42610
  const temporary = `${path}.${process.pid}.tmp`;
42227
42611
  await writeFile4(temporary, `${JSON.stringify(state, null, 2)}
42228
42612
  `, { mode: 384 });
42229
- await rename5(temporary, path);
42613
+ await rename6(temporary, path);
42230
42614
  }
42231
42615
  function isMissingFile2(error) {
42232
42616
  return error instanceof Error && "code" in error && error.code === "ENOENT";
@@ -42252,7 +42636,7 @@ function isResolvedResource(value2) {
42252
42636
  const candidate = value2;
42253
42637
  switch (candidate.kind) {
42254
42638
  case "package":
42255
- return ["mise", "brew", "brew-cask", "apt"].includes(String(candidate.manager)) && typeof candidate.name === "string" && (candidate.version === void 0 || typeof candidate.version === "string") && (candidate.lockedVersion === void 0 || typeof candidate.lockedVersion === "string") && (candidate.upgrade === void 0 || candidate.manager === "brew-cask" && isBrewCaskUpgradeOptions2(candidate.upgrade));
42639
+ return ["mise", "brew", "brew-cask", "apt", "dnf", "yum", "pacman", "flatpak", "mas"].includes(String(candidate.manager)) && isPackageName(candidate.manager, candidate.name) && (candidate.flatpak === void 0 || candidate.manager === "flatpak" && isFlatpakOptions(candidate.flatpak)) && (candidate.manager !== "mas" || candidate.lockedVersion === void 0) && (candidate.version === void 0 || typeof candidate.version === "string") && (candidate.lockedVersion === void 0 || typeof candidate.lockedVersion === "string") && (candidate.upgrade === void 0 || candidate.manager === "brew-cask" && isBrewCaskUpgradeOptions2(candidate.upgrade));
42256
42640
  case "symlink":
42257
42641
  return typeof candidate.source === "string" && typeof candidate.target === "string";
42258
42642
  case "launch-agent":
@@ -42295,24 +42679,25 @@ function isBrewCaskUpgradeOptions2(value2) {
42295
42679
  }
42296
42680
 
42297
42681
  // src/reconciliation/plan.ts
42298
- async function createPlan(config, runner) {
42682
+ async function createPlan(config, runner, onProgress) {
42299
42683
  const state = await readState(config.stateFile, config.context.machine);
42300
42684
  const desired = new Map(config.resources.map((resource) => [resourceId(resource), resource]));
42301
42685
  const actions = [];
42302
42686
  for (const [id, resource] of desired) {
42687
+ onProgress?.(` Inspect: ${id}`);
42303
42688
  const previous = state.resources[id];
42304
42689
  const inspection = await inspectResource(resource, runner);
42305
42690
  if (resource.kind === "custom-tool" && inspection.present && previous?.installedHash !== void 0 && inspection.installedHash !== previous.installedHash) {
42306
42691
  throw new Error(`Managed custom tool ${id} was changed outside Workstation`);
42307
42692
  }
42308
- if (resource.kind === "symlink" && !inspection.matches) await lstat5(resource.source);
42693
+ if (resource.kind === "symlink" && !inspection.matches) await lstat6(resource.source);
42309
42694
  if (!previous) {
42310
42695
  if (inspection.matches) {
42311
42696
  actions.push({ type: "adopt", id, resource, reason: "already present" });
42312
42697
  } else if (inspection.conflict) {
42313
42698
  throw new Error(`Cannot manage ${id}: ${inspection.conflict}`);
42314
42699
  } else if (inspection.present) {
42315
- actions.push({ type: "update", id, resource, reason: "upgrade available" });
42700
+ actions.push({ type: "update", id, resource, reason: inspection.migration ? "migrate from mise to Homebrew" : "upgrade available" });
42316
42701
  } else {
42317
42702
  actions.push({ type: "create", id, resource, reason: "not present" });
42318
42703
  }
@@ -42335,12 +42720,13 @@ async function createPlan(config, runner) {
42335
42720
  id,
42336
42721
  resource,
42337
42722
  previous,
42338
- reason: inspection.present ? "upgrade available" : "managed resource is missing"
42723
+ reason: inspection.migration ? "migrate from mise to Homebrew" : inspection.present ? "upgrade available" : "managed resource is missing"
42339
42724
  });
42340
42725
  }
42341
42726
  }
42342
42727
  for (const [id, previous] of Object.entries(state.resources)) {
42343
42728
  if (desired.has(id)) continue;
42729
+ onProgress?.(` Inspect removed declaration: ${id}`);
42344
42730
  const removable = previous.owned || previous.originalFile !== void 0;
42345
42731
  if (!removable) {
42346
42732
  actions.push({
@@ -42372,7 +42758,7 @@ async function createPlan(config, runner) {
42372
42758
  );
42373
42759
  }
42374
42760
  function canUpdateAdoptedInPlace(resource) {
42375
- return resource.kind === "package" && resource.manager === "brew-cask" || resource.kind === "generated-file" && resource.ifExists === "overwrite" || resource.kind === "custom-tool";
42761
+ return resource.kind === "package" && ["brew-cask", "flatpak", "mas", "pacman"].includes(resource.manager) || resource.kind === "generated-file" && resource.ifExists === "overwrite" || resource.kind === "custom-tool";
42376
42762
  }
42377
42763
  function canRepairManagedFile(resource, previous) {
42378
42764
  return resource.kind === "generated-file" && (previous.owned || previous.originalFile !== void 0 && resource.ifExists === "overwrite");
@@ -42390,11 +42776,66 @@ function actionOrder(action) {
42390
42776
  return 6;
42391
42777
  }
42392
42778
 
42779
+ // src/reconciliation/package-batch.ts
42780
+ function isPackageMutation(action) {
42781
+ return action.resource.kind === "package" && ["create", "update", "remove"].includes(action.type);
42782
+ }
42783
+ async function applyPackageBatch(actions, runner, checkpoint, onAction, onProgress) {
42784
+ const groups = [];
42785
+ const byCommand = /* @__PURE__ */ new Map();
42786
+ for (const action of actions) {
42787
+ const before = await inspectPackage(action.resource, runner);
42788
+ if (before.conflict) throw new Error(`Cannot manage ${action.id}: ${before.conflict}`);
42789
+ const mutation = action.type === "remove" ? before.present ? preparePackageRemoval(action.resource, action.previous?.installedVersion) : void 0 : before.migration ? void 0 : await preparePackageInstallation(action.resource, runner, before);
42790
+ const prepared = { action, before, ...mutation ? { mutation } : {} };
42791
+ const key = mutation ? packageCommandKey(mutation) : void 0;
42792
+ const group = key === void 0 ? void 0 : byCommand.get(key);
42793
+ if (group) group.push(prepared);
42794
+ else {
42795
+ const next = [prepared];
42796
+ groups.push(next);
42797
+ if (key !== void 0) byCommand.set(key, next);
42798
+ }
42799
+ }
42800
+ for (const group of groups) {
42801
+ const first = group[0];
42802
+ if (!first) continue;
42803
+ for (const item of group) onAction?.(item.action);
42804
+ onProgress?.(` ${group.length > 1 ? "Batch" : "Package"}: ${first.action.resource.manager} ${first.mutation?.args.join(" ") ?? first.action.type} (${group.length} package(s))`);
42805
+ const errors = [];
42806
+ try {
42807
+ if (first.before.migration && first.action.type !== "remove") {
42808
+ await reconcilePackage(first.action.resource, runner);
42809
+ } else if (first.mutation) {
42810
+ await runPackageCommand({ ...first.mutation, targets: group.flatMap((item) => item.mutation?.targets ?? []) }, runner);
42811
+ }
42812
+ } catch (error) {
42813
+ errors.push(error);
42814
+ }
42815
+ for (const { action, before } of group) {
42816
+ try {
42817
+ const after = await inspectPackage(action.resource, runner);
42818
+ const converged = action.type === "remove" ? !after.present : after.matches;
42819
+ if (converged || action.type !== "remove" && after.present && !after.conflict && !after.migration) {
42820
+ await checkpoint(action, before, after);
42821
+ onProgress?.(` ${converged ? "Done" : "Partial"}: ${action.type} ${action.id} (state saved)`);
42822
+ }
42823
+ if (!converged) errors.push(new Error(`${action.id} did not converge: expected ${action.type === "remove" ? "absent" : action.resource.lockedVersion ?? "no available upgrades"}; installed ${after.installedVersion ?? (after.present ? "version unknown" : "not present")}${after.conflict ? `; ${after.conflict}` : ""}`));
42824
+ } catch (error) {
42825
+ errors.push(error);
42826
+ }
42827
+ }
42828
+ if (errors.length > 0) {
42829
+ throw new AggregateError(errors, `Package batch failed; verified results were saved. ${errors.map((error) => error instanceof Error ? error.message : String(error)).join("; ")}`);
42830
+ }
42831
+ }
42832
+ }
42833
+
42393
42834
  // src/reconciliation/apply.ts
42394
42835
  import { mkdir as mkdir8, rmdir } from "fs/promises";
42395
- import { dirname as dirname9 } from "path";
42396
- async function applyPlan(config, runner, onAction) {
42397
- await mkdir8(dirname9(config.stateFile), { recursive: true, mode: 448 });
42836
+ import { dirname as dirname10 } from "path";
42837
+ async function applyPlan(config, runner, onAction, onProgress) {
42838
+ await mkdir8(dirname10(config.stateFile), { recursive: true, mode: 448 });
42398
42839
  const guard = `${config.stateFile}.lock`;
42399
42840
  try {
42400
42841
  await mkdir8(guard, { mode: 448 });
@@ -42405,15 +42846,42 @@ async function applyPlan(config, runner, onAction) {
42405
42846
  throw error;
42406
42847
  }
42407
42848
  try {
42408
- return await reconcile(config, runner, onAction);
42849
+ return await reconcile(config, runner, onAction, onProgress);
42409
42850
  } finally {
42410
42851
  await rmdir(guard);
42411
42852
  }
42412
42853
  }
42413
- async function reconcile(config, runner, onAction) {
42414
- const actions = await createPlan(config, runner);
42854
+ async function reconcile(config, runner, onAction, onProgress) {
42855
+ onProgress?.("Inspecting resources...");
42856
+ const actions = await createPlan(config, runner, onProgress);
42857
+ onProgress?.(`Plan: ${actions.length} action(s).`);
42415
42858
  let state = await readState(config.stateFile, config.context.machine);
42416
- for (const action of actions) {
42859
+ for (let index = 0; index < actions.length; index++) {
42860
+ const action = actions[index];
42861
+ if (!action) continue;
42862
+ if (isPackageMutation(action)) {
42863
+ const batch = [action];
42864
+ while (index + 1 < actions.length) {
42865
+ const next = actions[index + 1];
42866
+ if (!next || !isPackageMutation(next) || next.type === "remove" !== (action.type === "remove")) break;
42867
+ batch.push(next);
42868
+ index++;
42869
+ }
42870
+ await applyPackageBatch(batch, runner, async (completed, before, after) => {
42871
+ if (completed.type === "update" && after.matches) await removeReplacedMiseVersion(completed, runner);
42872
+ const resources2 = { ...state.resources };
42873
+ if (completed.type === "remove") delete resources2[completed.id];
42874
+ else {
42875
+ resources2[completed.id] = entry(completed, {
42876
+ owned: completed.previous?.owned === true || !before.present,
42877
+ inspection: after
42878
+ });
42879
+ }
42880
+ state = { ...state, resources: resources2 };
42881
+ await writeState(config.stateFile, state);
42882
+ }, onAction, onProgress);
42883
+ continue;
42884
+ }
42417
42885
  onAction?.(action);
42418
42886
  const resources = { ...state.resources };
42419
42887
  switch (action.type) {
@@ -42486,6 +42954,7 @@ async function reconcile(config, runner, onAction) {
42486
42954
  }
42487
42955
  state = { ...state, resources };
42488
42956
  await writeState(config.stateFile, state);
42957
+ onProgress?.(` Done: ${action.type} ${action.id} (state saved)`);
42489
42958
  }
42490
42959
  return actions;
42491
42960
  }
@@ -42522,9 +42991,24 @@ function entry(action, options) {
42522
42991
  // src/resources/runner.ts
42523
42992
  import { spawn } from "child_process";
42524
42993
  var ProcessRunner = class {
42994
+ /** Opt into command progress and streaming; verbose also prints captured query output. */
42995
+ constructor(logging = {}) {
42996
+ this.logging = logging;
42997
+ }
42998
+ /** Print resource progress only when logging is enabled. */
42999
+ report(message) {
43000
+ if (this.logging.progress || this.logging.verbose) process.stderr.write(` ${message}
43001
+ `);
43002
+ }
42525
43003
  /** Execute a command directly and return captured output and its exit code; spawn failures reject. */
42526
43004
  async run(command, args, options) {
42527
- return await new Promise((resolve10, reject) => {
43005
+ const started = Date.now();
43006
+ if (this.logging.progress || this.logging.verbose) {
43007
+ process.stderr.write(` $ ${[command, ...args].map((value2) => /^[\w./:@=+-]+$/.test(value2) ? value2 : JSON.stringify(value2)).join(" ")}${options?.cwd ? ` (in ${options.cwd})` : ""}
43008
+ `);
43009
+ }
43010
+ const stream = this.logging.verbose || this.logging.progress && options?.streamOutput;
43011
+ return await new Promise((resolve12, reject) => {
42528
43012
  const child = spawn(command, [...args], {
42529
43013
  cwd: options?.cwd,
42530
43014
  env: { ...process.env, ...options?.environment },
@@ -42534,20 +43018,29 @@ var ProcessRunner = class {
42534
43018
  let stderr = "";
42535
43019
  child.stdout.setEncoding("utf8");
42536
43020
  child.stderr.setEncoding("utf8");
42537
- child.stdout.on("data", (chunk) => stdout += chunk);
42538
- child.stderr.on("data", (chunk) => stderr += chunk);
43021
+ child.stdout.on("data", (chunk) => {
43022
+ stdout += chunk;
43023
+ if (stream) process.stdout.write(chunk);
43024
+ });
43025
+ child.stderr.on("data", (chunk) => {
43026
+ stderr += chunk;
43027
+ if (stream || this.logging.progress) process.stderr.write(chunk);
43028
+ });
42539
43029
  child.once("error", reject);
42540
- child.once(
42541
- "close",
42542
- (exitCode) => resolve10({ exitCode: exitCode ?? 1, stdout, stderr })
42543
- );
43030
+ child.once("close", (exitCode) => {
43031
+ if (this.logging.progress || this.logging.verbose) {
43032
+ process.stderr.write(` -> exit ${exitCode ?? 1} (${((Date.now() - started) / 1e3).toFixed(1)}s)
43033
+ `);
43034
+ }
43035
+ resolve12({ exitCode: exitCode ?? 1, stdout, stderr });
43036
+ });
42544
43037
  });
42545
43038
  }
42546
43039
  };
42547
43040
 
42548
43041
  // src/persistence/lock.ts
42549
- import { readFile as readFile8, rename as rename6, writeFile as writeFile5 } from "fs/promises";
42550
- import { dirname as dirname10, resolve as resolve8 } from "path";
43042
+ import { readFile as readFile9, rename as rename7, writeFile as writeFile5 } from "fs/promises";
43043
+ import { dirname as dirname11, resolve as resolve10 } from "path";
42551
43044
 
42552
43045
  // src/resources/package-version.ts
42553
43046
  async function resolvePackageVersion(resource, runner) {
@@ -42573,22 +43066,31 @@ async function resolvePackageVersion(resource, runner) {
42573
43066
  const version = await readAvailableBrewVersion(resource, runner);
42574
43067
  return version === "latest" ? void 0 : version;
42575
43068
  }
43069
+ case "dnf":
43070
+ case "yum":
43071
+ return await resolveRpmVersion(resource, runner);
42576
43072
  case "system":
42577
43073
  throw new Error("System package manager must be resolved before locking");
43074
+ case "pacman":
43075
+ return await resolvePacmanVersion(resource, runner);
43076
+ case "flatpak":
43077
+ return await resolveFlatpakVersion(resource, runner);
43078
+ case "mas":
43079
+ return void 0;
42578
43080
  }
42579
43081
  }
42580
43082
 
42581
43083
  // src/persistence/lock.ts
42582
43084
  var LOCK_VERSION = 1;
42583
43085
  function lockPath(configPath) {
42584
- return resolve8(dirname10(configPath), "workstation.lock");
43086
+ return resolve10(dirname11(configPath), "workstation.lock");
42585
43087
  }
42586
43088
  async function lockConfig(configPath, config, runner) {
42587
43089
  const path = lockPath(configPath);
42588
43090
  const previousText = await readOptional(path);
42589
43091
  const previous = previousText === void 0 ? emptyLock() : parseLock(previousText, path);
42590
- const target = previous.targets.find(({ machine }) => machine === config.context.machine);
42591
- const existing = new Map(target?.resources.map((entry2) => [entry2.id, entry2]));
43092
+ const target2 = previous.targets.find(({ machine }) => machine === config.context.machine);
43093
+ const existing = new Map(target2?.resources.map((entry2) => [entry2.id, entry2]));
42592
43094
  const resources = [];
42593
43095
  const entries = [];
42594
43096
  for (const resource of config.resources) {
@@ -42632,10 +43134,10 @@ function emptyLock() {
42632
43134
  function stringifyLock(lock) {
42633
43135
  return stringify({
42634
43136
  version: lock.version,
42635
- targets: lock.targets.map((target) => ({
42636
- machine: target.machine,
42637
- platform: target.platform,
42638
- resources: target.resources.map((entry2) => ({
43137
+ targets: lock.targets.map((target2) => ({
43138
+ machine: target2.machine,
43139
+ platform: target2.platform,
43140
+ resources: target2.resources.map((entry2) => ({
42639
43141
  id: entry2.id,
42640
43142
  fingerprint: entry2.fingerprint,
42641
43143
  ...entry2.lockedVersion ? { locked_version: entry2.lockedVersion } : {}
@@ -42651,19 +43153,19 @@ function parseLock(text, path) {
42651
43153
  if (!Array.isArray(document2.targets)) throw new Error(`Lock targets must be an array in ${path}`);
42652
43154
  const machines = /* @__PURE__ */ new Set();
42653
43155
  const targets = document2.targets.map((value2, targetIndex) => {
42654
- const target = requireTable2(value2, `targets[${targetIndex}]`);
42655
- const machine = requireString2(target.machine, `targets[${targetIndex}].machine`);
43156
+ const target2 = requireTable2(value2, `targets[${targetIndex}]`);
43157
+ const machine = requireString2(target2.machine, `targets[${targetIndex}].machine`);
42656
43158
  if (machines.has(machine)) throw new Error(`Duplicate machine ${machine} in ${path}`);
42657
43159
  machines.add(machine);
42658
- const platform = requireString2(target.platform, `targets[${targetIndex}].platform`);
43160
+ const platform = requireString2(target2.platform, `targets[${targetIndex}].platform`);
42659
43161
  if (platform !== "darwin" && platform !== "linux") {
42660
43162
  throw new Error(`Invalid platform for ${machine} in ${path}`);
42661
43163
  }
42662
- if (!Array.isArray(target.resources)) {
43164
+ if (!Array.isArray(target2.resources)) {
42663
43165
  throw new Error(`Lock resources for ${machine} must be an array in ${path}`);
42664
43166
  }
42665
43167
  const ids = /* @__PURE__ */ new Set();
42666
- const resources = target.resources.map((value3, resourceIndex) => {
43168
+ const resources = target2.resources.map((value3, resourceIndex) => {
42667
43169
  const entry2 = requireTable2(value3, `targets[${targetIndex}].resources[${resourceIndex}]`);
42668
43170
  const id = requireString2(entry2.id, "lock resource id");
42669
43171
  if (ids.has(id)) throw new Error(`Duplicate resource ${id} for ${machine} in ${path}`);
@@ -42690,7 +43192,7 @@ function requireString2(value2, field) {
42690
43192
  }
42691
43193
  async function readOptional(path) {
42692
43194
  try {
42693
- return await readFile8(path, "utf8");
43195
+ return await readFile9(path, "utf8");
42694
43196
  } catch (error) {
42695
43197
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
42696
43198
  return void 0;
@@ -42701,7 +43203,7 @@ async function readOptional(path) {
42701
43203
  async function atomicWrite2(path, contents) {
42702
43204
  const temporary = `${path}.${process.pid}.tmp`;
42703
43205
  await writeFile5(temporary, contents, { mode: 420 });
42704
- await rename6(temporary, path);
43206
+ await rename7(temporary, path);
42705
43207
  }
42706
43208
 
42707
43209
  // src/resources/tasks.ts
@@ -42718,7 +43220,7 @@ async function runTask(config, name, args, runner) {
42718
43220
 
42719
43221
  // src/config/init.ts
42720
43222
  import { mkdir as mkdir9, writeFile as writeFile6 } from "fs/promises";
42721
- import { dirname as dirname11, resolve as resolve9 } from "path";
43223
+ import { dirname as dirname12, resolve as resolve11 } from "path";
42722
43224
  var starter = `import type { ConfigDefinition } from "@dovocode/workstation";
42723
43225
 
42724
43226
  export default {
@@ -42739,17 +43241,17 @@ export default {
42739
43241
  } satisfies ConfigDefinition;
42740
43242
  `;
42741
43243
  async function initConfig(path = "workstation.config.ts") {
42742
- const target = resolve9(path);
42743
- await mkdir9(dirname11(target), { recursive: true });
43244
+ const target2 = resolve11(path);
43245
+ await mkdir9(dirname12(target2), { recursive: true });
42744
43246
  try {
42745
- await writeFile6(target, starter, { encoding: "utf8", flag: "wx" });
43247
+ await writeFile6(target2, starter, { encoding: "utf8", flag: "wx" });
42746
43248
  } catch (error) {
42747
43249
  if (error instanceof Error && "code" in error && error.code === "EEXIST") {
42748
- throw new Error(`Configuration already exists: ${target}. No changes made.`, { cause: error });
43250
+ throw new Error(`Configuration already exists: ${target2}. No changes made.`, { cause: error });
42749
43251
  }
42750
43252
  throw error;
42751
43253
  }
42752
- return target;
43254
+ return target2;
42753
43255
  }
42754
43256
 
42755
43257
  // src/cli.ts
@@ -42763,27 +43265,27 @@ async function main() {
42763
43265
  return;
42764
43266
  }
42765
43267
  const sourcePath = await findConfig(options.config);
43268
+ if (!options.listTasks && !options.task) console.log(`Loading configuration: ${sourcePath}`);
42766
43269
  const sourceConfig = await loadConfig(sourcePath, options.machine);
42767
- const runner = new ProcessRunner();
43270
+ const runner = new ProcessRunner({ progress: !options.listTasks, verbose: options.verbose });
42768
43271
  if (options.listTasks) {
42769
43272
  for (const [name, task] of Object.entries(sourceConfig.tasks ?? {})) console.log(`${name}${task.description ? ` ${task.description}` : ""}`);
42770
- for (const [name, target] of Object.entries(sourceConfig.aliases ?? {})) console.log(`${name} -> ${target}`);
43273
+ for (const [name, target2] of Object.entries(sourceConfig.aliases ?? {})) console.log(`${name} -> ${target2}`);
42771
43274
  return;
42772
43275
  }
42773
43276
  if (options.task) {
42774
- const result = await runTask(sourceConfig, options.task, options.taskArgs, runner);
42775
- process.stdout.write(result.stdout);
42776
- process.stderr.write(result.stderr);
43277
+ const result = await runTask(sourceConfig, options.task, options.taskArgs, new ProcessRunner({ progress: true, verbose: true }));
42777
43278
  process.exitCode = result.exitCode;
42778
43279
  return;
42779
43280
  }
43281
+ console.log(`Resolving package versions for ${sourceConfig.context.machine} (${sourceConfig.resources.length} resources)...`);
42780
43282
  const locked = await lockConfig(sourcePath, sourceConfig, runner);
43283
+ console.log(`Lock: ${locked.path}${locked.changed ? " (updated)" : " (unchanged)"}`);
42781
43284
  const outputPath = manifestPath(locked.config);
42782
43285
  await writeManifest(outputPath, locked.config);
42783
- const config = await readManifest(outputPath);
42784
- const actions = await applyPlan(config, runner, printAction);
42785
- console.log(`Lock: ${locked.path}${locked.changed ? " (updated)" : ""}`);
42786
43286
  console.log(`Manifest: ${outputPath}`);
43287
+ const config = await readManifest(outputPath);
43288
+ const actions = await applyPlan(config, runner, printAction, (message) => console.log(message));
42787
43289
  console.log(
42788
43290
  actions.length === 0 ? "Workstation is already converged." : `Reconciled ${actions.length} action(s).`
42789
43291
  );
@@ -42795,11 +43297,14 @@ function parseArguments(args) {
42795
43297
  let taskArgs = [];
42796
43298
  let listTasks = false;
42797
43299
  let init = false;
43300
+ let verbose = false;
42798
43301
  for (let index = 0; index < args.length; index += 1) {
42799
43302
  const argument = args[index];
42800
43303
  if (argument === "init") {
42801
43304
  if (init) throw new Error("init may only be specified once");
42802
43305
  init = true;
43306
+ } else if (argument === "--verbose" || argument === "-v") {
43307
+ verbose = true;
42803
43308
  } else if (argument === "--list-tasks") {
42804
43309
  listTasks = true;
42805
43310
  } else if (argument && !argument.startsWith("-") && argument !== "help") {
@@ -42820,7 +43325,7 @@ function parseArguments(args) {
42820
43325
  }
42821
43326
  if (listTasks && task) throw new Error("--list-tasks cannot be combined with a task name");
42822
43327
  if (init && (task || listTasks || machine)) throw new Error("init supports only --config PATH and --help");
42823
- return { init, taskArgs, listTasks, ...task ? { task } : {}, ...config ? { config } : {}, ...machine ? { machine } : {} };
43328
+ return { init, verbose, taskArgs, listTasks, ...task ? { task } : {}, ...config ? { config } : {}, ...machine ? { machine } : {} };
42824
43329
  }
42825
43330
  function requireValue(args, index, option) {
42826
43331
  const value2 = args[index];
@@ -42843,6 +43348,7 @@ Options:
42843
43348
  --config PATH Configuration entry point (default: workstation.config.ts)
42844
43349
  --machine NAME Override the short hostname
42845
43350
  --list-tasks List configured tasks and aliases
43351
+ -v, --verbose Also stream inspection and version-query output
42846
43352
  -h, --help Show this help`);
42847
43353
  }
42848
43354
  main().catch((error) => {