claude-usage-limits 1.9.2 → 1.11.2
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +196 -25
- package/bin/cli.js +21 -1
- package/commands/panel.md +14 -0
- package/commands/statusline.md +15 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +36 -0
- package/skills/usage-limits/references/how-it-works.md +115 -2
- package/skills/usage-limits/scripts/activity.js +198 -0
- package/skills/usage-limits/scripts/bars.js +340 -0
- package/skills/usage-limits/scripts/brief.js +59 -6
- package/skills/usage-limits/scripts/feed.js +400 -0
- package/skills/usage-limits/scripts/live.js +489 -0
- package/skills/usage-limits/scripts/panel.js +731 -0
- package/skills/usage-limits/scripts/pulse.js +24 -0
- package/skills/usage-limits/scripts/recommend.js +56 -5
- package/skills/usage-limits/scripts/sessionend.js +5 -1
- package/skills/usage-limits/scripts/statusline.js +305 -0
- package/skills/usage-limits/scripts/stop.js +5 -1
- package/skills/usage-limits/scripts/tally.js +4 -10
- package/skills/usage-limits/scripts/usage.js +477 -17
- package/skills/usage-limits/scripts/view.js +222 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The status line: one line of bars under the Claude Code prompt, in Claude's
|
|
5
|
+
// own colours, from Claude's own numbers.
|
|
6
|
+
//
|
|
7
|
+
// Claude Code runs this on every change and hands it JSON on stdin. Two things
|
|
8
|
+
// in that JSON are worth more than anything on disk: `rate_limits`, which
|
|
9
|
+
// Claude Code fills from the rate-limit headers on its own API responses, and
|
|
10
|
+
// `model`, which is the one certain answer to which model is running. Both
|
|
11
|
+
// are recorded in a small feed file, one slot per session, so the side panel
|
|
12
|
+
// (which never sees this JSON) can draw from them too.
|
|
13
|
+
//
|
|
14
|
+
// It must be fast and it must never fail: no transcript scan, no network, one
|
|
15
|
+
// read of a few small files, and any error prints nothing rather than a stack
|
|
16
|
+
// trace where the bars should be.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
const usage = require('./usage.js');
|
|
24
|
+
const host = require('./host.js');
|
|
25
|
+
const bars = require('./bars.js');
|
|
26
|
+
const view = require('./view.js');
|
|
27
|
+
const activity = require('./activity.js');
|
|
28
|
+
const statusline = require('./statusline.js');
|
|
29
|
+
const live = require('./live.js');
|
|
30
|
+
|
|
31
|
+
const KEEP_SESSIONS = 8;
|
|
32
|
+
// Two updates this close together mean Claude is mid-turn.
|
|
33
|
+
const WORKING_GAP_MS = 4000;
|
|
34
|
+
// A previous status line gets this long, then we go on without it.
|
|
35
|
+
const CHAIN_TIMEOUT_MS = 2000;
|
|
36
|
+
const STDIN_WAIT_MS = 500;
|
|
37
|
+
// Claude Code draws the status line inside its own margins, a few columns
|
|
38
|
+
// narrower than COLUMNS, and clips what does not fit.
|
|
39
|
+
const STATUSLINE_MARGIN = 4;
|
|
40
|
+
|
|
41
|
+
const SHORT = { five_hour: 'session', seven_day: 'week', spend_limit: 'spend' };
|
|
42
|
+
// When even that is too wide.
|
|
43
|
+
const SHORTER = { five_hour: '5h', seven_day: 'wk', spend_limit: 'spend' };
|
|
44
|
+
|
|
45
|
+
function configDir() {
|
|
46
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function feedFile() {
|
|
50
|
+
return path.join(configDir(), 'usage-limits-feed.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJson(file) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readFeed() {
|
|
62
|
+
const parsed = readJson(feedFile());
|
|
63
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
64
|
+
const slots = {};
|
|
65
|
+
for (const key of Object.keys(parsed)) {
|
|
66
|
+
const value = parsed[key];
|
|
67
|
+
if (value && typeof value === 'object' && Number.isFinite(value.at)) slots[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return slots;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeFeed(all) {
|
|
73
|
+
return live.writeAtomic(feedFile(), JSON.stringify(all));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Two updates a few seconds apart mean a turn in progress, unless the status
|
|
77
|
+
// line is on a timer that fires that often anyway, in which case the gap says
|
|
78
|
+
// nothing and only the hooks' marks do.
|
|
79
|
+
function gapMeansWorking(settings) {
|
|
80
|
+
const line = settings && settings.statusLine;
|
|
81
|
+
const every = line && Number.isFinite(line.refreshInterval) ? line.refreshInterval * 1000 : null;
|
|
82
|
+
return !(every !== null && every <= WORKING_GAP_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// This session's own mark, from the hooks. Another window working must not
|
|
86
|
+
// spin this one's line.
|
|
87
|
+
function ownState(marks, sessionId, now) {
|
|
88
|
+
const mine = sessionId && marks ? marks[sessionId] : null;
|
|
89
|
+
if (!mine || !Number.isFinite(mine.at) || now - mine.at > activity.STALE_MS) return { working: false, ultracode: false };
|
|
90
|
+
return { working: mine.state === 'working', ultracode: Boolean(mine.ultracode) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function number(value) {
|
|
94
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// What one status line update tells us, folded onto what the last one said.
|
|
98
|
+
// A run without rate_limits (they only appear after the first API response)
|
|
99
|
+
// keeps the previous ones rather than forgetting them.
|
|
100
|
+
function slotFrom(input, previous, now) {
|
|
101
|
+
const prior = previous && typeof previous === 'object' ? previous : {};
|
|
102
|
+
const model = input.model && typeof input.model === 'object' ? input.model : {};
|
|
103
|
+
const hasHeaders = Boolean(input.rate_limits && typeof input.rate_limits === 'object');
|
|
104
|
+
return {
|
|
105
|
+
at: now,
|
|
106
|
+
prevAt: Number.isFinite(prior.at) ? prior.at : null,
|
|
107
|
+
sessionId: input.session_id || null,
|
|
108
|
+
model: typeof model.id === 'string' ? model.id : prior.model || null,
|
|
109
|
+
modelName: typeof model.display_name === 'string' ? model.display_name : prior.modelName || null,
|
|
110
|
+
effort: input.effort && typeof input.effort.level === 'string' ? input.effort.level : prior.effort || null,
|
|
111
|
+
rateLimits: hasHeaders ? input.rate_limits : prior.rateLimits || null,
|
|
112
|
+
headersAt: hasHeaders ? now : Number.isFinite(prior.headersAt) ? prior.headersAt : null,
|
|
113
|
+
context: input.context_window ? number(input.context_window.used_percentage) : number(prior.context),
|
|
114
|
+
cost: input.cost ? number(input.cost.total_cost_usd) : number(prior.cost),
|
|
115
|
+
cwd: typeof input.cwd === 'string' ? input.cwd : prior.cwd || null,
|
|
116
|
+
version: typeof input.version === 'string' ? input.version : prior.version || null,
|
|
117
|
+
fastMode: input.fast_mode === true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function trim(all, keep) {
|
|
122
|
+
const ordered = Object.keys(all).sort((a, b) => (all[b].at || 0) - (all[a].at || 0));
|
|
123
|
+
const kept = {};
|
|
124
|
+
for (const key of ordered.slice(0, keep || KEEP_SESSIONS)) kept[key] = all[key];
|
|
125
|
+
return kept;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function record(all, input, now) {
|
|
129
|
+
if (!input || typeof input !== 'object' || !input.session_id) return all || {};
|
|
130
|
+
const next = Object.assign({}, all || {});
|
|
131
|
+
next[input.session_id] = slotFrom(input, next[input.session_id], now);
|
|
132
|
+
return trim(next);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function newest(all) {
|
|
136
|
+
let best = null;
|
|
137
|
+
for (const key of Object.keys(all || {})) {
|
|
138
|
+
const slot = all[key];
|
|
139
|
+
if (slot && Number.isFinite(slot.at) && (!best || slot.at > best.at)) best = slot;
|
|
140
|
+
}
|
|
141
|
+
return best;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isWorking(slot, now) {
|
|
145
|
+
if (!slot || !Number.isFinite(slot.at) || !Number.isFinite(slot.prevAt)) return false;
|
|
146
|
+
return now - slot.at < WORKING_GAP_MS && slot.at - slot.prevAt < WORKING_GAP_MS;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function shortLabel(row, shorter) {
|
|
150
|
+
const table = shorter ? SHORTER : SHORT;
|
|
151
|
+
if (table[row.key]) return table[row.key];
|
|
152
|
+
return row.family || row.key;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The line. Widest form first, then narrower bars, then no model, then no
|
|
156
|
+
// bars, so it always fits whatever COLUMNS says.
|
|
157
|
+
function line(built, options) {
|
|
158
|
+
const opts = options || {};
|
|
159
|
+
const columns = Number.isFinite(opts.columns) && opts.columns > 0 ? opts.columns : 80;
|
|
160
|
+
const mode = opts.mode || 'none';
|
|
161
|
+
const tick = Number.isFinite(opts.tick) ? opts.tick : 0;
|
|
162
|
+
const reduced = Boolean(opts.reduced);
|
|
163
|
+
const ascii = Boolean(opts.ascii);
|
|
164
|
+
|
|
165
|
+
if (!built || !built.rows || !built.rows.length) return '';
|
|
166
|
+
if (built.state === 'none') return bars.dim('usage: no reading yet', mode);
|
|
167
|
+
|
|
168
|
+
const glyph = built.working
|
|
169
|
+
? built.ultracode
|
|
170
|
+
? bars.rainbow(bars.spinner(tick, { ascii, reduced }), tick, { mode, reduced })
|
|
171
|
+
: bars.paint(bars.spinner(tick, { ascii, reduced }), bars.THEME.claude, mode)
|
|
172
|
+
: bars.paint(ascii ? '*' : '✻', bars.THEME.claude, mode);
|
|
173
|
+
// Ultracode is xhigh plus workflows, and Claude names it as its own level in
|
|
174
|
+
// the picker, so it is named here too, in the rainbow the picker uses.
|
|
175
|
+
const effortName = built.ultracode ? 'ultracode' : built.effort;
|
|
176
|
+
const effort = effortName ? bars.effortColour(effortName) : null;
|
|
177
|
+
const effortText = !effortName
|
|
178
|
+
? ''
|
|
179
|
+
: effort.rainbow
|
|
180
|
+
? bars.rainbow(effortName, tick, { mode, reduced })
|
|
181
|
+
: effort.shimmer && built.working
|
|
182
|
+
? bars.shimmer(effortName, tick, effort.rgb, effort.shimmer, { mode, reduced })
|
|
183
|
+
: bars.paint(effortName, effort.rgb, mode);
|
|
184
|
+
const head = glyph + ' ' + built.modelLabel + (effortText ? ' ' + bars.dim('·', mode) + ' ' + effortText : '');
|
|
185
|
+
|
|
186
|
+
const segment = (row, width, shorter) => {
|
|
187
|
+
const label = shortLabel(row, shorter);
|
|
188
|
+
const percent = row.level === 'fill' ? row.percentText : bars.paint(row.percentText, bars.levelColour(row.level), mode);
|
|
189
|
+
if (!width || row.percent === null) return label + ' ' + percent;
|
|
190
|
+
return label + ' ' + bars.bar(row.percent, width, { mode, level: row.level, ascii }) + ' ' + percent;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const attempts = [
|
|
194
|
+
{ width: 10, head: true },
|
|
195
|
+
{ width: 8, head: true },
|
|
196
|
+
{ width: 6, head: true },
|
|
197
|
+
{ width: 6, head: false },
|
|
198
|
+
{ width: 4, head: false },
|
|
199
|
+
{ width: 4, head: false, shorter: true },
|
|
200
|
+
{ width: 0, head: false },
|
|
201
|
+
{ width: 0, head: false, shorter: true },
|
|
202
|
+
{ width: 0, head: false, shorter: true, gap: ' ' },
|
|
203
|
+
];
|
|
204
|
+
// Another Claude spending the same budget is worth a word on the line.
|
|
205
|
+
const others =
|
|
206
|
+
Number.isFinite(built.othersWorking) && built.othersWorking > 0
|
|
207
|
+
? bars.paint('+' + built.othersWorking + ' working', bars.THEME.claude, mode)
|
|
208
|
+
: '';
|
|
209
|
+
let text = '';
|
|
210
|
+
for (const attempt of attempts) {
|
|
211
|
+
const parts = built.rows.map((row) => segment(row, attempt.width, attempt.shorter));
|
|
212
|
+
if (others) parts.push(others);
|
|
213
|
+
if (attempt.head) parts.unshift(head);
|
|
214
|
+
text = parts.join(attempt.gap || ' ');
|
|
215
|
+
if (bars.visibleWidth(text) <= columns) return text;
|
|
216
|
+
}
|
|
217
|
+
return text;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readStdin() {
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
if (process.stdin.isTTY) return resolve('');
|
|
223
|
+
let raw = '';
|
|
224
|
+
let settled = false;
|
|
225
|
+
const done = () => {
|
|
226
|
+
if (settled) return;
|
|
227
|
+
settled = true;
|
|
228
|
+
resolve(raw);
|
|
229
|
+
};
|
|
230
|
+
const timer = setTimeout(done, STDIN_WAIT_MS);
|
|
231
|
+
if (timer.unref) timer.unref();
|
|
232
|
+
process.stdin.setEncoding('utf8');
|
|
233
|
+
process.stdin.on('data', (chunk) => {
|
|
234
|
+
raw += chunk;
|
|
235
|
+
});
|
|
236
|
+
process.stdin.on('end', done);
|
|
237
|
+
process.stdin.on('error', done);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The status line that was there before ours, run with the same stdin, so
|
|
242
|
+
// installing this one loses nothing.
|
|
243
|
+
function runPrevious(command, raw, env, budgetMs) {
|
|
244
|
+
try {
|
|
245
|
+
const result = spawnSync(command, {
|
|
246
|
+
shell: true,
|
|
247
|
+
input: raw,
|
|
248
|
+
encoding: 'utf8',
|
|
249
|
+
timeout: Math.max(300, Number.isFinite(budgetMs) ? budgetMs : CHAIN_TIMEOUT_MS),
|
|
250
|
+
env: env || process.env,
|
|
251
|
+
windowsHide: true,
|
|
252
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
253
|
+
});
|
|
254
|
+
if (result.error || result.status !== 0) return '';
|
|
255
|
+
return String(result.stdout || '').replace(/\s+$/, '');
|
|
256
|
+
} catch (err) {
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function settingsFor(dir) {
|
|
262
|
+
return readJson(path.join(dir, 'settings.json')) || {};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function clockFor(settings, env) {
|
|
266
|
+
const e = env || process.env;
|
|
267
|
+
if (e.USAGE_LIMITS_CLOCK === '24h') return '24h';
|
|
268
|
+
if (e.USAGE_LIMITS_CLOCK === '12h') return '12h';
|
|
269
|
+
const format = settings && typeof settings.timeFormat === 'string' ? settings.timeFormat : '';
|
|
270
|
+
return format.indexOf('24') === 0 ? '24h' : '12h';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function motionOff(settings, env) {
|
|
274
|
+
const e = env || process.env;
|
|
275
|
+
const flag = String(e.USAGE_LIMITS_MOTION || '').toLowerCase();
|
|
276
|
+
if (flag === 'off' || flag === '0' || flag === 'false') return true;
|
|
277
|
+
return Boolean(settings && settings.prefersReducedMotion === true);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Written and flushed before the process is allowed to end: stdout is a pipe
|
|
281
|
+
// here, and a pipe write can still be in flight when process.exit runs.
|
|
282
|
+
function out(text) {
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
process.stdout.write(text, () => resolve());
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function main(argv) {
|
|
289
|
+
const env = process.env;
|
|
290
|
+
const started = Date.now();
|
|
291
|
+
const now = started;
|
|
292
|
+
let chained = '';
|
|
293
|
+
try {
|
|
294
|
+
usage.setHost(host.detect(argv || [], env));
|
|
295
|
+
const raw = await readStdin();
|
|
296
|
+
let input = null;
|
|
297
|
+
try {
|
|
298
|
+
input = raw.trim() ? JSON.parse(raw) : null;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
input = null;
|
|
301
|
+
}
|
|
302
|
+
if (input && typeof input !== 'object') input = null;
|
|
303
|
+
|
|
304
|
+
const state = statusline.readState();
|
|
305
|
+
if (state && state.chain && state.previous && state.previous.type === 'command' && state.previous.command) {
|
|
306
|
+
// Whatever the stdin wait used comes out of the previous line's time.
|
|
307
|
+
chained = runPrevious(state.previous.command, raw, env, CHAIN_TIMEOUT_MS - (Date.now() - started));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let all = readFeed();
|
|
311
|
+
if (input && input.session_id) {
|
|
312
|
+
all = record(all, input, now);
|
|
313
|
+
writeFeed(all);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const off = String(env.USAGE_LIMITS_STATUSLINE || '').toLowerCase();
|
|
317
|
+
if (off === 'off' || off === '0' || off === 'false') {
|
|
318
|
+
if (chained) await out(chained + '\n');
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
if (usage.isCodex()) {
|
|
322
|
+
if (chained) await out(chained + '\n');
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const slot = input && input.session_id ? all[input.session_id] : newest(all);
|
|
327
|
+
const collected = usage.collect(now);
|
|
328
|
+
const settings = settingsFor(configDir());
|
|
329
|
+
const marks = activity.read();
|
|
330
|
+
const mine = input && input.session_id ? input.session_id : null;
|
|
331
|
+
// This session's own state; only with no session id at all does the
|
|
332
|
+
// machine-wide picture stand in for it.
|
|
333
|
+
const own = mine ? ownState(marks, mine, now) : activity.summarise(marks, now);
|
|
334
|
+
// The other sessions working right now, so the line can say so.
|
|
335
|
+
const othersWorking = activity
|
|
336
|
+
.combine({ marks, feed: all }, now, activity.STALE_MS)
|
|
337
|
+
.filter((row) => row.state === 'working' && row.sessionId !== mine).length;
|
|
338
|
+
const built = view.build({
|
|
339
|
+
now,
|
|
340
|
+
utilization: collected.utilization,
|
|
341
|
+
fetchedAtMs: collected.snapshotFetchedAt,
|
|
342
|
+
source: collected.snapshotSource,
|
|
343
|
+
headers: slot ? slot.rateLimits : null,
|
|
344
|
+
headersAt: slot ? slot.headersAt : null,
|
|
345
|
+
model: slot ? slot.model : null,
|
|
346
|
+
modelName: slot ? slot.modelName : null,
|
|
347
|
+
effort: slot ? slot.effort : null,
|
|
348
|
+
working: own.working || (gapMeansWorking(settings) && isWorking(slot, now)),
|
|
349
|
+
ultracode: own.ultracode || settings.ultracode === true,
|
|
350
|
+
settingsModel: collected.settings ? collected.settings.model : null,
|
|
351
|
+
env,
|
|
352
|
+
});
|
|
353
|
+
const text = line(built, {
|
|
354
|
+
columns: Math.max(20, (Number(env.COLUMNS) || 80) - STATUSLINE_MARGIN),
|
|
355
|
+
// Claude Code captures the output, so stdout is never a TTY here, and
|
|
356
|
+
// ANSI is supported all the same.
|
|
357
|
+
mode: bars.colourMode(env, true),
|
|
358
|
+
tick: Math.floor(now / bars.TICK_MS),
|
|
359
|
+
reduced: motionOff(settings, env),
|
|
360
|
+
ascii: String(env.USAGE_LIMITS_ASCII || '') === '1',
|
|
361
|
+
clock: clockFor(settings, env),
|
|
362
|
+
});
|
|
363
|
+
await out((chained ? chained + '\n' : '') + text + '\n');
|
|
364
|
+
return 0;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (chained) await out(chained + '\n');
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = {
|
|
372
|
+
KEEP_SESSIONS,
|
|
373
|
+
WORKING_GAP_MS,
|
|
374
|
+
CHAIN_TIMEOUT_MS,
|
|
375
|
+
feedFile,
|
|
376
|
+
readFeed,
|
|
377
|
+
writeFeed,
|
|
378
|
+
slotFrom,
|
|
379
|
+
record,
|
|
380
|
+
newest,
|
|
381
|
+
isWorking,
|
|
382
|
+
gapMeansWorking,
|
|
383
|
+
ownState,
|
|
384
|
+
line,
|
|
385
|
+
runPrevious,
|
|
386
|
+
clockFor,
|
|
387
|
+
motionOff,
|
|
388
|
+
main,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (require.main === module) {
|
|
392
|
+
main(process.argv.slice(2)).then(
|
|
393
|
+
(code) => {
|
|
394
|
+
process.exitCode = code || 0;
|
|
395
|
+
},
|
|
396
|
+
() => {
|
|
397
|
+
process.exitCode = 0;
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
}
|