@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/README.md +17 -1
- package/dist/cli.js +710 -204
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +32 -3
- package/dist/index.js +227 -61
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
type Platform = "darwin" | "linux";
|
|
2
|
-
type PackageManager = "mise" | "brew" | "brew-cask" | "apt" | "system";
|
|
2
|
+
type PackageManager = "mise" | "brew" | "brew-cask" | "apt" | "dnf" | "yum" | "pacman" | "flatpak" | "mas" | "system";
|
|
3
|
+
/** Flatpak installation target; the selected remote must already be configured. */
|
|
4
|
+
interface FlatpakOptions {
|
|
5
|
+
readonly scope?: "user" | "system";
|
|
6
|
+
readonly remote?: string;
|
|
7
|
+
readonly branch?: string;
|
|
8
|
+
}
|
|
3
9
|
/** Homebrew cask upgrade behavior. */
|
|
4
10
|
interface BrewCaskUpgradeOptions {
|
|
5
11
|
/** Include auto-updating casks and refresh their lock pins on each run. */
|
|
@@ -26,6 +32,7 @@ interface PackageResource {
|
|
|
26
32
|
readonly name: string;
|
|
27
33
|
readonly version?: string;
|
|
28
34
|
readonly upgrade?: BrewCaskUpgradeOptions;
|
|
35
|
+
readonly flatpak?: FlatpakOptions;
|
|
29
36
|
}
|
|
30
37
|
interface ResolvedPackageResource extends PackageResource {
|
|
31
38
|
/** Concrete version resolved into workstation.lock. */
|
|
@@ -163,10 +170,14 @@ interface CommandResult {
|
|
|
163
170
|
}
|
|
164
171
|
/** Command execution boundary, replaceable in tests or embedded integrations. */
|
|
165
172
|
interface Runner {
|
|
173
|
+
/** Report a resource-level progress message when supported by the host. */
|
|
174
|
+
report?(message: string): void;
|
|
166
175
|
/** Execute a command directly and return captured output and its exit code; spawn failures reject. */
|
|
167
176
|
run(command: string, args: readonly string[], options?: RunOptions): Promise<CommandResult>;
|
|
168
177
|
}
|
|
169
178
|
interface RunOptions {
|
|
179
|
+
/** Stream captured output when the process runner has progress logging enabled. */
|
|
180
|
+
readonly streamOutput?: boolean;
|
|
170
181
|
readonly cwd?: string;
|
|
171
182
|
readonly environment?: Readonly<Record<string, string>>;
|
|
172
183
|
}
|
|
@@ -239,7 +250,17 @@ declare const tools: {
|
|
|
239
250
|
brewCask: (packages: readonly string[], upgrade?: BrewCaskUpgradeOptions) => PackageResource[];
|
|
240
251
|
/** Install Debian/Ubuntu packages through APT. Mutations request sudo. */
|
|
241
252
|
apt: (packages: readonly string[]) => PackageResource[];
|
|
242
|
-
/**
|
|
253
|
+
/** Install RPM packages through DNF. Mutations request sudo. */
|
|
254
|
+
dnf: (packages: readonly string[]) => PackageResource[];
|
|
255
|
+
/** Install RPM packages through legacy YUM. Mutations request sudo. */
|
|
256
|
+
yum: (packages: readonly string[]) => PackageResource[];
|
|
257
|
+
/** Install Arch Linux repository packages through pacman; does not refresh databases or upgrade the whole system. */
|
|
258
|
+
pacman: (packages: readonly string[]) => PackageResource[];
|
|
259
|
+
/** Install Flatpak app IDs; defaults to the user's flathub remote and stable branch. */
|
|
260
|
+
flatpak: (packages: readonly string[], options?: FlatpakOptions) => PackageResource[];
|
|
261
|
+
/** Install previously acquired Mac App Store apps by numeric ID; follows available updates without version pins. */
|
|
262
|
+
mas: (ids: readonly (string | number)[]) => PackageResource[];
|
|
263
|
+
/** Use the configured manager, Homebrew on macOS, or detect APT/DNF/YUM/pacman on Linux. */
|
|
243
264
|
system: (packages: readonly string[]) => PackageResource[];
|
|
244
265
|
};
|
|
245
266
|
/**
|
|
@@ -518,8 +539,16 @@ declare function lockConfig(configPath: string, config: ResolvedConfig, runner:
|
|
|
518
539
|
|
|
519
540
|
/** Run child processes directly, inheriting stdin and capturing stdout/stderr. No implicit shell is used. */
|
|
520
541
|
declare class ProcessRunner implements Runner {
|
|
542
|
+
private readonly logging;
|
|
543
|
+
/** Opt into command progress and streaming; verbose also prints captured query output. */
|
|
544
|
+
constructor(logging?: {
|
|
545
|
+
readonly progress?: boolean;
|
|
546
|
+
readonly verbose?: boolean;
|
|
547
|
+
});
|
|
548
|
+
/** Print resource progress only when logging is enabled. */
|
|
549
|
+
report(message: string): void;
|
|
521
550
|
/** Execute a command directly and return captured output and its exit code; spawn failures reject. */
|
|
522
551
|
run(command: string, args: readonly string[], options?: RunOptions): Promise<CommandResult>;
|
|
523
552
|
}
|
|
524
553
|
|
|
525
|
-
export { type Action, type ActionType, type BrewCaskUpgradeOptions, type CommandResult, type CommandSpec, type ConfigDefinition, type ConfigFactory, type ConfigInput, type ConfigValue, type Context, type CustomToolResource, type GeneratedFileOptions, type GeneratedFileResource, type IfExistsPolicy, type JsoncCommand, JsoncDocument, type JsoncLine, type LaunchAgentResource, type LockedConfigResult, type OriginalFile, type PackageManager, type PackageResource, type Platform, ProcessRunner, type ResolvedConfig, type ResolvedPackageResource, type ResolvedResource, type Resource, type ResourceInput, type RunOptions, type Runner, type Shell, type ShellCommand, type ShellCondition, type ShellExpression, type ShellFileOptions, type ShellStatement, type ShellValue, type StateEntry, type StructuredFormat, type SymlinkResource, type SystemdScope, type SystemdServiceResource, type TaskDefinition, type WorkstationConfig, type WorkstationState, bash, configure, customTool, darwin, defineConfig, files, findConfig, fingerprint, jsonc, launchAgent, linux, loadConfig, lockConfig, lockPath, machine, manifestPath, readManifest, renderShell, resourceId, runTask, shell, symlink, systemdService, task, tools, when, writeManifest, zsh };
|
|
554
|
+
export { type Action, type ActionType, type BrewCaskUpgradeOptions, type CommandResult, type CommandSpec, type ConfigDefinition, type ConfigFactory, type ConfigInput, type ConfigValue, type Context, type CustomToolResource, type FlatpakOptions, type GeneratedFileOptions, type GeneratedFileResource, type IfExistsPolicy, type JsoncCommand, JsoncDocument, type JsoncLine, type LaunchAgentResource, type LockedConfigResult, type OriginalFile, type PackageManager, type PackageResource, type Platform, ProcessRunner, type ResolvedConfig, type ResolvedPackageResource, type ResolvedResource, type Resource, type ResourceInput, type RunOptions, type Runner, type Shell, type ShellCommand, type ShellCondition, type ShellExpression, type ShellFileOptions, type ShellStatement, type ShellValue, type StateEntry, type StructuredFormat, type SymlinkResource, type SystemdScope, type SystemdServiceResource, type TaskDefinition, type WorkstationConfig, type WorkstationState, bash, configure, customTool, darwin, defineConfig, files, findConfig, fingerprint, jsonc, launchAgent, linux, loadConfig, lockConfig, lockPath, machine, manifestPath, readManifest, renderShell, resourceId, runTask, shell, symlink, systemdService, task, tools, when, writeManifest, zsh };
|
package/dist/index.js
CHANGED
|
@@ -109,11 +109,11 @@ function resolveTasks(definition, context) {
|
|
|
109
109
|
const cwd = value.cwd?.replace(/^~(?=\/|$)/, context.home) ?? context.configDir;
|
|
110
110
|
tasks[name] = { ...value, cwd: isAbsolute(cwd) ? cwd : resolve(context.configDir, cwd) };
|
|
111
111
|
}
|
|
112
|
-
for (const [name,
|
|
112
|
+
for (const [name, target2] of Object.entries(definition.aliases ?? {})) {
|
|
113
113
|
validateName(name);
|
|
114
114
|
if (Object.hasOwn(tasks, name)) throw new Error(`Task and alias share a name: ${name}`);
|
|
115
|
-
if (typeof
|
|
116
|
-
aliases[name] =
|
|
115
|
+
if (typeof target2 !== "string") throw new Error(`Invalid alias target: ${name}`);
|
|
116
|
+
aliases[name] = target2;
|
|
117
117
|
}
|
|
118
118
|
for (const name of Object.keys(aliases)) resolveTaskName(name, tasks, aliases);
|
|
119
119
|
return { tasks, aliases };
|
|
@@ -124,9 +124,9 @@ function resolveTaskName(name, tasks, aliases) {
|
|
|
124
124
|
while (Object.hasOwn(aliases, current)) {
|
|
125
125
|
if (visited.has(current)) throw new Error(`Task alias cycle: ${[...visited, current].join(" -> ")}`);
|
|
126
126
|
visited.add(current);
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
current =
|
|
127
|
+
const target2 = aliases[current];
|
|
128
|
+
if (target2 === void 0) throw new Error(`Invalid alias: ${current}`);
|
|
129
|
+
current = target2;
|
|
130
130
|
}
|
|
131
131
|
if (!Object.hasOwn(tasks, current)) throw new Error(`Unknown task: ${current}`);
|
|
132
132
|
return current;
|
|
@@ -179,20 +179,35 @@ var tools = {
|
|
|
179
179
|
brewCask: (packages, upgrade) => packageList("brew-cask", packages, upgrade),
|
|
180
180
|
/** Install Debian/Ubuntu packages through APT. Mutations request sudo. */
|
|
181
181
|
apt: (packages) => packageList("apt", packages),
|
|
182
|
-
/**
|
|
182
|
+
/** Install RPM packages through DNF. Mutations request sudo. */
|
|
183
|
+
dnf: (packages) => packageList("dnf", packages),
|
|
184
|
+
/** Install RPM packages through legacy YUM. Mutations request sudo. */
|
|
185
|
+
yum: (packages) => packageList("yum", packages),
|
|
186
|
+
/** Install Arch Linux repository packages through pacman; does not refresh databases or upgrade the whole system. */
|
|
187
|
+
pacman: (packages) => packageList("pacman", packages),
|
|
188
|
+
/** Install Flatpak app IDs; defaults to the user's flathub remote and stable branch. */
|
|
189
|
+
flatpak: (packages, options = {}) => packages.map((name) => ({ kind: "package", manager: "flatpak", name, flatpak: { ...options } })),
|
|
190
|
+
/** Install previously acquired Mac App Store apps by numeric ID; follows available updates without version pins. */
|
|
191
|
+
mas: (ids) => ids.map((id) => {
|
|
192
|
+
if (typeof id === "number" && !Number.isSafeInteger(id) || !/^[1-9]\d*$/.test(String(id))) {
|
|
193
|
+
throw new Error(`Invalid Mac App Store ID: ${id}`);
|
|
194
|
+
}
|
|
195
|
+
return { kind: "package", manager: "mas", name: String(id) };
|
|
196
|
+
}),
|
|
197
|
+
/** Use the configured manager, Homebrew on macOS, or detect APT/DNF/YUM/pacman on Linux. */
|
|
183
198
|
system: (packages) => packageList("system", packages)
|
|
184
199
|
};
|
|
185
|
-
function symlink(source,
|
|
186
|
-
return { kind: "symlink", source, target };
|
|
200
|
+
function symlink(source, target2) {
|
|
201
|
+
return { kind: "symlink", source, target: target2 };
|
|
187
202
|
}
|
|
188
203
|
function launchAgent(label, options) {
|
|
189
204
|
return { kind: "launch-agent", label, ...options };
|
|
190
205
|
}
|
|
191
|
-
function generatedFile(format,
|
|
206
|
+
function generatedFile(format, target2, value, options = {}) {
|
|
192
207
|
return {
|
|
193
208
|
kind: "generated-file",
|
|
194
209
|
format,
|
|
195
|
-
target,
|
|
210
|
+
target: target2,
|
|
196
211
|
value,
|
|
197
212
|
ifExists: options.ifExists ?? "overwrite",
|
|
198
213
|
...options.mode !== void 0 ? { mode: options.mode } : {}
|
|
@@ -203,16 +218,16 @@ var files = {
|
|
|
203
218
|
* Generate TOML. Values must be representable in TOML (for example, no null).
|
|
204
219
|
* @example files.toml("~/.config/app/config.toml", { server: { port: 3000 } })
|
|
205
220
|
*/
|
|
206
|
-
toml: (
|
|
221
|
+
toml: (target2, value, options) => generatedFile("toml", target2, value, options),
|
|
207
222
|
/** Generate YAML with the standard overwrite/restore policy. */
|
|
208
|
-
yaml: (
|
|
223
|
+
yaml: (target2, value, options) => generatedFile("yaml", target2, value, options),
|
|
209
224
|
/**
|
|
210
225
|
* Generate formatted JSON.
|
|
211
226
|
* @example files.json("~/.config/app/config.json", { enabled: true })
|
|
212
227
|
*/
|
|
213
|
-
json: (
|
|
228
|
+
json: (target2, value, options) => generatedFile("json", target2, value, options),
|
|
214
229
|
/** Generate JSONC from plain data or jsonc.concat/object commands with comments. */
|
|
215
|
-
jsonc: (
|
|
230
|
+
jsonc: (target2, value, options) => value instanceof JsoncDocument ? { ...generatedFile("jsonc", target2, null, options), renderedContent: value.content } : generatedFile("jsonc", target2, value, options)
|
|
216
231
|
};
|
|
217
232
|
function systemdService(name, options) {
|
|
218
233
|
return { kind: "systemd-service", name, scope: options.scope ?? "user", ...options };
|
|
@@ -392,21 +407,21 @@ var bash = {
|
|
|
392
407
|
/** Generate ~/.profile using Bash syntax; use only where Bash will read it. */
|
|
393
408
|
profile: (statements, options) => shellFile("bash", "~/.profile", statements, options)
|
|
394
409
|
};
|
|
395
|
-
function renderShell(statements,
|
|
396
|
-
return `${statements.map((statement) => renderStatement(statement,
|
|
410
|
+
function renderShell(statements, target2) {
|
|
411
|
+
return `${statements.map((statement) => renderStatement(statement, target2, 0)).join("\n")}
|
|
397
412
|
`;
|
|
398
413
|
}
|
|
399
|
-
function shellFile(format,
|
|
414
|
+
function shellFile(format, target2, statements, options = {}) {
|
|
400
415
|
return {
|
|
401
416
|
kind: "generated-file",
|
|
402
|
-
target,
|
|
417
|
+
target: target2,
|
|
403
418
|
format,
|
|
404
419
|
value: renderShell(statements, format),
|
|
405
420
|
ifExists: options.ifExists ?? "overwrite",
|
|
406
421
|
mode: options.mode ?? 420
|
|
407
422
|
};
|
|
408
423
|
}
|
|
409
|
-
function renderStatement(statement,
|
|
424
|
+
function renderStatement(statement, target2, depth) {
|
|
410
425
|
const indent = " ".repeat(depth);
|
|
411
426
|
switch (statement.kind) {
|
|
412
427
|
case "export":
|
|
@@ -428,11 +443,11 @@ function renderStatement(statement, target, depth) {
|
|
|
428
443
|
case "if":
|
|
429
444
|
return [
|
|
430
445
|
`${indent}if ${renderCondition(statement.condition)}; then`,
|
|
431
|
-
...statement.statements.map((child) => renderStatement(child,
|
|
446
|
+
...statement.statements.map((child) => renderStatement(child, target2, depth + 1)),
|
|
432
447
|
`${indent}fi`
|
|
433
448
|
].join("\n");
|
|
434
449
|
case "zsh-setopt":
|
|
435
|
-
if (
|
|
450
|
+
if (target2 !== "zsh") throw new Error("setopt is only valid in Zsh configuration");
|
|
436
451
|
return `${indent}setopt ${statement.options.join(" ")}`;
|
|
437
452
|
case "raw":
|
|
438
453
|
return statement.code.split("\n").map((line) => `${indent}${line}`).join("\n");
|
|
@@ -495,14 +510,15 @@ function validateVariable(name) {
|
|
|
495
510
|
|
|
496
511
|
// src/config/load.ts
|
|
497
512
|
import { hostname as readHostname, homedir, platform as readPlatform } from "os";
|
|
498
|
-
import { dirname, isAbsolute as isAbsolute2, resolve as
|
|
499
|
-
import { access } from "fs/promises";
|
|
513
|
+
import { dirname, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
|
|
514
|
+
import { access as access2 } from "fs/promises";
|
|
500
515
|
|
|
501
516
|
// src/config/identity.ts
|
|
502
517
|
import { createHash } from "crypto";
|
|
503
518
|
function resourceId(resource) {
|
|
504
519
|
switch (resource.kind) {
|
|
505
520
|
case "package":
|
|
521
|
+
if (resource.manager === "flatpak") return `package:flatpak:${resource.flatpak?.scope ?? "user"}:${resource.name}:${resource.flatpak?.branch ?? "stable"}`;
|
|
506
522
|
return `package:${resource.manager}:${resource.name}`;
|
|
507
523
|
case "symlink":
|
|
508
524
|
return `file:${resource.target}`;
|
|
@@ -557,6 +573,20 @@ async function addPathToHash(hash, path, relativePath) {
|
|
|
557
573
|
}
|
|
558
574
|
}
|
|
559
575
|
|
|
576
|
+
// src/config/package-options.ts
|
|
577
|
+
function isFlatpakOptions(value) {
|
|
578
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
579
|
+
const options = value;
|
|
580
|
+
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));
|
|
581
|
+
}
|
|
582
|
+
function isPackageName(manager, name) {
|
|
583
|
+
if (typeof name !== "string" || name.length === 0) return false;
|
|
584
|
+
if (manager === "mas") return /^[1-9]\d*$/.test(name);
|
|
585
|
+
if (manager === "flatpak") return /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*){2,}$/.test(name);
|
|
586
|
+
if (manager === "pacman") return /^[A-Za-z0-9@_+][A-Za-z0-9@._+-]*$/.test(name);
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
560
590
|
// src/config/validation.ts
|
|
561
591
|
function validateResource(value) {
|
|
562
592
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -565,7 +595,7 @@ function validateResource(value) {
|
|
|
565
595
|
const candidate = value;
|
|
566
596
|
switch (candidate.kind) {
|
|
567
597
|
case "package":
|
|
568
|
-
if (!["mise", "brew", "brew-cask", "apt", "system"].includes(String(candidate.manager)) ||
|
|
598
|
+
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)) {
|
|
569
599
|
throw new Error("Invalid package resource");
|
|
570
600
|
}
|
|
571
601
|
return;
|
|
@@ -622,12 +652,33 @@ function isBrewCaskUpgradeOptions(value) {
|
|
|
622
652
|
|
|
623
653
|
// src/config/load.ts
|
|
624
654
|
import { createJiti } from "jiti/static";
|
|
655
|
+
|
|
656
|
+
// src/config/system-manager.ts
|
|
657
|
+
import { access, stat } from "fs/promises";
|
|
658
|
+
import { constants } from "fs";
|
|
659
|
+
import { delimiter, resolve as resolve3 } from "path";
|
|
660
|
+
async function detectLinuxManager(searchPath = process.env.PATH ?? "") {
|
|
661
|
+
for (const [manager, command] of [["apt", "apt-get"], ["dnf", "dnf"], ["yum", "yum"], ["pacman", "pacman"]]) {
|
|
662
|
+
for (const directory of searchPath.split(delimiter).filter(Boolean)) {
|
|
663
|
+
const path = resolve3(directory, command);
|
|
664
|
+
try {
|
|
665
|
+
await access(path, constants.X_OK);
|
|
666
|
+
if ((await stat(path)).isFile()) return manager;
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (!(error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR", "EACCES"].includes(String(error.code)))) throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
throw new Error("No supported Linux package manager found on PATH (apt-get, dnf, yum, pacman). Install one or set managers.linux explicitly.");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/config/load.ts
|
|
625
676
|
var DEFAULT_CONFIG_FILE = "workstation.config.ts";
|
|
626
677
|
async function findConfig(explicit) {
|
|
627
|
-
if (explicit) return
|
|
628
|
-
const path =
|
|
678
|
+
if (explicit) return resolve4(explicit);
|
|
679
|
+
const path = resolve4(DEFAULT_CONFIG_FILE);
|
|
629
680
|
try {
|
|
630
|
-
await
|
|
681
|
+
await access2(path);
|
|
631
682
|
return path;
|
|
632
683
|
} catch {
|
|
633
684
|
throw new Error(`No configuration found (${DEFAULT_CONFIG_FILE})`);
|
|
@@ -673,7 +724,11 @@ async function loadConfig(configPath, machineOverride) {
|
|
|
673
724
|
}
|
|
674
725
|
async function resolveResource(resource, definition, context) {
|
|
675
726
|
if (resource.kind === "package") {
|
|
676
|
-
const manager = resolveManager(resource.manager, definition, context);
|
|
727
|
+
const manager = await resolveManager(resource.manager, definition, context);
|
|
728
|
+
if (!isPackageName(manager, resource.name)) throw new Error(`Invalid ${manager} package ID: ${resource.name}`);
|
|
729
|
+
if ((manager === "pacman" || manager === "flatpak") && context.platform !== "linux") throw new Error(`${manager} requires Linux`);
|
|
730
|
+
if (manager === "mas" && context.platform !== "darwin") throw new Error("mas requires macOS");
|
|
731
|
+
if (resource.flatpak !== void 0 && manager !== "flatpak") throw new Error(`Flatpak options require the flatpak backend (${resource.name})`);
|
|
677
732
|
if (resource.upgrade !== void 0 && manager !== "brew-cask") {
|
|
678
733
|
throw new Error(`Upgrade options are only supported for Homebrew casks (${resource.name})`);
|
|
679
734
|
}
|
|
@@ -683,7 +738,11 @@ async function resolveResource(resource, definition, context) {
|
|
|
683
738
|
if (resource.version !== void 0) {
|
|
684
739
|
throw new Error(`${manager} package ${resource.name} cannot declare a version`);
|
|
685
740
|
}
|
|
686
|
-
return { ...resource, manager
|
|
741
|
+
return { ...resource, manager, ...manager === "flatpak" ? { flatpak: {
|
|
742
|
+
scope: resource.flatpak?.scope ?? "user",
|
|
743
|
+
remote: resource.flatpak?.remote ?? "flathub",
|
|
744
|
+
branch: resource.flatpak?.branch ?? "stable"
|
|
745
|
+
} } : {} };
|
|
687
746
|
}
|
|
688
747
|
if (resource.kind === "symlink") {
|
|
689
748
|
return {
|
|
@@ -707,11 +766,11 @@ async function resolveResource(resource, definition, context) {
|
|
|
707
766
|
}
|
|
708
767
|
if (resource.kind === "custom-tool") {
|
|
709
768
|
const source = expandPath(resource.source, context, true);
|
|
710
|
-
const
|
|
769
|
+
const target2 = expandPath(resource.target, context, false);
|
|
711
770
|
return {
|
|
712
771
|
...resource,
|
|
713
772
|
source,
|
|
714
|
-
target,
|
|
773
|
+
target: target2,
|
|
715
774
|
sourceHash: await hashSource(source),
|
|
716
775
|
build: {
|
|
717
776
|
...resource.build,
|
|
@@ -729,14 +788,16 @@ async function resolveResource(resource, definition, context) {
|
|
|
729
788
|
...resource.stderrPath ? { stderrPath: expandPath(resource.stderrPath, context, false) } : {}
|
|
730
789
|
};
|
|
731
790
|
}
|
|
732
|
-
function resolveManager(manager, definition, context) {
|
|
791
|
+
async function resolveManager(manager, definition, context) {
|
|
733
792
|
if (manager !== "system") return manager;
|
|
734
|
-
|
|
793
|
+
const configured = definition.managers?.[context.platform];
|
|
794
|
+
if (configured !== void 0) return configured;
|
|
795
|
+
return context.platform === "darwin" ? "brew" : await detectLinuxManager();
|
|
735
796
|
}
|
|
736
797
|
function expandPath(path, context, relativeToConfig) {
|
|
737
798
|
const expanded = path === "~" ? context.home : path.replace(/^~\//, `${context.home}/`);
|
|
738
|
-
if (isAbsolute2(expanded)) return
|
|
739
|
-
return
|
|
799
|
+
if (isAbsolute2(expanded)) return resolve4(expanded);
|
|
800
|
+
return resolve4(relativeToConfig ? context.configDir : context.home, expanded);
|
|
740
801
|
}
|
|
741
802
|
function flatten2(input) {
|
|
742
803
|
if (!input) return [];
|
|
@@ -774,11 +835,11 @@ function mergeDefinitions(definitions) {
|
|
|
774
835
|
|
|
775
836
|
// src/persistence/manifest.ts
|
|
776
837
|
import { mkdir, readFile as readFile2, rename, writeFile } from "fs/promises";
|
|
777
|
-
import { dirname as dirname2, resolve as
|
|
838
|
+
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
778
839
|
import { parse, stringify } from "smol-toml";
|
|
779
840
|
var MANIFEST_VERSION = 1;
|
|
780
841
|
function manifestPath(config) {
|
|
781
|
-
return
|
|
842
|
+
return resolve5(dirname2(config.stateFile), "config.toml");
|
|
782
843
|
}
|
|
783
844
|
async function writeManifest(path, config) {
|
|
784
845
|
const document = {
|
|
@@ -820,6 +881,11 @@ function toTomlResource(resource) {
|
|
|
820
881
|
name: resource.name,
|
|
821
882
|
...resource.version ? { version: resource.version } : {},
|
|
822
883
|
...resource.lockedVersion ? { locked_version: resource.lockedVersion } : {},
|
|
884
|
+
...resource.flatpak ? { flatpak: {
|
|
885
|
+
...resource.flatpak.scope ? { scope: resource.flatpak.scope } : {},
|
|
886
|
+
...resource.flatpak.remote ? { remote: resource.flatpak.remote } : {},
|
|
887
|
+
...resource.flatpak.branch ? { branch: resource.flatpak.branch } : {}
|
|
888
|
+
} } : {},
|
|
823
889
|
...resource.upgrade ? {
|
|
824
890
|
upgrade: {
|
|
825
891
|
...resource.upgrade.greedy !== void 0 ? { greedy: resource.upgrade.greedy } : {},
|
|
@@ -970,6 +1036,9 @@ function parsePackage(value) {
|
|
|
970
1036
|
if (!isPackageManager(manager)) {
|
|
971
1037
|
throw new Error(`Invalid manifest package manager: ${manager}`);
|
|
972
1038
|
}
|
|
1039
|
+
if (!isPackageName(manager, value.name)) throw new Error(`Invalid ${manager} package ID`);
|
|
1040
|
+
if (value.flatpak !== void 0 && (manager !== "flatpak" || !isFlatpakOptions(value.flatpak))) throw new Error("Invalid Flatpak options");
|
|
1041
|
+
if (manager === "mas" && value.locked_version !== void 0) throw new Error("mas does not support version pins");
|
|
973
1042
|
const upgrade = value.upgrade === void 0 ? void 0 : requireTable(value.upgrade, "package.upgrade");
|
|
974
1043
|
if (value.version !== void 0 && manager !== "mise") {
|
|
975
1044
|
throw new Error(`Manifest ${manager} package cannot declare a version`);
|
|
@@ -981,6 +1050,7 @@ function parsePackage(value) {
|
|
|
981
1050
|
kind: "package",
|
|
982
1051
|
manager,
|
|
983
1052
|
name: requireString(value.name, "package.name"),
|
|
1053
|
+
...isFlatpakOptions(value.flatpak) ? { flatpak: value.flatpak } : {},
|
|
984
1054
|
...value.version !== void 0 ? { version: requireString(value.version, "package.version") } : {},
|
|
985
1055
|
...value.locked_version !== void 0 ? { lockedVersion: requireString(value.locked_version, "package.locked_version") } : {},
|
|
986
1056
|
...upgrade ? {
|
|
@@ -1000,13 +1070,13 @@ function parseSymlink(value) {
|
|
|
1000
1070
|
}
|
|
1001
1071
|
function parseLaunchAgent(value) {
|
|
1002
1072
|
const args = value.args === void 0 ? void 0 : requireStringArray(value.args, "launch-agent.args");
|
|
1003
|
-
const
|
|
1073
|
+
const environment3 = value.environment === void 0 ? void 0 : requireStringTable(value.environment, "launch-agent.environment");
|
|
1004
1074
|
return {
|
|
1005
1075
|
kind: "launch-agent",
|
|
1006
1076
|
label: requireString(value.label, "launch-agent.label"),
|
|
1007
1077
|
program: requireString(value.program, "launch-agent.program"),
|
|
1008
1078
|
...args ? { args } : {},
|
|
1009
|
-
...
|
|
1079
|
+
...environment3 ? { environment: environment3 } : {},
|
|
1010
1080
|
...value.run_at_load !== void 0 ? { runAtLoad: requireBoolean(value.run_at_load, "launch-agent.run_at_load") } : {},
|
|
1011
1081
|
...value.keep_alive !== void 0 ? { keepAlive: requireBoolean(value.keep_alive, "launch-agent.keep_alive") } : {},
|
|
1012
1082
|
...value.stdout_path !== void 0 ? { stdoutPath: requireString(value.stdout_path, "launch-agent.stdout_path") } : {},
|
|
@@ -1043,7 +1113,7 @@ function requireStringTable(value, field) {
|
|
|
1043
1113
|
return result;
|
|
1044
1114
|
}
|
|
1045
1115
|
function isPackageManager(value) {
|
|
1046
|
-
return value === "mise" || value === "brew" || value === "brew-cask" || value === "apt";
|
|
1116
|
+
return value === "mise" || value === "brew" || value === "brew-cask" || value === "apt" || value === "dnf" || value === "yum" || value === "pacman" || value === "flatpak" || value === "mas";
|
|
1047
1117
|
}
|
|
1048
1118
|
function isConfigValue2(value) {
|
|
1049
1119
|
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -1055,7 +1125,7 @@ function isConfigValue2(value) {
|
|
|
1055
1125
|
|
|
1056
1126
|
// src/persistence/lock.ts
|
|
1057
1127
|
import { readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
1058
|
-
import { dirname as dirname3, resolve as
|
|
1128
|
+
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
1059
1129
|
import { parse as parse2, stringify as stringify2 } from "smol-toml";
|
|
1060
1130
|
|
|
1061
1131
|
// src/resources/shared.ts
|
|
@@ -1103,6 +1173,69 @@ function isRecord(value) {
|
|
|
1103
1173
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1104
1174
|
}
|
|
1105
1175
|
|
|
1176
|
+
// src/resources/rpm.ts
|
|
1177
|
+
var environment = { LC_ALL: "C" };
|
|
1178
|
+
var versionPattern = /^\d+:[A-Za-z0-9._+~^]+-[A-Za-z0-9._+~^]+\.[A-Za-z0-9_]+$/;
|
|
1179
|
+
async function resolveRpmVersion(resource, runner) {
|
|
1180
|
+
const architecture = (await requireSuccess(runner, "rpm", ["--eval", "%{_arch}"], { environment })).stdout.trim();
|
|
1181
|
+
if (!/^[A-Za-z0-9_]+$/.test(architecture)) throw new Error("RPM did not report a valid native architecture");
|
|
1182
|
+
const result = await requireSuccess(runner, resource.manager, ["--quiet", "--color=never", "list", resource.name], { environment });
|
|
1183
|
+
const candidates = [];
|
|
1184
|
+
let available = false;
|
|
1185
|
+
const lines = result.stdout.replace(/^(\S+\.\S+)\n[ \t]+(?=\S+\s+\S+)/gm, "$1 ").split("\n");
|
|
1186
|
+
for (const line of lines) {
|
|
1187
|
+
if (/^Available packages\s*$/i.test(line.trim())) {
|
|
1188
|
+
available = true;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (/^Installed packages\s*$/i.test(line.trim())) {
|
|
1192
|
+
available = false;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const [nameArch, evr, repository] = line.trim().split(/\s+/);
|
|
1196
|
+
if (!nameArch || !evr || !repository) continue;
|
|
1197
|
+
const dot = nameArch.lastIndexOf(".");
|
|
1198
|
+
const name = nameArch.slice(0, dot);
|
|
1199
|
+
const arch = nameArch.slice(dot + 1);
|
|
1200
|
+
if (nameArch !== resource.name && (name !== resource.name || arch !== architecture && arch !== "noarch")) continue;
|
|
1201
|
+
const version = `${evr.includes(":") ? evr : `0:${evr}`}.${arch}`;
|
|
1202
|
+
if (!versionPattern.test(version)) throw new Error(`${resource.manager} reported an invalid RPM version for ${resource.name}: ${version}`);
|
|
1203
|
+
candidates.push({ version, native: arch === architecture, available });
|
|
1204
|
+
}
|
|
1205
|
+
candidates.sort((left, right) => Number(right.available) - Number(left.available) || Number(right.native) - Number(left.native));
|
|
1206
|
+
const candidate = candidates[0];
|
|
1207
|
+
if (!candidate) throw new Error(`${resource.manager} has no installation candidate for ${resource.name}`);
|
|
1208
|
+
return candidate.version;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/resources/pacman.ts
|
|
1212
|
+
var environment2 = { LC_ALL: "C" };
|
|
1213
|
+
async function resolvePacmanVersion(resource, runner) {
|
|
1214
|
+
const result = await requireSuccess(runner, "pacman", ["-Sp", "--print-format", "%n %v", resource.name], { environment: environment2 });
|
|
1215
|
+
const versions = result.stdout.trim().split("\n").flatMap((line) => {
|
|
1216
|
+
const [name, version, extra] = line.trim().split(/\s+/);
|
|
1217
|
+
return name === resource.name && version && !extra ? [version] : [];
|
|
1218
|
+
});
|
|
1219
|
+
if (versions.length !== 1 || !versions[0]) throw new Error(`pacman did not report a unique version for ${resource.name}`);
|
|
1220
|
+
return versions[0];
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/resources/flatpak.ts
|
|
1224
|
+
function target(resource) {
|
|
1225
|
+
const scope = resource.flatpak?.scope ?? "user";
|
|
1226
|
+
const branch = resource.flatpak?.branch ?? "stable";
|
|
1227
|
+
return { scope, branch, flag: `--${scope}`, remote: resource.flatpak?.remote ?? "flathub", ref: `app/${resource.name}//${branch}` };
|
|
1228
|
+
}
|
|
1229
|
+
function commit(value) {
|
|
1230
|
+
const hash = value.trim();
|
|
1231
|
+
if (!/^[a-f0-9]{64}$/.test(hash)) throw new Error(`Flatpak did not report a full commit ID: ${hash}`);
|
|
1232
|
+
return hash;
|
|
1233
|
+
}
|
|
1234
|
+
async function resolveFlatpakVersion(resource, runner) {
|
|
1235
|
+
const { flag, remote, ref } = target(resource);
|
|
1236
|
+
return commit((await requireSuccess(runner, "flatpak", ["remote-info", flag, "--show-commit", remote, ref])).stdout);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1106
1239
|
// src/resources/package-version.ts
|
|
1107
1240
|
async function resolvePackageVersion(resource, runner) {
|
|
1108
1241
|
if (resource.kind !== "package") return void 0;
|
|
@@ -1127,22 +1260,31 @@ async function resolvePackageVersion(resource, runner) {
|
|
|
1127
1260
|
const version = await readAvailableBrewVersion(resource, runner);
|
|
1128
1261
|
return version === "latest" ? void 0 : version;
|
|
1129
1262
|
}
|
|
1263
|
+
case "dnf":
|
|
1264
|
+
case "yum":
|
|
1265
|
+
return await resolveRpmVersion(resource, runner);
|
|
1130
1266
|
case "system":
|
|
1131
1267
|
throw new Error("System package manager must be resolved before locking");
|
|
1268
|
+
case "pacman":
|
|
1269
|
+
return await resolvePacmanVersion(resource, runner);
|
|
1270
|
+
case "flatpak":
|
|
1271
|
+
return await resolveFlatpakVersion(resource, runner);
|
|
1272
|
+
case "mas":
|
|
1273
|
+
return void 0;
|
|
1132
1274
|
}
|
|
1133
1275
|
}
|
|
1134
1276
|
|
|
1135
1277
|
// src/persistence/lock.ts
|
|
1136
1278
|
var LOCK_VERSION = 1;
|
|
1137
1279
|
function lockPath(configPath) {
|
|
1138
|
-
return
|
|
1280
|
+
return resolve6(dirname3(configPath), "workstation.lock");
|
|
1139
1281
|
}
|
|
1140
1282
|
async function lockConfig(configPath, config, runner) {
|
|
1141
1283
|
const path = lockPath(configPath);
|
|
1142
1284
|
const previousText = await readOptional(path);
|
|
1143
1285
|
const previous = previousText === void 0 ? emptyLock() : parseLock(previousText, path);
|
|
1144
|
-
const
|
|
1145
|
-
const existing = new Map(
|
|
1286
|
+
const target2 = previous.targets.find(({ machine: machine2 }) => machine2 === config.context.machine);
|
|
1287
|
+
const existing = new Map(target2?.resources.map((entry) => [entry.id, entry]));
|
|
1146
1288
|
const resources = [];
|
|
1147
1289
|
const entries = [];
|
|
1148
1290
|
for (const resource of config.resources) {
|
|
@@ -1186,10 +1328,10 @@ function emptyLock() {
|
|
|
1186
1328
|
function stringifyLock(lock) {
|
|
1187
1329
|
return stringify2({
|
|
1188
1330
|
version: lock.version,
|
|
1189
|
-
targets: lock.targets.map((
|
|
1190
|
-
machine:
|
|
1191
|
-
platform:
|
|
1192
|
-
resources:
|
|
1331
|
+
targets: lock.targets.map((target2) => ({
|
|
1332
|
+
machine: target2.machine,
|
|
1333
|
+
platform: target2.platform,
|
|
1334
|
+
resources: target2.resources.map((entry) => ({
|
|
1193
1335
|
id: entry.id,
|
|
1194
1336
|
fingerprint: entry.fingerprint,
|
|
1195
1337
|
...entry.lockedVersion ? { locked_version: entry.lockedVersion } : {}
|
|
@@ -1205,19 +1347,19 @@ function parseLock(text, path) {
|
|
|
1205
1347
|
if (!Array.isArray(document.targets)) throw new Error(`Lock targets must be an array in ${path}`);
|
|
1206
1348
|
const machines = /* @__PURE__ */ new Set();
|
|
1207
1349
|
const targets = document.targets.map((value, targetIndex) => {
|
|
1208
|
-
const
|
|
1209
|
-
const machine2 = requireString2(
|
|
1350
|
+
const target2 = requireTable2(value, `targets[${targetIndex}]`);
|
|
1351
|
+
const machine2 = requireString2(target2.machine, `targets[${targetIndex}].machine`);
|
|
1210
1352
|
if (machines.has(machine2)) throw new Error(`Duplicate machine ${machine2} in ${path}`);
|
|
1211
1353
|
machines.add(machine2);
|
|
1212
|
-
const platform = requireString2(
|
|
1354
|
+
const platform = requireString2(target2.platform, `targets[${targetIndex}].platform`);
|
|
1213
1355
|
if (platform !== "darwin" && platform !== "linux") {
|
|
1214
1356
|
throw new Error(`Invalid platform for ${machine2} in ${path}`);
|
|
1215
1357
|
}
|
|
1216
|
-
if (!Array.isArray(
|
|
1358
|
+
if (!Array.isArray(target2.resources)) {
|
|
1217
1359
|
throw new Error(`Lock resources for ${machine2} must be an array in ${path}`);
|
|
1218
1360
|
}
|
|
1219
1361
|
const ids = /* @__PURE__ */ new Set();
|
|
1220
|
-
const resources =
|
|
1362
|
+
const resources = target2.resources.map((value2, resourceIndex) => {
|
|
1221
1363
|
const entry = requireTable2(value2, `targets[${targetIndex}].resources[${resourceIndex}]`);
|
|
1222
1364
|
const id = requireString2(entry.id, "lock resource id");
|
|
1223
1365
|
if (ids.has(id)) throw new Error(`Duplicate resource ${id} for ${machine2} in ${path}`);
|
|
@@ -1261,9 +1403,24 @@ async function atomicWrite(path, contents) {
|
|
|
1261
1403
|
// src/resources/runner.ts
|
|
1262
1404
|
import { spawn } from "child_process";
|
|
1263
1405
|
var ProcessRunner = class {
|
|
1406
|
+
/** Opt into command progress and streaming; verbose also prints captured query output. */
|
|
1407
|
+
constructor(logging = {}) {
|
|
1408
|
+
this.logging = logging;
|
|
1409
|
+
}
|
|
1410
|
+
/** Print resource progress only when logging is enabled. */
|
|
1411
|
+
report(message) {
|
|
1412
|
+
if (this.logging.progress || this.logging.verbose) process.stderr.write(` ${message}
|
|
1413
|
+
`);
|
|
1414
|
+
}
|
|
1264
1415
|
/** Execute a command directly and return captured output and its exit code; spawn failures reject. */
|
|
1265
1416
|
async run(command, args, options) {
|
|
1266
|
-
|
|
1417
|
+
const started = Date.now();
|
|
1418
|
+
if (this.logging.progress || this.logging.verbose) {
|
|
1419
|
+
process.stderr.write(` $ ${[command, ...args].map((value) => /^[\w./:@=+-]+$/.test(value) ? value : JSON.stringify(value)).join(" ")}${options?.cwd ? ` (in ${options.cwd})` : ""}
|
|
1420
|
+
`);
|
|
1421
|
+
}
|
|
1422
|
+
const stream = this.logging.verbose || this.logging.progress && options?.streamOutput;
|
|
1423
|
+
return await new Promise((resolve7, reject) => {
|
|
1267
1424
|
const child = spawn(command, [...args], {
|
|
1268
1425
|
cwd: options?.cwd,
|
|
1269
1426
|
env: { ...process.env, ...options?.environment },
|
|
@@ -1273,13 +1430,22 @@ var ProcessRunner = class {
|
|
|
1273
1430
|
let stderr = "";
|
|
1274
1431
|
child.stdout.setEncoding("utf8");
|
|
1275
1432
|
child.stderr.setEncoding("utf8");
|
|
1276
|
-
child.stdout.on("data", (chunk) =>
|
|
1277
|
-
|
|
1433
|
+
child.stdout.on("data", (chunk) => {
|
|
1434
|
+
stdout += chunk;
|
|
1435
|
+
if (stream) process.stdout.write(chunk);
|
|
1436
|
+
});
|
|
1437
|
+
child.stderr.on("data", (chunk) => {
|
|
1438
|
+
stderr += chunk;
|
|
1439
|
+
if (stream || this.logging.progress) process.stderr.write(chunk);
|
|
1440
|
+
});
|
|
1278
1441
|
child.once("error", reject);
|
|
1279
|
-
child.once(
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1442
|
+
child.once("close", (exitCode) => {
|
|
1443
|
+
if (this.logging.progress || this.logging.verbose) {
|
|
1444
|
+
process.stderr.write(` -> exit ${exitCode ?? 1} (${((Date.now() - started) / 1e3).toFixed(1)}s)
|
|
1445
|
+
`);
|
|
1446
|
+
}
|
|
1447
|
+
resolve7({ exitCode: exitCode ?? 1, stdout, stderr });
|
|
1448
|
+
});
|
|
1283
1449
|
});
|
|
1284
1450
|
}
|
|
1285
1451
|
};
|