agy-powerline 1.2.0 → 1.3.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.
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const {
6
+ saveConfig,
7
+ setConfigDir,
8
+ getConfigPath,
9
+ mergeConfig,
10
+ renderStatusline,
11
+ sampleData,
12
+ detectStyle,
13
+ } = require("../lib/shared");
14
+ const { resolveInstallDir, writeFileAtomic, PRIVATE_FILE_MODE } = require("../lib/paths");
15
+ const { createRl, confirm } = require("../lib/prompt");
16
+ const { loadEditableConfig } = require("./config-guard");
17
+ const { version } = require("../../package.json");
18
+
19
+ const DIM = "\x1b[2m";
20
+ const BOLD = "\x1b[1m";
21
+ const GREEN = "\x1b[32m";
22
+ const RED = "\x1b[31m";
23
+ const YELLOW = "\x1b[33m";
24
+ const CYAN = "\x1b[36m";
25
+ const RESET = "\x1b[0m";
26
+
27
+ // A config is a few KB; anything this big isn't one.
28
+ const MAX_IMPORT_BYTES = 256 * 1024;
29
+ const BACKUP_SUFFIX = ".bak";
30
+ const { visible, droppedSettings } = require("../lib/config-check");
31
+ const SETTINGS = ["emojis", "ascii", "color", "pacing", "minimalist", "rowGap", "log"];
32
+
33
+ function readImportFile(file) {
34
+ const target = path.resolve(String(file));
35
+ let stat;
36
+ try {
37
+ stat = fs.statSync(target);
38
+ } catch {
39
+ return { target, error: "the file doesn't exist" };
40
+ }
41
+ if (!stat.isFile()) {
42
+ return { target, error: "it isn't a file" };
43
+ }
44
+ if (stat.size > MAX_IMPORT_BYTES) {
45
+ return {
46
+ target,
47
+ error: `it is larger than ${MAX_IMPORT_BYTES / 1024} KB, so it isn't a config`,
48
+ };
49
+ }
50
+
51
+ let data;
52
+ try {
53
+ data = JSON.parse(fs.readFileSync(target, "utf8").replace(/^\uFEFF/, ""));
54
+ } catch (err) {
55
+ // The parser quotes the text it failed on; keep only where.
56
+ const at = String(err.message).match(/position \d+|line \d+ column \d+/)?.[0];
57
+ return { target, error: `it isn't valid JSON${at ? ` (${at})` : ""}` };
58
+ }
59
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
60
+ return { target, error: "its top-level value isn't an object" };
61
+ }
62
+ return { target, data };
63
+ }
64
+
65
+ function majorOf(value) {
66
+ const major = parseInt(String(value ?? "").match(/^(\d+)\./)?.[1] ?? "", 10);
67
+ return Number.isFinite(major) ? major : null;
68
+ }
69
+
70
+ // "model context git / fiveHourBar fiveHourReset ...", one row per line break.
71
+ function widgetRows(widgets) {
72
+ const rows = [[]];
73
+ for (const widget of widgets) {
74
+ if (widget.id === "lineBreak") {
75
+ rows.push([]);
76
+ } else if (widget.id !== "separator") {
77
+ rows[rows.length - 1].push(widget.id);
78
+ }
79
+ }
80
+ return rows.map((row) => row.join(" ") || "(empty row)");
81
+ }
82
+
83
+ function changes(before, after) {
84
+ const lines = [];
85
+ const show = (value) => (value === undefined || value === null ? "off" : JSON.stringify(value));
86
+ const compare = (name, a, b) => {
87
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
88
+ lines.push(` ${name.padEnd(11)} ${DIM}${visible(show(a))}${RESET} → ${visible(show(b))}`);
89
+ }
90
+ };
91
+
92
+ compare("theme", before.theme, after.theme);
93
+ compare("layout", before.layout, after.layout);
94
+ compare(
95
+ "style",
96
+ detectStyle(before.powerline) ?? "custom",
97
+ detectStyle(after.powerline) ?? "custom"
98
+ );
99
+ compare("gradient", before.gradient, after.gradient);
100
+ compare("meterStyle", before.meterStyle, after.meterStyle);
101
+ for (const key of SETTINGS) {
102
+ compare(key, before[key], after[key]);
103
+ }
104
+ compare("thresholds", before.thresholds, after.thresholds);
105
+ compare("clock", before.clock, after.clock);
106
+ compare("customTheme", before.customTheme, after.customTheme);
107
+
108
+ const rowsBefore = widgetRows(before.widgets);
109
+ const rowsAfter = widgetRows(after.widgets);
110
+ if (JSON.stringify(before.widgets) !== JSON.stringify(after.widgets)) {
111
+ lines.push(" widgets");
112
+ rowsBefore.forEach((row) => lines.push(` ${DIM}- ${visible(row)}${RESET}`));
113
+ rowsAfter.forEach((row) => lines.push(` + ${visible(row)}`));
114
+ if (JSON.stringify(rowsBefore) === JSON.stringify(rowsAfter)) {
115
+ lines.push(` ${DIM}(same widgets, different options)${RESET}`);
116
+ }
117
+ }
118
+ return lines;
119
+ }
120
+
121
+ // Exec commands the import would add: these run on every refresh.
122
+ function newCommands(before, after) {
123
+ const known = new Set(
124
+ before.widgets.filter((widget) => widget.id === "exec").map((widget) => String(widget.cmd))
125
+ );
126
+ return after.widgets
127
+ .filter((widget) => widget.id === "exec" && String(widget.cmd || "").trim())
128
+ .map((widget) => String(widget.cmd))
129
+ .filter((cmd) => !known.has(cmd));
130
+ }
131
+
132
+ function fail(message, hint) {
133
+ console.error(`${RED}error${RESET} ${message}`);
134
+ if (hint) {
135
+ console.error(` ${hint}`);
136
+ }
137
+ process.exitCode = 1;
138
+ }
139
+
140
+ async function importCommand(file, options = {}) {
141
+ if (!file) {
142
+ fail("Name the file to import.", "Usage: npx agy-powerline import <file> [--merge] [--yes]");
143
+ return;
144
+ }
145
+
146
+ const installDir = resolveInstallDir();
147
+ if (!installDir) {
148
+ return;
149
+ }
150
+ setConfigDir(installDir);
151
+ // A broken config is never replaced: the user can still fix it.
152
+ const current = loadEditableConfig();
153
+ if (!current) {
154
+ return;
155
+ }
156
+
157
+ const { target, data, error } = readImportFile(file);
158
+ if (error) {
159
+ fail(`Can't import ${target}: ${error}. Nothing was changed.`);
160
+ return;
161
+ }
162
+
163
+ const { agyPowerline, ...raw } = data;
164
+ const fileMajor = majorOf(agyPowerline?.version);
165
+ if (fileMajor !== null && fileMajor > majorOf(version)) {
166
+ fail(
167
+ `${target} comes from agy-powerline ${visible(agyPowerline.version)}, newer than this one (${version}). Nothing was changed.`,
168
+ "Update first: npx agy-powerline@latest import <file>"
169
+ );
170
+ return;
171
+ }
172
+
173
+ const merge = Boolean(options.merge);
174
+ const next = mergeConfig(merge ? { ...current, ...raw } : raw);
175
+
176
+ console.log();
177
+ console.log(`${BOLD} Import ${RESET}${DIM}${target}${RESET} ${merge ? "(merge)" : "(replace)"}`);
178
+ const dropped = droppedSettings(raw);
179
+ if (dropped.length) {
180
+ console.log(`${YELLOW} Left out${RESET} (this version can't use them):`);
181
+ dropped.forEach((item) => console.log(` ${item}`));
182
+ }
183
+
184
+ const diff = changes(current, next);
185
+ if (!diff.length) {
186
+ console.log(" Nothing to change: the file matches your config.");
187
+ console.log();
188
+ return;
189
+ }
190
+ console.log(" Changes:");
191
+ diff.forEach((line) => console.log(line));
192
+ console.log();
193
+ console.log(" Preview:");
194
+ console.log(renderStatusline(sampleData(), next));
195
+ console.log();
196
+
197
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
198
+ const added = newCommands(current, next);
199
+ const rl =
200
+ interactive && (!options.yes || (added.length && !options["allow-exec"])) ? createRl() : null;
201
+
202
+ try {
203
+ if (added.length) {
204
+ console.log(
205
+ `${YELLOW} The file adds ${added.length} shell command${added.length === 1 ? "" : "s"}${RESET}, run on every refresh:`
206
+ );
207
+ added.forEach((cmd) => console.log(` ${CYAN}${visible(cmd)}${RESET}`));
208
+ console.log();
209
+ if (!options["allow-exec"]) {
210
+ if (!rl) {
211
+ fail(
212
+ "The file adds shell commands. Nothing was changed.",
213
+ "Check them above, then pass --allow-exec to import them."
214
+ );
215
+ return;
216
+ }
217
+ if (!(await confirm(rl, "Run these commands in your bar?", false))) {
218
+ console.log(" Nothing was changed.");
219
+ return;
220
+ }
221
+ }
222
+ }
223
+
224
+ if (!options.yes) {
225
+ if (!rl) {
226
+ fail("No TTY. Pass --yes to import non-interactively. Nothing was changed.");
227
+ return;
228
+ }
229
+ if (!(await confirm(rl, "Apply this config?", false))) {
230
+ console.log(" Nothing was changed.");
231
+ return;
232
+ }
233
+ }
234
+ } finally {
235
+ rl?.close();
236
+ }
237
+
238
+ // One import can be undone by importing the backup.
239
+ const configPath = getConfigPath();
240
+ const backup = `${configPath}${BACKUP_SUFFIX}`;
241
+ let backedUp = false;
242
+ try {
243
+ const previous = fs.readFileSync(configPath, "utf8");
244
+ writeFileAtomic(backup, previous, { mode: PRIVATE_FILE_MODE });
245
+ backedUp = true;
246
+ } catch (err) {
247
+ if (err.code !== "ENOENT") {
248
+ fail(`Can't back up ${configPath} (${err.code || "error"}). Nothing was changed.`);
249
+ return;
250
+ }
251
+ }
252
+
253
+ saveConfig(next);
254
+
255
+ console.log(`${GREEN}✔${RESET} Config imported into ${DIM}${configPath}${RESET}`);
256
+ if (backedUp) {
257
+ console.log(
258
+ ` previous config ${DIM}${backup}${RESET} (undo: npx agy-powerline import "${backup}")`
259
+ );
260
+ }
261
+ console.log(" The bar shows it on its next refresh.");
262
+ console.log();
263
+ }
264
+
265
+ module.exports = {
266
+ importCommand,
267
+ readImportFile,
268
+ droppedSettings,
269
+ newCommands,
270
+ visible,
271
+ MAX_IMPORT_BYTES,
272
+ BACKUP_SUFFIX,
273
+ };
package/src/cli/init.js CHANGED
@@ -1,6 +1,24 @@
1
+ "use strict";
2
+
1
3
  const fs = require("fs");
