@v1nvn/statusline 0.22.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/README.md +92 -0
- package/assets/payloads/p1.json +92 -0
- package/assets/payloads/p2.json +62 -0
- package/assets/payloads/p3.json +69 -0
- package/assets/payloads/p4.json +86 -0
- package/assets/ticks/multi.json +70 -0
- package/dist/index.js +1346 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region ../core/dist/index.js
|
|
9
|
+
function printUsageAndExit(program) {
|
|
10
|
+
console.error(program.helpInformation());
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/resolve.ts
|
|
15
|
+
var DATA_REL = join(".claude", "plugins", "data", "statusline-agentic");
|
|
16
|
+
function capturePath(home, surface) {
|
|
17
|
+
return join(home, DATA_REL, "captures", `${surface}.json`);
|
|
18
|
+
}
|
|
19
|
+
function backupPath(home) {
|
|
20
|
+
return join(home, DATA_REL, "backup.json");
|
|
21
|
+
}
|
|
22
|
+
var KEY_RESOLVER = "d=$(printf '%s\\n' ~/.claude/plugins/cache/agentic/statusline/*/ | sort -V | tail -1)";
|
|
23
|
+
var MAIN_PREFIX = `${KEY_RESOLVER}; `;
|
|
24
|
+
var MAIN_SUFFIX = "bash \"${d}runtime/statusline.sh\" 2>/dev/null || true";
|
|
25
|
+
function mainKeyValue(layout, assignments) {
|
|
26
|
+
return `${MAIN_PREFIX}${[`STATUSLINE_LAB_LAYOUT='${layout}'`, ...assignments].join(" ")} ${MAIN_SUFFIX}`;
|
|
27
|
+
}
|
|
28
|
+
var subagentKeyValue = `${KEY_RESOLVER}; bash "\${d}runtime/subagent.sh" 2>/dev/null || true`;
|
|
29
|
+
function isOurMainCommand(command) {
|
|
30
|
+
return command.startsWith(MAIN_PREFIX) && command.endsWith(` ${MAIN_SUFFIX}`);
|
|
31
|
+
}
|
|
32
|
+
function mainKeyMiddle(command) {
|
|
33
|
+
if (!isOurMainCommand(command)) return null;
|
|
34
|
+
return command.slice(MAIN_PREFIX.length, command.length - 52 - 1);
|
|
35
|
+
}
|
|
36
|
+
function settingsCommand(home) {
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = readFileSync(join(home, ".claude", "settings.json"), "utf8");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const member = parsed?.statusLine;
|
|
50
|
+
if (typeof member !== "object" || member === null) return null;
|
|
51
|
+
const command = member.command;
|
|
52
|
+
return typeof command === "string" ? command : null;
|
|
53
|
+
}
|
|
54
|
+
function readKeyConfig(home) {
|
|
55
|
+
const command = settingsCommand(home);
|
|
56
|
+
const middle = command === null ? null : mainKeyMiddle(command);
|
|
57
|
+
if (middle === null) return {
|
|
58
|
+
layout: null,
|
|
59
|
+
values: {}
|
|
60
|
+
};
|
|
61
|
+
const layout = /^STATUSLINE_LAB_LAYOUT='([^']*)'/.exec(middle);
|
|
62
|
+
if (layout === null) return {
|
|
63
|
+
layout: null,
|
|
64
|
+
values: {}
|
|
65
|
+
};
|
|
66
|
+
const values = {};
|
|
67
|
+
for (const [, name, alt] of middle.matchAll(/(?:^| )STATUSLINE_LAB_([A-Z][A-Z0-9_]*)=([a-z0-9]+)/g)) values[name.toLowerCase()] = alt;
|
|
68
|
+
return {
|
|
69
|
+
layout: layout[1],
|
|
70
|
+
values
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function byVersion(a, b) {
|
|
74
|
+
const left = a.split(".");
|
|
75
|
+
const right = b.split(".");
|
|
76
|
+
for (let i = 0; i < left.length || i < right.length; i += 1) {
|
|
77
|
+
const delta = (parseInt(left[i] ?? "0", 10) || 0) - (parseInt(right[i] ?? "0", 10) || 0);
|
|
78
|
+
if (delta !== 0) return delta;
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
function newestCacheDir(cacheRoot) {
|
|
83
|
+
let versions;
|
|
84
|
+
try {
|
|
85
|
+
versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (versions.length === 0) return null;
|
|
90
|
+
versions.sort(byVersion);
|
|
91
|
+
return join(cacheRoot, versions[versions.length - 1]);
|
|
92
|
+
}
|
|
93
|
+
function readAlternatives(dir) {
|
|
94
|
+
const declared = /* @__PURE__ */ new Map();
|
|
95
|
+
const components = join(dir, "components");
|
|
96
|
+
for (const file of readdirSync(components).sort()) {
|
|
97
|
+
if (!file.endsWith(".sh")) continue;
|
|
98
|
+
for (const line of readFileSync(join(components, file), "utf8").split("\n")) {
|
|
99
|
+
if (!line.startsWith("#")) break;
|
|
100
|
+
const match = /alternatives:\s*(.+)$/.exec(line);
|
|
101
|
+
if (match) declared.set(file.slice(0, -3), match[1].split("|").map((alt) => alt.trim().replace(/\s*\(current\)$/, "")));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return declared;
|
|
105
|
+
}
|
|
106
|
+
function readItems(dir) {
|
|
107
|
+
const alternatives = readAlternatives(dir);
|
|
108
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const [, item, alt] of readFileSync(join(dir, "lib.sh"), "utf8").matchAll(/([a-z]+)\) echo ([a-z]+) ;;/g)) defaults.set(item, alt);
|
|
110
|
+
const match = /^COMPS="(.+)"$/m.exec(readFileSync(join(dir, "statusline.sh"), "utf8"));
|
|
111
|
+
if (match === null) throw new Error(`statusline.sh in ${dir} declares no COMPS order`);
|
|
112
|
+
return match[1].split(" ").map((item) => {
|
|
113
|
+
const alt = defaults.get(item);
|
|
114
|
+
if (alt === void 0) throw new Error(`lib.sh in ${dir} declares no default for '${item}'`);
|
|
115
|
+
return {
|
|
116
|
+
alternatives: alternatives.get(item) ?? [],
|
|
117
|
+
default: alt,
|
|
118
|
+
item
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function readDefaultLayout(dir) {
|
|
123
|
+
const match = /^export DEFAULT_LAYOUT='(.*)'$/m.exec(readFileSync(join(dir, "lib.sh"), "utf8"));
|
|
124
|
+
if (match === null) throw new Error(`lib.sh in ${dir} declares no DEFAULT_LAYOUT`);
|
|
125
|
+
return match[1];
|
|
126
|
+
}
|
|
127
|
+
function resolveRuntime({ home }) {
|
|
128
|
+
const pluginDir = newestCacheDir(join(home, ".claude", "plugins", "cache", "agentic", "statusline"));
|
|
129
|
+
const dir = pluginDir === null ? null : join(pluginDir, "runtime");
|
|
130
|
+
if (dir === null || !existsSync(join(dir, "statusline.sh"))) throw new Error(`no statusline runtime under ${home} — install the plugin first: claude plugin install statusline@agentic`);
|
|
131
|
+
return {
|
|
132
|
+
defaultLayout: readDefaultLayout(dir),
|
|
133
|
+
dir,
|
|
134
|
+
items: readItems(dir)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/catalog.ts
|
|
139
|
+
function catalog(options) {
|
|
140
|
+
const runtime = resolveRuntime({ home: options.home });
|
|
141
|
+
const live = readKeyConfig(options.home).values;
|
|
142
|
+
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
143
|
+
return (options.items ?? runtime.items.map((item) => item.item)).map((item) => {
|
|
144
|
+
const entry = byItem.get(item);
|
|
145
|
+
if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
|
|
146
|
+
const current = live[item] ?? entry.default;
|
|
147
|
+
return `${item}: ${entry.alternatives.map((alt) => alt === current ? `${alt}*` : alt).join(" | ")}`;
|
|
148
|
+
}).join("\n");
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/cli.ts
|
|
152
|
+
var VERSION = {
|
|
153
|
+
name: "@v1nvn/statusline",
|
|
154
|
+
version: "0.22.0",
|
|
155
|
+
description: "Browse statusline variants and configure the live status line and agent panel — the statusline CLI behind the statusline plugin.",
|
|
156
|
+
type: "module",
|
|
157
|
+
bin: "dist/index.js",
|
|
158
|
+
scripts: {
|
|
159
|
+
"build": "vite build && chmod +x dist/index.js",
|
|
160
|
+
"test": "vitest run",
|
|
161
|
+
"test:watch": "vitest"
|
|
162
|
+
},
|
|
163
|
+
keywords: [
|
|
164
|
+
"claude-code",
|
|
165
|
+
"statusline",
|
|
166
|
+
"cli"
|
|
167
|
+
],
|
|
168
|
+
author: "v1nvn",
|
|
169
|
+
license: "MIT",
|
|
170
|
+
repository: {
|
|
171
|
+
"type": "git",
|
|
172
|
+
"url": "git+https://github.com/v1nvn/agentic.git",
|
|
173
|
+
"directory": "packages/statusline"
|
|
174
|
+
},
|
|
175
|
+
files: ["dist", "assets"],
|
|
176
|
+
publishConfig: { "access": "public" },
|
|
177
|
+
engines: { "node": ">=22" },
|
|
178
|
+
dependencies: {
|
|
179
|
+
"@v1nvn/agentic-core": "workspace:^",
|
|
180
|
+
"commander": "^15.0.0"
|
|
181
|
+
},
|
|
182
|
+
devDependencies: {
|
|
183
|
+
"vite": "^8.1.4",
|
|
184
|
+
"vitest": "^4.1.10"
|
|
185
|
+
}
|
|
186
|
+
}.version;
|
|
187
|
+
var ITEM_IDS = [
|
|
188
|
+
"model",
|
|
189
|
+
"effort",
|
|
190
|
+
"state",
|
|
191
|
+
"cwd",
|
|
192
|
+
"branch",
|
|
193
|
+
"status",
|
|
194
|
+
"ahead",
|
|
195
|
+
"pr",
|
|
196
|
+
"bar",
|
|
197
|
+
"tokens",
|
|
198
|
+
"cache",
|
|
199
|
+
"cost",
|
|
200
|
+
"duration",
|
|
201
|
+
"lines",
|
|
202
|
+
"rate",
|
|
203
|
+
"style"
|
|
204
|
+
];
|
|
205
|
+
var QUIET = {
|
|
206
|
+
writeOut: () => void 0,
|
|
207
|
+
writeErr: () => void 0
|
|
208
|
+
};
|
|
209
|
+
var HelpRequested = class extends Error {
|
|
210
|
+
command;
|
|
211
|
+
constructor(command) {
|
|
212
|
+
super(`${command} --help`);
|
|
213
|
+
this.command = command;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
function quiet(name, command) {
|
|
217
|
+
return command.exitOverride((err) => {
|
|
218
|
+
const code = err.code;
|
|
219
|
+
if (code === "commander.help" || code === "commander.helpDisplayed") throw new HelpRequested(name);
|
|
220
|
+
throw err;
|
|
221
|
+
}).configureOutput(QUIET).allowExcessArguments(false);
|
|
222
|
+
}
|
|
223
|
+
function buildProgram(onSubcommand) {
|
|
224
|
+
const catalog = quiet("catalog", new Command("catalog")).description("print one line per item — * marks the live variant").option("--home <dir>", "operate on this home instead of $HOME");
|
|
225
|
+
for (const item of ITEM_IDS) catalog.option(`--${item}`, `cut the listing to the ${item} item`);
|
|
226
|
+
catalog.action((options) => onSubcommand?.("catalog", options));
|
|
227
|
+
const configure = quiet("configure", new Command("configure")).description("write both settings keys with the inline lab commands").option("--home <dir>", "operate on this home instead of $HOME").option("--layout <spec>", "brace clusters of item ids, e.g. '{cwd branch} {model effort}'").addOption(new Option("--fallback <mode>", "fill unflagged layout items from defaults or the existing config").choices(["default", "existing"])).option("--dry-run", "render both surfaces, write nothing").option("--force", "take over foreign settings keys");
|
|
228
|
+
for (const item of ITEM_IDS) configure.option(`--${item} <alt>`, `variant for the ${item} item`);
|
|
229
|
+
configure.action((options) => onSubcommand?.("configure", options));
|
|
230
|
+
const restore = quiet("restore", new Command("restore")).description("revert both settings keys to their pre-lab values, clean the data dir").option("--home <dir>", "operate on this home instead of $HOME").option("--dry-run", "print the plan, write nothing").option("--force", "splice the saved value over a key changed after the takeover");
|
|
231
|
+
restore.action((options) => onSubcommand?.("restore", options));
|
|
232
|
+
const status = quiet("status", new Command("status")).description("check install, keys, config, and backup — exit 0 healthy, 1 when a row needs action").option("--home <dir>", "operate on this home instead of $HOME");
|
|
233
|
+
status.action((options) => onSubcommand?.("status", options));
|
|
234
|
+
return new Command().name("statusline").description("Configure the status line and agent panel designs").option("-V, --version", "print the lab version and exit").action(() => void 0).addCommand(catalog).addCommand(configure).addCommand(restore).addCommand(status);
|
|
235
|
+
}
|
|
236
|
+
function subcommandHelp(name) {
|
|
237
|
+
const command = buildProgram().commands.find((c) => c.name() === name);
|
|
238
|
+
if (command === void 0) throw new Error(`no '${name}' command to describe`);
|
|
239
|
+
return command.helpInformation();
|
|
240
|
+
}
|
|
241
|
+
function parseArgs(args) {
|
|
242
|
+
let chosen;
|
|
243
|
+
const program = buildProgram((command, options) => {
|
|
244
|
+
chosen = {
|
|
245
|
+
command,
|
|
246
|
+
options
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
program.allowExcessArguments(false).exitOverride().configureOutput(QUIET).parse([...args], { from: "user" });
|
|
251
|
+
} catch (e) {
|
|
252
|
+
if (e instanceof HelpRequested) return {
|
|
253
|
+
version: false,
|
|
254
|
+
help: e.command
|
|
255
|
+
};
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const { version } = program.opts();
|
|
259
|
+
if (version) return { version: true };
|
|
260
|
+
if (chosen === void 0) return;
|
|
261
|
+
const options = chosen.options;
|
|
262
|
+
if (chosen.command === "catalog") {
|
|
263
|
+
const items = ITEM_IDS.filter((item) => options[item] === true);
|
|
264
|
+
return {
|
|
265
|
+
version: false,
|
|
266
|
+
command: "catalog",
|
|
267
|
+
...items.length > 0 ? { items } : {},
|
|
268
|
+
...typeof options.home === "string" ? { home: options.home } : {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (chosen.command === "restore") return {
|
|
272
|
+
version: false,
|
|
273
|
+
command: "restore",
|
|
274
|
+
...typeof options.dryRun === "boolean" ? { dryRun: options.dryRun } : {},
|
|
275
|
+
...typeof options.force === "boolean" ? { force: options.force } : {},
|
|
276
|
+
...typeof options.home === "string" ? { home: options.home } : {}
|
|
277
|
+
};
|
|
278
|
+
if (chosen.command === "status") return {
|
|
279
|
+
version: false,
|
|
280
|
+
command: "status",
|
|
281
|
+
...typeof options.home === "string" ? { home: options.home } : {}
|
|
282
|
+
};
|
|
283
|
+
const variants = {};
|
|
284
|
+
for (const item of ITEM_IDS) if (typeof options[item] === "string") variants[item] = options[item];
|
|
285
|
+
return {
|
|
286
|
+
version: false,
|
|
287
|
+
command: "configure",
|
|
288
|
+
...Object.keys(variants).length > 0 ? { variants } : {},
|
|
289
|
+
...typeof options.dryRun === "boolean" ? { dryRun: options.dryRun } : {},
|
|
290
|
+
...typeof options.fallback === "string" ? { fallback: options.fallback } : {},
|
|
291
|
+
...typeof options.force === "boolean" ? { force: options.force } : {},
|
|
292
|
+
...typeof options.home === "string" ? { home: options.home } : {},
|
|
293
|
+
...typeof options.layout === "string" ? { layout: options.layout } : {}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/demo-repo.ts
|
|
298
|
+
var NAME = "Lab";
|
|
299
|
+
var EMAIL = "lab@local";
|
|
300
|
+
var D_INIT = "2026-09-01T10:00:00+00:00";
|
|
301
|
+
var D_REMOTE = "2026-09-01T11:00:00+00:00";
|
|
302
|
+
var D_LOCAL1 = "2026-09-02T09:00:00+00:00";
|
|
303
|
+
var D_LOCAL2 = "2026-09-02T09:30:00+00:00";
|
|
304
|
+
var D_STASH = "2026-09-02T10:00:00+00:00";
|
|
305
|
+
var API_INIT = "export const api = {\n host: 'demo.local',\n};\n";
|
|
306
|
+
function materializeDemoRepo(home) {
|
|
307
|
+
const dir = join(home, "demo", "atlas-web");
|
|
308
|
+
for (const sub of [
|
|
309
|
+
"src",
|
|
310
|
+
"docs",
|
|
311
|
+
"scratch",
|
|
312
|
+
"scripts"
|
|
313
|
+
]) mkdirSync(join(dir, sub), { recursive: true });
|
|
314
|
+
function git(args, date) {
|
|
315
|
+
return execFileSync("git", [
|
|
316
|
+
"-c",
|
|
317
|
+
"commit.gpgsign=false",
|
|
318
|
+
...args
|
|
319
|
+
], {
|
|
320
|
+
cwd: dir,
|
|
321
|
+
encoding: "utf8",
|
|
322
|
+
stdio: [
|
|
323
|
+
"ignore",
|
|
324
|
+
"pipe",
|
|
325
|
+
"pipe"
|
|
326
|
+
],
|
|
327
|
+
env: {
|
|
328
|
+
PATH: process.env.PATH ?? "",
|
|
329
|
+
HOME: home,
|
|
330
|
+
LC_ALL: "C",
|
|
331
|
+
TZ: "UTC",
|
|
332
|
+
GIT_AUTHOR_NAME: NAME,
|
|
333
|
+
GIT_AUTHOR_EMAIL: EMAIL,
|
|
334
|
+
GIT_COMMITTER_NAME: NAME,
|
|
335
|
+
GIT_COMMITTER_EMAIL: EMAIL,
|
|
336
|
+
...date ? {
|
|
337
|
+
GIT_AUTHOR_DATE: date,
|
|
338
|
+
GIT_COMMITTER_DATE: date
|
|
339
|
+
} : {}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function write(rel, body) {
|
|
344
|
+
writeFileSync(join(dir, rel), body);
|
|
345
|
+
}
|
|
346
|
+
function commit(message, date) {
|
|
347
|
+
git(["add", "-A"]);
|
|
348
|
+
git([
|
|
349
|
+
"commit",
|
|
350
|
+
"-m",
|
|
351
|
+
message
|
|
352
|
+
], date);
|
|
353
|
+
}
|
|
354
|
+
git([
|
|
355
|
+
"init",
|
|
356
|
+
"-b",
|
|
357
|
+
"main"
|
|
358
|
+
]);
|
|
359
|
+
git([
|
|
360
|
+
"config",
|
|
361
|
+
"user.name",
|
|
362
|
+
NAME
|
|
363
|
+
]);
|
|
364
|
+
git([
|
|
365
|
+
"config",
|
|
366
|
+
"user.email",
|
|
367
|
+
EMAIL
|
|
368
|
+
]);
|
|
369
|
+
write("README.md", "atlas-web demo repo\n");
|
|
370
|
+
write("src/index.js", "export function start() {}\n");
|
|
371
|
+
write("src/api.js", API_INIT);
|
|
372
|
+
write("src/style.css", "body { margin: 0; }\n");
|
|
373
|
+
write("src/util.js", "export const id = (x) => x;\n");
|
|
374
|
+
commit("init", D_INIT);
|
|
375
|
+
write("src/api.js", `${API_INIT}// remote: pinned host\n`);
|
|
376
|
+
commit("remote1", D_REMOTE);
|
|
377
|
+
const originMain = git(["rev-parse", "HEAD"]).trim();
|
|
378
|
+
git([
|
|
379
|
+
"checkout",
|
|
380
|
+
"-b",
|
|
381
|
+
"feature/login-flow",
|
|
382
|
+
"HEAD~1"
|
|
383
|
+
]);
|
|
384
|
+
write("src/util.js", "export const id = (x) => x;\nexport const tag = (x) => x;\n");
|
|
385
|
+
commit("local1", D_LOCAL1);
|
|
386
|
+
write("src/render.js", "export function render() {}\n");
|
|
387
|
+
commit("local2", D_LOCAL2);
|
|
388
|
+
git([
|
|
389
|
+
"update-ref",
|
|
390
|
+
"refs/remotes/origin/main",
|
|
391
|
+
originMain
|
|
392
|
+
]);
|
|
393
|
+
git([
|
|
394
|
+
"config",
|
|
395
|
+
"remote.origin.url",
|
|
396
|
+
"../atlas-web.git"
|
|
397
|
+
]);
|
|
398
|
+
git([
|
|
399
|
+
"config",
|
|
400
|
+
"remote.origin.fetch",
|
|
401
|
+
"+refs/heads/*:refs/remotes/origin/*"
|
|
402
|
+
]);
|
|
403
|
+
git([
|
|
404
|
+
"config",
|
|
405
|
+
"branch.feature/login-flow.remote",
|
|
406
|
+
"origin"
|
|
407
|
+
]);
|
|
408
|
+
git([
|
|
409
|
+
"config",
|
|
410
|
+
"branch.feature/login-flow.merge",
|
|
411
|
+
"refs/heads/main"
|
|
412
|
+
]);
|
|
413
|
+
write("README.md", "atlas-web demo repo (wip)\n");
|
|
414
|
+
git([
|
|
415
|
+
"stash",
|
|
416
|
+
"push",
|
|
417
|
+
"-m",
|
|
418
|
+
"wip: context refactor",
|
|
419
|
+
"--",
|
|
420
|
+
"README.md"
|
|
421
|
+
], D_STASH);
|
|
422
|
+
write("README.md", "atlas-web demo repo\n\nstaged: pin the render loop\n");
|
|
423
|
+
write("src/index.js", "export function start() {\n return Promise.resolve();\n}\n");
|
|
424
|
+
git([
|
|
425
|
+
"add",
|
|
426
|
+
"README.md",
|
|
427
|
+
"src/index.js"
|
|
428
|
+
]);
|
|
429
|
+
write("src/api.js", `${API_INIT}// local tweak\n`);
|
|
430
|
+
write("src/style.css", "body { margin: 0; padding: 0; }\n");
|
|
431
|
+
write("docs/wip.txt", "");
|
|
432
|
+
write("scratch/todo.md", "");
|
|
433
|
+
write("scripts/deploy.sh", "");
|
|
434
|
+
write("note1.md", "");
|
|
435
|
+
write("note2.md", "");
|
|
436
|
+
return dir;
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/payloads.ts
|
|
440
|
+
var PAYLOADS_DIR = fileURLToPath(new URL("../assets/payloads", import.meta.url));
|
|
441
|
+
var TICKS_DIR = fileURLToPath(new URL("../assets/ticks", import.meta.url));
|
|
442
|
+
var P1_WARM_IN = 1920;
|
|
443
|
+
var P1_RESETS_IN = {
|
|
444
|
+
five_hour: 13830,
|
|
445
|
+
seven_day: 518400,
|
|
446
|
+
spend_limit: 950400
|
|
447
|
+
};
|
|
448
|
+
var PANEL_STEP_S = 300;
|
|
449
|
+
function isObject$1(value) {
|
|
450
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
451
|
+
}
|
|
452
|
+
function anchoredP1(repoDir, now) {
|
|
453
|
+
const payload = JSON.parse(readFileSync(join(PAYLOADS_DIR, "p1.json"), "utf8"));
|
|
454
|
+
const cache = payload.prompt_cache;
|
|
455
|
+
if (isObject$1(cache)) {
|
|
456
|
+
if (cache.warm === true) {
|
|
457
|
+
cache.expires_at = now + P1_WARM_IN;
|
|
458
|
+
cache.last_miss_at = now + P1_WARM_IN - 3900;
|
|
459
|
+
} else {
|
|
460
|
+
cache.expires_at = now - 10;
|
|
461
|
+
delete cache.last_miss_at;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const limits = payload.rate_limits;
|
|
465
|
+
if (isObject$1(limits)) for (const [limitKey, offset] of Object.entries(P1_RESETS_IN)) {
|
|
466
|
+
const limit = limits[limitKey];
|
|
467
|
+
if (isObject$1(limit)) limit.resets_at = now + offset;
|
|
468
|
+
}
|
|
469
|
+
payload.workspace.current_dir = repoDir;
|
|
470
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
471
|
+
}
|
|
472
|
+
function hasStart(value) {
|
|
473
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
474
|
+
}
|
|
475
|
+
function anchoredTick(now) {
|
|
476
|
+
const tick = JSON.parse(readFileSync(join(TICKS_DIR, "multi.json"), "utf8"));
|
|
477
|
+
const tasks = Array.isArray(tick.tasks) ? tick.tasks : [];
|
|
478
|
+
let started = 0;
|
|
479
|
+
const staggered = {
|
|
480
|
+
...tick,
|
|
481
|
+
tasks: tasks.map((task) => {
|
|
482
|
+
if (!hasStart(task.startTime)) return task;
|
|
483
|
+
started += 1;
|
|
484
|
+
return {
|
|
485
|
+
...task,
|
|
486
|
+
startTime: now - PANEL_STEP_S * started
|
|
487
|
+
};
|
|
488
|
+
})
|
|
489
|
+
};
|
|
490
|
+
return `${JSON.stringify(staggered, null, 2)}\n`;
|
|
491
|
+
}
|
|
492
|
+
function previewSources(home, now) {
|
|
493
|
+
const mainCapture = capturePath(home, "main");
|
|
494
|
+
if (existsSync(mainCapture)) {
|
|
495
|
+
const tickCapture = capturePath(home, "tick");
|
|
496
|
+
return {
|
|
497
|
+
cleanup: () => void 0,
|
|
498
|
+
main: readFileSync(mainCapture, "utf8"),
|
|
499
|
+
tick: existsSync(tickCapture) ? readFileSync(tickCapture, "utf8") : anchoredTick(now)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const demoHome = mkdtempSync(join(tmpdir(), "statusline-preview-"));
|
|
503
|
+
return {
|
|
504
|
+
cleanup: () => {
|
|
505
|
+
rmSync(demoHome, {
|
|
506
|
+
recursive: true,
|
|
507
|
+
force: true
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
main: anchoredP1(materializeDemoRepo(demoHome), now),
|
|
511
|
+
tick: anchoredTick(now)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function firstPanelRow(output) {
|
|
515
|
+
const [row] = output.split("\n");
|
|
516
|
+
if (row === "") return "";
|
|
517
|
+
const content = JSON.parse(row).content;
|
|
518
|
+
return typeof content === "string" ? content : "";
|
|
519
|
+
}
|
|
520
|
+
function runtimeRenderer(spec) {
|
|
521
|
+
const run = spawnSync("bash", [spec.bin], {
|
|
522
|
+
input: spec.stdin,
|
|
523
|
+
env: {
|
|
524
|
+
PATH: process.env.PATH ?? "",
|
|
525
|
+
...spec.env,
|
|
526
|
+
LC_ALL: "C",
|
|
527
|
+
TZ: "UTC"
|
|
528
|
+
},
|
|
529
|
+
timeout: 3e4
|
|
530
|
+
});
|
|
531
|
+
if (run.status !== 0) throw new Error(`render failed: ${run.stderr.toString("utf8")}`);
|
|
532
|
+
const stdout = run.stdout.toString("utf8");
|
|
533
|
+
const warn = run.stderr.toString("utf8").trim();
|
|
534
|
+
return warn === "" ? stdout : `${stdout}${warn}\n`;
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/configure.ts
|
|
538
|
+
var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
|
|
539
|
+
function layoutItems(layout, valid) {
|
|
540
|
+
const known = new Set(valid);
|
|
541
|
+
const items = [];
|
|
542
|
+
for (const cluster of parseClusters(layout)) for (const word of cluster) {
|
|
543
|
+
if (!known.has(word)) throw new Error(`unknown layout item '${word}' — valid items: ${valid.join(" ")}`);
|
|
544
|
+
if (!items.includes(word)) items.push(word);
|
|
545
|
+
}
|
|
546
|
+
return items;
|
|
547
|
+
}
|
|
548
|
+
function parseClusters(layout) {
|
|
549
|
+
const clusters = [];
|
|
550
|
+
let words = [];
|
|
551
|
+
let word = "";
|
|
552
|
+
let open = false;
|
|
553
|
+
function pushWord() {
|
|
554
|
+
if (word !== "") {
|
|
555
|
+
words.push(word);
|
|
556
|
+
word = "";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (const c of layout) if (c === "{") {
|
|
560
|
+
if (open) throw new Error(`layout '${layout}': '{' inside a cluster`);
|
|
561
|
+
open = true;
|
|
562
|
+
words = [];
|
|
563
|
+
} else if (c === "}") {
|
|
564
|
+
if (!open) throw new Error(`layout '${layout}': '}' outside a cluster`);
|
|
565
|
+
pushWord();
|
|
566
|
+
open = false;
|
|
567
|
+
if (words.length > 0) clusters.push(words);
|
|
568
|
+
} else if (c === " ") {
|
|
569
|
+
if (open) pushWord();
|
|
570
|
+
else if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
571
|
+
} else if (/[a-z0-9]/.test(c)) word += c;
|
|
572
|
+
else throw new Error(`layout '${layout}': '${c}' is not layout grammar (braces, item ids, spaces)`);
|
|
573
|
+
if (open) throw new Error(`layout '${layout}': unterminated cluster`);
|
|
574
|
+
if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
575
|
+
if (clusters.length === 0) throw new Error(`layout '${layout}': no clusters`);
|
|
576
|
+
return clusters;
|
|
577
|
+
}
|
|
578
|
+
function isObject(value) {
|
|
579
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
580
|
+
}
|
|
581
|
+
function readOrNull(file) {
|
|
582
|
+
try {
|
|
583
|
+
return readFileSync(file, "utf8");
|
|
584
|
+
} catch (e) {
|
|
585
|
+
if (e.code === "ENOENT") return null;
|
|
586
|
+
throw e;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function parseSettings(file, raw) {
|
|
590
|
+
let parsed;
|
|
591
|
+
try {
|
|
592
|
+
parsed = JSON.parse(raw);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
throw new Error(`${file} is not valid JSON`, { cause: e });
|
|
595
|
+
}
|
|
596
|
+
if (!isObject(parsed)) throw new Error(`${file} does not hold a settings object`);
|
|
597
|
+
return parsed;
|
|
598
|
+
}
|
|
599
|
+
function memberCommand(value) {
|
|
600
|
+
if (!isObject(value) || value.type !== "command") return null;
|
|
601
|
+
return typeof value.command === "string" ? value.command : null;
|
|
602
|
+
}
|
|
603
|
+
function isOurMember(key, value) {
|
|
604
|
+
const command = memberCommand(value);
|
|
605
|
+
if (command === null) return false;
|
|
606
|
+
return key === "statusLine" ? isOurMainCommand(command) : command === subagentKeyValue;
|
|
607
|
+
}
|
|
608
|
+
function planSettings(home, mainCommand, force) {
|
|
609
|
+
const file = join(home, ".claude", "settings.json");
|
|
610
|
+
const wanted = {
|
|
611
|
+
statusLine: mainCommand,
|
|
612
|
+
subagentStatusLine: subagentKeyValue
|
|
613
|
+
};
|
|
614
|
+
const raw = readOrNull(file);
|
|
615
|
+
if (raw === null) return {
|
|
616
|
+
adds: SETTINGS_KEYS.map((key) => [key, wanted[key]]),
|
|
617
|
+
file,
|
|
618
|
+
raw: null,
|
|
619
|
+
repoints: []
|
|
620
|
+
};
|
|
621
|
+
const parsed = parseSettings(file, raw);
|
|
622
|
+
const adds = [];
|
|
623
|
+
const repoints = [];
|
|
624
|
+
for (const key of SETTINGS_KEYS) {
|
|
625
|
+
const command = wanted[key];
|
|
626
|
+
const value = parsed[key];
|
|
627
|
+
if (value === void 0) adds.push([key, command]);
|
|
628
|
+
else if (memberCommand(value) === command) continue;
|
|
629
|
+
else if (isOurMember(key, value) || force) repoints.push([key, command]);
|
|
630
|
+
else throw new Error(`settings.json ${key} is foreign — rerun with --force added to your configuration flags to take it over`);
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
adds,
|
|
634
|
+
file,
|
|
635
|
+
raw,
|
|
636
|
+
repoints
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function settingsValue(command) {
|
|
640
|
+
return JSON.stringify({
|
|
641
|
+
command,
|
|
642
|
+
type: "command"
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function skipString(raw, at) {
|
|
646
|
+
let i = at + 1;
|
|
647
|
+
while (i < raw.length) if (raw[i] === "\\") i += 2;
|
|
648
|
+
else if (raw[i] === "\"") return i + 1;
|
|
649
|
+
else i += 1;
|
|
650
|
+
throw new Error("unterminated string in settings.json");
|
|
651
|
+
}
|
|
652
|
+
function skipValue(raw, at) {
|
|
653
|
+
const first = raw[at];
|
|
654
|
+
if (first === "\"") return skipString(raw, at);
|
|
655
|
+
if (first === "{" || first === "[") {
|
|
656
|
+
let depth = 0;
|
|
657
|
+
let i = at;
|
|
658
|
+
while (i < raw.length) {
|
|
659
|
+
const c = raw[i];
|
|
660
|
+
if (c === "\"") i = skipString(raw, i);
|
|
661
|
+
else if (c === "{" || c === "[") {
|
|
662
|
+
depth += 1;
|
|
663
|
+
i += 1;
|
|
664
|
+
} else if (c === "}" || c === "]") {
|
|
665
|
+
depth -= 1;
|
|
666
|
+
i += 1;
|
|
667
|
+
if (depth === 0) return i;
|
|
668
|
+
} else i += 1;
|
|
669
|
+
}
|
|
670
|
+
throw new Error("unterminated container in settings.json");
|
|
671
|
+
}
|
|
672
|
+
let i = at;
|
|
673
|
+
while (i < raw.length && !",}]".includes(raw[i] ?? "") && !/\s/.test(raw[i] ?? "~")) i += 1;
|
|
674
|
+
return i;
|
|
675
|
+
}
|
|
676
|
+
function rootMembers(raw) {
|
|
677
|
+
const members = [];
|
|
678
|
+
let i = raw.indexOf("{") + 1;
|
|
679
|
+
while (i < raw.length) {
|
|
680
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
681
|
+
if (raw[i] === "}") break;
|
|
682
|
+
if (raw[i] !== "\"") throw new Error("malformed member in settings.json");
|
|
683
|
+
const nameStart = i;
|
|
684
|
+
const nameEnd = skipString(raw, i);
|
|
685
|
+
const name = JSON.parse(raw.slice(i, nameEnd));
|
|
686
|
+
i = nameEnd;
|
|
687
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
688
|
+
if (raw[i] !== ":") throw new Error("malformed member in settings.json");
|
|
689
|
+
i += 1;
|
|
690
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
691
|
+
const valueStart = i;
|
|
692
|
+
const valueEnd = skipValue(raw, i);
|
|
693
|
+
members.push({
|
|
694
|
+
name,
|
|
695
|
+
nameStart,
|
|
696
|
+
valueEnd,
|
|
697
|
+
valueStart
|
|
698
|
+
});
|
|
699
|
+
i = valueEnd;
|
|
700
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
701
|
+
if (raw[i] === ",") i += 1;
|
|
702
|
+
else if (raw[i] !== "}") throw new Error("malformed member in settings.json");
|
|
703
|
+
}
|
|
704
|
+
return members;
|
|
705
|
+
}
|
|
706
|
+
function rootMemberValueSpan(raw, key) {
|
|
707
|
+
const member = rootMembers(raw).find((m) => m.name === key);
|
|
708
|
+
return member === void 0 ? null : [member.valueStart, member.valueEnd];
|
|
709
|
+
}
|
|
710
|
+
function removeMembers(raw, keys) {
|
|
711
|
+
let text = raw;
|
|
712
|
+
for (const key of keys) {
|
|
713
|
+
const members = rootMembers(text);
|
|
714
|
+
const at = members.findIndex((m) => m.name === key);
|
|
715
|
+
if (at === -1) throw new Error(`cannot find the "${key}" member to remove in settings.json`);
|
|
716
|
+
if (members.length === 1) return "{}";
|
|
717
|
+
const member = members[at];
|
|
718
|
+
if (at > 0) {
|
|
719
|
+
let start = member.nameStart;
|
|
720
|
+
while (/\s/.test(text[start - 1] ?? "~")) start -= 1;
|
|
721
|
+
text = `${text.slice(0, start - 1)}${text.slice(member.valueEnd)}`;
|
|
722
|
+
} else text = `${text.slice(0, member.nameStart)}${text.slice(members[1].nameStart)}`;
|
|
723
|
+
}
|
|
724
|
+
return text;
|
|
725
|
+
}
|
|
726
|
+
function insertMembers(raw, members) {
|
|
727
|
+
const close = raw.lastIndexOf("}");
|
|
728
|
+
if (close === -1) throw new Error("settings.json has no closing brace to splice into");
|
|
729
|
+
let at = close;
|
|
730
|
+
while (/\s/.test(raw[at - 1] ?? "~")) at -= 1;
|
|
731
|
+
const rendered = members.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
|
|
732
|
+
const comma = raw.replace(/\s/g, "") === "{}" ? "" : ",";
|
|
733
|
+
return `${raw.slice(0, at)}${comma}\n ${rendered}${raw.slice(at)}`;
|
|
734
|
+
}
|
|
735
|
+
function splicedSettings(raw, plan) {
|
|
736
|
+
let text = plan.adds.length > 0 ? insertMembers(raw, plan.adds) : raw;
|
|
737
|
+
for (const [key, command] of plan.repoints) {
|
|
738
|
+
const span = rootMemberValueSpan(text, key);
|
|
739
|
+
if (span === null) throw new Error(`cannot find the "${key}" member to repoint in settings.json`);
|
|
740
|
+
text = `${text.slice(0, span[0])}${settingsValue(command)}${text.slice(span[1])}`;
|
|
741
|
+
}
|
|
742
|
+
return text;
|
|
743
|
+
}
|
|
744
|
+
function writeBackupIfAbsent(home, plan) {
|
|
745
|
+
const file = backupPath(home);
|
|
746
|
+
if (existsSync(file)) return;
|
|
747
|
+
const keys = {};
|
|
748
|
+
if (plan.raw !== null) for (const key of SETTINGS_KEYS) {
|
|
749
|
+
const span = rootMemberValueSpan(plan.raw, key);
|
|
750
|
+
if (span === null) continue;
|
|
751
|
+
const memberRaw = plan.raw.slice(span[0], span[1]);
|
|
752
|
+
if (isOurMember(key, JSON.parse(memberRaw))) continue;
|
|
753
|
+
keys[key] = memberRaw;
|
|
754
|
+
}
|
|
755
|
+
const dir = dirname(file);
|
|
756
|
+
mkdirSync(dir, { recursive: true });
|
|
757
|
+
const tmp = `${file}.tmp`;
|
|
758
|
+
writeFileSync(tmp, `${JSON.stringify({
|
|
759
|
+
createdFile: plan.raw === null,
|
|
760
|
+
keys
|
|
761
|
+
}, null, 2)}\n`);
|
|
762
|
+
renameSync(tmp, file);
|
|
763
|
+
}
|
|
764
|
+
function commitSettings(plan) {
|
|
765
|
+
if (plan.raw === null) {
|
|
766
|
+
mkdirSync(dirname(plan.file), { recursive: true });
|
|
767
|
+
const members = plan.adds.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
|
|
768
|
+
writeFileSync(plan.file, `{\n ${members}\n}\n`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (plan.adds.length === 0 && plan.repoints.length === 0) return;
|
|
772
|
+
writeFileSync(plan.file, splicedSettings(plan.raw, plan));
|
|
773
|
+
}
|
|
774
|
+
function renderPreview(runtime, home, values, layout, now) {
|
|
775
|
+
const sources = previewSources(home, Number(now));
|
|
776
|
+
try {
|
|
777
|
+
const variantEnv = Object.fromEntries(Object.entries(values).map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
|
|
778
|
+
return `dry-run at 200 columns — nothing written\n${runtimeRenderer({
|
|
779
|
+
bin: join(runtime.dir, "statusline.sh"),
|
|
780
|
+
env: {
|
|
781
|
+
COLUMNS: "200",
|
|
782
|
+
HOME: home,
|
|
783
|
+
NOW: now,
|
|
784
|
+
STATUSLINE_LAB_LAYOUT: layout,
|
|
785
|
+
...variantEnv
|
|
786
|
+
},
|
|
787
|
+
stdin: sources.main
|
|
788
|
+
}).replace(/\n+$/, "")}\npanel ${firstPanelRow(runtimeRenderer({
|
|
789
|
+
bin: join(runtime.dir, "subagent.sh"),
|
|
790
|
+
env: {
|
|
791
|
+
COLUMNS: "200",
|
|
792
|
+
HOME: home,
|
|
793
|
+
NOW: now
|
|
794
|
+
},
|
|
795
|
+
stdin: sources.tick
|
|
796
|
+
}).replace(/\n+$/, ""))}\n`;
|
|
797
|
+
} finally {
|
|
798
|
+
sources.cleanup();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function printedConfig(runtime, home) {
|
|
802
|
+
const existing = readKeyConfig(home);
|
|
803
|
+
return [...[`layout='${existing.layout ?? runtime.defaultLayout}'`, ...runtime.items.map((item) => `${item.item}=${item.item in existing.values ? existing.values[item.item] : item.default}`)], "nothing written — pass variants (`statusline configure --model block`) or run bare on a TTY for the wizard"].join("\n");
|
|
804
|
+
}
|
|
805
|
+
function existingValue(values, item) {
|
|
806
|
+
return item in values ? values[item] : void 0;
|
|
807
|
+
}
|
|
808
|
+
function configure(options) {
|
|
809
|
+
const runtime = resolveRuntime({ home: options.home });
|
|
810
|
+
const existing = readKeyConfig(options.home);
|
|
811
|
+
const variants = options.variants ?? {};
|
|
812
|
+
const explicit = options.layout !== void 0 || Object.keys(variants).length > 0;
|
|
813
|
+
if (!explicit && options.fallback === void 0 && !options.dryRun) return {
|
|
814
|
+
mode: "printed",
|
|
815
|
+
text: printedConfig(runtime, options.home)
|
|
816
|
+
};
|
|
817
|
+
const layout = options.layout === void 0 || options.layout === "" ? existing.layout ?? runtime.defaultLayout : options.layout;
|
|
818
|
+
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
819
|
+
const items = layoutItems(layout, runtime.items.map((item) => item.item));
|
|
820
|
+
for (const item of Object.keys(variants)) {
|
|
821
|
+
if (!byItem.has(item)) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
|
|
822
|
+
if (!items.includes(item)) throw new Error(`variant for '${item}' is not in the layout (layout items: ${items.join(" ")})`);
|
|
823
|
+
}
|
|
824
|
+
const values = {};
|
|
825
|
+
for (const item of items) {
|
|
826
|
+
let base;
|
|
827
|
+
if (options.fallback === "default") base = byItem.get(item)?.default;
|
|
828
|
+
else if (options.fallback === "existing") base = existingValue(existing.values, item);
|
|
829
|
+
else if (!explicit) base = existingValue(existing.values, item) ?? byItem.get(item)?.default;
|
|
830
|
+
if (base !== void 0) values[item] = base;
|
|
831
|
+
}
|
|
832
|
+
const missing = items.filter((item) => !(item in values) && !(item in variants));
|
|
833
|
+
if (missing.length > 0) throw new Error(`no variant for layout item${missing.length > 1 ? "s" : ""} ${missing.join(" ")} — pass --<item> <alt> for each or use --fallback=default|existing`);
|
|
834
|
+
for (const [item, alt] of Object.entries(variants)) values[item] = alt;
|
|
835
|
+
for (const item of items) {
|
|
836
|
+
const alt = values[item];
|
|
837
|
+
const offered = byItem.get(item)?.alternatives ?? [];
|
|
838
|
+
if (!offered.includes(alt)) throw new Error(`unknown variant '${alt}' for item '${item}' — valid: ${offered.join(" | ")}`);
|
|
839
|
+
}
|
|
840
|
+
if (options.dryRun) {
|
|
841
|
+
const now = String(Math.floor(Date.now() / 1e3));
|
|
842
|
+
return {
|
|
843
|
+
mode: "dry-run",
|
|
844
|
+
text: renderPreview(runtime, options.home, values, layout, now)
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
const assignments = items.map((item) => `STATUSLINE_LAB_${item.toUpperCase()}=${values[item]}`);
|
|
848
|
+
const plan = planSettings(options.home, mainKeyValue(layout, assignments), options.force ?? false);
|
|
849
|
+
if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
|
|
850
|
+
commitSettings(plan);
|
|
851
|
+
return { mode: "written" };
|
|
852
|
+
}
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region src/restore.ts
|
|
855
|
+
function readBackup(home) {
|
|
856
|
+
const file = backupPath(home);
|
|
857
|
+
const raw = readOrNull(file);
|
|
858
|
+
if (raw === null) return null;
|
|
859
|
+
let parsed;
|
|
860
|
+
try {
|
|
861
|
+
parsed = JSON.parse(raw);
|
|
862
|
+
} catch (e) {
|
|
863
|
+
throw new Error(`${file} is not valid JSON`, { cause: e });
|
|
864
|
+
}
|
|
865
|
+
if (!isObject(parsed) || typeof parsed.createdFile !== "boolean" || !isObject(parsed.keys)) throw new Error(`${file} is not a lab backup`);
|
|
866
|
+
const keys = {};
|
|
867
|
+
for (const key of SETTINGS_KEYS) {
|
|
868
|
+
const saved = parsed.keys[key];
|
|
869
|
+
if (typeof saved === "string") keys[key] = saved;
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
createdFile: parsed.createdFile,
|
|
873
|
+
keys
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function rmdirIfEmpty(dir) {
|
|
877
|
+
try {
|
|
878
|
+
rmdirSync(dir);
|
|
879
|
+
} catch (e) {
|
|
880
|
+
const code = e.code;
|
|
881
|
+
if (code !== "ENOTEMPTY" && code !== "ENOENT" && code !== "ENOTDIR") throw e;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function clearCaptures(dir) {
|
|
885
|
+
if (!existsSync(dir)) return;
|
|
886
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isFile()) rmSync(join(dir, entry.name), { force: true });
|
|
887
|
+
rmdirIfEmpty(dir);
|
|
888
|
+
}
|
|
889
|
+
function planActions(raw, backup, force) {
|
|
890
|
+
const actions = [];
|
|
891
|
+
for (const key of SETTINGS_KEYS) {
|
|
892
|
+
const span = rootMemberValueSpan(raw, key);
|
|
893
|
+
if (span === null) continue;
|
|
894
|
+
const memberRaw = raw.slice(span[0], span[1]);
|
|
895
|
+
const saved = backup === null ? void 0 : backup.keys[key];
|
|
896
|
+
if (isOurMember(key, JSON.parse(memberRaw))) actions.push(saved === void 0 ? {
|
|
897
|
+
kind: "remove",
|
|
898
|
+
key
|
|
899
|
+
} : {
|
|
900
|
+
kind: "splice",
|
|
901
|
+
key,
|
|
902
|
+
text: saved
|
|
903
|
+
});
|
|
904
|
+
else if (saved !== void 0 && memberRaw !== saved) {
|
|
905
|
+
if (!force) throw new Error(`settings.json ${key} changed since the lab took it over — rerun with --force to put the saved value back`);
|
|
906
|
+
actions.push({
|
|
907
|
+
kind: "splice",
|
|
908
|
+
key,
|
|
909
|
+
text: saved
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return actions;
|
|
914
|
+
}
|
|
915
|
+
function restore(options) {
|
|
916
|
+
const settings = join(options.home, ".claude", "settings.json");
|
|
917
|
+
const backup = readBackup(options.home);
|
|
918
|
+
const raw = readOrNull(settings);
|
|
919
|
+
const actions = raw === null ? [] : planActions(raw, backup, options.force ?? false);
|
|
920
|
+
const removals = [];
|
|
921
|
+
const splices = [];
|
|
922
|
+
for (const action of actions) if (action.kind === "remove") removals.push(action.key);
|
|
923
|
+
else splices.push(action);
|
|
924
|
+
let next = raw;
|
|
925
|
+
let deleteFile = false;
|
|
926
|
+
if (actions.length > 0 && raw !== null) {
|
|
927
|
+
let text = removals.length > 0 ? removeMembers(raw, removals) : raw;
|
|
928
|
+
for (const splice of splices) {
|
|
929
|
+
const span = rootMemberValueSpan(text, splice.key);
|
|
930
|
+
if (span === null) throw new Error(`cannot find the "${splice.key}" member to restore in settings.json`);
|
|
931
|
+
text = `${text.slice(0, span[0])}${splice.text}${text.slice(span[1])}`;
|
|
932
|
+
}
|
|
933
|
+
const membersLeft = Object.keys(JSON.parse(text)).length;
|
|
934
|
+
if (backup !== null && backup.createdFile && membersLeft === 0) {
|
|
935
|
+
deleteFile = true;
|
|
936
|
+
next = null;
|
|
937
|
+
} else next = text;
|
|
938
|
+
}
|
|
939
|
+
const dataDir = join(options.home, DATA_REL);
|
|
940
|
+
const capturesDir = join(dataDir, "captures");
|
|
941
|
+
const backupFile = backupPath(options.home);
|
|
942
|
+
if (actions.length === 0 && !existsSync(backupFile) && !existsSync(capturesDir)) return {
|
|
943
|
+
mode: "nothing",
|
|
944
|
+
text: "nothing to restore — no lab keys in settings.json, no backup, no captures"
|
|
945
|
+
};
|
|
946
|
+
if (options.dryRun) {
|
|
947
|
+
const lines = ["restore plan — nothing written"];
|
|
948
|
+
for (const action of actions) lines.push(action.kind === "remove" ? ` remove ${action.key} (absent before the lab)` : ` splice the saved ${action.key} value back`);
|
|
949
|
+
if (deleteFile) lines.push(" delete settings.json (the lab created it and it is now empty)");
|
|
950
|
+
if (existsSync(capturesDir)) lines.push(" delete captures/");
|
|
951
|
+
if (existsSync(backupFile)) lines.push(" delete backup.json");
|
|
952
|
+
lines.push(" remove the data dir if empty");
|
|
953
|
+
return {
|
|
954
|
+
mode: "dry-run",
|
|
955
|
+
text: lines.join("\n")
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
if (deleteFile) rmSync(settings, { force: true });
|
|
959
|
+
else if (next !== null && actions.length > 0) writeFileSync(settings, next);
|
|
960
|
+
clearCaptures(capturesDir);
|
|
961
|
+
rmSync(backupFile, { force: true });
|
|
962
|
+
rmdirIfEmpty(dataDir);
|
|
963
|
+
let line;
|
|
964
|
+
if (deleteFile) line = "restored — settings.json is gone, exactly as before the lab";
|
|
965
|
+
else if (actions.length > 0) line = "keys restored — settings.json holds its pre-lab values again";
|
|
966
|
+
else line = "lab data cleaned — no lab keys in settings.json";
|
|
967
|
+
return {
|
|
968
|
+
mode: "restored",
|
|
969
|
+
text: line
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/status.ts
|
|
974
|
+
var INSTALL_FIX = "claude plugin install statusline@agentic";
|
|
975
|
+
function keyState(key, value) {
|
|
976
|
+
if (value === void 0) return { kind: "absent" };
|
|
977
|
+
if (isOurMember(key, value)) return { kind: "ours" };
|
|
978
|
+
return {
|
|
979
|
+
kind: "foreign",
|
|
980
|
+
command: memberCommand(value)
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function keyRow(key, state, detail = "") {
|
|
984
|
+
if (state.kind === "ours") return `${key}: ours${detail === "" ? "" : ` — ${detail}`}`;
|
|
985
|
+
if (state.kind === "absent") return `${key}: absent — fix: rerun configure --fallback=default`;
|
|
986
|
+
return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --fallback=default`;
|
|
987
|
+
}
|
|
988
|
+
function layoutItemsOf(layout) {
|
|
989
|
+
const items = [];
|
|
990
|
+
for (const cluster of parseClusters(layout)) for (const item of cluster) if (!items.includes(item)) items.push(item);
|
|
991
|
+
return items;
|
|
992
|
+
}
|
|
993
|
+
function configDetail(config) {
|
|
994
|
+
if (config.layout === null) return "";
|
|
995
|
+
const assignments = layoutItemsOf(config.layout).filter((item) => Object.hasOwn(config.values, item)).map((item) => `${item}=${config.values[item]}`);
|
|
996
|
+
return [`layout='${config.layout}'`, ...assignments].join(" ");
|
|
997
|
+
}
|
|
998
|
+
function findingText(finding) {
|
|
999
|
+
return finding.kind === "unknown-item" ? `unknown item '${finding.item}'` : `unknown variant '${finding.alt}' for '${finding.item}'`;
|
|
1000
|
+
}
|
|
1001
|
+
function driftFindings(config, runtime) {
|
|
1002
|
+
if (config.layout === null) return [];
|
|
1003
|
+
const byItem = new Map(runtime.items.map((entry) => [entry.item, entry]));
|
|
1004
|
+
const findings = [];
|
|
1005
|
+
for (const item of layoutItemsOf(config.layout)) {
|
|
1006
|
+
const entry = byItem.get(item);
|
|
1007
|
+
if (entry === void 0) {
|
|
1008
|
+
findings.push({
|
|
1009
|
+
item,
|
|
1010
|
+
kind: "unknown-item"
|
|
1011
|
+
});
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const alt = Object.hasOwn(config.values, item) ? config.values[item] : null;
|
|
1015
|
+
if (alt !== null && !entry.alternatives.includes(alt)) findings.push({
|
|
1016
|
+
alt,
|
|
1017
|
+
item,
|
|
1018
|
+
kind: "unknown-variant"
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return findings;
|
|
1022
|
+
}
|
|
1023
|
+
function runtimeRow(runtime) {
|
|
1024
|
+
if (runtime === null) return `runtime: missing — fix: ${INSTALL_FIX}`;
|
|
1025
|
+
return `runtime: ${basename(dirname(runtime.dir))} — ${runtime.items.length} items`;
|
|
1026
|
+
}
|
|
1027
|
+
function configRow(findings, runtime) {
|
|
1028
|
+
if (findings.length === 0) return "config: no drift";
|
|
1029
|
+
const fix = findings.some((finding) => finding.kind === "unknown-item") ? `rerun configure --layout '${runtime.defaultLayout}' --fallback=default` : "rerun configure --fallback=default";
|
|
1030
|
+
return `config: drift — ${findings.map(findingText).join(", ")} — fix: ${fix}`;
|
|
1031
|
+
}
|
|
1032
|
+
function backupRow(home) {
|
|
1033
|
+
let backup;
|
|
1034
|
+
try {
|
|
1035
|
+
backup = readBackup(home);
|
|
1036
|
+
} catch {
|
|
1037
|
+
return `backup: unreadable — fix: delete ~/${DATA_REL}/backup.json`;
|
|
1038
|
+
}
|
|
1039
|
+
if (backup === null) return "backup: absent";
|
|
1040
|
+
const saved = SETTINGS_KEYS.filter((key) => backup.keys[key] !== void 0);
|
|
1041
|
+
return `backup: present — ${[...backup.createdFile ? ["created settings.json"] : [], saved.length > 0 ? `saved ${saved.join(", ")}` : "saved nothing"].join(", ")}`;
|
|
1042
|
+
}
|
|
1043
|
+
function ageText(ms) {
|
|
1044
|
+
const minutes = Math.floor(ms / 6e4);
|
|
1045
|
+
if (minutes < 1) return `${Math.floor(ms / 1e3)}s`;
|
|
1046
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1047
|
+
const hours = Math.floor(minutes / 60);
|
|
1048
|
+
if (hours < 24) return `${hours}h`;
|
|
1049
|
+
return `${Math.floor(hours / 24)}d`;
|
|
1050
|
+
}
|
|
1051
|
+
function captureText(home, surface) {
|
|
1052
|
+
try {
|
|
1053
|
+
return `${ageText(Date.now() - statSync(capturePath(home, surface)).mtimeMs)} ago`;
|
|
1054
|
+
} catch {
|
|
1055
|
+
return "absent";
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function capturesRow(home) {
|
|
1059
|
+
return `captures: ${["main", "tick"].map((surface) => `${surface} ${captureText(home, surface)}`).join(", ")}`;
|
|
1060
|
+
}
|
|
1061
|
+
function settingsMembers(home) {
|
|
1062
|
+
const file = join(home, ".claude", "settings.json");
|
|
1063
|
+
const raw = readOrNull(file);
|
|
1064
|
+
return raw === null ? {} : parseSettings(file, raw);
|
|
1065
|
+
}
|
|
1066
|
+
function resolveOrNull(home) {
|
|
1067
|
+
try {
|
|
1068
|
+
return resolveRuntime({ home });
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function status(options) {
|
|
1074
|
+
const runtime = resolveOrNull(options.home);
|
|
1075
|
+
const members = settingsMembers(options.home);
|
|
1076
|
+
const main = keyState("statusLine", members.statusLine);
|
|
1077
|
+
const subagent = keyState("subagentStatusLine", members.subagentStatusLine);
|
|
1078
|
+
const config = readKeyConfig(options.home);
|
|
1079
|
+
const findings = runtime === null || main.kind !== "ours" ? [] : driftFindings(config, runtime);
|
|
1080
|
+
const rows = [
|
|
1081
|
+
runtimeRow(runtime),
|
|
1082
|
+
keyRow("statusLine", main, configDetail(config)),
|
|
1083
|
+
keyRow("subagentStatusLine", subagent),
|
|
1084
|
+
...runtime !== null && main.kind === "ours" ? [configRow(findings, runtime)] : [],
|
|
1085
|
+
backupRow(options.home),
|
|
1086
|
+
capturesRow(options.home)
|
|
1087
|
+
];
|
|
1088
|
+
const healthy = runtime !== null && main.kind === "ours" && subagent.kind === "ours" && findings.length === 0;
|
|
1089
|
+
return {
|
|
1090
|
+
healthy,
|
|
1091
|
+
rows: [...rows, healthy ? "healthy" : "unhealthy"]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/wizard-tui.ts
|
|
1096
|
+
function nextKey(buffer) {
|
|
1097
|
+
if (buffer === "") return;
|
|
1098
|
+
const head = buffer[0];
|
|
1099
|
+
if (head !== "\x1B") return [head, buffer.slice(1)];
|
|
1100
|
+
if (buffer.length < 3) return;
|
|
1101
|
+
if (buffer[1] === "[" && "ABCD".includes(buffer[2])) return [buffer.slice(0, 3), buffer.slice(3)];
|
|
1102
|
+
return ["", buffer.slice(1)];
|
|
1103
|
+
}
|
|
1104
|
+
function terminalDeps(input = process.stdin, output = process.stdout) {
|
|
1105
|
+
return {
|
|
1106
|
+
async *readKeys() {
|
|
1107
|
+
const chunks = [];
|
|
1108
|
+
const waiters = [];
|
|
1109
|
+
const state = { closed: false };
|
|
1110
|
+
function notify() {
|
|
1111
|
+
for (const wake of waiters.splice(0)) wake();
|
|
1112
|
+
}
|
|
1113
|
+
function onData(chunk) {
|
|
1114
|
+
chunks.push(chunk.toString("utf8"));
|
|
1115
|
+
notify();
|
|
1116
|
+
}
|
|
1117
|
+
function onEnd() {
|
|
1118
|
+
state.closed = true;
|
|
1119
|
+
notify();
|
|
1120
|
+
}
|
|
1121
|
+
input.on("data", onData);
|
|
1122
|
+
input.on("end", onEnd);
|
|
1123
|
+
input.on("close", onEnd);
|
|
1124
|
+
if (input.isTTY) input.setRawMode(true);
|
|
1125
|
+
input.resume();
|
|
1126
|
+
try {
|
|
1127
|
+
let buffer = "";
|
|
1128
|
+
for (;;) {
|
|
1129
|
+
for (let chunk = chunks.shift(); chunk !== void 0; chunk = chunks.shift()) buffer += chunk;
|
|
1130
|
+
while (buffer !== "") {
|
|
1131
|
+
const next = nextKey(buffer);
|
|
1132
|
+
if (next === void 0) break;
|
|
1133
|
+
buffer = next[1];
|
|
1134
|
+
if (next[0] !== "") yield next[0];
|
|
1135
|
+
}
|
|
1136
|
+
if (state.closed && chunks.length === 0) return;
|
|
1137
|
+
await new Promise((resolve) => {
|
|
1138
|
+
waiters.push(resolve);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
} finally {
|
|
1142
|
+
if (input.isTTY) input.setRawMode(false);
|
|
1143
|
+
input.pause();
|
|
1144
|
+
input.off("data", onData);
|
|
1145
|
+
input.off("end", onEnd);
|
|
1146
|
+
input.off("close", onEnd);
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
render(frame) {
|
|
1150
|
+
output.write(`\x1b[2J\x1b[H${frame}\n`);
|
|
1151
|
+
},
|
|
1152
|
+
preview(spec) {
|
|
1153
|
+
return runtimeRenderer(spec);
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/wizard.ts
|
|
1159
|
+
var WIDTHS = [
|
|
1160
|
+
80,
|
|
1161
|
+
120,
|
|
1162
|
+
200
|
|
1163
|
+
];
|
|
1164
|
+
var KEYMAP = "j/k move · h/l variant · s none · w width · enter save · q cancel";
|
|
1165
|
+
async function createWizard(options, deps) {
|
|
1166
|
+
const runtime = resolveRuntime({ home: options.home });
|
|
1167
|
+
const existing = readKeyConfig(options.home);
|
|
1168
|
+
const layout = existing.layout ?? runtime.defaultLayout;
|
|
1169
|
+
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
1170
|
+
const names = layoutItems(layout, runtime.items.map((item) => item.item));
|
|
1171
|
+
const offered = [];
|
|
1172
|
+
const draft = /* @__PURE__ */ new Map();
|
|
1173
|
+
for (const name of names) {
|
|
1174
|
+
const entry = byItem.get(name);
|
|
1175
|
+
if (entry === void 0) continue;
|
|
1176
|
+
offered.push({
|
|
1177
|
+
alternatives: entry.alternatives,
|
|
1178
|
+
item: name
|
|
1179
|
+
});
|
|
1180
|
+
draft.set(name, name in existing.values && entry.alternatives.includes(existing.values[name]) ? existing.values[name] : entry.default);
|
|
1181
|
+
}
|
|
1182
|
+
const sources = previewSources(options.home, Number(options.now));
|
|
1183
|
+
const mainBin = join(runtime.dir, "statusline.sh");
|
|
1184
|
+
const panelBin = join(runtime.dir, "subagent.sh");
|
|
1185
|
+
let focus = 0;
|
|
1186
|
+
let widthAt = 0;
|
|
1187
|
+
function variantEnv() {
|
|
1188
|
+
return Object.fromEntries([...draft].map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
|
|
1189
|
+
}
|
|
1190
|
+
function env() {
|
|
1191
|
+
return {
|
|
1192
|
+
COLUMNS: String(WIDTHS[widthAt]),
|
|
1193
|
+
HOME: options.home,
|
|
1194
|
+
NOW: options.now,
|
|
1195
|
+
STATUSLINE_LAB_LAYOUT: layout,
|
|
1196
|
+
...variantEnv()
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function cycle(delta) {
|
|
1200
|
+
const focused = offered.at(focus);
|
|
1201
|
+
if (focused === void 0 || focused.alternatives.length === 0) return;
|
|
1202
|
+
const alts = focused.alternatives;
|
|
1203
|
+
const at = alts.indexOf(draft.get(focused.item) ?? "");
|
|
1204
|
+
draft.set(focused.item, alts[(at + delta + alts.length) % alts.length]);
|
|
1205
|
+
}
|
|
1206
|
+
function step(key) {
|
|
1207
|
+
if (key === "\x1B[B" || key === "j") focus = (focus + 1) % offered.length;
|
|
1208
|
+
else if (key === "\x1B[A" || key === "k") focus = (focus + offered.length - 1) % offered.length;
|
|
1209
|
+
else if (key === "\x1B[C" || key === "l") cycle(1);
|
|
1210
|
+
else if (key === "\x1B[D" || key === "h") cycle(-1);
|
|
1211
|
+
else if (key === "s") {
|
|
1212
|
+
const focused = offered.at(focus);
|
|
1213
|
+
if (focused?.alternatives.includes("none")) draft.set(focused.item, "none");
|
|
1214
|
+
} else if (key === "w") widthAt = (widthAt + 1) % WIDTHS.length;
|
|
1215
|
+
}
|
|
1216
|
+
function draw() {
|
|
1217
|
+
const focused = offered.at(focus);
|
|
1218
|
+
if (focused === void 0) return;
|
|
1219
|
+
const samples = focused.alternatives.map((alt) => deps.preview({
|
|
1220
|
+
bin: mainBin,
|
|
1221
|
+
env: {
|
|
1222
|
+
...env(),
|
|
1223
|
+
STATUSLINE_LAB_LAYOUT: `{${focused.item}}`,
|
|
1224
|
+
[`STATUSLINE_LAB_${focused.item.toUpperCase()}`]: alt
|
|
1225
|
+
},
|
|
1226
|
+
stdin: sources.main
|
|
1227
|
+
}).replace(/\n+$/, ""));
|
|
1228
|
+
const line = deps.preview({
|
|
1229
|
+
bin: mainBin,
|
|
1230
|
+
env: env(),
|
|
1231
|
+
stdin: sources.main
|
|
1232
|
+
}).replace(/\n+$/, "");
|
|
1233
|
+
const tick = JSON.parse(sources.tick);
|
|
1234
|
+
tick.columns = WIDTHS[widthAt];
|
|
1235
|
+
const panel = deps.preview({
|
|
1236
|
+
bin: panelBin,
|
|
1237
|
+
env: {
|
|
1238
|
+
COLUMNS: String(WIDTHS[widthAt]),
|
|
1239
|
+
HOME: options.home,
|
|
1240
|
+
NOW: options.now
|
|
1241
|
+
},
|
|
1242
|
+
stdin: `${JSON.stringify(tick, null, 2)}\n`
|
|
1243
|
+
}).replace(/\n+$/, "");
|
|
1244
|
+
const rows = offered.map((entry, at) => `${at === focus ? ">" : " "} ${entry.item.padEnd(9)} ${draft.get(entry.item)}`);
|
|
1245
|
+
deps.render([
|
|
1246
|
+
`statusline configure · ${WIDTHS[widthAt]} columns (w cycles)`,
|
|
1247
|
+
"",
|
|
1248
|
+
` ${line}`,
|
|
1249
|
+
` panel ${firstPanelRow(panel)}`,
|
|
1250
|
+
"",
|
|
1251
|
+
`${focused.item}: ${focused.alternatives.join(" | ")}`,
|
|
1252
|
+
...focused.alternatives.map((alt, at) => ` ${alt === draft.get(focused.item) ? "*" : " "} ${samples[at]}`),
|
|
1253
|
+
"",
|
|
1254
|
+
...rows,
|
|
1255
|
+
"",
|
|
1256
|
+
KEYMAP
|
|
1257
|
+
].join("\n"));
|
|
1258
|
+
}
|
|
1259
|
+
function finish() {
|
|
1260
|
+
try {
|
|
1261
|
+
configure({
|
|
1262
|
+
home: options.home,
|
|
1263
|
+
layout,
|
|
1264
|
+
variants: Object.fromEntries(draft)
|
|
1265
|
+
});
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
deps.render(`save failed: ${e.message}\nfix it and rerun: npx -y @v1nvn/statusline configure\n`);
|
|
1268
|
+
return "save-failed";
|
|
1269
|
+
}
|
|
1270
|
+
deps.render("saved — live on the next paint\n");
|
|
1271
|
+
return "saved";
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
const keys = deps.readKeys();
|
|
1275
|
+
draw();
|
|
1276
|
+
for await (const key of keys) {
|
|
1277
|
+
if (key === "\r") return finish();
|
|
1278
|
+
if (key === "q" || key === "") break;
|
|
1279
|
+
step(key);
|
|
1280
|
+
draw();
|
|
1281
|
+
}
|
|
1282
|
+
deps.render("cancelled — nothing written\n");
|
|
1283
|
+
return "cancelled";
|
|
1284
|
+
} finally {
|
|
1285
|
+
sources.cleanup();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
//#endregion
|
|
1289
|
+
//#region src/index.ts
|
|
1290
|
+
var parsed = parseArgs(process.argv.slice(2)) ?? printUsageAndExit(buildProgram());
|
|
1291
|
+
function homeOf(flag) {
|
|
1292
|
+
const home = flag ?? process.env.HOME;
|
|
1293
|
+
if (home === void 0 || home === "") throw new Error("no home to operate on: set $HOME or pass --home");
|
|
1294
|
+
return home;
|
|
1295
|
+
}
|
|
1296
|
+
function run(job) {
|
|
1297
|
+
try {
|
|
1298
|
+
job();
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
console.error(e.message);
|
|
1301
|
+
process.exitCode = 1;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (parsed.help !== void 0) console.log(subcommandHelp(parsed.help));
|
|
1305
|
+
else if (parsed.version) console.log(VERSION);
|
|
1306
|
+
else if (parsed.command === "catalog") run(() => {
|
|
1307
|
+
console.log(catalog({
|
|
1308
|
+
home: homeOf(parsed.home),
|
|
1309
|
+
items: parsed.items
|
|
1310
|
+
}));
|
|
1311
|
+
});
|
|
1312
|
+
else if (parsed.command === "configure") {
|
|
1313
|
+
const home = homeOf(parsed.home);
|
|
1314
|
+
if (parsed.layout === void 0 && parsed.variants === void 0 && parsed.fallback === void 0 && !parsed.dryRun && !parsed.force && process.stdin.isTTY) {
|
|
1315
|
+
if (await createWizard({
|
|
1316
|
+
home,
|
|
1317
|
+
now: String(Math.floor(Date.now() / 1e3))
|
|
1318
|
+
}, terminalDeps()) === "save-failed") process.exitCode = 1;
|
|
1319
|
+
} else run(() => {
|
|
1320
|
+
const result = configure({
|
|
1321
|
+
dryRun: parsed.dryRun,
|
|
1322
|
+
fallback: parsed.fallback,
|
|
1323
|
+
force: parsed.force,
|
|
1324
|
+
home,
|
|
1325
|
+
layout: parsed.layout,
|
|
1326
|
+
variants: parsed.variants
|
|
1327
|
+
});
|
|
1328
|
+
console.log(result.mode === "written" ? "configured — live on the next paint" : result.text);
|
|
1329
|
+
});
|
|
1330
|
+
} else if (parsed.command === "restore") run(() => {
|
|
1331
|
+
const result = restore({
|
|
1332
|
+
dryRun: parsed.dryRun,
|
|
1333
|
+
force: parsed.force,
|
|
1334
|
+
home: homeOf(parsed.home)
|
|
1335
|
+
});
|
|
1336
|
+
console.log(result.text);
|
|
1337
|
+
});
|
|
1338
|
+
else if (parsed.command === "status") run(() => {
|
|
1339
|
+
const result = status({ home: homeOf(parsed.home) });
|
|
1340
|
+
console.log(result.rows.join("\n"));
|
|
1341
|
+
process.exitCode = result.healthy ? 0 : 1;
|
|
1342
|
+
});
|
|
1343
|
+
//#endregion
|
|
1344
|
+
export {};
|
|
1345
|
+
|
|
1346
|
+
//# sourceMappingURL=index.js.map
|