@vibe-cafe/vibe-usage 0.10.14 → 0.10.15
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/README.md +36 -4
- package/package.json +1 -1
- package/src/index.js +21 -1
- package/src/init.js +36 -5
- package/src/reset.js +14 -7
- package/src/sync.js +143 -43
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Track your AI coding tool token usage and sync to [vibecafe.ai](https://vibecafe
|
|
|
8
8
|
npx @vibe-cafe/vibe-usage
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
That's it.
|
|
11
|
+
That's it. On first setup, the CLI asks whether project names and the device name may leave the machine (both default to **No**), then opens [vibecafe.ai/usage/device](https://vibecafe.ai/usage/device) in your browser. Sign in, confirm the verification code shown in your terminal, click 「确认链接」, and the CLI receives an API key automatically.
|
|
12
12
|
|
|
13
13
|
After approval, it will:
|
|
14
|
-
1. Save your API key to `~/.vibe-usage/config.json`
|
|
14
|
+
1. Save your API key and local privacy choices to `~/.vibe-usage/config.json`
|
|
15
15
|
2. Detect installed AI coding tools
|
|
16
16
|
3. Run an initial sync of your usage data
|
|
17
17
|
4. Prompt you to enable the background daemon for continuous syncing (recommended)
|
|
@@ -32,6 +32,8 @@ npx @vibe-cafe/vibe-usage init # Re-run setup via browser login
|
|
|
32
32
|
npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> # Skip browser, use pre-issued key (CI/headless)
|
|
33
33
|
npx @vibe-cafe/vibe-usage sync # Manual sync
|
|
34
34
|
npx @vibe-cafe/vibe-usage sync --extra-codex-home /path/to/.codex # Add another Codex Home for this run only
|
|
35
|
+
npx @vibe-cafe/vibe-usage config set uploadProject false # Never upload project names
|
|
36
|
+
npx @vibe-cafe/vibe-usage config set uploadHostname false # Use an opaque per-install device id
|
|
35
37
|
npx @vibe-cafe/vibe-usage summary # Print last 7 days as markdown (cost / tokens / by model / by project)
|
|
36
38
|
npx @vibe-cafe/vibe-usage summary --days N # Same, over the last N days (1-90)
|
|
37
39
|
npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 30m, foreground)
|
|
@@ -155,10 +157,40 @@ Config stored at `~/.vibe-usage/config.json` (dev: `config.dev.json`).
|
|
|
155
157
|
|-----|-------------|
|
|
156
158
|
| `apiKey` | Your API key (starts with `vbu_`) |
|
|
157
159
|
| `apiUrl` | Server URL (default: `https://vibecafe.ai`) |
|
|
158
|
-
| `hostname` | Stable device name
|
|
160
|
+
| `hostname` | Stable device name or user-chosen alias; stays local when `uploadHostname=false` |
|
|
161
|
+
| `uploadProject` | Local project-name control. `false` always wins over the Web setting |
|
|
162
|
+
| `uploadHostname` | Local device-name control. `false` replaces the name at the final network boundary |
|
|
163
|
+
| `deviceId` | Generated opaque per-install identity used when `uploadHostname=false` |
|
|
159
164
|
| `codexExtraHome` | Optional additional Codex Home scanned together with `$CODEX_HOME` / `~/.codex` |
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
New setups default both local upload controls to `false`. Existing configs without
|
|
167
|
+
these keys retain their previous behavior until you choose a value: the Web
|
|
168
|
+
project-name setting remains authoritative, and the configured device name is
|
|
169
|
+
uploaded.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Local false cannot be overridden by a later Web setting.
|
|
173
|
+
npx @vibe-cafe/vibe-usage config set uploadProject false
|
|
174
|
+
|
|
175
|
+
# Replaces the device name in buckets, sessions, and sync metadata with a
|
|
176
|
+
# persistent random id such as device-0011223344556677.
|
|
177
|
+
npx @vibe-cafe/vibe-usage config set uploadHostname false
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The sanitization happens after every parser and before hashing or HTTP
|
|
181
|
+
serialization. `cursor-cloud` remains a fixed, non-identifying sentinel so
|
|
182
|
+
Cursor account exports still deduplicate across computers.
|
|
183
|
+
|
|
184
|
+
These controls prevent future transmissions; they do not silently delete data
|
|
185
|
+
already stored in the cloud. To remove previously uploaded identifiers, run
|
|
186
|
+
`vibe-usage reset` after enabling both controls. A full reset deletes the
|
|
187
|
+
account's existing usage before re-uploading the logs available on this
|
|
188
|
+
computer, so coordinate first if the account syncs multiple computers.
|
|
189
|
+
|
|
190
|
+
When device-name upload is enabled, `hostname` is captured once during `init`
|
|
191
|
+
and reused for all future syncs. This prevents macOS mDNS hostname changes
|
|
192
|
+
(for example, `MacBook-Pro` → `MacBook-Pro-2`) from creating duplicate device
|
|
193
|
+
entries. It can also be set to a non-identifying alias:
|
|
162
194
|
|
|
163
195
|
```bash
|
|
164
196
|
npx @vibe-cafe/vibe-usage config set hostname my-device-name
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -24,6 +24,8 @@ async function showStatus() {
|
|
|
24
24
|
if (config.codexExtraHome) {
|
|
25
25
|
console.log(` Extra Codex Home: ${config.codexExtraHome}`);
|
|
26
26
|
}
|
|
27
|
+
console.log(` Project names: ${config.uploadProject === false ? 'hidden locally' : 'server setting'}`);
|
|
28
|
+
console.log(` Device name: ${config.uploadHostname === false ? 'anonymous device id' : 'uploaded'}`);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
console.log('\n Detected tools:');
|
|
@@ -48,7 +50,15 @@ async function showStatus() {
|
|
|
48
50
|
console.log();
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
const
|
|
53
|
+
const BOOLEAN_CONFIG_KEYS = new Set(['uploadProject', 'uploadHostname']);
|
|
54
|
+
const VALID_CONFIG_KEYS = [
|
|
55
|
+
'apiKey',
|
|
56
|
+
'apiUrl',
|
|
57
|
+
'hostname',
|
|
58
|
+
'uploadProject',
|
|
59
|
+
'uploadHostname',
|
|
60
|
+
'codexExtraHome',
|
|
61
|
+
];
|
|
52
62
|
|
|
53
63
|
function handleConfig(args) {
|
|
54
64
|
const sub = args[0];
|
|
@@ -81,6 +91,14 @@ function handleConfig(args) {
|
|
|
81
91
|
console.error(`Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
82
92
|
process.exit(1);
|
|
83
93
|
}
|
|
94
|
+
if (BOOLEAN_CONFIG_KEYS.has(key)) {
|
|
95
|
+
const normalized = value.toLowerCase();
|
|
96
|
+
if (normalized !== 'true' && normalized !== 'false') {
|
|
97
|
+
console.error(`${key} must be true or false.`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
value = normalized === 'true';
|
|
101
|
+
}
|
|
84
102
|
if (key === 'codexExtraHome' && value !== '') {
|
|
85
103
|
const validation = validateExtraCodexHome(value);
|
|
86
104
|
if (!validation.ok) {
|
|
@@ -233,6 +251,8 @@ export async function run(rawArgs) {
|
|
|
233
251
|
npx @vibe-cafe/vibe-usage config get <key> Get a config value
|
|
234
252
|
npx @vibe-cafe/vibe-usage config set <key> <value> Set a config value
|
|
235
253
|
npx @vibe-cafe/vibe-usage config set codexExtraHome <path> Persist another Codex Home
|
|
254
|
+
npx @vibe-cafe/vibe-usage config set uploadProject false Never upload project names
|
|
255
|
+
npx @vibe-cafe/vibe-usage config set uploadHostname false Replace the device name with an anonymous id
|
|
236
256
|
npx @vibe-cafe/vibe-usage help Show this help
|
|
237
257
|
`);
|
|
238
258
|
break;
|
package/src/init.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { platform } from 'node:os';
|
|
4
4
|
import { loadConfig, saveConfig } from './config.js';
|
|
5
5
|
import { ingest, requestDeviceCode, pollDeviceCode } from './api.js';
|
|
6
|
-
import { runSync } from './sync.js';
|
|
6
|
+
import { resolveOptionalBoolean, resolveSyncHostname, runSync } from './sync.js';
|
|
7
7
|
import { detectInstalledTools } from './tools.js';
|
|
8
8
|
import { bigHeader, success, failure, warn, arrow, link, dim, divider } from './output.js';
|
|
9
9
|
|
|
@@ -30,6 +30,14 @@ function isDaemonPlatform() {
|
|
|
30
30
|
return process.platform === 'linux' || process.platform === 'darwin';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
async function resolvePrivacyChoice(existingValue, key, question) {
|
|
34
|
+
const configured = resolveOptionalBoolean(existingValue, key);
|
|
35
|
+
if (configured !== undefined) return configured;
|
|
36
|
+
if (!process.stdin.isTTY) return false;
|
|
37
|
+
const answer = (await prompt(question)).toLowerCase();
|
|
38
|
+
return answer === 'y' || answer === 'yes';
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export async function runInit(options = {}) {
|
|
34
42
|
const { apiKey: providedKey, codexExtraHome } = options;
|
|
35
43
|
|
|
@@ -51,7 +59,31 @@ export async function runInit(options = {}) {
|
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
const apiUrl = process.env.VIBE_USAGE_API_URL || 'https://vibecafe.ai';
|
|
54
|
-
|
|
62
|
+
let uploadProject;
|
|
63
|
+
let uploadHostname;
|
|
64
|
+
let draftConfig;
|
|
65
|
+
let host;
|
|
66
|
+
try {
|
|
67
|
+
uploadProject = await resolvePrivacyChoice(
|
|
68
|
+
existing?.uploadProject,
|
|
69
|
+
'uploadProject',
|
|
70
|
+
'上传项目名以查看按项目统计?项目名可能包含客户或内部代号。 [y/N] ',
|
|
71
|
+
);
|
|
72
|
+
uploadHostname = await resolvePrivacyChoice(
|
|
73
|
+
existing?.uploadHostname,
|
|
74
|
+
'uploadHostname',
|
|
75
|
+
'上传设备名以区分电脑?选择否将使用匿名设备 ID。 [y/N] ',
|
|
76
|
+
);
|
|
77
|
+
draftConfig = {
|
|
78
|
+
...(existing || {}),
|
|
79
|
+
uploadProject,
|
|
80
|
+
uploadHostname,
|
|
81
|
+
};
|
|
82
|
+
host = resolveSyncHostname(draftConfig).hostname;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(failure(err.message));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
55
87
|
|
|
56
88
|
let apiKey;
|
|
57
89
|
if (providedKey) {
|
|
@@ -77,10 +109,9 @@ export async function runInit(options = {}) {
|
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
const config = {
|
|
112
|
+
...draftConfig,
|
|
80
113
|
apiKey,
|
|
81
114
|
apiUrl,
|
|
82
|
-
hostname: host,
|
|
83
|
-
...(existing?.codexExtraHome ? { codexExtraHome: existing.codexExtraHome } : {}),
|
|
84
115
|
};
|
|
85
116
|
saveConfig(config);
|
|
86
117
|
|
package/src/reset.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
-
import {
|
|
3
|
-
import { loadConfig } from './config.js';
|
|
2
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
4
3
|
import { deleteAllData } from './api.js';
|
|
5
|
-
import { runSync } from './sync.js';
|
|
4
|
+
import { resolveSyncHostname, runSync } from './sync.js';
|
|
6
5
|
import { clearState } from './state.js';
|
|
7
6
|
import { success, failure, arrow, link, dim } from './output.js';
|
|
8
7
|
|
|
@@ -34,10 +33,18 @@ export async function runReset(args = [], deps = {}) {
|
|
|
34
33
|
process.exit(1);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
// Target the
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
36
|
+
// Target the exact privacy-safe identity sync.js uploads under. This keeps
|
|
37
|
+
// `reset --local` aligned with both the stable configured hostname and the
|
|
38
|
+
// anonymous per-install device id used when hostname upload is disabled.
|
|
39
|
+
let hostIdentity;
|
|
40
|
+
try {
|
|
41
|
+
hostIdentity = resolveSyncHostname(config);
|
|
42
|
+
if (hostIdentity.changed) saveConfig(config);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(failure(err.message));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const currentHost = hostIdentity.hostname;
|
|
41
48
|
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
42
49
|
|
|
43
50
|
if (hostOnly) {
|
package/src/sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hostname as osHostname } from 'node:os';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
2
3
|
import { loadConfig, saveConfig } from './config.js';
|
|
3
4
|
import {
|
|
4
5
|
loadState, saveState, pruneState,
|
|
@@ -14,13 +15,83 @@ import { success, failure, warn, arrow, link, dim } from './output.js';
|
|
|
14
15
|
const BATCH_SIZE = 100;
|
|
15
16
|
const SESSION_BATCH_SIZE = 500;
|
|
16
17
|
|
|
18
|
+
const ANONYMOUS_DEVICE_ID_PATTERN = /^device-[0-9a-f]{16}$/;
|
|
19
|
+
const SHARED_HOSTNAME_SENTINELS = new Set(['cursor-cloud']);
|
|
20
|
+
|
|
21
|
+
export function resolveOptionalBoolean(value, key) {
|
|
22
|
+
if (value === undefined) return undefined;
|
|
23
|
+
if (typeof value === 'boolean') return value;
|
|
24
|
+
const error = new Error(`配置 ${key} 必须是 true 或 false。`);
|
|
25
|
+
error.code = 'INVALID_CONFIG';
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveSyncHostname(config, {
|
|
30
|
+
systemHostname = () => osHostname().replace(/\.local$/, ''),
|
|
31
|
+
createDeviceId = () => `device-${randomBytes(8).toString('hex')}`,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const uploadHostname = resolveOptionalBoolean(config.uploadHostname, 'uploadHostname') ?? true;
|
|
34
|
+
const previousHostname = typeof config.hostname === 'string' && config.hostname.trim()
|
|
35
|
+
? config.hostname.trim()
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
if (!uploadHostname) {
|
|
39
|
+
const existingDeviceId = typeof config.deviceId === 'string'
|
|
40
|
+
? config.deviceId.trim().toLowerCase()
|
|
41
|
+
: '';
|
|
42
|
+
const hostname = ANONYMOUS_DEVICE_ID_PATTERN.test(existingDeviceId)
|
|
43
|
+
? existingDeviceId
|
|
44
|
+
: createDeviceId();
|
|
45
|
+
const changed = config.deviceId !== hostname;
|
|
46
|
+
if (changed) config.deviceId = hostname;
|
|
47
|
+
return { hostname, previousHostname, uploadHostname, changed };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hostname = previousHostname || systemHostname();
|
|
51
|
+
const changed = config.hostname !== hostname;
|
|
52
|
+
if (changed) config.hostname = hostname;
|
|
53
|
+
return { hostname, previousHostname, uploadHostname, changed };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function applyHostnamePrivacy(records, hostname, uploadHostname) {
|
|
57
|
+
for (const record of records) {
|
|
58
|
+
if (uploadHostname) {
|
|
59
|
+
if (!record.hostname) record.hostname = hostname;
|
|
60
|
+
} else if (!SHARED_HOSTNAME_SENTINELS.has(record.hostname)) {
|
|
61
|
+
record.hostname = hostname;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A hostname privacy toggle changes the server bucket key. Carry unchanged
|
|
67
|
+
// local state across that key change so enabling privacy does not re-upload
|
|
68
|
+
// all historical buckets beside their older server rows. Changed buckets still
|
|
69
|
+
// upload under the anonymous id; `reset` remains the explicit way to remove
|
|
70
|
+
// identifiers that were uploaded before the local control was enabled.
|
|
71
|
+
export function migrateHiddenHostnameState(state, buckets, previousHostname, hostname) {
|
|
72
|
+
if (!previousHostname || previousHostname === hostname) return false;
|
|
73
|
+
let changed = false;
|
|
74
|
+
for (const bucket of buckets) {
|
|
75
|
+
if (bucket.hostname !== hostname) continue;
|
|
76
|
+
const oldKey = bucketKey({ ...bucket, hostname: previousHostname });
|
|
77
|
+
const newKey = bucketKey(bucket);
|
|
78
|
+
const currentHash = bucketHash(bucket);
|
|
79
|
+
if (state.buckets[oldKey] !== currentHash) continue;
|
|
80
|
+
if (!(newKey in state.buckets)) state.buckets[newKey] = currentHash;
|
|
81
|
+
delete state.buckets[oldKey];
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
return changed;
|
|
85
|
+
}
|
|
86
|
+
|
|
17
87
|
function formatBytes(bytes) {
|
|
18
88
|
if (bytes < 1024) return `${bytes}B`;
|
|
19
89
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
20
90
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
21
91
|
}
|
|
22
92
|
|
|
23
|
-
export function resolveUploadProjectSetting(settings) {
|
|
93
|
+
export function resolveUploadProjectSetting(settings, localSetting) {
|
|
94
|
+
if (localSetting === false) return false;
|
|
24
95
|
if (typeof settings?.uploadProject !== 'boolean') {
|
|
25
96
|
const error = new Error('SETTINGS_UNAVAILABLE');
|
|
26
97
|
error.code = 'SETTINGS_UNAVAILABLE';
|
|
@@ -90,6 +161,18 @@ export async function runSync({
|
|
|
90
161
|
saveConfig(config);
|
|
91
162
|
}
|
|
92
163
|
|
|
164
|
+
let localUploadProject;
|
|
165
|
+
let hostIdentity;
|
|
166
|
+
try {
|
|
167
|
+
localUploadProject = resolveOptionalBoolean(config.uploadProject, 'uploadProject');
|
|
168
|
+
hostIdentity = resolveSyncHostname(config);
|
|
169
|
+
if (hostIdentity.changed) saveConfig(config);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(failure(err.message));
|
|
172
|
+
if (throws) throw err;
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
93
176
|
// Privacy is a required input, not an optional hint. If the settings API is
|
|
94
177
|
// unavailable, treating it as `false` changes every project-bearing item's
|
|
95
178
|
// incremental identity to `unknown` and can trigger a full-history upload.
|
|
@@ -97,36 +180,45 @@ export async function runSync({
|
|
|
97
180
|
// no-op: no data upload and no state mutation.
|
|
98
181
|
const apiUrl = config.apiUrl || 'https://vibecafe.ai';
|
|
99
182
|
let uploadProject;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
183
|
+
if (localUploadProject === false) {
|
|
184
|
+
// A local deny is authoritative and needs no server round trip. This is
|
|
185
|
+
// both fail-closed and usable when the settings endpoint is unavailable.
|
|
186
|
+
uploadProject = false;
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
const settings = await fetchSettings(apiUrl, config.apiKey);
|
|
190
|
+
uploadProject = resolveUploadProjectSetting(settings, localUploadProject);
|
|
191
|
+
// Scope the cached privacy choice to the server that returned it. Reusing
|
|
192
|
+
// the value after `apiUrl` changes could expose project names to a
|
|
193
|
+
// different server during its first settings outage.
|
|
194
|
+
if (
|
|
195
|
+
config.lastUploadProject !== uploadProject
|
|
196
|
+
|| config.lastUploadProjectApiUrl !== apiUrl
|
|
197
|
+
) {
|
|
198
|
+
config.lastUploadProject = uploadProject;
|
|
199
|
+
config.lastUploadProjectApiUrl = apiUrl;
|
|
200
|
+
saveConfig(config);
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err.message === 'UNAUTHORIZED') {
|
|
204
|
+
console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
|
|
205
|
+
if (throws) throw err;
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
// Settings endpoint unreachable (not auth): degrade to the last confirmed
|
|
209
|
+
// choice for this same server rather than hard-aborting every upload.
|
|
210
|
+
const cachedUploadProject = resolveCachedUploadProjectSetting(config, apiUrl);
|
|
211
|
+
if (typeof cachedUploadProject === 'boolean') {
|
|
212
|
+
uploadProject = resolveUploadProjectSetting(
|
|
213
|
+
{ uploadProject: cachedUploadProject },
|
|
214
|
+
localUploadProject,
|
|
215
|
+
);
|
|
216
|
+
if (!quiet) console.log(warn('设置接口不可用,沿用上次的项目名设置。'));
|
|
217
|
+
} else {
|
|
218
|
+
console.error(failure('暂时无法读取上传设置,本次同步已安全取消(未上传数据)。请稍后重试。'));
|
|
219
|
+
if (throws) throw err;
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
130
222
|
}
|
|
131
223
|
}
|
|
132
224
|
|
|
@@ -221,23 +313,24 @@ export async function runSync({
|
|
|
221
313
|
}
|
|
222
314
|
}
|
|
223
315
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// Cloud-sourced parsers (e.g. cursor) pre-set their own hostname sentinel so
|
|
231
|
-
// the same account data isn't stored as separate rows per machine.
|
|
232
|
-
for (const b of allBuckets) if (!b.hostname) b.hostname = host;
|
|
233
|
-
for (const s of allSessions) if (!s.hostname) s.hostname = host;
|
|
316
|
+
const host = hostIdentity.hostname;
|
|
317
|
+
// Cloud-backed parsers use explicit non-identifying sentinels (currently
|
|
318
|
+
// `cursor-cloud`) so the same account data deduplicates across computers.
|
|
319
|
+
// Every other hostname is assigned here, at the final network boundary.
|
|
320
|
+
applyHostnamePrivacy(allBuckets, host, hostIdentity.uploadHostname);
|
|
321
|
+
applyHostnamePrivacy(allSessions, host, hostIdentity.uploadHostname);
|
|
234
322
|
|
|
235
323
|
if (!quiet) {
|
|
236
324
|
if (uploadProject) {
|
|
237
|
-
console.log(dim(' 项目名:
|
|
325
|
+
console.log(dim(' 项目名: 上传(本机或 Web 设置均可关闭)'));
|
|
238
326
|
} else {
|
|
239
327
|
console.log(dim(' 项目名: 已隐藏'));
|
|
240
328
|
}
|
|
329
|
+
console.log(dim(
|
|
330
|
+
hostIdentity.uploadHostname
|
|
331
|
+
? ' 设备名: 上传'
|
|
332
|
+
: ` 设备名: 已替换为匿名 ID (${host})`,
|
|
333
|
+
));
|
|
241
334
|
}
|
|
242
335
|
if (!uploadProject) {
|
|
243
336
|
for (const b of allBuckets) b.project = 'unknown';
|
|
@@ -253,6 +346,13 @@ export async function runSync({
|
|
|
253
346
|
// Missing/corrupt state.json => empty maps => one-time full upload, then
|
|
254
347
|
// incremental forever after.
|
|
255
348
|
const state = loadState();
|
|
349
|
+
const migratedHostnameState = !hostIdentity.uploadHostname
|
|
350
|
+
&& migrateHiddenHostnameState(
|
|
351
|
+
state,
|
|
352
|
+
allBuckets,
|
|
353
|
+
hostIdentity.previousHostname,
|
|
354
|
+
host,
|
|
355
|
+
);
|
|
256
356
|
const changedBuckets = [];
|
|
257
357
|
const changedSessions = [];
|
|
258
358
|
const liveBucketKeys = new Set();
|
|
@@ -290,7 +390,7 @@ export async function runSync({
|
|
|
290
390
|
const before = Object.keys(state.buckets).length + Object.keys(state.sessions).length;
|
|
291
391
|
pruneState(state, liveBucketKeys, liveSessionKeys, okSources);
|
|
292
392
|
const pruned = before - (Object.keys(state.buckets).length + Object.keys(state.sessions).length);
|
|
293
|
-
if (pruned > 0) saveState(state);
|
|
393
|
+
if (pruned > 0 || migratedHostnameState) saveState(state);
|
|
294
394
|
|
|
295
395
|
if (changedBuckets.length === 0 && changedSessions.length === 0) {
|
|
296
396
|
if (!quiet) console.log(dim('无新增数据。'));
|