2
4
  const path = require("path");
3
- const { defaultConfig, saveConfig, setConfigDir } = require("../lib/shared");
5
+ const {
6
+ defaultConfig,
7
+ saveConfig,
8
+ setConfigDir,
9
+ readConfigFile,
10
+ applyPreset,
11
+ renderStatusline,
12
+ sampleData,
13
+ resolveColorDepth,
14
+ PRESETS,
15
+ PRESET_ORDER,
16
+ LAYOUTS,
17
+ LAYOUT_ORDER,
18
+ THEMES,
19
+ THEME_ORDER,
20
+ canonicalThemeId,
21
+ } = require("../lib/shared");
4
22
  const {
5
23
  AGY_SETTINGS,
6
24
  RUNTIME_FILES,
@@ -17,8 +35,10 @@ const {
17
35
  cmdBreakingChars,
18
36
  cmdBreakingMessage,
19
37
  } = require("../lib/paths");
20
- const { createRl, confirm } = require("../lib/prompt");
38
+ const { createRl, confirm, choose } = require("../lib/prompt");
39
+ const { printInvalidConfig } = require("./config-guard");
21
40
  const { buildRuntimeBundle } = require("./bundle");
41
+ const { REPO_URL } = require("./repo-url");
22
42
 
23
43
  const DIM = "\x1b[2m";
24
44
  const BOLD = "\x1b[1m";
@@ -58,6 +78,117 @@ function assertUnixNode() {
58
78
  process.exit(1);
59
79
  }
60
80
 
81
+ function unknownChoice(kind, value, ids) {
82
+ console.error(`${RED}error${RESET} Unknown ${kind} "${value}".`);
83
+ console.error(` Available ${kind}s: ${ids.join(", ")}`);
84
+ console.error(` Run ${CYAN}npx agy-powerline ${kind} --list${RESET} to view details.`);
85
+ process.exitCode = 1;
86
+ return null;
87
+ }
88
+
89
+ // --preset, --layout and --theme, checked before anything is written. Returns
90
+ // the ids given ({} for none), or null after printing what's wrong.
91
+ function readChoiceFlags(options) {
92
+ const choices = {};
93
+ if (options.preset !== undefined) {
94
+ if (!PRESETS[options.preset]) {
95
+ return unknownChoice("preset", options.preset, PRESET_ORDER);
96
+ }
97
+ choices.preset = options.preset;
98
+ }
99
+ if (options.layout !== undefined) {
100
+ if (!LAYOUTS[options.layout]) {
101
+ return unknownChoice("layout", options.layout, LAYOUT_ORDER);
102
+ }
103
+ choices.layout = options.layout;
104
+ }
105
+ if (options.theme !== undefined) {
106
+ // Aliases (dark, light, colorblind) name a theme too.
107
+ const theme = canonicalThemeId(options.theme);
108
+ if (!THEMES[theme]) {
109
+ return unknownChoice("theme", options.theme, THEME_ORDER);
110
+ }
111
+ choices.theme = theme;
112
+ }
113
+ return choices;
114
+ }
115
+
116
+ function applyChoices(config, choices) {
117
+ const next = choices.preset ? applyPreset(config, choices.preset) : { ...config };
118
+ if (choices.layout) {
119
+ next.layout = choices.layout;
120
+ }
121
+ if (choices.theme) {
122
+ next.theme = choices.theme;
123
+ }
124
+ return next;
125
+ }
126
+
127
+ // The bar a choice gives, fitted to the terminal's width (less the indent).
128
+ // exec widgets keep their [cmd] placeholder: nothing runs here.
129
+ function preview(config, rows = Infinity) {
130
+ const width = Math.max(40, (process.stdout.columns || 80) - 6);
131
+ const color = resolveColorDepth({ configColor: config.color, env: process.env });
132
+ return renderStatusline({ ...sampleData(), terminal_width: width }, config, { color })
133
+ .split("\n")
134
+ .slice(0, rows)
135
+ .join("\n");
136
+ }
137
+
138
+ const BACKGROUNDS = [
139
+ ["dark", "dark"],
140
+ ["light", "light"],
141
+ ["colorblind", "color-blind"],
142
+ ];
143
+
144
+ // The questions of a first install in a terminal, for what the flags left open.
145
+ async function askChoices(rl, given) {
146
+ const choices = { ...given };
147
+ const base = defaultConfig();
148
+ const option = (id, description, config, rows) =>
149
+ `${id}${description ? ` ${DIM}${description}${RESET}` : ""}\n${preview(config, rows)}`;
150
+
151
+ if (!choices.preset) {
152
+ const index = await choose(
153
+ rl,
154
+ `\n${BOLD} Which widgets?${RESET}`,
155
+ PRESET_ORDER.map((id) =>
156
+ option(id, PRESETS[id].description, applyChoices(base, { ...choices, preset: id }))
157
+ )
158
+ );
159
+ choices.preset = PRESET_ORDER[index];
160
+ }
161
+
162
+ if (!choices.layout) {
163
+ const index = await choose(
164
+ rl,
165
+ `\n${BOLD} Which layout?${RESET}`,
166
+ LAYOUT_ORDER.map((id) =>
167
+ option(id, LAYOUTS[id].description, applyChoices(base, { ...choices, layout: id }))
168
+ )
169
+ );
170
+ choices.layout = LAYOUT_ORDER[index];
171
+ }
172
+
173
+ if (!choices.theme) {
174
+ const background = await choose(
175
+ rl,
176
+ `\n${BOLD} Your terminal's background?${RESET}`,
177
+ BACKGROUNDS.map(([, label]) => label)
178
+ );
179
+ const kind = BACKGROUNDS[background][0];
180
+ const ids = THEME_ORDER.filter((id) => THEMES[id]?.kind === kind);
181
+ const index = await choose(
182
+ rl,
183
+ `\n${BOLD} Which theme?${RESET}`,
184
+ ids.map((id) => option(id, "", applyChoices(base, { ...choices, theme: id }), 1))
185
+ );
186
+ choices.theme = ids[index];
187
+ }
188
+
189
+ return choices;
190
+ }
191
+
61
192
  function reportUnreadableSettings(error, command) {
62
193
  console.error(`${RED}error${RESET} ${AGY_SETTINGS} is not valid JSON, so it was left untouched.`);
63
194
  console.error(` ${error}`);
@@ -74,12 +205,33 @@ async function initCommand(options = {}) {
74
205
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
75
206
  const installDir = resolveInstallDir();
76
207
  const command = statuslineCommand(installDir);
208
+ const destConfig = path.join(installDir, "statusline.config.json");
77
209
 
78
210
  console.log();
79
211
  console.log(`${CYAN}${BOLD} agy-powerline${RESET}`);
80
212
  console.log(`${DIM} Antigravity statusline installer${RESET}`);
81
213
  console.log();
82
214
 
215
+ let choices = readChoiceFlags(options);
216
+ if (!choices) {
217
+ return;
218
+ }
219
+
220
+ // Re-running init never replaces a config; flags change only their settings
221
+ // in it, and a config with a JSON error isn't touched at all.
222
+ const firstInstall = !fs.existsSync(destConfig);
223
+ let existingConfig = null;
224
+ if (!firstInstall && Object.keys(choices).length) {
225
+ setConfigDir(installDir);
226
+ const existing = readConfigFile();
227
+ if (existing.error) {
228
+ printInvalidConfig(existing.error);
229
+ process.exitCode = 1;
230
+ return;
231
+ }
232
+ existingConfig = existing.config;
233
+ }
234
+
83
235
  // Validate settings.json before writing anything, so a file we cannot
84
236
  // safely update never ends up half-installed or overwritten.
85
237
  const current = readSettingsForUpdate();
@@ -101,11 +253,18 @@ async function initCommand(options = {}) {
101
253
  const rl = interactive && !yes ? createRl() : null;
102
254
 
103
255
  try {
256
+ if (firstInstall && rl) {
257
+ choices = await askChoices(rl, choices);
258
+ console.log();
259
+ console.log(` ${BOLD}${choices.preset} · ${choices.layout} · ${choices.theme}${RESET}`);
260
+ console.log(preview(applyChoices(defaultConfig(), choices)));
261
+ console.log();
262
+ }
263
+
104
264
  fs.mkdirSync(installDir, { recursive: true });
105
265
  restrictPrivateFiles(installDir);
106
266
 
107
267
  const destStatusline = path.join(installDir, "statusline.js");
108
- const destConfig = path.join(installDir, "statusline.config.json");
109
268
 
110
269
  let writeScripts = true;
111
270
 
@@ -144,9 +303,11 @@ async function initCommand(options = {}) {
144
303
 
145
304
  writeInstallMeta(installDir);
146
305
 
147
- if (!fs.existsSync(destConfig)) {
148
- setConfigDir(installDir);
149
- saveConfig(defaultConfig());
306
+ setConfigDir(installDir);
307
+ if (firstInstall) {
308
+ saveConfig(applyChoices(defaultConfig(), choices));
309
+ } else if (existingConfig) {
310
+ saveConfig(applyChoices(existingConfig, choices));
150
311
  }
151
312
 
152
313
  const statusLine = await updateAgySettings(command, current, {
@@ -166,12 +327,24 @@ async function initCommand(options = {}) {
166
327
  if (typeof statusLine?.padding === "number") {
167
328
  console.log(` padding ${DIM}${statusLine.padding} line(s)${RESET}`);
168
329
  }
330
+ if (existingConfig) {
331
+ const changed = Object.entries(choices).map(([key, value]) => `${key} ${value}`);
332
+ console.log(` config ${DIM}kept yours, with ${changed.join(", ")}${RESET}`);
333
+ } else if (!firstInstall) {
334
+ console.log(` config ${DIM}kept yours${RESET}`);
335
+ }
169
336
  console.log();
170
337
  console.log(" Next:");
171
338
  console.log(`${CYAN} npx agy-powerline configure${RESET}`);
339
+ console.log(
340
+ `${CYAN} npx agy-powerline preset${RESET} / ${CYAN}layout${RESET} / ${CYAN}theme${RESET} ${DIM}change one of your choices${RESET}`
341
+ );
172
342
  console.log(`${CYAN} npx agy-powerline doctor${RESET}`);
173
343
  console.log(" then restart Antigravity / open a new session.");
174
344
  console.log();
345
+ console.log(`${DIM} Like it? A star on GitHub helps others find it:${RESET}`);
346
+ console.log(`${DIM} ${REPO_URL}${RESET}`);
347
+ console.log();
175
348
  } finally {
176
349
  rl?.close();
177
350
  }
@@ -222,7 +395,7 @@ async function updateAgySettings(command, { exists, settings }, { yes, rl, stack
222
395
  statusLine.padding = settings.statusLine.padding;
223
396
  }
224
397
 
225
- writeJsonSafely(AGY_SETTINGS, { ...settings, statusLine });
398
+ const originalBackup = writeJsonSafely(AGY_SETTINGS, { ...settings, statusLine });
226
399
 
227
400
  console.log(`${DIM} ${exists ? "updated" : "created"} ${AGY_SETTINGS}${RESET}`);
228
401
  // init means "show the bar", so a statusLine switched off with enabled: false
@@ -233,10 +406,16 @@ async function updateAgySettings(command, { exists, settings }, { yes, rl, stack
233
406
  if (exists) {
234
407
  console.log(`${DIM} backup ${settingsBackupPath(AGY_SETTINGS)}${RESET}`);
235
408
  }
409
+ if (originalBackup) {
410
+ console.log(
411
+ `${DIM} original ${originalBackup} (kept as it was before agy-powerline)${RESET}`
412
+ );
413
+ }
236
414
 
237
415
  return statusLine;
238
416
  }
239
417
 
240
418
  module.exports = {
241
419
  initCommand,
420
+ askChoices,
242
421
  };
package/src/cli/layout.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const {
2
4
  LAYOUTS,
3
5
  LAYOUT_ORDER,
package/src/cli/preset.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const {
2
4
  PRESETS,
3
5
  PRESET_ORDER,
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ const { repository } = require("../../package.json");
4
+
5
+ const REPO_URL = repository.url.replace(/^git\+/, "").replace(/\.git$/, "");
6
+
7
+ module.exports = { REPO_URL };
package/src/cli/theme.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const {
2
4
  THEMES,
3
5
  THEME_ORDER,
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const fs = require("fs");
2
4
  const path = require("path");
3
5
  const {
@@ -66,8 +68,10 @@ function removeStatusLine(settings) {
66
68
  delete next.statusLine;
67
69
 
68
70
  if (Object.keys(next).length === 0) {
69
- removeSettingsFile(AGY_SETTINGS);
70
- return "settings file was empty, deleted";
71
+ // A symlinked settings.json is emptied rather than deleted (see paths.js).
72
+ return removeSettingsFile(AGY_SETTINGS) === "emptied"
73
+ ? "settings file was empty, emptied (it is a symlink)"
74
+ : "settings file was empty, deleted";
71
75
  }
72
76
 
73
77
  writeJsonSafely(AGY_SETTINGS, next);
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ "use strict";
2
3
 
3
4
  const fs = require("fs");
4
5
  const path = require("path");
@@ -9,6 +10,9 @@ const { themeCommand } = require("./cli/theme");
9
10
  const { layoutCommand } = require("./cli/layout");
10
11
  const { presetCommand } = require("./cli/preset");
11
12
  const { capsCommand } = require("./cli/caps");
13
+ const { gradientCommand } = require("./cli/gradient");
14
+ const { exportCommand } = require("./cli/export");
15
+ const { importCommand } = require("./cli/import");
12
16
  const { doctorCommand } = require("./cli/doctor");
13
17
  const { uninstallCommand } = require("./cli/uninstall");
14
18
  const { reportCommand } = require("./cli/report");
@@ -46,6 +50,17 @@ const COMMANDS = [
46
50
  value: "<lines>",
47
51
  help: "set padding blank lines above statusline",
48
52
  },
53
+ preset: {
54
+ type: "string",
55
+ value: "<id>",
56
+ help: "start from this widget preset (skips the question)",
57
+ },
58
+ layout: { type: "string", value: "<id>", help: "use this layout (skips the question)" },
59
+ theme: {
60
+ type: "string",
61
+ value: "<id>",
62
+ help: "use this theme, or dark, light, colorblind (skips the question)",
63
+ },
49
64
  },
50
65
  run: (_arg, opts) =>
51
66
  initCommand({
@@ -103,6 +118,42 @@ const COMMANDS = [
103
118
  },
104
119
  run: (arg, opts) => capsCommand(arg, opts),
105
120
  },
121
+ {
122
+ name: "gradient",
123
+ arg: "[name]",
124
+ description:
125
+ "View or set a gradient across each row (a preset, hex colors like #ff5f6d-#ffc371, or off)",
126
+ options: {
127
+ list: { type: "boolean", short: "l", help: "list all gradient presets with a preview" },
128
+ },
129
+ run: (arg, opts) => gradientCommand(arg, opts),
130
+ },
131
+ {
132
+ name: "export",
133
+ arg: "[file]",
134
+ description: "Write your config to a file (or to stdout without one) to back it up or share it",
135
+ options: {
136
+ force: { type: "boolean", help: "replace the file if it already exists" },
137
+ },
138
+ run: (arg, opts) => exportCommand(arg, opts),
139
+ },
140
+ {
141
+ name: "import",
142
+ arg: "<file>",
143
+ description: "Load a config from a file, after showing what changes",
144
+ options: {
145
+ merge: {
146
+ type: "boolean",
147
+ help: "change only the settings the file has (default: replace the whole config)",
148
+ },
149
+ yes: { type: "boolean", short: "y", help: "apply without asking" },
150
+ "allow-exec": {
151
+ type: "boolean",
152
+ help: "accept the shell commands (exec widgets) the file adds; --yes alone doesn't",
153
+ },
154
+ },
155
+ run: (arg, opts) => importCommand(arg, opts),
156
+ },
106
157
  {
107
158
  name: "doctor",
108
159
  description: