@vibe-cafe/vibe-usage 0.14.0 → 0.14.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/package.json +1 -1
- package/src/config.js +4 -0
- package/src/init.js +10 -1
- package/src/opencode-roots.js +12 -1
- package/src/parsers/amp.js +9 -1
- package/src/parsers/codex.js +3 -1
- package/src/parsers/qwen-code.js +3 -1
- package/src/path-repair.js +27 -0
- package/src/state.js +4 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, chmodSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { moveDirectoryOutOfFilePath } from './path-repair.js';
|
|
4
5
|
|
|
5
6
|
// VIBE_USAGE_CONFIG_DIR overrides the dir (test hook).
|
|
6
7
|
const CONFIG_DIR = process.env.VIBE_USAGE_CONFIG_DIR?.trim() || join(homedir(), '.vibe-usage');
|
|
@@ -22,6 +23,9 @@ export function loadConfig() {
|
|
|
22
23
|
|
|
23
24
|
export function saveConfig(config) {
|
|
24
25
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
// A directory sitting where the config file belongs would make this write
|
|
27
|
+
// throw EISDIR forever; move it aside so the run can save.
|
|
28
|
+
moveDirectoryOutOfFilePath(CONFIG_FILE);
|
|
25
29
|
// The file holds the vbu_ API key — never leave it group/world-readable.
|
|
26
30
|
// mode only applies at file creation, so chmod explicitly for pre-existing
|
|
27
31
|
// files written before this hardening.
|
package/src/init.js
CHANGED
|
@@ -21,7 +21,16 @@ function prompt(question) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function openBrowser(url) {
|
|
24
|
-
|
|
24
|
+
// `start` is a cmd.exe builtin, not an executable: execFile('start', …) fails
|
|
25
|
+
// outright on Windows, so the device-flow login never opened a tab and the
|
|
26
|
+
// user was left staring at the code with no browser to type it into. Go
|
|
27
|
+
// through cmd, and escape a bare `&` (present in verificationUriComplete's
|
|
28
|
+
// query string) or cmd would read it as a command separator.
|
|
29
|
+
if (platform() === 'win32') {
|
|
30
|
+
execFile('cmd', ['/c', 'start', '', url.replace(/&/g, '^&')], { windowsHide: true }, () => {});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const cmds = { darwin: 'open', linux: 'xdg-open' };
|
|
25
34
|
const cmd = cmds[platform()] || cmds.linux;
|
|
26
35
|
// Use execFile with args array to avoid shell injection via VIBE_USAGE_API_URL
|
|
27
36
|
execFile(cmd, [url], () => {});
|
package/src/opencode-roots.js
CHANGED
|
@@ -24,10 +24,21 @@ export function openCodeStore(root) {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// XDG first, then the Windows per-user store. A root that does not resolve is
|
|
28
|
+
// skipped by the caller, so probing costs nothing when it is absent.
|
|
29
|
+
function defaultOpenCodeRoots() {
|
|
30
|
+
const xdg = join(homedir(), '.local', 'share', 'opencode');
|
|
31
|
+
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
32
|
+
if (process.platform === 'win32' && localAppData) {
|
|
33
|
+
return [xdg, join(localAppData, 'opencode')];
|
|
34
|
+
}
|
|
35
|
+
return [xdg];
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function getOpenCodeStores({ extraRoots = [], onWarning = () => {} } = {}) {
|
|
28
39
|
const override = process.env.VIBE_USAGE_OPENCODE_DIRS?.trim();
|
|
29
40
|
const defaults = override ? override.split(delimiter).map(p => p.trim()).filter(Boolean)
|
|
30
|
-
:
|
|
41
|
+
: defaultOpenCodeRoots();
|
|
31
42
|
const seen = new Set(), stores = [];
|
|
32
43
|
for (const root of [...defaults, ...extraRoots]) {
|
|
33
44
|
try {
|
package/src/parsers/amp.js
CHANGED
|
@@ -3,9 +3,17 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Threads directory, XDG first and %LOCALAPPDATA% on Windows. Exported so the
|
|
7
|
+
* Windows branch can be exercised from a POSIX test host. */
|
|
8
|
+
export function resolveThreadsDir() {
|
|
7
9
|
if (process.env.AMP_DATA_DIR) return process.env.AMP_DATA_DIR;
|
|
8
10
|
if (process.env.XDG_DATA_HOME) return join(process.env.XDG_DATA_HOME, 'amp', 'threads');
|
|
11
|
+
// Amp on Windows keeps its threads under %LOCALAPPDATA%; without this the
|
|
12
|
+
// source silently collected nothing there.
|
|
13
|
+
if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
|
|
14
|
+
const winDir = join(process.env.LOCALAPPDATA, 'amp', 'threads');
|
|
15
|
+
if (existsSync(winDir)) return winDir;
|
|
16
|
+
}
|
|
9
17
|
return join(homedir(), '.local', 'share', 'amp', 'threads');
|
|
10
18
|
}
|
|
11
19
|
|
package/src/parsers/codex.js
CHANGED
|
@@ -104,7 +104,9 @@ function extractProject(meta) {
|
|
|
104
104
|
const match = meta.git.repository_url.match(/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
105
105
|
if (match) return match[1];
|
|
106
106
|
}
|
|
107
|
-
|
|
107
|
+
// Windows reports cwd with backslashes, so splitting on '/' alone yields the
|
|
108
|
+
// whole path as the "project".
|
|
109
|
+
if (meta.cwd) return meta.cwd.split(/[\\/]/).pop() || 'unknown';
|
|
108
110
|
return 'unknown';
|
|
109
111
|
}
|
|
110
112
|
|
package/src/parsers/qwen-code.js
CHANGED
|
@@ -40,7 +40,9 @@ function findSessionFiles(baseDir) {
|
|
|
40
40
|
|
|
41
41
|
function extractProject(cwd, filePath) {
|
|
42
42
|
if (cwd) {
|
|
43
|
-
|
|
43
|
+
// Windows reports cwd with backslashes; split on both separators so the
|
|
44
|
+
// project is the last path segment rather than the whole path.
|
|
45
|
+
const parts = cwd.split(/[\\/]/).filter(Boolean);
|
|
44
46
|
if (parts.length > 0) return parts[parts.length - 1];
|
|
45
47
|
}
|
|
46
48
|
const tmpPrefix = QWEN_TMP_DIR + sep;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { statSync, renameSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Move a directory that is occupying a path we need as a file.
|
|
5
|
+
*
|
|
6
|
+
* Both the config file and the state file are written by this CLI, and a
|
|
7
|
+
* customer hit a layout where the *file* path had become a directory (the store
|
|
8
|
+
* was created by a different tool, or an earlier run was interrupted between
|
|
9
|
+
* mkdir and open). `writeFileSync` then fails with EISDIR and the CLI is stuck:
|
|
10
|
+
* every later run reads nothing, and the save that would fix it is the one that
|
|
11
|
+
* throws.
|
|
12
|
+
*
|
|
13
|
+
* Renaming it aside keeps whatever the user's data was — never delete it, the
|
|
14
|
+
* directory may hold real content — while letting the current run write a fresh
|
|
15
|
+
* file. Absence is not a repair, so ENOENT stays silent; anything else (a
|
|
16
|
+
* permission error, a read-only volume) is the caller's to report rather than
|
|
17
|
+
* something to swallow.
|
|
18
|
+
*/
|
|
19
|
+
export function moveDirectoryOutOfFilePath(path) {
|
|
20
|
+
try {
|
|
21
|
+
if (statSync(path).isDirectory()) {
|
|
22
|
+
renameSync(path, `${path}.directory-backup-${Date.now()}`);
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/state.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, existsSync, renameS
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash, randomBytes } from 'node:crypto';
|
|
5
|
+
import { moveDirectoryOutOfFilePath } from './path-repair.js';
|
|
5
6
|
|
|
6
7
|
// Persisted sync state, kept next to config.js's files (same dir + dev split).
|
|
7
8
|
// Maps a stable item key -> hash of its mutable fields, recording what we have
|
|
@@ -79,6 +80,9 @@ export function loadState(identity) {
|
|
|
79
80
|
|
|
80
81
|
export function saveState(state, identity) {
|
|
81
82
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
83
|
+
// Same EISDIR repair as config: a directory where the state file belongs
|
|
84
|
+
// would block every save, and with it every incremental sync.
|
|
85
|
+
moveDirectoryOutOfFilePath(STATE_FILE);
|
|
82
86
|
// Only the durable fields are written: `identityChanged` is loadState()'s
|
|
83
87
|
// one-run signal, not state. A CLI older than 0.11.1 reads just
|
|
84
88
|
// buckets/sessions, so the extra top-level `identity` key is ignored there —
|