subconscious-cli 5.0.3 → 6.0.1

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/bin/usage.js CHANGED
@@ -2,8 +2,8 @@
2
2
  * Usage display for `subc usage` — fetches billing/quota from the platform API.
3
3
  */
4
4
 
5
- import { c } from './colors.js';
6
5
  import { getApiKey, getPlatformUrl } from './auth.js';
6
+ import { c } from './colors.js';
7
7
  import { DEFAULT_PROFILE } from './profiles.js';
8
8
  import { printLoginUpgradeWarning } from './upgrade.js';
9
9
 
@@ -33,13 +33,18 @@ export function formatCurrency(value) {
33
33
 
34
34
  export function shortModelSlug(slug) {
35
35
  const text = String(slug ?? '').trim();
36
- return text.startsWith('subconscious/') ? text.slice('subconscious/'.length) : text;
36
+ return text.startsWith('subconscious/')
37
+ ? text.slice('subconscious/'.length)
38
+ : text;
37
39
  }
38
40
 
39
41
  export function isLegacyUsagePayload(data) {
40
42
  if (!data || typeof data !== 'object') return true;
41
- if ('credits' in data && data.plan && typeof data.plan === 'object') return false;
42
- return 'dailyTokenCeiling' in data || 'consumedToday' in data || !('plan' in data);
43
+ if ('credits' in data && data.plan && typeof data.plan === 'object')
44
+ return false;
45
+ return (
46
+ 'dailyTokenCeiling' in data || 'consumedToday' in data || !('plan' in data)
47
+ );
43
48
  }
44
49
 
45
50
  export function meterColor(percentage) {
@@ -120,7 +125,9 @@ export function formatUsageDisplay(data) {
120
125
  );
121
126
  } else {
122
127
  const label =
123
- described.kind === 'credit' ? 'of daily credit used' : 'of daily tokens used';
128
+ described.kind === 'credit'
129
+ ? 'of daily credit used'
130
+ : 'of daily tokens used';
124
131
  lines.push(
125
132
  ` ${renderProgressBar(described.percent)} ${formatAllowancePercent(described.percent)} ${label}`,
126
133
  );
@@ -155,9 +162,7 @@ export function formatUsageDisplay(data) {
155
162
  );
156
163
  for (const model of models) {
157
164
  const label = shortModelSlug(model.slug).padEnd(slugWidth);
158
- lines.push(
159
- ` ${label} ${formatTokens(model.consumedToday ?? 0)}`,
160
- );
165
+ lines.push(` ${label} ${formatTokens(model.consumedToday ?? 0)}`);
161
166
  }
162
167
  }
163
168
 
@@ -166,10 +171,13 @@ export function formatUsageDisplay(data) {
166
171
  }
167
172
 
168
173
  export async function fetchUsage(apiKey, platformUrl, fetchImpl = fetch) {
169
- const res = await fetchImpl(`${platformUrl.replace(/\/$/, '')}${USAGE_API_PATH}`, {
170
- headers: { Authorization: `Bearer ${apiKey}` },
171
- signal: AbortSignal.timeout(5000),
172
- });
174
+ const res = await fetchImpl(
175
+ `${platformUrl.replace(/\/$/, '')}${USAGE_API_PATH}`,
176
+ {
177
+ headers: { Authorization: `Bearer ${apiKey}` },
178
+ signal: AbortSignal.timeout(5000),
179
+ },
180
+ );
173
181
  return res;
174
182
  }
175
183
 
@@ -183,7 +191,9 @@ export async function usageCommand(argv = [], options = {}) {
183
191
  const auth = await getApiKey(options.profile);
184
192
  if (!auth) {
185
193
  console.log(`\n ${c.dim}Not logged in.${c.reset}`);
186
- console.log(` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`);
194
+ console.log(
195
+ ` Run ${c.cyan}subc ${profileFlag}login${c.reset} to get started.\n`,
196
+ );
187
197
  return;
188
198
  }
189
199
 
@@ -236,7 +246,9 @@ export async function usageCommand(argv = [], options = {}) {
236
246
  console.log(formatUsageDisplay(data));
237
247
  } catch (error) {
238
248
  if (error.name === 'TimeoutError' || error.name === 'AbortError') {
239
- console.log(`\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`);
249
+ console.log(
250
+ `\n ${c.yellow}Could not reach the platform (timed out).${c.reset}`,
251
+ );
240
252
  console.log(` ${c.dim}Host: ${platformUrl}${c.reset}\n`);
241
253
  return;
242
254
  }
@@ -1,79 +1,182 @@
1
- import path from 'node:path';
2
1
  import os from 'node:os';
2
+ import path from 'node:path';
3
3
  import readline from 'node:readline';
4
- import { windowsEnv, windowsBinDirs, windowsPath, resolveWindowsExecutable, readWindowsVersion, runWindows } from './process.js';
5
- import { windowsInstallSpec, installWindowsAgent } from './install.js';
6
- import { windowsSetup } from './setup.js';
7
- import { windowsLaunch, executeWindowsLaunch } from './launch.js';
8
4
  import { parseOptions } from './common.js';
5
+ import { installWindowsAgent, windowsInstallSpec } from './install.js';
6
+ import { executeWindowsLaunch, windowsLaunch } from './launch.js';
7
+ import {
8
+ readWindowsVersion,
9
+ resolveWindowsExecutable,
10
+ runWindows,
11
+ windowsBinDirs,
12
+ windowsEnv,
13
+ windowsPath,
14
+ } from './process.js';
15
+ import { windowsSetup } from './setup.js';
9
16
 
10
- const askInstall = name => new Promise(resolve => {
11
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
- rl.question(` ${name} isn't installed. Install it now? [Y/n] `, answer => { rl.close(); resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase())); });
13
- });
17
+ const askInstall = (name) =>
18
+ new Promise((resolve) => {
19
+ const rl = readline.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout,
22
+ });
23
+ rl.question(
24
+ ` ${name} isn't installed. Install it now? [Y/n] `,
25
+ (answer) => {
26
+ rl.close();
27
+ resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase()));
28
+ },
29
+ );
30
+ });
14
31
 
