@wirecat/cli-core 0.422.0 → 0.424.0
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/CHANGELOG.md +29 -0
- package/dist/analysis/settings.d.ts +0 -6
- package/dist/analysis/settings.js +2 -5
- package/dist/cli/analysis-choice.d.ts +0 -1
- package/dist/cli/analysis-choice.js +9 -22
- package/dist/cli/bot/context.js +1 -1
- package/dist/cli/config-command.js +33 -25
- package/dist/cli/config-issues.d.ts +52 -0
- package/dist/cli/config-issues.js +187 -0
- package/dist/cli/config-keys.js +4 -28
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +3 -1
- package/dist/cli/embedding-choice.d.ts +1 -2
- package/dist/cli/embedding-choice.js +2 -4
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/messenger/context.js +1 -1
- package/dist/cli/messenger/conversations-command.js +5 -9
- package/dist/cli/messenger/doctor-command.js +38 -4
- package/dist/cli/messenger/inbox.js +2 -6
- package/dist/cli/messenger/private-people-command.js +1 -2
- package/dist/cli/messenger/review.js +2 -5
- package/dist/cli/permission-keys.d.ts +9 -2
- package/dist/cli/permission-keys.js +21 -6
- package/dist/cli/preview.d.ts +3 -2
- package/dist/cli/preview.js +4 -3
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +14 -9
- package/dist/cli/runs/search.d.ts +1 -1
- package/dist/cli/settings.d.ts +34 -117
- package/dist/cli/settings.js +130 -118
- package/dist/cli/shared-command-exports.d.ts +4 -232
- package/dist/entities/index.d.ts +3 -3
- package/dist/entities/recorded-run.d.ts +3 -3
- package/dist/entities/recorded-run.js +1 -1
- package/dist/mcp/tool.js +3 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/models/settings.d.ts +1 -7
- package/dist/models/settings.js +1 -18
- package/dist/runs/recording.d.ts +2 -1
- package/dist/runs/recording.js +2 -2
- package/dist/runs/run.d.ts +3 -2
- package/dist/runs/run.js +1 -1
- package/dist/search/account-list.js +8 -7
- package/package.json +1 -1
package/dist/cli/settings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { CliError, configFilePath, resolvePaths, saveConfigFile } from "@wirecat/cli-base";
|
|
3
3
|
import * as v from "valibot";
|
|
4
4
|
import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
|
|
@@ -7,7 +7,7 @@ import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
|
7
7
|
import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
|
|
8
8
|
import { storePath } from "../store/path.js";
|
|
9
9
|
import { sharedConfigPath } from "../store/shared-config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { issueMessage, parseConfigText, publicIssue, savedPermissionKeys, schemaIssues, withoutIssues, } from "./config-issues.js";
|
|
11
11
|
import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
12
12
|
import { profilesHere } from "./messenger/accounts.js";
|
|
13
13
|
import { usableProfileName } from "./profile.js";
|
|
@@ -81,6 +81,26 @@ const SHARED_DEFAULTS_ONLY = {
|
|
|
81
81
|
export const NONE_NAMED = "none named";
|
|
82
82
|
/** A name for a command that speaks for no account; never chosen for one that does. */
|
|
83
83
|
const NO_PROFILE = "default";
|
|
84
|
+
/** The profile a run is for: none when nothing named one, even though `Settings.profile` holds a placeholder. */
|
|
85
|
+
export const namedProfile = (settings) => settings.sources?.profile === NONE_NAMED ? undefined : settings.profile;
|
|
86
|
+
/**
|
|
87
|
+
* No profile named and none on this computer. The settings cannot see the program, so the program swaps the
|
|
88
|
+
* `setup` this suggests for `withoutSetup` when it has no such command.
|
|
89
|
+
*/
|
|
90
|
+
export class NoProfileHere extends CliError {
|
|
91
|
+
app;
|
|
92
|
+
constructor(app) {
|
|
93
|
+
super("validation_error", `name a profile: there is none on this computer yet — create one with \`${app.command} personal setup\` ` +
|
|
94
|
+
"(any name in place of personal)", { profiles: [] });
|
|
95
|
+
this.app = app;
|
|
96
|
+
}
|
|
97
|
+
/** `words`: the command that was typed, without its arguments, so the suggestion runs as written. */
|
|
98
|
+
withoutSetup(words) {
|
|
99
|
+
const command = words ? `${this.app.command} personal ${words}` : `${this.app.command} personal …`;
|
|
100
|
+
return new CliError("validation_error", `name a profile: there is none on this computer yet — any name works, such as \`${command}\` ` +
|
|
101
|
+
`(or set ${envName(this.app, "PROFILE")})`, { profiles: [] });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
84
104
|
/** The first candidate that is set, and which layer it came from. */
|
|
85
105
|
export const first = (candidates, fallback) => {
|
|
86
106
|
for (const [from, value] of candidates)
|
|
@@ -108,7 +128,6 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
108
128
|
profiles: v.optional(v.record(v.string(), strictOf(entries), "has to be an object of profiles, by name")),
|
|
109
129
|
}, objectMessage);
|
|
110
130
|
const configEntries = {
|
|
111
|
-
default_profile: v.optional(v.string(plain("has to be a profile name, in quotes"))),
|
|
112
131
|
defaults: v.optional(strictOf(defaultsEntries)),
|
|
113
132
|
profiles: v.optional(v.record(v.string(), strictOf(profileEntries), "has to be an object of profiles, by name"), {}),
|
|
114
133
|
personal: v.optional(kindSection(profileEntries)),
|
|
@@ -117,22 +136,35 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
117
136
|
const schema = strictOf(configEntries);
|
|
118
137
|
const defaultsOnlyKeys = Object.keys(defaultsOnly);
|
|
119
138
|
const botOnlyKeys = Object.keys(BOT_ONLY_ENTRIES);
|
|
120
|
-
const allSettings = [...Object.keys(profileEntries), ...botOnlyKeys, ...defaultsOnlyKeys
|
|
139
|
+
const allSettings = [...Object.keys(profileEntries), ...botOnlyKeys, ...defaultsOnlyKeys];
|
|
121
140
|
const PROFILE = envName(app, "PROFILE");
|
|
122
141
|
const PROFILE_LOCK = envName(app, "PROFILE_LOCK");
|
|
123
142
|
const TIMEOUT = envName(app, "TIMEOUT");
|
|
124
143
|
const REQUESTS_PER_MINUTE = envName(app, "REQUESTS_PER_MINUTE");
|
|
125
144
|
const configPathFor = ({ env = process.env, configDir }) => configFilePath(configDir ?? resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).config);
|
|
126
|
-
const
|
|
145
|
+
const readSchema = extension.schema ?? schema;
|
|
146
|
+
const context = (file) => ({ file, tool: app.command });
|
|
147
|
+
/** The file as written, `undefined` when there is none, or the issue that says it is not JSON. */
|
|
148
|
+
const readRaw = (path) => {
|
|
149
|
+
let text;
|
|
127
150
|
try {
|
|
128
|
-
|
|
151
|
+
text = readFileSync(path, "utf8");
|
|
129
152
|
}
|
|
130
|
-
catch
|
|
131
|
-
|
|
132
|
-
throw new CliError("configuration_error", message.includes("catch_up_marks_read")
|
|
133
|
-
? `${message}; remove catch_up_marks_read from the config file and use ${app.command} <profile> chats mark-read <chat>`
|
|
134
|
-
: message);
|
|
153
|
+
catch {
|
|
154
|
+
return { raw: undefined };
|
|
135
155
|
}
|
|
156
|
+
return parseConfigText(text, path);
|
|
157
|
+
};
|
|
158
|
+
/** The file with every problem found; what loads is the file without the settings they name. */
|
|
159
|
+
const loadConfig = (path) => {
|
|
160
|
+
const read = readRaw(path);
|
|
161
|
+
if ("issue" in read)
|
|
162
|
+
return { config: { profiles: {} }, issues: [read.issue], raw: undefined };
|
|
163
|
+
if (read.raw === undefined)
|
|
164
|
+
return { config: { profiles: {} }, issues: [], raw: undefined };
|
|
165
|
+
const issues = schemaIssues(read.raw, readSchema, context(path));
|
|
166
|
+
const parsed = v.safeParse(readSchema, withoutIssues(read.raw, issues));
|
|
167
|
+
return { config: (parsed.success ? parsed.output : { profiles: {} }), issues, raw: read.raw };
|
|
136
168
|
};
|
|
137
169
|
/**
|
|
138
170
|
* A lock pins a process to one profile: the owner sets it where an agent runs, and a first word
|
|
@@ -157,15 +189,10 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
157
189
|
const unnamed = (config, env, needed) => {
|
|
158
190
|
if (!needed)
|
|
159
191
|
return { value: NO_PROFILE, from: NONE_NAMED };
|
|
160
|
-
if (config.default_profile !== undefined) {
|
|
161
|
-
throw new CliError("validation_error", `default_profile is no longer read — name the profile: \`${app.command} ${config.default_profile} …\`; ` +
|
|
162
|
-
`\`${app.command} config unset default_profile\` removes it from the config file`, { profiles: profilesOf(config, env) });
|
|
163
|
-
}
|
|
164
192
|
const here = profilesOf(config, env);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
: `name a profile: \`${app.command} <profile> …\` — profiles here: ${here.join(", ")} (or set ${PROFILE})`, { profiles: here });
|
|
193
|
+
if (here.length === 0)
|
|
194
|
+
throw new NoProfileHere(app);
|
|
195
|
+
throw new CliError("validation_error", `name a profile: \`${app.command} <profile> …\` — profiles here: ${here.join(", ")} (or set ${PROFILE})`, { profiles: here });
|
|
169
196
|
};
|
|
170
197
|
const profilesOf = (config, env) => profilesHere(app, [...namedProfiles(config), ...(extension.profiles?.(env) ?? [])], env);
|
|
171
198
|
const resolveSettings = (flags = {}, options = {}) => {
|
|
@@ -178,7 +205,10 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
178
205
|
catch (error) {
|
|
179
206
|
throw new CliError("configuration_error", error instanceof Error ? error.message : String(error));
|
|
180
207
|
}
|
|
181
|
-
const
|
|
208
|
+
const loaded = loadConfig(configPath);
|
|
209
|
+
if (loaded.issues.length > 0 && !options.withIssues)
|
|
210
|
+
throw new CliError("configuration_error", issueMessage(loaded.issues.map(publicIssue)));
|
|
211
|
+
const { config } = loaded;
|
|
182
212
|
const lock = given(env[PROFILE_LOCK]);
|
|
183
213
|
const named = first([
|
|
184
214
|
["first word", flags.profile],
|
|
@@ -202,9 +232,6 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
202
232
|
[extension.sourcePaths ? "config file: defaults" : "config defaults", shared],
|
|
203
233
|
];
|
|
204
234
|
const fromLayers = (key, fallback, only = layers) => first(only.map(([from, scope]) => [from, scope?.[key]]), fallback);
|
|
205
|
-
if (fromLayers("catch_up_marks_read", undefined).value !== undefined) {
|
|
206
|
-
throw new CliError("configuration_error", `remove catch_up_marks_read from the config file and use ${app.command} <profile> chats mark-read <chat>`);
|
|
207
|
-
}
|
|
208
235
|
const kindLayers = layers.filter(([from]) => from.startsWith(`config file: ${kind}.`));
|
|
209
236
|
const limit = flags.limit === undefined ? fromLayers("limit", DEFAULT_LIMIT) : { value: flags.limit, from: "flag" };
|
|
210
237
|
const record = flags.record === undefined ? fromLayers("record", false) : { value: flags.record, from: "flag" };
|
|
@@ -303,6 +330,8 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
303
330
|
},
|
|
304
331
|
configured,
|
|
305
332
|
shared,
|
|
333
|
+
configIssues: loaded.issues.map(publicIssue),
|
|
334
|
+
savedPermissions: savedPermissionKeys(config),
|
|
306
335
|
};
|
|
307
336
|
// The file was checked by the schema; a flag was not, and `--limit abc` is `NaN` by the time it
|
|
308
337
|
// gets here, which slices an array to nothing without complaining.
|
|
@@ -319,16 +348,52 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
319
348
|
const extra = extension.resolve?.({ flags, settings, layers, fromLayers });
|
|
320
349
|
return { ...settings, ...extra?.values, sources: { ...settings.sources, ...extra?.sources } };
|
|
321
350
|
};
|
|
322
|
-
const configuredProfiles = (options = {}) =>
|
|
323
|
-
|
|
324
|
-
|
|
351
|
+
const configuredProfiles = (options = {}) => {
|
|
352
|
+
const { config, issues } = loadConfig(configPathFor(options));
|
|
353
|
+
if (issues.length > 0 && !options.withIssues)
|
|
354
|
+
throw new CliError("configuration_error", issueMessage(issues.map(publicIssue)));
|
|
355
|
+
return namedProfiles(config);
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* `config set` and `config unset`, on the file as written, so they also run on a file with problems. A set is
|
|
359
|
+
* refused only for a problem it would add; an unset removes the setting whether or not the schema knows it.
|
|
360
|
+
* `root` reaches a path outside any profile or `defaults`, and only one that has a problem.
|
|
361
|
+
*/
|
|
362
|
+
const changeSetting = (path, { profile, setting, value, kind, root = false, }) => {
|
|
325
363
|
const [name = setting, ...rest] = setting.split(".");
|
|
326
364
|
const level = name === "permissions" && rest.length > 0 ? rest.join(".") : undefined;
|
|
327
365
|
const modelField = /^models\.([a-z][a-z0-9-]*)\.(provider|model|base_url)$/.exec(setting);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
366
|
+
const known = allSettings.includes(setting) || level !== undefined || modelField !== null;
|
|
367
|
+
const read = readRaw(path);
|
|
368
|
+
if ("issue" in read)
|
|
369
|
+
throw new CliError("configuration_error", issueMessage([publicIssue(read.issue)]));
|
|
370
|
+
const raw = read.raw === undefined ? {} : read.raw;
|
|
371
|
+
if (!isScope(raw))
|
|
372
|
+
throw new CliError("configuration_error", `${path}: the file has to be an object, in braces`);
|
|
373
|
+
const scopePath = root
|
|
374
|
+
? []
|
|
375
|
+
: [...(kind === undefined ? [] : [kind]), ...(profile === undefined ? ["defaults"] : ["profiles", profile])];
|
|
376
|
+
const keyPath = root
|
|
377
|
+
? setting.split(".")
|
|
378
|
+
: level !== undefined
|
|
379
|
+
? ["permissions", level]
|
|
380
|
+
: modelField
|
|
381
|
+
? ["models", modelField[1], modelField[2]]
|
|
382
|
+
: [setting];
|
|
383
|
+
const at = [...scopePath, ...keyPath];
|
|
384
|
+
const before = schemaIssues(raw, readSchema, context(path));
|
|
385
|
+
if (value === undefined) {
|
|
386
|
+
if (root && !before.some((issue) => issue.at.join(".") === at.join(".")))
|
|
387
|
+
throw new CliError("validation_error", `${setting} has no problem to remove — name the profile, or add --defaults, to unset a setting`);
|
|
388
|
+
if (!known && valueAt(raw, at) === undefined)
|
|
389
|
+
throw new CliError("validation_error", `no setting called "${setting}" — one of: ${allSettings.join(", ")}`);
|
|
390
|
+
const changed = structuredClone(raw);
|
|
391
|
+
removeAt(changed, at);
|
|
392
|
+
saveConfigFile(path, changed);
|
|
393
|
+
return null;
|
|
331
394
|
}
|
|
395
|
+
if (!known)
|
|
396
|
+
throw new CliError("validation_error", `no setting called "${setting}" — one of: ${allSettings.join(", ")}`);
|
|
332
397
|
if (profile !== undefined && defaultsOnlyKeys.includes(setting)) {
|
|
333
398
|
throw new CliError("validation_error", `${setting} is one setting for the whole program, not per profile — add --defaults`);
|
|
334
399
|
}
|
|
@@ -338,98 +403,45 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
338
403
|
if (kind !== "bot" && botOnlyKeys.includes(setting)) {
|
|
339
404
|
throw new CliError("validation_error", `${setting} is a bot's setting — add --bot`);
|
|
340
405
|
}
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
saveConfigFile(path, rest);
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
const section = kind === undefined ? undefined : (config[kind] ?? {});
|
|
354
|
-
const current = section === undefined
|
|
355
|
-
? profile === undefined
|
|
356
|
-
? config.defaults
|
|
357
|
-
: config.profiles[profile]
|
|
358
|
-
: profile === undefined
|
|
359
|
-
? section.defaults
|
|
360
|
-
: section.profiles?.[profile];
|
|
361
|
-
const scope = { ...current };
|
|
362
|
-
if (modelField) {
|
|
363
|
-
const purpose = modelField[1];
|
|
364
|
-
const field = modelField[2];
|
|
365
|
-
const models = { ...scope.models };
|
|
366
|
-
const target = { ...models[purpose] };
|
|
367
|
-
if (value === undefined)
|
|
368
|
-
delete target[field];
|
|
369
|
-
else
|
|
370
|
-
target[field] = parseValue(value);
|
|
371
|
-
if (Object.keys(target).length > 0)
|
|
372
|
-
models[purpose] = target;
|
|
373
|
-
else
|
|
374
|
-
delete models[purpose];
|
|
375
|
-
if (Object.keys(models).length > 0)
|
|
376
|
-
scope.models = models;
|
|
377
|
-
else
|
|
378
|
-
delete scope.models;
|
|
379
|
-
}
|
|
380
|
-
else if (level !== undefined) {
|
|
381
|
-
const levels = { ...scope.permissions };
|
|
382
|
-
if (value === undefined)
|
|
383
|
-
delete levels[level];
|
|
384
|
-
else
|
|
385
|
-
levels[level] = value.trim();
|
|
386
|
-
if (Object.keys(levels).length > 0)
|
|
387
|
-
scope.permissions = levels;
|
|
388
|
-
else
|
|
389
|
-
delete scope.permissions;
|
|
390
|
-
}
|
|
391
|
-
else if (value === undefined)
|
|
392
|
-
delete scope[setting];
|
|
393
|
-
else
|
|
394
|
-
scope[setting] = parseValue(value);
|
|
395
|
-
const empty = Object.keys(scope).length === 0;
|
|
396
|
-
let changed;
|
|
397
|
-
if (kind === undefined || section === undefined) {
|
|
398
|
-
changed =
|
|
399
|
-
profile === undefined
|
|
400
|
-
? { ...config, defaults: scope }
|
|
401
|
-
: { ...config, profiles: { ...config.profiles, [profile]: scope } };
|
|
402
|
-
if (profile === undefined && empty)
|
|
403
|
-
delete changed.defaults;
|
|
404
|
-
if (profile !== undefined && empty)
|
|
405
|
-
delete changed.profiles[profile];
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const next = profile === undefined
|
|
409
|
-
? { ...section, defaults: scope }
|
|
410
|
-
: { ...section, profiles: { ...section.profiles, [profile]: scope } };
|
|
411
|
-
if (profile === undefined && empty)
|
|
412
|
-
delete next.defaults;
|
|
413
|
-
if (profile !== undefined && empty && next.profiles)
|
|
414
|
-
delete next.profiles[profile];
|
|
415
|
-
if (next.profiles && Object.keys(next.profiles).length === 0)
|
|
416
|
-
delete next.profiles;
|
|
417
|
-
changed = { ...config, [kind]: next };
|
|
418
|
-
if (Object.keys(next).length === 0)
|
|
419
|
-
delete changed[kind];
|
|
420
|
-
}
|
|
421
|
-
const checked = v.safeParse(extension.schema ?? schema, changed);
|
|
422
|
-
if (!checked.success) {
|
|
423
|
-
throw new CliError("validation_error", `${setting} cannot be "${value}": ${checked.issues[0]?.message ?? "invalid"}`);
|
|
424
|
-
}
|
|
425
|
-
saveConfigFile(path, checked.output);
|
|
426
|
-
if (modelField)
|
|
427
|
-
return (scope.models?.[modelField[1]]?.[modelField[2]] ??
|
|
428
|
-
null);
|
|
429
|
-
return level === undefined ? (scope[setting] ?? null) : (scope.permissions?.[level] ?? null);
|
|
406
|
+
const changed = structuredClone(raw);
|
|
407
|
+
setAt(changed, at, level === undefined ? parseValue(value) : value.trim());
|
|
408
|
+
const had = new Set(before.map((issue) => issue.at.join(".")));
|
|
409
|
+
const added = schemaIssues(changed, readSchema, context(path)).find((issue) => !had.has(issue.at.join(".")) || issue.at.join(".") === at.join("."));
|
|
410
|
+
if (added)
|
|
411
|
+
throw new CliError("validation_error", `${setting} cannot be "${value}": ${added.what}`);
|
|
412
|
+
const checked = v.safeParse(readSchema, changed);
|
|
413
|
+
saveConfigFile(path, checked.success ? checked.output : changed);
|
|
414
|
+
return valueAt(checked.success ? checked.output : changed, at) ?? null;
|
|
430
415
|
};
|
|
431
416
|
return { resolveSettings, configuredProfiles, changeSetting, allSettings, defaultsOnlyKeys, schema };
|
|
432
417
|
};
|
|
418
|
+
const isScope = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
419
|
+
const valueAt = (node, path) => path.reduce((at, key) => (isScope(at) ? at[key] : undefined), node);
|
|
420
|
+
const setAt = (node, path, value) => {
|
|
421
|
+
let at = node;
|
|
422
|
+
for (const key of path.slice(0, -1)) {
|
|
423
|
+
if (!isScope(at[key]))
|
|
424
|
+
at[key] = {};
|
|
425
|
+
at = at[key];
|
|
426
|
+
}
|
|
427
|
+
at[path.at(-1)] = value;
|
|
428
|
+
};
|
|
429
|
+
/** Removes the value and every section it leaves empty, so an unset leaves no `{}` behind. */
|
|
430
|
+
const removeAt = (node, path) => {
|
|
431
|
+
const [key, ...rest] = path;
|
|
432
|
+
if (key === undefined)
|
|
433
|
+
return;
|
|
434
|
+
if (rest.length === 0) {
|
|
435
|
+
delete node[key];
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const child = node[key];
|
|
439
|
+
if (!isScope(child))
|
|
440
|
+
return;
|
|
441
|
+
removeAt(child, rest);
|
|
442
|
+
if (Object.keys(child).length === 0)
|
|
443
|
+
delete node[key];
|
|
444
|
+
};
|
|
433
445
|
/** Every profile the file names, in any section. */
|
|
434
446
|
const namedProfiles = (config) => [
|
|
435
447
|
...new Set([
|