vlt
Version:
The vlt CLI
450 lines (445 loc) • 13.2 kB
JavaScript
var global = globalThis;
import {Buffer} from "node:buffer";
import {setTimeout,clearTimeout,setImmediate,clearImmediate,setInterval,clearInterval} from "node:timers";
import {createRequire as _vlt_createRequire} from "node:module";
var require = _vlt_createRequire(import.meta.filename);
import {
PackageInfoClient
} from "./chunk-FZMPFIDM.js";
import {
Monorepo
} from "./chunk-D36DAG56.js";
import {
PackageJson
} from "./chunk-YWPMIBJS.js";
import {
find,
load,
reload,
save
} from "./chunk-5UBJ3ZBM.js";
import {
PathScurry
} from "./chunk-GY4L7O2Y.js";
import {
assertRecordStringString,
assertRecordStringT,
isRecordStringString
} from "./chunk-X4RDKJKD.js";
import {
commands,
definition,
getCommand,
isRecordField,
recordFields
} from "./chunk-BNCOU5ZT.js";
import {
error
} from "./chunk-RV3EHS4P.js";
// ../../src/cli-sdk/src/config/index.ts
import { readFile, rm, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
// ../../src/cli-sdk/src/config/merge.ts
var merge = (base, add) => Object.fromEntries(
Object.entries(base).map(([k, v]) => [
k,
add[k] === void 0 ? v : Array.isArray(v) && Array.isArray(add[k]) ? [
.../* @__PURE__ */ new Set([
...v,
...add[k]
])
] : Array.isArray(v) || Array.isArray(add[k]) ? add[k] : !!v && typeof v === "object" && !!add[k] && typeof add[k] === "object" ? merge(v, add[k]) : add[k]
]).concat(
// already merged together if existing, so just get new additions
Object.entries(add).filter(([k]) => base[k] === void 0)
)
);
// ../../src/cli-sdk/src/config/index.ts
var kCustomInspect = Symbol.for("nodejs.util.inspect.custom");
var reducePairs = (pairs) => {
const record = {};
for (const kv of pairs) {
const eq = kv.indexOf("=");
if (eq === -1) record[kv] = "";
else {
const key = kv.substring(0, eq);
const val = kv.substring(eq + 1);
record[key] = val;
}
}
return record;
};
var isRecordFieldValue = (k, v) => Array.isArray(v) && recordFields.includes(k);
var pairsToRecords = (obj) => {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k,
k === "command" && v && typeof v === "object" ? Object.fromEntries(
Object.entries(v).map(([k2, v2]) => [
k2,
pairsToRecords(v2)
])
) : isRecordFieldValue(k, v) ? reducePairs(v) : v
])
// hard cast because TS can't see through the entries/fromEntries
);
};
var recordsToPairs = (obj) => {
return Object.fromEntries(
Object.entries(obj).filter(
([k]) => !(k === "scurry" || k === "packageJson" || k === "monorepo" || k === "projectRoot" || k === "packageInfo")
).map(([k, v]) => [
k,
k === "command" && v && typeof v === "object" ? recordsToPairs(v) : !v || typeof v !== "object" || Array.isArray(v) || !isRecordField(k) ? v : Object.entries(v).map(([k2, v2]) => `${k2}=${v2}`)
])
);
};
var kRecord = Symbol("parsed key=value record");
var Config = class _Config {
/**
* The {@link https://npmjs.com/jackspeak | JackSpeak} object
* representing vlt's configuration
*/
jack;
/**
* Parsed values in effect
*/
values;
/**
* Command-specific config values
*/
commandValues = {};
/**
* A flattened object of the parsed configuration
*/
get options() {
if (this.#options) return this.#options;
const scurry = new PathScurry(this.projectRoot);
const packageJson = new PackageJson();
const asRecords = pairsToRecords(this.parse().values);
const extras = {
projectRoot: this.projectRoot,
scurry,
packageJson,
monorepo: Monorepo.maybeLoad(this.projectRoot, {
scurry,
packageJson,
load: {
paths: asRecords.workspace,
groups: asRecords["workspace-group"]
}
}),
catalog: load(
"catalog",
assertRecordStringString
),
catalogs: load(
"catalogs",
(o) => assertRecordStringT(
o,
isRecordStringString,
"Record<string, Record<string, string>>"
)
)
};
const options = Object.assign(
asRecords,
extras
);
this.#options = Object.assign(options, {
packageInfo: new PackageInfoClient(options),
[kCustomInspect]() {
return Object.fromEntries(
Object.entries(options).filter(
([k]) => k !== "monorepo" && k !== "scurry" && k !== "packageJson" && k !== "packageInfo"
)
);
}
});
return this.#options;
}
/**
* Reset the options value, optionally setting a new project root
* to recalculate the options.
*/
resetOptions(projectRoot = process.cwd()) {
this.projectRoot = projectRoot;
this.#options = void 0;
}
// memoized options() getter value
#options;
/**
* positional arguments to the vlt process
*/
positionals;
/**
* Original arguments used for parsing (stored for reload purposes)
* @internal
*/
#originalArgs;
/**
* The root of the project where a vlt.json, vlt.json,
* package.json, or .git was found. Not necessarily the `process.cwd()`,
* though that is the default location.
*
* Never walks up as far as `$HOME`. So for example, if a project is in
* `~/projects/xyz`, then the highest dir it will check is `~/projects`
*/
projectRoot;
/**
* `Record<alias, canonical name>` to dereference command aliases.
*/
commands;
/**
* Which command name to use for overriding with command-specific values,
* determined from the argv when parse() is called.
*/
command;
constructor(jack = definition, projectRoot = process.cwd()) {
this.projectRoot = projectRoot;
this.commands = commands;
this.jack = jack;
}
/**
* Parse the arguments and set configuration and positionals accordingly.
*/
parse(args = process.argv) {
if (isParsed(this)) return this;
this.#originalArgs = [...args];
this.jack.loadEnvDefaults();
const p = this.jack.parseRaw(args);
const fallback = getCommand(p.values["fallback-command"]);
this.command = getCommand(p.positionals[0]);
const cmdOrFallback = this.command ?? fallback;
const cmdSpecific = cmdOrFallback && this.commandValues[cmdOrFallback];
if (cmdSpecific) {
this.jack.setConfigValues(recordsToPairs(cmdSpecific));
}
this.jack.applyDefaults(p);
this.jack.writeEnv(p);
if (this.command) p.positionals.shift();
else this.command = getCommand(p.values["fallback-command"]);
Object.assign(this, p);
if (!isParsed(this)) throw error("failed to parse config");
return this;
}
/**
* Get a `key=value` list option value as an object.
*
* For example, a list option with a vlaue of `['key=value', 'xyz=as=df' ]`
* would be returned as `{key: 'value', xyz: 'as=df'}`
*
* Results are memoized, so subsequent calls for the same key will return the
* same object. If new strings are added to the list, then the memoized value
* is *not* updated, so only use once configurations have been fully loaded.
*
* If the config value is not set at all, an empty object is returned.
*/
getRecord(k) {
const pairs = this.get(k);
if (!pairs) return {};
if (pairs[kRecord]) return pairs[kRecord];
const kv = pairs.reduce((kv2, pair) => {
const eq = pair.indexOf("=");
if (eq === -1) return kv2;
const key = pair.substring(0, eq);
const val = pair.substring(eq + 1);
kv2[key] = val;
return kv2;
}, {});
Object.assign(pairs, { [kRecord]: kv });
return kv;
}
/**
* Get a configuration value.
*
* Note: `key=value` pair configs are returned as a string array. To get them
* as an object, use {@link Config#getRecord}.
*/
get(k) {
return (this.values ?? this.parse().values)[k];
}
/**
* Write the config values to the user or project config file.
*/
async writeConfigFile(which, values) {
save("config", pairsToRecords(values), which);
await this.#reloadConfig();
}
/**
* Fold in the provided fields with the existing properties
* in the config file.
*/
async addConfigToFile(which, values) {
return this.writeConfigFile(
which,
merge(await this.#maybeLoadConfigFile(which) ?? {}, values)
);
}
// called in this weird bound way so that it can be used by the
// vlt-json config loading module.
#validator = function(c, file) {
this.#validateConfig(c, file);
}.bind(this);
#validateConfig(c, file) {
if (!c || typeof c !== "object" || Array.isArray(c)) {
throw error("invalid config, expected object", {
path: file,
found: c,
wanted: "ConfigFileData"
});
}
const { command, ...values } = recordsToPairs(c);
if (command) {
for (const [c2, opts] of Object.entries(command)) {
const cmd = getCommand(c2);
if (cmd) {
this.commandValues[cmd] = merge(
this.commandValues[cmd] ?? {},
opts
);
}
}
}
this.jack.setConfigValues(values, file);
}
/**
* if the file exists, parse and load it. returns object if data was
* loaded, or undefined if not.
*/
async #maybeLoadConfigFile(whichConfig) {
return load("config", this.#validator, whichConfig);
}
/**
* Deletes the specified config fields from the named file
* Returns `true` if anything was changed.
*/
async deleteConfigKeys(which, fields) {
const data = await this.#maybeLoadConfigFile(which);
if (!data) return false;
let didSomething = false;
for (const f of fields) {
const [key, ...sk] = f.split(".");
const subs = sk.join(".");
const k = key;
const v = data[k];
if (v === void 0) continue;
if (subs && v && typeof v === "object") {
if (Array.isArray(v)) {
const i = v.findIndex(
(subvalue) => subvalue.startsWith(`${subs}=`)
);
if (i !== -1) {
if (v.length === 1) delete data[k];
else v.splice(i, 1);
didSomething = true;
}
} else {
if (v[subs] !== void 0) {
delete v[subs];
if (Object.keys(v).length === 0) delete data[k];
didSomething = true;
}
}
} else {
didSomething = true;
delete data[k];
}
}
if (didSomething) await this.writeConfigFile(which, data);
return didSomething;
}
/**
* Edit the user or project configuration file.
*
* If the file isn't present, then it starts with `{}` so the user has
* something to work with.
*
* If the result is not valid, or no config settings are contained in the
* file after editing, then it's restored to what it was before, which might
* mean deleting the file.
*/
async editConfigFile(which, edit) {
const file = find(which);
const backup = await readFile(file, "utf8").catch(() => void 0);
if (!backup) {
await writeFile(
file,
JSON.stringify({ config: {} }, null, 2) + "\n"
);
}
let valid = false;
try {
await edit(file);
const result = reload("config", which);
save("config", result ?? {}, which);
valid = true;
} finally {
if (!valid) {
if (backup) {
await writeFile(file, backup);
reload(which);
} else {
await rm(file, { force: true });
}
}
}
}
/**
* Find the local config file and load both it and the user-level config in
* the XDG config home.
*/
async loadConfigFile() {
await this.#maybeLoadConfigFile("user");
this.projectRoot = dirname(find("project", this.projectRoot));
await this.#maybeLoadConfigFile("project");
return this;
}
/**
* Clear cached config values to force re-reading from updated files.
* @internal
*/
async #reloadConfig() {
this.#options = void 0;
}
/**
* Force a complete reload of config files from disk.
* This clears all caches and re-reads config files.
* Useful for long-running processes that need to pick up config changes.
*/
async reloadFromDisk() {
this.#options = void 0;
this.values = void 0;
this.positionals = void 0;
this.command = void 0;
const { unload } = await import("./src-YAH6SXNK.js");
unload("user");
unload("project");
await this.#maybeLoadConfigFile("user");
await this.#maybeLoadConfigFile("project");
this.parse(this.#originalArgs);
}
/**
* cache of the loaded config
*/
static #loaded;
/**
* Load the configuration and return a Promise to a
* {@link Config} object
*/
static async load(projectRoot = process.cwd(), argv = process.argv, reload2 = false) {
if (this.#loaded && !reload2) return this.#loaded;
const a = new _Config(definition, projectRoot);
const b = await a.loadConfigFile();
this.#loaded = b.parse(argv);
return this.#loaded;
}
};
var isParsed = (c) => !!(c.values && c.positionals && c.command);
export {
kCustomInspect,
pairsToRecords,
recordsToPairs,
Config
};
//# sourceMappingURL=chunk-FRVD5QAW.js.map