15
32
  export async function runWindowsAgent(agent, argv, dependencies) {
16
- const { profile, parseAgentAction, extractModel, requireApiKey, resolvedModelsForLaunch, selectLaunchModel, runbookEnv } = dependencies;
33
+ const {
34
+ profile,
35
+ parseAgentAction,
36
+ extractModel,
37
+ requireApiKey,
38
+ resolvedModelsForLaunch,
39
+ selectLaunchModel,
40
+ runbookEnv,
41
+ } = dependencies;
17
42
  const parsed = parseAgentAction(agent, argv);
18
- let args = parsed.action !== 'launch' && parsed.action === argv[0] ? argv.slice(1) : argv;
43
+ let args =
44
+ parsed.action !== 'launch' && parsed.action === argv[0]
45
+ ? argv.slice(1)
46
+ : argv;
19
47
  const environment = windowsEnv(profile?.values, process.env);
20
48
  const preferredDirs = agent.runbook?.installDir
21
- ? [path.resolve(environment[agent.runbook.installDirEnv] || path.join(os.homedir(), agent.runbook.installDir))] : [];
22
- environment.PATH = windowsPath([...preferredDirs, ...(environment.PATH || '').split(';'), ...windowsBinDirs(environment)]);
49
+ ? [
50
+ path.resolve(
51
+ environment[agent.runbook.installDirEnv] ||
52
+ path.join(os.homedir(), agent.runbook.installDir),
53
+ ),
54
+ ]
55
+ : [];
56
+ environment.PATH = windowsPath([
57
+ ...preferredDirs,
58
+ ...(environment.PATH || '').split(';'),
59
+ ...windowsBinDirs(environment),
60
+ ]);
23
61
  if (agent.id === 'subconscious-code' && parsed.action === 'install') {
24
62
  if (args.length) throw new Error('Usage: subc marathon install');
25
63
  return finish(await installWindowsAgent(agent.id, environment));
26
64
  }
27
65
  if (['status', 'uninstall'].includes(parsed.action)) {
28
- return finish(await windowsSetup(agent.id, parsed.action, args, environment));
66
+ return finish(
67
+ await windowsSetup(agent.id, parsed.action, args, environment),
68
+ );
29
69
  }
30
70
 
31
71
  // Honor -- after which all arguments belong to the underlying agent.
32
72
  const boundary = args.indexOf('--');
33
73
  const tail = boundary < 0 ? [] : args.slice(boundary);
34
- const extracted = extractModel(boundary < 0 ? args : args.slice(0, boundary), profile);
74
+ const extracted = extractModel(
75
+ boundary < 0 ? args : args.slice(0, boundary),
76
+ profile,
77
+ );
35
78
  args = [...extracted.rest, ...tail];
36
79
  let explicit = {};
37
80
  if (parsed.action === 'install' || agent.id === 'claude-code') {
38
- const handled = parseOptions(boundary < 0 ? args : args.slice(0, args.indexOf('--')), { '--api-key': 1, '--gateway-url': 1 });
81
+ const handled = parseOptions(
82
+ boundary < 0 ? args : args.slice(0, args.indexOf('--')),
83
+ { '--api-key': 1, '--gateway-url': 1 },
84
+ );
39
85
  explicit = handled.options;
40
86
  }
41
- const key = explicit['--api-key'] || await requireApiKey(profile, agent);
87
+ const key = explicit['--api-key'] || (await requireApiKey(profile, agent));
42
88
  if (!key) return finish(1);
43
- const catalogProfile = explicit['--gateway-url'] ? { ...profile, values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] } } : profile;
44
- const catalog = await resolvedModelsForLaunch(catalogProfile, key, extracted.model);
45
- const model = selectLaunchModel(extracted.model, extracted.modelSource, catalog);
46
- if (extracted.model && model !== extracted.model) console.error(`Configured model ${extracted.model} is not in the live catalog; using ${model}.`);
89
+ const catalogProfile = explicit['--gateway-url']
90
+ ? {
91
+ ...profile,
92
+ values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] },
93
+ }
94
+ : profile;
95
+ const catalog = await resolvedModelsForLaunch(
96
+ catalogProfile,
97
+ key,
98
+ extracted.model,
99
+ );
100
+ const model = selectLaunchModel(
101
+ extracted.model,
102
+ extracted.modelSource,
103
+ catalog,
104
+ );
105
+ if (extracted.model && model !== extracted.model)
106
+ console.error(
107
+ `Configured model ${extracted.model} is not in the live catalog; using ${model}.`,
108
+ );
47
109
  let executable;
