vibeusage 0.2.8
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 +21 -0
- package/README.md +182 -0
- package/README.zh-CN.md +182 -0
- package/bin/tracker.js +28 -0
- package/package.json +46 -0
- package/src/cli.js +64 -0
- package/src/commands/diagnostics.js +39 -0
- package/src/commands/init.js +798 -0
- package/src/commands/status.js +155 -0
- package/src/commands/sync.js +479 -0
- package/src/commands/uninstall.js +153 -0
- package/src/lib/browser-auth.js +175 -0
- package/src/lib/claude-config.js +190 -0
- package/src/lib/cli-ui.js +179 -0
- package/src/lib/codex-config.js +224 -0
- package/src/lib/debug-flags.js +9 -0
- package/src/lib/diagnostics.js +190 -0
- package/src/lib/fs.js +62 -0
- package/src/lib/gemini-config.js +284 -0
- package/src/lib/init-flow.js +48 -0
- package/src/lib/insforge-client.js +75 -0
- package/src/lib/insforge.js +23 -0
- package/src/lib/opencode-config.js +98 -0
- package/src/lib/progress.js +77 -0
- package/src/lib/prompt.js +20 -0
- package/src/lib/rollout.js +1317 -0
- package/src/lib/tracker-paths.js +66 -0
- package/src/lib/upload-throttle.js +129 -0
- package/src/lib/uploader.js +116 -0
- package/src/lib/vibescore-api.js +222 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { ensureDir, readJson, writeJson } = require('./fs');
|
|
5
|
+
|
|
6
|
+
async function upsertNotify({ configPath, notifyCmd, notifyOriginalPath, configLabel }) {
|
|
7
|
+
const originalText = await fs.readFile(configPath, 'utf8').catch(() => null);
|
|
8
|
+
if (originalText == null) {
|
|
9
|
+
const label = typeof configLabel === 'string' && configLabel.length > 0 ? configLabel : 'Config';
|
|
10
|
+
throw new Error(`${label} not found: ${configPath}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const existingNotify = extractNotify(originalText);
|
|
14
|
+
const already = arraysEqual(existingNotify, notifyCmd);
|
|
15
|
+
|
|
16
|
+
if (!already) {
|
|
17
|
+
// Persist original notify once (for uninstall + chaining).
|
|
18
|
+
if (existingNotify && existingNotify.length > 0) {
|
|
19
|
+
await ensureDir(path.dirname(notifyOriginalPath));
|
|
20
|
+
const existing = await readJson(notifyOriginalPath);
|
|
21
|
+
if (!existing) {
|
|
22
|
+
await writeJson(notifyOriginalPath, { notify: existingNotify, capturedAt: new Date().toISOString() });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const updated = setNotify(originalText, notifyCmd);
|
|
27
|
+
const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
28
|
+
await fs.copyFile(configPath, backupPath);
|
|
29
|
+
await fs.writeFile(configPath, updated, 'utf8');
|
|
30
|
+
return { changed: true, backupPath };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { changed: false, backupPath: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function restoreNotify({ configPath, notifyOriginalPath, expectedNotify }) {
|
|
37
|
+
const text = await fs.readFile(configPath, 'utf8').catch(() => null);
|
|
38
|
+
if (text == null) return { restored: false, skippedReason: 'config-missing' };
|
|
39
|
+
|
|
40
|
+
const original = await readJson(notifyOriginalPath);
|
|
41
|
+
const originalNotify = Array.isArray(original?.notify) ? original.notify : null;
|
|
42
|
+
const currentNotify = extractNotify(text);
|
|
43
|
+
|
|
44
|
+
if (!originalNotify && expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
|
|
45
|
+
return { restored: false, skippedReason: 'no-backup-not-installed' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const updated = originalNotify ? setNotify(text, originalNotify) : removeNotify(text);
|
|
49
|
+
if (updated === text) return { restored: false, skippedReason: 'no-change' };
|
|
50
|
+
|
|
51
|
+
const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
52
|
+
await fs.copyFile(configPath, backupPath).catch(() => {});
|
|
53
|
+
await fs.writeFile(configPath, updated, 'utf8');
|
|
54
|
+
return { restored: true, skippedReason: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function loadNotifyOriginal(notifyOriginalPath) {
|
|
58
|
+
const original = await readJson(notifyOriginalPath);
|
|
59
|
+
return Array.isArray(original?.notify) ? original.notify : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readNotify(configPath) {
|
|
63
|
+
const text = await fs.readFile(configPath, 'utf8').catch(() => null);
|
|
64
|
+
if (text == null) return null;
|
|
65
|
+
return extractNotify(text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function upsertCodexNotify({ codexConfigPath, notifyCmd, notifyOriginalPath }) {
|
|
69
|
+
return upsertNotify({
|
|
70
|
+
configPath: codexConfigPath,
|
|
71
|
+
notifyCmd,
|
|
72
|
+
notifyOriginalPath,
|
|
73
|
+
configLabel: 'Codex config'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function restoreCodexNotify({ codexConfigPath, notifyOriginalPath, notifyCmd }) {
|
|
78
|
+
return restoreNotify({
|
|
79
|
+
configPath: codexConfigPath,
|
|
80
|
+
notifyOriginalPath,
|
|
81
|
+
expectedNotify: notifyCmd
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function loadCodexNotifyOriginal(notifyOriginalPath) {
|
|
86
|
+
return loadNotifyOriginal(notifyOriginalPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readCodexNotify(codexConfigPath) {
|
|
90
|
+
return readNotify(codexConfigPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function upsertEveryCodeNotify({ codeConfigPath, notifyCmd, notifyOriginalPath }) {
|
|
94
|
+
return upsertNotify({
|
|
95
|
+
configPath: codeConfigPath,
|
|
96
|
+
notifyCmd,
|
|
97
|
+
notifyOriginalPath,
|
|
98
|
+
configLabel: 'Every Code config'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function restoreEveryCodeNotify({ codeConfigPath, notifyOriginalPath, notifyCmd }) {
|
|
103
|
+
return restoreNotify({
|
|
104
|
+
configPath: codeConfigPath,
|
|
105
|
+
notifyOriginalPath,
|
|
106
|
+
expectedNotify: notifyCmd
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadEveryCodeNotifyOriginal(notifyOriginalPath) {
|
|
111
|
+
return loadNotifyOriginal(notifyOriginalPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readEveryCodeNotify(codeConfigPath) {
|
|
115
|
+
return readNotify(codeConfigPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractNotify(text) {
|
|
119
|
+
// Heuristic parse: find a line that starts with "notify =".
|
|
120
|
+
const lines = text.split(/\r?\n/);
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const m = line.match(/^\s*notify\s*=\s*(.+)\s*$/);
|
|
123
|
+
if (m) {
|
|
124
|
+
const rhs = m[1].trim();
|
|
125
|
+
const parsed = parseTomlStringArray(rhs);
|
|
126
|
+
if (parsed) return parsed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setNotify(text, notifyCmd) {
|
|
133
|
+
const lines = text.split(/\r?\n/);
|
|
134
|
+
const notifyLine = `notify = ${formatTomlStringArray(notifyCmd)}`;
|
|
135
|
+
|
|
136
|
+
const out = [];
|
|
137
|
+
let replaced = false;
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i];
|
|
140
|
+
const isNotify = /^\s*notify\s*=/.test(line);
|
|
141
|
+
if (isNotify) {
|
|
142
|
+
if (!replaced) {
|
|
143
|
+
out.push(notifyLine);
|
|
144
|
+
replaced = true;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
out.push(line);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!replaced) {
|
|
152
|
+
// Insert at top-level, before the first table header.
|
|
153
|
+
const firstTableIdx = out.findIndex((l) => /^\s*\[/.test(l));
|
|
154
|
+
const headerIdx = firstTableIdx === -1 ? out.length : firstTableIdx;
|
|
155
|
+
out.splice(headerIdx, 0, notifyLine);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return out.join('\n').replace(/\n+$/, '\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function removeNotify(text) {
|
|
162
|
+
const lines = text.split(/\r?\n/);
|
|
163
|
+
const out = lines.filter((l) => !/^\s*notify\s*=/.test(l));
|
|
164
|
+
return out.join('\n').replace(/\n+$/, '\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseTomlStringArray(rhs) {
|
|
168
|
+
// Minimal parser for ["a", "b"] string arrays.
|
|
169
|
+
// Assumes there are no escapes in strings (good enough for our usage).
|
|
170
|
+
if (!rhs.startsWith('[') || !rhs.endsWith(']')) return null;
|
|
171
|
+
const inner = rhs.slice(1, -1).trim();
|
|
172
|
+
if (!inner) return [];
|
|
173
|
+
|
|
174
|
+
const parts = [];
|
|
175
|
+
let current = '';
|
|
176
|
+
let inString = false;
|
|
177
|
+
let quote = null;
|
|
178
|
+
for (let i = 0; i < inner.length; i++) {
|
|
179
|
+
const ch = inner[i];
|
|
180
|
+
if (!inString) {
|
|
181
|
+
if (ch === '"' || ch === "'") {
|
|
182
|
+
inString = true;
|
|
183
|
+
quote = ch;
|
|
184
|
+
current = '';
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (ch === quote) {
|
|
189
|
+
parts.push(current);
|
|
190
|
+
inString = false;
|
|
191
|
+
quote = null;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
current += ch;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return parts.length > 0 ? parts : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatTomlStringArray(arr) {
|
|
201
|
+
return `[${arr.map((s) => JSON.stringify(String(s))).join(', ')}]`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function arraysEqual(a, b) {
|
|
205
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
206
|
+
if (a.length !== b.length) return false;
|
|
207
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
upsertNotify,
|
|
213
|
+
restoreNotify,
|
|
214
|
+
loadNotifyOriginal,
|
|
215
|
+
readNotify,
|
|
216
|
+
upsertCodexNotify,
|
|
217
|
+
restoreCodexNotify,
|
|
218
|
+
loadCodexNotifyOriginal,
|
|
219
|
+
readCodexNotify,
|
|
220
|
+
upsertEveryCodeNotify,
|
|
221
|
+
restoreEveryCodeNotify,
|
|
222
|
+
loadEveryCodeNotifyOriginal,
|
|
223
|
+
readEveryCodeNotify
|
|
224
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function stripDebugFlag(argv, env = process.env) {
|
|
2
|
+
const filtered = Array.isArray(argv) ? argv.filter((arg) => arg !== '--debug') : [];
|
|
3
|
+
const debugEnv =
|
|
4
|
+
String(env?.VIBEUSAGE_DEBUG || '') === '1' ||
|
|
5
|
+
String(env?.VIBESCORE_DEBUG || '') === '1';
|
|
6
|
+
return { argv: filtered, debug: filtered.length !== (argv || []).length || debugEnv };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { stripDebugFlag };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
|
|
5
|
+
const { readJson } = require('./fs');
|
|
6
|
+
const { readCodexNotify, readEveryCodeNotify } = require('./codex-config');
|
|
7
|
+
const { isClaudeHookConfigured, buildClaudeHookCommand } = require('./claude-config');
|
|
8
|
+
const {
|
|
9
|
+
resolveGeminiConfigDir,
|
|
10
|
+
resolveGeminiSettingsPath,
|
|
11
|
+
buildGeminiHookCommand,
|
|
12
|
+
isGeminiHookConfigured
|
|
13
|
+
} = require('./gemini-config');
|
|
14
|
+
const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require('./opencode-config');
|
|
15
|
+
const { normalizeState: normalizeUploadState } = require('./upload-throttle');
|
|
16
|
+
const { resolveTrackerPaths } = require('./tracker-paths');
|
|
17
|
+
|
|
18
|
+
async function collectTrackerDiagnostics({
|
|
19
|
+
home = os.homedir(),
|
|
20
|
+
codexHome = process.env.CODEX_HOME || path.join(home, '.codex'),
|
|
21
|
+
codeHome = process.env.CODE_HOME || path.join(home, '.code')
|
|
22
|
+
} = {}) {
|
|
23
|
+
const { trackerDir, binDir } = await resolveTrackerPaths({ home, migrate: true });
|
|
24
|
+
const configPath = path.join(trackerDir, 'config.json');
|
|
25
|
+
const queuePath = path.join(trackerDir, 'queue.jsonl');
|
|
26
|
+
const queueStatePath = path.join(trackerDir, 'queue.state.json');
|
|
27
|
+
const cursorsPath = path.join(trackerDir, 'cursors.json');
|
|
28
|
+
const notifySignalPath = path.join(trackerDir, 'notify.signal');
|
|
29
|
+
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
30
|
+
const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
|
|
31
|
+
const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
|
|
32
|
+
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
33
|
+
const codeConfigPath = path.join(codeHome, 'config.toml');
|
|
34
|
+
const claudeConfigPath = path.join(home, '.claude', 'settings.json');
|
|
35
|
+
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
36
|
+
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
37
|
+
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
38
|
+
|
|
39
|
+
const config = await readJson(configPath);
|
|
40
|
+
const cursors = await readJson(cursorsPath);
|
|
41
|
+
const queueState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
42
|
+
const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
|
|
43
|
+
const autoRetry = await readJson(autoRetryPath);
|
|
44
|
+
|
|
45
|
+
const queueSize = await safeStatSize(queuePath);
|
|
46
|
+
const offsetBytes = Number(queueState.offset || 0);
|
|
47
|
+
const pendingBytes = Math.max(0, queueSize - offsetBytes);
|
|
48
|
+
|
|
49
|
+
const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
|
|
50
|
+
const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
|
|
51
|
+
|
|
52
|
+
const codexNotifyRaw = await readCodexNotify(codexConfigPath);
|
|
53
|
+
const notifyConfigured = Array.isArray(codexNotifyRaw) && codexNotifyRaw.length > 0;
|
|
54
|
+
const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
|
|
55
|
+
const everyCodeNotifyRaw = await readEveryCodeNotify(codeConfigPath);
|
|
56
|
+
const everyCodeConfigured = Array.isArray(everyCodeNotifyRaw) && everyCodeNotifyRaw.length > 0;
|
|
57
|
+
const everyCodeNotify = everyCodeConfigured ? everyCodeNotifyRaw.map((v) => redactValue(v, home)) : null;
|
|
58
|
+
const claudeHookCommand = buildClaudeHookCommand(path.join(binDir, 'notify.cjs'));
|
|
59
|
+
const claudeHookConfigured = await isClaudeHookConfigured({
|
|
60
|
+
settingsPath: claudeConfigPath,
|
|
61
|
+
hookCommand: claudeHookCommand
|
|
62
|
+
});
|
|
63
|
+
const geminiHookCommand = buildGeminiHookCommand(path.join(binDir, 'notify.cjs'));
|
|
64
|
+
const geminiHookConfigured = await isGeminiHookConfigured({
|
|
65
|
+
settingsPath: geminiSettingsPath,
|
|
66
|
+
hookCommand: geminiHookCommand
|
|
67
|
+
});
|
|
68
|
+
const opencodePluginConfigured = await isOpencodePluginInstalled({ configDir: opencodeConfigDir });
|
|
69
|
+
|
|
70
|
+
const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
|
|
71
|
+
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
version: 1,
|
|
76
|
+
generated_at: new Date().toISOString(),
|
|
77
|
+
env: {
|
|
78
|
+
node: process.version,
|
|
79
|
+
platform: process.platform,
|
|
80
|
+
arch: process.arch
|
|
81
|
+
},
|
|
82
|
+
paths: {
|
|
83
|
+
tracker_dir: redactValue(trackerDir, home),
|
|
84
|
+
codex_home: redactValue(codexHome, home),
|
|
85
|
+
codex_config: redactValue(codexConfigPath, home),
|
|
86
|
+
code_home: redactValue(codeHome, home),
|
|
87
|
+
code_config: redactValue(codeConfigPath, home),
|
|
88
|
+
claude_config: redactValue(claudeConfigPath, home),
|
|
89
|
+
gemini_config: redactValue(geminiSettingsPath, home),
|
|
90
|
+
opencode_config: redactValue(opencodeConfigDir, home)
|
|
91
|
+
},
|
|
92
|
+
config: {
|
|
93
|
+
base_url: typeof config?.baseUrl === 'string' ? config.baseUrl : null,
|
|
94
|
+
device_token: config?.deviceToken ? 'set' : 'unset',
|
|
95
|
+
device_id: maskId(config?.deviceId),
|
|
96
|
+
installed_at: typeof config?.installedAt === 'string' ? config.installedAt : null
|
|
97
|
+
},
|
|
98
|
+
parse: {
|
|
99
|
+
updated_at: typeof cursors?.updatedAt === 'string' ? cursors.updatedAt : null,
|
|
100
|
+
file_count: cursors?.files && typeof cursors.files === 'object' ? Object.keys(cursors.files).length : null
|
|
101
|
+
},
|
|
102
|
+
queue: {
|
|
103
|
+
size_bytes: queueSize,
|
|
104
|
+
offset_bytes: offsetBytes,
|
|
105
|
+
pending_bytes: pendingBytes,
|
|
106
|
+
updated_at: typeof queueState.updatedAt === 'string' ? queueState.updatedAt : null
|
|
107
|
+
},
|
|
108
|
+
notify: {
|
|
109
|
+
last_notify: lastNotify,
|
|
110
|
+
last_notify_triggered_sync: lastNotifySpawn,
|
|
111
|
+
codex_notify_configured: notifyConfigured,
|
|
112
|
+
codex_notify: codexNotify,
|
|
113
|
+
every_code_notify_configured: everyCodeConfigured,
|
|
114
|
+
every_code_notify: everyCodeNotify,
|
|
115
|
+
claude_hook_configured: claudeHookConfigured,
|
|
116
|
+
gemini_hook_configured: geminiHookConfigured,
|
|
117
|
+
opencode_plugin_configured: opencodePluginConfigured
|
|
118
|
+
},
|
|
119
|
+
upload: {
|
|
120
|
+
last_success_at: lastSuccessAt,
|
|
121
|
+
next_allowed_after: parseEpochMsToIso(uploadThrottle.nextAllowedAtMs || null),
|
|
122
|
+
backoff_until: parseEpochMsToIso(uploadThrottle.backoffUntilMs || null),
|
|
123
|
+
last_error: uploadThrottle.lastError
|
|
124
|
+
? {
|
|
125
|
+
at: uploadThrottle.lastErrorAt || null,
|
|
126
|
+
message: redactError(String(uploadThrottle.lastError), home)
|
|
127
|
+
}
|
|
128
|
+
: null
|
|
129
|
+
},
|
|
130
|
+
auto_retry: autoRetryAt
|
|
131
|
+
? {
|
|
132
|
+
next_retry_at: autoRetryAt,
|
|
133
|
+
reason: typeof autoRetry?.reason === 'string' ? autoRetry.reason : null,
|
|
134
|
+
pending_bytes: Number.isFinite(Number(autoRetry?.pendingBytes))
|
|
135
|
+
? Math.max(0, Number(autoRetry.pendingBytes))
|
|
136
|
+
: null,
|
|
137
|
+
scheduled_at: typeof autoRetry?.scheduledAt === 'string' ? autoRetry.scheduledAt : null,
|
|
138
|
+
source: typeof autoRetry?.source === 'string' ? autoRetry.source : null
|
|
139
|
+
}
|
|
140
|
+
: null
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function maskId(v) {
|
|
145
|
+
if (typeof v !== 'string') return null;
|
|
146
|
+
const s = v.trim();
|
|
147
|
+
if (s.length < 12) return null;
|
|
148
|
+
return `${s.slice(0, 8)}…${s.slice(-4)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function redactValue(value, home) {
|
|
152
|
+
if (typeof value !== 'string') return value;
|
|
153
|
+
if (typeof home !== 'string' || home.length === 0) return value;
|
|
154
|
+
const homeNorm = home.endsWith(path.sep) ? home.slice(0, -1) : home;
|
|
155
|
+
return value.startsWith(homeNorm) ? `~${value.slice(homeNorm.length)}` : value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function redactError(message, home) {
|
|
159
|
+
if (typeof message !== 'string') return message;
|
|
160
|
+
if (typeof home !== 'string' || home.length === 0) return message;
|
|
161
|
+
const homeNorm = home.endsWith(path.sep) ? home.slice(0, -1) : home;
|
|
162
|
+
return message.split(homeNorm).join('~');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function safeStatSize(p) {
|
|
166
|
+
try {
|
|
167
|
+
const st = await fs.stat(p);
|
|
168
|
+
return st && st.isFile() ? st.size : 0;
|
|
169
|
+
} catch (_e) {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function safeReadText(p) {
|
|
175
|
+
try {
|
|
176
|
+
return await fs.readFile(p, 'utf8');
|
|
177
|
+
} catch (_e) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseEpochMsToIso(v) {
|
|
183
|
+
const ms = Number(v);
|
|
184
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
185
|
+
const d = new Date(ms);
|
|
186
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
187
|
+
return d.toISOString();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { collectTrackerDiagnostics };
|
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
async function ensureDir(p) {
|
|
5
|
+
await fs.mkdir(p, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function writeFileAtomic(filePath, content) {
|
|
9
|
+
const dir = path.dirname(filePath);
|
|
10
|
+
await ensureDir(dir);
|
|
11
|
+
const tmp = `${filePath}.tmp.${Date.now()}`;
|
|
12
|
+
await fs.writeFile(tmp, content, { encoding: 'utf8' });
|
|
13
|
+
await fs.rename(tmp, filePath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readJson(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
} catch (_e) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function writeJson(filePath, obj) {
|
|
26
|
+
await writeFileAtomic(filePath, JSON.stringify(obj, null, 2) + '\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function chmod600IfPossible(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
await fs.chmod(filePath, 0o600);
|
|
32
|
+
} catch (_e) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function openLock(lockPath, { quietIfLocked }) {
|
|
36
|
+
try {
|
|
37
|
+
const handle = await fs.open(lockPath, 'wx');
|
|
38
|
+
return {
|
|
39
|
+
async release() {
|
|
40
|
+
await handle.close().catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e && e.code === 'EEXIST') {
|
|
45
|
+
if (!quietIfLocked) {
|
|
46
|
+
process.stdout.write('Another sync is already running.\n');
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
ensureDir,
|
|
56
|
+
writeFileAtomic,
|
|
57
|
+
readJson,
|
|
58
|
+
writeJson,
|
|
59
|
+
chmod600IfPossible,
|
|
60
|
+
openLock
|
|
61
|
+
};
|
|
62
|
+
|