acbridge 0.0.1 → 1.0.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/LICENSE +40 -0
- package/README.md +69 -3
- package/dist/acbridge.mjs +317 -0
- package/hub/dist/hub.cjs +189 -0
- package/package.json +32 -4
- package/plugin/.claude-plugin/marketplace.json +17 -0
- package/plugin/README.md +223 -0
- package/plugin/plugins/alexa/.claude-plugin/plugin.json +13 -0
- package/plugin/plugins/alexa/.mcp.json +8 -0
- package/plugin/plugins/alexa/dist/channel.mjs +165 -0
- package/plugin/plugins/alexa/hooks/hooks.json +166 -0
- package/plugin/plugins/alexa/scripts/codex-signal.mjs +406 -0
- package/plugin/plugins/alexa/scripts/gate.mjs +99 -0
- package/plugin/plugins/alexa/scripts/scope.mjs +270 -0
- package/plugin/plugins/alexa/scripts/signal.mjs +503 -0
- package/plugin/plugins/alexa/scripts/statusline-guard.mjs +75 -0
- package/plugin/plugins/alexa/scripts/statusline.mjs +322 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Statusline TEE for the Alexa voice-bridge plugin (states expansion). Installed as Claude Code's
|
|
3
|
+
// `statusLine` command by `acbridge` (cli/src/statusline-install.ts — reversible; off-switch
|
|
4
|
+
// BRIDGE_STATUSLINE=0 or `"autoStatusline": false` in ~/.claude-bridge/config.json).
|
|
5
|
+
//
|
|
6
|
+
// Claude runs this on every message with a JSON snapshot on stdin. We tee it two ways, in parallel:
|
|
7
|
+
// 1. POST the EXACT context % (context_window.used_percentage) and the rate-limit percentages
|
|
8
|
+
// (rate_limits.five_hour/seven_day — Pro/Max only) to the hub's /local/session/context. This is
|
|
9
|
+
// the data feed for the usageHigh threshold state (hooks can't see rate limits) and the freshest
|
|
10
|
+
// source for contextLow/contextCritical (message-cadence vs the hooks' tool-cadence).
|
|
11
|
+
// 2. RENDER: chain the user's ORIGINAL statusline command (stored at install as config.json
|
|
12
|
+
// `statusline.prev`) with our stdin passed through verbatim — or print a default one-liner.
|
|
13
|
+
//
|
|
14
|
+
// A statusline must never hang Claude's repaint: the POST is fire-and-forget (short timeout, runs
|
|
15
|
+
// while the chain renders), a hard deadline bounds the whole run, and EVERY path prints something and
|
|
16
|
+
// exits 0. Zero-dep and self-contained like signal.mjs (a plugin script can't import the workspace).
|
|
17
|
+
//
|
|
18
|
+
// It must also never REPRODUCE. The deadline bounds each process's lifetime, not the chain's depth, so a
|
|
19
|
+
// `prev` pointing back at this script spawns an unbounded treadmill (2026-07-30: 372 node.exe / 18.65 GB).
|
|
20
|
+
// statusline-guard.mjs holds the refusal predicates; every spawn here goes through them.
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { request } from "node:http";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { readActiveUserSub, resolveBridgeHome, resolveHubTokenFile, writePlainSessionHandshakeIfAbsent } from "./scope.mjs";
|
|
29
|
+
import { chainChildEnv, selectStatuslinePrev, shouldChain } from "./statusline-guard.mjs";
|
|
30
|
+
|
|
31
|
+
const DEFAULT_HUB_PORT = 8790; // mirrors @bridge/node-core HUB_PORT — the plugin can't import the workspace
|
|
32
|
+
// The home of the hub this box is actually running (scope.mjs Part C) — the same resolution signal.mjs
|
|
33
|
+
// does, so the statusline's context ferry and the state signals can never target different hub homes.
|
|
34
|
+
const BRIDGE_HOME = resolveBridgeHome();
|
|
35
|
+
const SECRET_DIR = BRIDGE_HOME.dir;
|
|
36
|
+
// `statusline.prev` is LOCAL-INSTALLER state: node-core writes it to THIS machine's own ~/.claude-bridge
|
|
37
|
+
// (node-core's SECRET_DIR is never peer-resolved). BRIDGE_HOME, by contrast, IS the peer home (WSL↔Windows,
|
|
38
|
+
// scope.mjs Part C) whenever the live hub sits on the other side of that boundary — so reading `prev` from
|
|
39
|
+
// it would chain a command written for the OTHER OS's shell: a `node "C:\…"` string handed to /bin/sh. That
|
|
40
|
+
// fails, silently replacing the user's real statusline with our default line. Hub coordinates (port, token,
|
|
41
|
+
// handshakes) legitimately come from BRIDGE_HOME; `prev` alone is local. Env override mirrors
|
|
42
|
+
// resolveBridgeHomeWith's own explicit-dir branch, so the two agree in tests and under BRIDGE_SECRET_DIR.
|
|
43
|
+
const LOCAL_DIR = process.env.BRIDGE_SECRET_DIR || path.join(os.homedir(), ".claude-bridge");
|
|
44
|
+
/** This file's real path — the guard's strongest self-recognition signal. */
|
|
45
|
+
const SELF_PATH = (() => {
|
|
46
|
+
try {
|
|
47
|
+
return fileURLToPath(import.meta.url);
|
|
48
|
+
} catch {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
const POST_TIMEOUT_MS = 400; // a statusline repaint budget is tight — never wait on a slow hub
|
|
53
|
+
const DEADLINE_MS = 1500; // hard ceiling for the whole run (chain included)
|
|
54
|
+
|
|
55
|
+
let rendered = false;
|
|
56
|
+
function render(line) {
|
|
57
|
+
if (rendered) return;
|
|
58
|
+
rendered = true;
|
|
59
|
+
process.stdout.write(line.endsWith("\n") ? line : line + "\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolves when the tee POST settles (response drained / error / timeout); null when no POST was made.
|
|
63
|
+
* finish() waits on it (bounded) — a hard process.exit while the request is in flight would cut it. */
|
|
64
|
+
let postPending = null;
|
|
65
|
+
|
|
66
|
+
/** The chained child, while one is running — so finish() can kill it instead of orphaning it. */
|
|
67
|
+
let activeChild = null;
|
|
68
|
+
|
|
69
|
+
/** Exit 0 no matter what, printing SOMETHING first — a blank statusline beats a hung one. The LINE is
|
|
70
|
+
* already on stdout before we wait for the tee, so the repaint never blocks on the hub. */
|
|
71
|
+
function finish(line) {
|
|
72
|
+
if (!rendered) render(line ?? "⏺ Claude");
|
|
73
|
+
// Kill any chained child before exiting. process.exit() alone ORPHANS it — and an orphan that keeps
|
|
74
|
+
// spawning is precisely how the 2026-07-30 treadmill outlived every parent's DEADLINE_MS.
|
|
75
|
+
if (activeChild) {
|
|
76
|
+
const c = activeChild;
|
|
77
|
+
activeChild = null;
|
|
78
|
+
try {
|
|
79
|
+
c.kill();
|
|
80
|
+
} catch {
|
|
81
|
+
/* already gone */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!postPending) process.exit(0);
|
|
85
|
+
const t = setTimeout(() => process.exit(0), POST_TIMEOUT_MS + 100);
|
|
86
|
+
t.unref?.();
|
|
87
|
+
postPending.then(() => process.exit(0), () => process.exit(0));
|
|
88
|
+
}
|
|
89
|
+
setTimeout(() => finish(), DEADLINE_MS).unref?.();
|
|
90
|
+
process.on("uncaughtException", () => finish());
|
|
91
|
+
|
|
92
|
+
// ── stdin: the statusline JSON snapshot. Unparsable → still render a stub and exit 0. ────────────────
|
|
93
|
+
let raw = "";
|
|
94
|
+
let snap = {};
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(0, "utf8");
|
|
97
|
+
snap = JSON.parse(raw || "{}");
|
|
98
|
+
} catch {
|
|
99
|
+
snap = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Hub coordinates: the same three-tier resolve as signal.mjs (env → handshake file → self-derive). ──
|
|
103
|
+
const isPlaceholder = (v) => Boolean(v) && /^\$\{.*\}$/.test(v);
|
|
104
|
+
const envVar = (v) => (v && !isPlaceholder(v) ? v : undefined);
|
|
105
|
+
|
|
106
|
+
function readConfigIn(dir) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(path.join(dir, "config.json"), "utf8")) || {};
|
|
109
|
+
} catch {
|
|
110
|
+
return {}; // no config file → env + hostname defaults
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readCliConfig() {
|
|
115
|
+
const file = readConfigIn(SECRET_DIR);
|
|
116
|
+
// See LOCAL_DIR: `prev` comes from the LOCAL home, never a peer's. Same object in the common case
|
|
117
|
+
// (homes match), so this costs one extra read only on a genuinely cross-home box.
|
|
118
|
+
const localFile = LOCAL_DIR === SECRET_DIR ? file : readConfigIn(LOCAL_DIR);
|
|
119
|
+
return {
|
|
120
|
+
machine: envVar(process.env.BRIDGE_MACHINE) || file.machine || os.hostname().split(".")[0] || "node",
|
|
121
|
+
// The resolved home's beacon carries the port the hub actually bound — authoritative over the default
|
|
122
|
+
// when the hub lives in a peer home (WSL↔Windows) whose config.json this side doesn't own.
|
|
123
|
+
hubPort: Number(envVar(process.env.BRIDGE_HUB_PORT) || BRIDGE_HOME.beacon?.port || file.hubPort || DEFAULT_HUB_PORT),
|
|
124
|
+
autoProfiles: file.autoProfiles,
|
|
125
|
+
statuslinePrev: selectStatuslinePrev({ local: localFile, resolved: file }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveHandshake(cfg, sessionId, cwd) {
|
|
130
|
+
const envId = envVar(process.env.BRIDGE_SESSION_ID);
|
|
131
|
+
if (envId) {
|
|
132
|
+
return {
|
|
133
|
+
id: envId,
|
|
134
|
+
hubPort: Number(envVar(process.env.BRIDGE_HUB_PORT)) || DEFAULT_HUB_PORT,
|
|
135
|
+
// AS-T9f Part A: env wins; else scoped step (users/<seg>/hub-token) → machine-global.
|
|
136
|
+
hubTokenFile: resolveHubTokenFile(SECRET_DIR, envVar(process.env.BRIDGE_HUB_TOKEN_FILE)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (sessionId) {
|
|
140
|
+
try {
|
|
141
|
+
const h = JSON.parse(readFileSync(path.join(SECRET_DIR, "sessions", `${sessionId}.json`), "utf8"));
|
|
142
|
+
if (h && h.id) {
|
|
143
|
+
// AS-T9f Part A: the file's scoped hubTokenFile is authoritative; a legacy file without it falls
|
|
144
|
+
// to the scoped step → machine-global.
|
|
145
|
+
return { id: h.id, hubPort: Number(h.hubPort) || DEFAULT_HUB_PORT, hubTokenFile: resolveHubTokenFile(SECRET_DIR, h.hubTokenFile) };
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
/* no per-session handshake → try self-derive */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// AUTO-PROFILES self-derive (default on): a hub is set up on this machine ⇒ derive project@machine.
|
|
152
|
+
const env = process.env.BRIDGE_AUTO_PROFILES;
|
|
153
|
+
if (env === "0" || env === "false" || cfg.autoProfiles === false) return null;
|
|
154
|
+
// AS-T9f Part A: scoped-first hub-token; the dormancy gate reads whichever the chain resolves.
|
|
155
|
+
const hubTokenFile = resolveHubTokenFile(SECRET_DIR, undefined);
|
|
156
|
+
try {
|
|
157
|
+
if (!readFileSync(hubTokenFile, "utf8").trim()) return null;
|
|
158
|
+
} catch {
|
|
159
|
+
return null; // no hub token (scoped or machine-global) → bridge not set up here → render-only
|
|
160
|
+
}
|
|
161
|
+
const dir = typeof cwd === "string" && cwd ? cwd : process.cwd();
|
|
162
|
+
const id = `${path.basename(dir) || "session"}@${cfg.machine}`;
|
|
163
|
+
// AS-T9f Part B (owner-approved restore): stamp a plain session's handshake for the SIGNED-IN account so
|
|
164
|
+
// the hub adopts its context/usage ticks (same rule as signal.mjs — the statusline tee fires on every
|
|
165
|
+
// message, so whichever of the two hooks runs first writes; the other no-ops on existsSync). No account ⇒
|
|
166
|
+
// nothing written; an acbridge run / channel file is never overwritten.
|
|
167
|
+
writePlainSessionHandshakeIfAbsent({
|
|
168
|
+
secretDir: SECRET_DIR,
|
|
169
|
+
id,
|
|
170
|
+
hubPort: cfg.hubPort,
|
|
171
|
+
hubTokenFile,
|
|
172
|
+
cwd: dir,
|
|
173
|
+
claudeSessionId: typeof sessionId === "string" ? sessionId : undefined,
|
|
174
|
+
sub: readActiveUserSub(SECRET_DIR),
|
|
175
|
+
});
|
|
176
|
+
return { id, hubPort: cfg.hubPort, hubTokenFile };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── The tee POST (fire-and-forget; failures are invisible — rendering never depends on it). ──────────
|
|
180
|
+
function postReading(cfg) {
|
|
181
|
+
if (!snap || typeof snap !== "object") return;
|
|
182
|
+
const cw = snap.context_window;
|
|
183
|
+
const rl = snap.rate_limits;
|
|
184
|
+
const pct = cw && typeof cw.used_percentage === "number" ? Math.min(100, Math.max(0, cw.used_percentage)) : undefined;
|
|
185
|
+
const win = (w) =>
|
|
186
|
+
w && typeof w.used_percentage === "number"
|
|
187
|
+
? { usedPct: Math.min(100, Math.max(0, w.used_percentage)), ...(typeof w.resets_at === "number" ? { resetsAt: Math.round(w.resets_at * 1000) } : {}) }
|
|
188
|
+
: undefined;
|
|
189
|
+
const fiveHour = win(rl && rl.five_hour);
|
|
190
|
+
const sevenDay = win(rl && rl.seven_day);
|
|
191
|
+
// Mode/model FERRY (raw, undiffed — the hub synthesizes modelChanged/mode states from per-session diffs).
|
|
192
|
+
// The statusline JSON carries model {id, display_name} but NO permission_mode today (July 2026) — the
|
|
193
|
+
// guard is future-proofing; the mode ferry's live source is signal.mjs's transcript read.
|
|
194
|
+
const modelId = snap.model && typeof snap.model.id === "string" && snap.model.id ? snap.model.id : undefined;
|
|
195
|
+
const modelName = snap.model && typeof snap.model.display_name === "string" && snap.model.display_name ? snap.model.display_name : undefined;
|
|
196
|
+
const mode = typeof snap.permission_mode === "string" && snap.permission_mode ? snap.permission_mode : undefined;
|
|
197
|
+
// Claude review tee (S2): the PR review-state snapshot field (docs/CLAUDE-SIGNALS.md §5's verified
|
|
198
|
+
// schema — `pr.review_state`, one of approved|pending|changes_requested|draft). Ferried verbatim,
|
|
199
|
+
// additive, OMITTED (never null) when absent — the hub's tracker does the baseline-quiet diff.
|
|
200
|
+
const review = snap.pr && typeof snap.pr === "object" && typeof snap.pr.review_state === "string" && snap.pr.review_state ? snap.pr.review_state : undefined;
|
|
201
|
+
// Cost FERRY: the statusline JSON carries a running total spend (USD) for the session — feeds the
|
|
202
|
+
// hub's costHigh threshold state the same way pct/rateLimits feed usageHigh/contextHigh.
|
|
203
|
+
const cost = snap.cost && typeof snap.cost.total_cost_usd === "number" ? snap.cost.total_cost_usd : undefined;
|
|
204
|
+
if (pct === undefined && !fiveHour && !sevenDay) return; // nothing to report (early session)
|
|
205
|
+
|
|
206
|
+
const cwd = typeof snap.cwd === "string" ? snap.cwd : undefined;
|
|
207
|
+
const handshake = resolveHandshake(cfg, typeof snap.session_id === "string" ? snap.session_id : undefined, cwd);
|
|
208
|
+
if (!handshake) return;
|
|
209
|
+
let token;
|
|
210
|
+
try {
|
|
211
|
+
token = readFileSync(handshake.hubTokenFile, "utf8").trim();
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!token) return;
|
|
216
|
+
|
|
217
|
+
const payload = JSON.stringify({
|
|
218
|
+
sessionId: handshake.id,
|
|
219
|
+
...(typeof snap.session_id === "string" ? { claudeSessionId: snap.session_id } : {}),
|
|
220
|
+
...(cwd ? { cwd } : {}),
|
|
221
|
+
ts: Date.now(),
|
|
222
|
+
...(pct !== undefined
|
|
223
|
+
? {
|
|
224
|
+
context: {
|
|
225
|
+
pct,
|
|
226
|
+
...(cw && typeof cw.total_input_tokens === "number" ? { usedTokens: cw.total_input_tokens } : {}),
|
|
227
|
+
...(cw && typeof cw.context_window_size === "number" ? { windowTokens: cw.context_window_size } : {}),
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
: {}),
|
|
231
|
+
...(fiveHour || sevenDay ? { rateLimits: { ...(fiveHour ? { fiveHour } : {}), ...(sevenDay ? { sevenDay } : {}) } } : {}),
|
|
232
|
+
...(modelId ? { model: modelId } : {}),
|
|
233
|
+
...(modelName ? { modelName } : {}),
|
|
234
|
+
...(mode ? { mode } : {}),
|
|
235
|
+
...(review ? { review } : {}),
|
|
236
|
+
...(typeof cost === "number" ? { cost } : {}),
|
|
237
|
+
});
|
|
238
|
+
// node:http for zero deps + no Origin header (the loopback guard 403s any Origin) + a hard timeout.
|
|
239
|
+
postPending = new Promise((resolve) => {
|
|
240
|
+
const req = request(
|
|
241
|
+
{
|
|
242
|
+
host: "127.0.0.1",
|
|
243
|
+
port: handshake.hubPort,
|
|
244
|
+
path: "/local/session/context",
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload), "x-bridge-hub-token": token },
|
|
247
|
+
timeout: POST_TIMEOUT_MS,
|
|
248
|
+
},
|
|
249
|
+
(res) => {
|
|
250
|
+
res.on("data", () => {});
|
|
251
|
+
res.on("end", resolve);
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
req.on("timeout", () => {
|
|
255
|
+
req.destroy();
|
|
256
|
+
resolve();
|
|
257
|
+
});
|
|
258
|
+
req.on("error", resolve); // hub down → silent; the render must not care
|
|
259
|
+
req.end(payload);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Render: chain the stored original statusline, or print the default line. ─────────────────────────
|
|
264
|
+
function defaultLine() {
|
|
265
|
+
if (!snap || typeof snap !== "object") return "⏺ Claude";
|
|
266
|
+
const model = (snap.model && (snap.model.display_name || snap.model.id)) || "Claude";
|
|
267
|
+
const parts = [`⏺ ${model}`];
|
|
268
|
+
const cw = snap.context_window;
|
|
269
|
+
if (cw && typeof cw.used_percentage === "number") parts.push(`ctx ${Math.round(cw.used_percentage)}%`);
|
|
270
|
+
const fh = snap.rate_limits && snap.rate_limits.five_hour;
|
|
271
|
+
if (fh && typeof fh.used_percentage === "number") parts.push(`5h ${Math.round(fh.used_percentage)}%`);
|
|
272
|
+
return parts.join(" · ");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderChained(prev) {
|
|
276
|
+
// chainChildEnv stamps the depth fuse, so whatever we spawn cannot itself chain — the chain is capped at
|
|
277
|
+
// depth 1 even if `prev` turns out to reach this script by a route the guard didn't recognise.
|
|
278
|
+
const child = spawn(prev, { shell: true, stdio: ["pipe", "pipe", "ignore"], env: chainChildEnv(process.env) });
|
|
279
|
+
activeChild = child;
|
|
280
|
+
let out = "";
|
|
281
|
+
let settled = false;
|
|
282
|
+
const settle = (line) => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
settled = true;
|
|
285
|
+
finish(line);
|
|
286
|
+
};
|
|
287
|
+
child.stdout.on("data", (c) => (out += c.toString()));
|
|
288
|
+
// A chained statusline is under no obligation to READ the snapshot — starship, oh-my-posh and most shell
|
|
289
|
+
// one-liners never touch stdin. When such a child exits before our write drains, the pipe breaks and node
|
|
290
|
+
// emits EPIPE on child.stdin; an 'error' event with no listener is THROWN, so the uncaughtException handler
|
|
291
|
+
// above calls finish() before anything has rendered and the user's real line is replaced by "⏺ Claude".
|
|
292
|
+
// The try/catch around .end() cannot see it — EPIPE arrives asynchronously. Swallowing it is correct on its
|
|
293
|
+
// own terms too: the child not wanting our stdin says nothing about whether its OUTPUT is good, and close/
|
|
294
|
+
// error below already decide the line.
|
|
295
|
+
child.stdin.on("error", () => {});
|
|
296
|
+
child.on("error", () => {
|
|
297
|
+
activeChild = null;
|
|
298
|
+
settle(defaultLine()); // prev vanished → fall back, never blank
|
|
299
|
+
});
|
|
300
|
+
child.on("close", () => {
|
|
301
|
+
activeChild = null;
|
|
302
|
+
render(out.trimEnd() || defaultLine());
|
|
303
|
+
settle();
|
|
304
|
+
});
|
|
305
|
+
try {
|
|
306
|
+
child.stdin.end(raw); // the SAME stdin snapshot, byte-for-byte — the chained line sees what we saw
|
|
307
|
+
} catch {
|
|
308
|
+
settle(defaultLine());
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const cfg = readCliConfig();
|
|
313
|
+
try {
|
|
314
|
+
postReading(cfg);
|
|
315
|
+
} catch {
|
|
316
|
+
/* the tee must never break the render */
|
|
317
|
+
}
|
|
318
|
+
// Guarded: refuse a `prev` that is (or re-enters) THIS script, and refuse to chain at all when we are
|
|
319
|
+
// already a chained child. Either refusal falls through to defaultLine() — the documented fallback, i.e. a
|
|
320
|
+
// cosmetically plainer statusline instead of an unbounded spawn chain.
|
|
321
|
+
if (shouldChain(cfg.statuslinePrev, { env: process.env, selfPath: SELF_PATH })) renderChained(cfg.statuslinePrev);
|
|
322
|
+
else finish(defaultLine());
|