48
110
  if (parsed.action === 'launch') {
49
- executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
111
+ executable = resolveWindowsExecutable(agent.bin, {
112
+ env: environment,
113
+ preferredDirs,
114
+ });
50
115
  if (!executable) {
51
116
  const installer = windowsInstallSpec(agent.id, environment);
52
- if (!installer || !process.stdin.isTTY || !process.stdout.isTTY || !await askInstall(agent.name)) {
53
- console.error(`${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`);
117
+ if (
118
+ !installer ||
119
+ !process.stdin.isTTY ||
120
+ !process.stdout.isTTY ||
121
+ !(await askInstall(agent.name))
122
+ ) {
123
+ console.error(
124
+ `${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`,
125
+ );
54
126
  return finish(127);
55
127
  }
56
128
  const installed = await installWindowsAgent(agent.id, environment);
57
129
  if (installed) return finish(installed);
58
- executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
130
+ executable = resolveWindowsExecutable(agent.bin, {
131
+ env: environment,
132
+ preferredDirs,
133
+ });
59
134
  if (!executable) {
60
- console.error(`Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`);
135
+ console.error(
136
+ `Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`,
137
+ );
61
138
  return finish(127);
62
139
  }
63
140
  }
64
141
  if (agent.id === 'claude-code') {
65
- const version = dependencies.parseClaudeVersion(readWindowsVersion(executable, environment));
66
- if (dependencies.claudeVersionNeedsUpgrade(version)) console.error(`Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`);
142
+ const version = dependencies.parseClaudeVersion(
143
+ readWindowsVersion(executable, environment),
144
+ );
145
+ if (dependencies.claudeVersionNeedsUpgrade(version))
146
+ console.error(
147
+ `Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`,
148
+ );
67
149
  }
68
150
  }
69
151
  // Reuse profile/catalog semantics, but use Windows path and environment rules.
70
- const env = windowsEnv(runbookEnv(key, model, executable && path.dirname(executable), catalogProfile, agent, catalog.models), { PATH: windowsPath(executable ? [path.dirname(executable)] : [], environment.PATH) });
152
+ const env = windowsEnv(
153
+ runbookEnv(
154
+ key,
155
+ model,
156
+ executable && path.dirname(executable),
157
+ catalogProfile,
158
+ agent,
159
+ catalog.models,
160
+ ),
161
+ {
162
+ PATH: windowsPath(
163
+ executable ? [path.dirname(executable)] : [],
164
+ environment.PATH,
165
+ ),
166
+ },
167
+ );
71
168
  if (explicit['--gateway-url']) env.GATEWAY_URL = explicit['--gateway-url'];
72
- if (parsed.action === 'install') return finish(await windowsSetup(agent.id, 'install', args, env));
169
+ if (parsed.action === 'install')
170
+ return finish(await windowsSetup(agent.id, 'install', args, env));
73
171
  if (agent.id === 'pi') await windowsSetup('pi', 'install', [], env);
74
172
  if (agent.id === 'codex') {
75
- try { await windowsSetup('codex', 'install', [], env, { log: () => {} }); }
76
- catch (error) { console.error(`Could not refresh Codex hooks (launch will continue): ${error.message}`); }
173
+ try {
174
+ await windowsSetup('codex', 'install', [], env, { log: () => {} });
175
+ } catch (error) {
176
+ console.error(
177
+ `Could not refresh Codex hooks (launch will continue): ${error.message}`,
178
+ );
179
+ }
77
180
  }
78
181
  const spec = await windowsLaunch(agent.id, args, env);
79
182
  if (spec.command === agent.bin) spec.command = executable;
@@ -1,37 +1,65 @@
1
1
  // Windows-only equivalent of the existing Claude shell launch.
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolvedModelSetting } from '../profiles.js';
4
- import { positiveInteger, parseOptions } from './common.js';
5
- const DEFAULTS = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
4
+ import { parseOptions, positiveInteger } from './common.js';
5
+
6
+ const DEFAULTS = JSON.parse(
7
+ readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'),
8
+ ).defaults;
6
9
  function claudePickerSettings(models) {
7
- return { availableModels: models, modelPicker: { replaceBuiltInOptions: true, options: models.map(model => ({model, label: model, description: 'Subconscious model ' + model})) } };
10
+ return {
11
+ availableModels: models,
12
+ modelPicker: {
13
+ replaceBuiltInOptions: true,
14
+ options: models.map((model) => ({
15
+ model,
16
+ label: model,
17
+ description: `Subconscious model ${model}`,
18
+ })),
19
+ },
20
+ };
8
21
  }
9
22
 
10
23
  export function claudeNativeLaunch(argv, environment) {
11
24
  const { options, rest: passthrough } = parseOptions(argv, {
12
- '--gateway-url': 1, '--api-key': 1, '--model': 1,
13
- '--compact-window': 1, '--max-context-tokens': 1,
25
+ '--gateway-url': 1,
26
+ '--api-key': 1,
27
+ '--model': 1,
28
+ '--compact-window': 1,
29
+ '--max-context-tokens': 1,
14
30
  });
15
- const gatewayUrl = options['--gateway-url'] || environment.GATEWAY_URL?.trim() || '';
16
- const apiKey = options['--api-key'] || environment.CLAUDE_CODE_API_KEY?.trim() || environment.API_KEY?.trim() || '';
17
- const model = options['--model'] || environment.MODEL?.trim() || DEFAULTS.model;
18
- const compactWindow = options['--compact-window'] ||
31
+ const gatewayUrl =
32
+ options['--gateway-url'] || environment.GATEWAY_URL?.trim() || '';
33
+ const apiKey =
34
+ options['--api-key'] ||
35
+ environment.CLAUDE_CODE_API_KEY?.trim() ||
36
+ environment.API_KEY?.trim() ||
37
+ '';
38
+ const model =
39
+ options['--model'] || environment.MODEL?.trim() || DEFAULTS.model;
40
+ const compactWindow =
41
+ options['--compact-window'] ||
19
42
  environment.CLAUDE_CODE_AUTO_COMPACT_WINDOW?.trim() ||
20
43
  environment.COMPACT_WINDOW?.trim() ||
21
44
  '1000000';
22
- const maxContextTokens = options['--max-context-tokens'] ||
45
+ const maxContextTokens =
46
+ options['--max-context-tokens'] ||
23
47
  environment.CLAUDE_CODE_MAX_CONTEXT_TOKENS?.trim() ||
24
48
  environment.MAX_CONTEXT_TOKENS?.trim() ||
25
49
  '3000000';
26
50
 
27
51
  if (!gatewayUrl || !apiKey) {
28
- throw new Error('GATEWAY_URL and API_KEY are required to launch Claude Code');
52
+ throw new Error(
53
+ 'GATEWAY_URL and API_KEY are required to launch Claude Code',
54
+ );
29
55
  }
30
56
  positiveInteger(compactWindow, 'compact-window');
31
57
  positiveInteger(maxContextTokens, 'max-context-tokens');
32
58
 
33
- const effectiveGatewayUrl = environment.CLAUDE_GATEWAY_URL?.trim() || gatewayUrl;
34
- const subagentModel = resolvedModelSetting(environment.CLAUDE_CODE_SUBAGENT_MODEL) || model;
59
+ const effectiveGatewayUrl =
60
+ environment.CLAUDE_GATEWAY_URL?.trim() || gatewayUrl;
61
+ const subagentModel =
62
+ resolvedModelSetting(environment.CLAUDE_CODE_SUBAGENT_MODEL) || model;
35
63
  const settings =
36
64
  environment.SUBC_CLAUDE_SETTINGS?.trim() ||
37
65
  JSON.stringify(claudePickerSettings([model], model));
@@ -41,7 +69,8 @@ export function claudeNativeLaunch(argv, environment) {
41
69
  ANTHROPIC_BASE_URL: effectiveGatewayUrl,
42
70
  ANTHROPIC_AUTH_TOKEN: apiKey,
43
71
  ANTHROPIC_MODEL: environment.ANTHROPIC_MODEL?.trim() || model,
44
- ANTHROPIC_SMALL_FAST_MODEL: environment.ANTHROPIC_SMALL_FAST_MODEL?.trim() || model,
72
+ ANTHROPIC_SMALL_FAST_MODEL:
73
+ environment.ANTHROPIC_SMALL_FAST_MODEL?.trim() || model,
45
74
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:
46
75
  environment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY?.trim() || '0',
47
76
  CLAUDE_CODE_SUBAGENT_MODEL: subagentModel,
@@ -1,58 +1,95 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
1
  import { randomUUID } from 'node:crypto';
4
2
  import { readFileSync } from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
5
 
6
- export const defaults = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
6
+ export const defaults = JSON.parse(
7
+ readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'),
8
+ ).defaults;
7
9
  export const value = (env, key, fallback = '') => env[key]?.trim() || fallback;
8
- export const origin = url => url.replace(/\/+$/, '').replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
10
+ export const origin = (url) =>
11
+ url
12
+ .replace(/\/+$/, '')
13
+ .replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
9
14
  export function positiveInteger(input, name) {
10
- if (!/^[1-9][0-9]*$/.test(String(input)) || !Number.isSafeInteger(Number(input))) throw new Error(`${name} must be a positive integer`);
15
+ if (
16
+ !/^[1-9][0-9]*$/.test(String(input)) ||
17
+ !Number.isSafeInteger(Number(input))
18
+ )
19
+ throw new Error(`${name} must be a positive integer`);
11
20
  return Number(input);
12
21
  }
13
22
  export function modelIds(env) {
14
- const models = [...new Set([value(env, 'MODEL', defaults.model), ...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(/\r?\n/)].filter(Boolean))];
15
- for (const model of models) if (!/^[-A-Za-z0-9._:/+]+$/.test(model)) throw new Error(`Invalid model id: ${model}`);
23
+ const models = [
24
+ ...new Set(
25
+ [
26
+ value(env, 'MODEL', defaults.model),
27
+ ...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(
28
+ /\r?\n/,
29
+ ),
30
+ ].filter(Boolean),
31
+ ),
32
+ ];
33
+ for (const model of models)
34
+ if (!/^[-A-Za-z0-9._:/+]+$/.test(model))
35
+ throw new Error(`Invalid model id: ${model}`);
16
36
  return models;
17
37
  }
18
38
  export function parseOptions(argv, schema, { strict = false } = {}) {
19
- const options = {}, rest = [];
39
+ const options = {},
40
+ rest = [];
20
41
  for (let i = 0; i < argv.length; i++) {
21
42
  if (argv[i] === '--') {
22
- if (strict && i + 1 < argv.length) throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
23
- rest.push(...argv.slice(i + 1)); break;
43
+ if (strict && i + 1 < argv.length)
44
+ throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
45
+ rest.push(...argv.slice(i + 1));
46
+ break;
24
47
  }
25
48
  const [flag, ...equals] = argv[i].split('=');
26
49
  if (!Object.hasOwn(schema, flag)) {
27
50
  if (strict) throw new Error(`Unknown setup option: ${argv[i]}`);
28
- rest.push(argv[i]); continue;
51
+ rest.push(argv[i]);
52
+ continue;
29
53
  }
30
54
  if (schema[flag] === true) {
31
55
  if (equals.length) throw new Error(`${flag} does not take a value`);
32
- options[flag] = true; continue;
56
+ options[flag] = true;
57
+ continue;
33
58
  }
34
59
  const next = equals.length ? equals.join('=') : argv[++i];
35
- if (!next || next.startsWith('--')) throw new Error(`${flag} requires a value`);
60
+ if (!next || next.startsWith('--'))
61
+ throw new Error(`${flag} requires a value`);
36
62
  options[flag] = next;
37
63
  }
38
64
  return { options, rest };
39
65
  }
40
66
  export async function readJson(file, fallback) {
41
- try { return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
42
- catch (error) {
67
+ try {
68
+ return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, ''));
69
+ } catch (error) {
43
70
  if (error.code === 'ENOENT') return fallback;
44
- throw new Error(`Cannot read ${file}; leaving it unchanged: ${error.message}`);
71
+ throw new Error(
72
+ `Cannot read ${file}; leaving it unchanged: ${error.message}`,
73
+ );
45
74
  }
46
75
  }
47
76
  export function object(value, description) {
48
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${description} must be a JSON object; leaving it unchanged`);
77
+ if (!value || typeof value !== 'object' || Array.isArray(value))
78
+ throw new Error(
79
+ `${description} must be a JSON object; leaving it unchanged`,
80
+ );
49
81
  return value;
50
82
  }
51
83
  export async function writeJson(file, data) {
52
84
  await fs.mkdir(path.dirname(file), { recursive: true });
53
85
  const temp = `${file}.${randomUUID()}.tmp`;
54
86
  try {
55
- await fs.writeFile(temp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
87
+ await fs.writeFile(temp, `${JSON.stringify(data, null, 2)}\n`, {
88
+ mode: 0o600,
89
+ flag: 'wx',
90
+ });
56
91
  await fs.rename(temp, file);
57
- } finally { await fs.rm(temp, { force: true }); }
92
+ } finally {
93
+ await fs.rm(temp, { force: true });
94
+ }
58
95
  }