memoir-cli 3.9.0 → 3.10.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 +3 -3
- package/src/adapters/index.js +12 -10
- package/src/commands/autopush.js +29 -15
- package/src/commands/push.js +95 -24
- package/src/commands/restore.js +8 -1
- package/src/commands/tidy.js +20 -0
- package/src/commands/why.js +10 -3
- package/src/context/capture.js +86 -7
- package/src/events/log.js +113 -0
- package/src/mcp.js +7 -1
- package/src/providers/index.js +10 -0
- package/src/session/inject.js +12 -0
- package/src/session/lock.js +102 -0
- package/src/session/migrations.js +98 -0
- package/src/session/render.js +7 -1
- package/src/session/state.js +210 -110
- package/src/utils/platform.js +0 -47
package/src/session/state.js
CHANGED
|
@@ -9,13 +9,25 @@ import fs from 'fs-extra';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import os from 'os';
|
|
11
11
|
import crypto from 'crypto';
|
|
12
|
+
import { withSessionLock } from './lock.js';
|
|
13
|
+
import { SCHEMA_VERSION, migrateSessionData, emptySession } from './migrations.js';
|
|
14
|
+
// NOTE: events/log.js imports getMachineId FROM this module — this is a
|
|
15
|
+
// circular import, safe here because both appendEvent (used below) and
|
|
16
|
+
// getMachineId (used by events/log.js) are hoisted function declarations
|
|
17
|
+
// used only inside other functions' bodies, never at module-evaluation
|
|
18
|
+
// time. Verified working; see test-event-log.mjs.
|
|
19
|
+
import { appendEvent } from '../events/log.js';
|
|
12
20
|
|
|
13
21
|
const home = os.homedir();
|
|
14
22
|
const CONFIG_DIR = path.join(home, '.config', 'memoir');
|
|
15
23
|
const SESSION_PATH = path.join(CONFIG_DIR, 'session.json');
|
|
16
24
|
const MACHINE_ID_PATH = path.join(CONFIG_DIR, 'machine.id');
|
|
25
|
+
const SESSION_LOCK_PATH = path.join(CONFIG_DIR, 'session.json.lock');
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
// Re-exported for external consumers (e.g. test-session.mjs asserts against
|
|
28
|
+
// state.SCHEMA_VERSION) — the canonical constant now lives in migrations.js
|
|
29
|
+
// alongside the migration ladder it governs.
|
|
30
|
+
export { SCHEMA_VERSION, emptySession };
|
|
19
31
|
|
|
20
32
|
// Maximum items kept in each list before oldest entries rotate into history.
|
|
21
33
|
// Prevents unbounded growth of the live pinned block.
|
|
@@ -44,66 +56,107 @@ export async function getMachineId() {
|
|
|
44
56
|
return { id, label: os.hostname() };
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
// ── Schema ───────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
function emptySession() {
|
|
50
|
-
return {
|
|
51
|
-
version: SCHEMA_VERSION,
|
|
52
|
-
created_at: new Date().toISOString(),
|
|
53
|
-
updated_at: new Date().toISOString(),
|
|
54
|
-
machines: {}, // { [machineId]: { label, last_seen } }
|
|
55
|
-
current: {
|
|
56
|
-
goals: [], // { text, machine_id, set_on }
|
|
57
|
-
next_actions: [], // { text, machine_id, added, completed? }
|
|
58
|
-
open_questions: [],// { text, machine_id, asked }
|
|
59
|
-
decisions: [], // { text, why?, rejected?, machine_id, date }
|
|
60
|
-
},
|
|
61
|
-
history: [], // { date, machine_id, summary, files_touched, duration_min? }
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
59
|
// ── Read / write ─────────────────────────────────────────────────
|
|
60
|
+
//
|
|
61
|
+
// Forward-version guard: if session.json's version is NEWER than this
|
|
62
|
+
// build's SCHEMA_VERSION (the file came from a newer memoir install — e.g.
|
|
63
|
+
// another machine upgraded first and this one hasn't yet), readSession()
|
|
64
|
+
// backs up the original file (mirroring the corrupted-JSON quarantine
|
|
65
|
+
// pattern below) and returns a safe, empty-but-valid session instead of
|
|
66
|
+
// misinterpreting an unknown shape. This is centralized HERE, not in
|
|
67
|
+
// individual callers, so all ~20 call sites across mcp.js (8 MCP tool
|
|
68
|
+
// handlers), commands/session.js, commands/why.js, commands/auto-refresh.js,
|
|
69
|
+
// commands/push.js, commands/restore.js automatically get safe behavior
|
|
70
|
+
// with zero changes required at each call site — and critically, no MCP
|
|
71
|
+
// tool call is ever allowed to throw/crash because of a schema mismatch.
|
|
72
|
+
let warnedForwardVersion = false; // print the upgrade warning once per process, not once per call
|
|
73
|
+
|
|
74
|
+
// Opportunistic cleanup so the .corrupted-<ts> / .pre-migration-<ts> backup
|
|
75
|
+
// patterns don't accumulate forever on a machine that repeatedly hits
|
|
76
|
+
// either quarantine path. Keeps the N most recent of EACH pattern, deletes
|
|
77
|
+
// older ones. Best-effort — a cleanup failure never blocks the caller.
|
|
78
|
+
const MAX_BACKUPS_PER_PATTERN = 3;
|
|
79
|
+
function cleanupOldBackups(suffixPrefix) {
|
|
80
|
+
try {
|
|
81
|
+
const dir = path.dirname(SESSION_PATH);
|
|
82
|
+
const base = path.basename(SESSION_PATH); // "session.json"
|
|
83
|
+
const marker = `${base}.${suffixPrefix}-`;
|
|
84
|
+
const matches = fs.readdirSync(dir)
|
|
85
|
+
.filter((f) => f.startsWith(marker))
|
|
86
|
+
.map((f) => {
|
|
87
|
+
let mtime = 0;
|
|
88
|
+
try { mtime = fs.statSync(path.join(dir, f)).mtimeMs; } catch {}
|
|
89
|
+
return { name: f, mtime };
|
|
90
|
+
})
|
|
91
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
92
|
+
for (const f of matches.slice(MAX_BACKUPS_PER_PATTERN)) {
|
|
93
|
+
try { fs.unlinkSync(path.join(dir, f.name)); } catch {}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Best-effort — never block the caller.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
66
99
|
|
|
67
|
-
// Atomic read with graceful recovery from corrupted JSON
|
|
100
|
+
// Atomic read with graceful recovery from corrupted JSON AND from a
|
|
101
|
+
// too-new schema version.
|
|
68
102
|
export async function readSession() {
|
|
69
103
|
if (!await fs.pathExists(SESSION_PATH)) return emptySession();
|
|
70
104
|
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = await fs.readFile(SESSION_PATH, 'utf8');
|
|
108
|
+
} catch {
|
|
109
|
+
// Unreadable (permissions, race with a concurrent delete, etc.) —
|
|
110
|
+
// degrade to a safe empty session rather than throwing.
|
|
111
|
+
return emptySession();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let parsed;
|
|
71
115
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return migrateIfNeeded(parsed);
|
|
75
|
-
} catch (err) {
|
|
116
|
+
parsed = JSON.parse(raw);
|
|
117
|
+
} catch {
|
|
76
118
|
// Corrupted — preserve it for inspection, start fresh.
|
|
77
119
|
const backup = `${SESSION_PATH}.corrupted-${Date.now()}`;
|
|
78
120
|
try { await fs.copy(SESSION_PATH, backup); } catch {}
|
|
121
|
+
cleanupOldBackups('corrupted');
|
|
79
122
|
return emptySession();
|
|
80
123
|
}
|
|
124
|
+
|
|
125
|
+
const { future, state } = migrateSessionData(parsed);
|
|
126
|
+
|
|
127
|
+
if (future) {
|
|
128
|
+
const backup = `${SESSION_PATH}.pre-migration-${Date.now()}`;
|
|
129
|
+
try { await fs.copy(SESSION_PATH, backup); } catch {}
|
|
130
|
+
cleanupOldBackups('pre-migration');
|
|
131
|
+
if (!warnedForwardVersion) {
|
|
132
|
+
warnedForwardVersion = true;
|
|
133
|
+
try {
|
|
134
|
+
process.stderr.write(
|
|
135
|
+
`memoir: session.json is from a newer version of memoir than this install understands ` +
|
|
136
|
+
`(schema v${parsed?.version} > v${SCHEMA_VERSION}). It has been backed up to ${backup}. ` +
|
|
137
|
+
`Run: npm i -g memoir-cli@latest\n`
|
|
138
|
+
);
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return state;
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
// Atomic write: write to tmp, rename. Prevents torn writes on crash.
|
|
147
|
+
// Unconditionally stamps version — every write lands at the CURRENT
|
|
148
|
+
// SCHEMA_VERSION, since it always passed through readSession/migrateSessionData
|
|
149
|
+
// (or emptySession()) to get here. Always called from within a locked
|
|
150
|
+
// critical section (see the mutators below and lock.js).
|
|
84
151
|
export async function writeSession(state) {
|
|
85
152
|
await fs.ensureDir(CONFIG_DIR);
|
|
153
|
+
state.version = SCHEMA_VERSION;
|
|
86
154
|
state.updated_at = new Date().toISOString();
|
|
87
155
|
const tmp = `${SESSION_PATH}.tmp-${process.pid}`;
|
|
88
156
|
await fs.writeFile(tmp, JSON.stringify(state, null, 2));
|
|
89
157
|
await fs.move(tmp, SESSION_PATH, { overwrite: true });
|
|
90
158
|
}
|
|
91
159
|
|
|
92
|
-
function migrateIfNeeded(state) {
|
|
93
|
-
if (state && state.version === SCHEMA_VERSION) return state;
|
|
94
|
-
// Future versions: add migration steps here.
|
|
95
|
-
// For now, if version mismatch, merge defaults to fill gaps.
|
|
96
|
-
const fresh = emptySession();
|
|
97
|
-
return {
|
|
98
|
-
...fresh,
|
|
99
|
-
...state,
|
|
100
|
-
version: SCHEMA_VERSION,
|
|
101
|
-
current: { ...fresh.current, ...(state?.current || {}) },
|
|
102
|
-
machines: { ...fresh.machines, ...(state?.machines || {}) },
|
|
103
|
-
history: Array.isArray(state?.history) ? state.history : [],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
160
|
// ── Machine registration ────────────────────────────────────────
|
|
108
161
|
|
|
109
162
|
async function touchMachine(state) {
|
|
@@ -117,98 +170,123 @@ async function touchMachine(state) {
|
|
|
117
170
|
|
|
118
171
|
// ── Mutators ────────────────────────────────────────────────────
|
|
119
172
|
|
|
173
|
+
// Every mutator below wraps its ENTIRE read -> mutate -> write cycle in
|
|
174
|
+
// withSessionLock — not just the write. Locking only the write would still
|
|
175
|
+
// allow two processes to both read the same stale snapshot before either
|
|
176
|
+
// writes; the read must be inside the lock too so the second process reads
|
|
177
|
+
// the FIRST process's already-written change rather than a stale copy.
|
|
178
|
+
|
|
120
179
|
export async function addGoal(text) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
return withSessionLock(SESSION_LOCK_PATH, async () => {
|
|
181
|
+
const state = await readSession();
|
|
182
|
+
const machineId = await touchMachine(state);
|
|
183
|
+
state.current.goals.unshift({
|
|
184
|
+
text,
|
|
185
|
+
machine_id: machineId,
|
|
186
|
+
set_on: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
state.current.goals = state.current.goals.slice(0, MAX_GOALS);
|
|
189
|
+
await writeSession(state);
|
|
190
|
+
await appendEvent('goal_set', {}); // no PII/content — count-and-type only
|
|
191
|
+
return state;
|
|
127
192
|
});
|
|
128
|
-
state.current.goals = state.current.goals.slice(0, MAX_GOALS);
|
|
129
|
-
await writeSession(state);
|
|
130
|
-
return state;
|
|
131
193
|
}
|
|
132
194
|
|
|
133
195
|
export async function addNext(text) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
196
|
+
return withSessionLock(SESSION_LOCK_PATH, async () => {
|
|
197
|
+
const state = await readSession();
|
|
198
|
+
const machineId = await touchMachine(state);
|
|
199
|
+
// Dedupe by text (case-insensitive)
|
|
200
|
+
const normalized = text.trim().toLowerCase();
|
|
201
|
+
const exists = state.current.next_actions.some(a => a.text.trim().toLowerCase() === normalized);
|
|
202
|
+
if (!exists) {
|
|
203
|
+
state.current.next_actions.push({
|
|
204
|
+
text,
|
|
205
|
+
machine_id: machineId,
|
|
206
|
+
added: new Date().toISOString(),
|
|
207
|
+
});
|
|
208
|
+
state.current.next_actions = state.current.next_actions.slice(-MAX_NEXT);
|
|
209
|
+
}
|
|
210
|
+
await writeSession(state);
|
|
211
|
+
return state;
|
|
212
|
+
});
|
|
149
213
|
}
|
|
150
214
|
|
|
151
215
|
export async function completeNext(textOrIndex) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
216
|
+
return withSessionLock(SESSION_LOCK_PATH, async () => {
|
|
217
|
+
const state = await readSession();
|
|
218
|
+
await touchMachine(state);
|
|
219
|
+
let idx = -1;
|
|
220
|
+
if (typeof textOrIndex === 'number') {
|
|
221
|
+
idx = textOrIndex;
|
|
222
|
+
} else {
|
|
223
|
+
const normalized = String(textOrIndex).trim().toLowerCase();
|
|
224
|
+
idx = state.current.next_actions.findIndex(a => a.text.trim().toLowerCase().includes(normalized));
|
|
225
|
+
}
|
|
226
|
+
const completed = idx >= 0;
|
|
227
|
+
if (completed) {
|
|
228
|
+
state.current.next_actions.splice(idx, 1);
|
|
229
|
+
}
|
|
230
|
+
await writeSession(state);
|
|
231
|
+
// Only when something was actually completed — the event should mean
|
|
232
|
+
// "something happened," not "this function was called with no match."
|
|
233
|
+
if (completed) await appendEvent('next_completed', {});
|
|
234
|
+
return state;
|
|
235
|
+
});
|
|
166
236
|
}
|
|
167
237
|
|
|
168
238
|
export async function addNote(text, opts = {}) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
239
|
+
return withSessionLock(SESSION_LOCK_PATH, async () => {
|
|
240
|
+
const state = await readSession();
|
|
241
|
+
const machineId = await touchMachine(state);
|
|
242
|
+
const decision = {
|
|
243
|
+
text,
|
|
244
|
+
machine_id: machineId,
|
|
245
|
+
date: new Date().toISOString(),
|
|
246
|
+
};
|
|
247
|
+
if (opts.why) decision.why = opts.why;
|
|
248
|
+
if (opts.rejected) decision.rejected = opts.rejected;
|
|
249
|
+
state.current.decisions.unshift(decision);
|
|
250
|
+
state.current.decisions = state.current.decisions.slice(0, MAX_DECISIONS_RECENT);
|
|
251
|
+
await writeSession(state);
|
|
252
|
+
// Count/booleans only — never the decision text itself.
|
|
253
|
+
await appendEvent('decision_captured', { has_why: !!opts.why, has_rejected: !!opts.rejected });
|
|
254
|
+
return state;
|
|
255
|
+
});
|
|
182
256
|
}
|
|
183
257
|
|
|
184
258
|
export async function addQuestion(text) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
259
|
+
return withSessionLock(SESSION_LOCK_PATH, async () => {
|
|
260
|
+
const state = await readSession();
|
|
261
|
+
const machineId = await touchMachine(state);
|
|
262
|
+
state.current.open_questions.push({
|
|
263
|
+
text,
|
|
264
|
+
machine_id: machineId,
|
|
265
|
+
asked: new Date().toISOString(),
|
|
266
|
+
});
|
|
267
|
+
state.current.open_questions = state.current.open_questions.slice(-MAX_QUESTIONS);
|
|
268
|
+
await writeSession(state);
|
|
269
|
+
return state;
|
|
191
270
|
});
|
|
192
|
-
state.current.open_questions = state.current.open_questions.slice(-MAX_QUESTIONS);
|
|
193
|
-
await writeSession(state);
|
|
194
|
-
return state;
|
|
195
271
|
}
|
|
196
272
|
|
|
197
273
|
// Roll up the current state into a history entry. Use at session end / push.
|
|
198
274
|
// Does not clear `current` — these are "the working set," not per-session scratch.
|
|
199
275
|
export async function recordSessionEnd({ summary, filesTouched = [], durationMin = null } = {}) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
276
|
+
return withSessionLock(SESSION_LOCK_PATH, async () => {
|
|
277
|
+
const state = await readSession();
|
|
278
|
+
const machineId = await touchMachine(state);
|
|
279
|
+
state.history.unshift({
|
|
280
|
+
date: new Date().toISOString(),
|
|
281
|
+
machine_id: machineId,
|
|
282
|
+
summary: summary || '',
|
|
283
|
+
files_touched: filesTouched.slice(0, 20),
|
|
284
|
+
duration_min: durationMin,
|
|
285
|
+
});
|
|
286
|
+
state.history = state.history.slice(0, MAX_HISTORY);
|
|
287
|
+
await writeSession(state);
|
|
288
|
+
return state;
|
|
208
289
|
});
|
|
209
|
-
state.history = state.history.slice(0, MAX_HISTORY);
|
|
210
|
-
await writeSession(state);
|
|
211
|
-
return state;
|
|
212
290
|
}
|
|
213
291
|
|
|
214
292
|
// ── Cross-machine merge ─────────────────────────────────────────
|
|
@@ -255,6 +333,27 @@ function unionByText(a = [], b = [], dateField, cap) {
|
|
|
255
333
|
byText.set(key, item);
|
|
256
334
|
}
|
|
257
335
|
}
|
|
336
|
+
|
|
337
|
+
// A tombstone is STICKY: once any machine marks an entry hidden, the merged
|
|
338
|
+
// result stays hidden, whatever the dates say.
|
|
339
|
+
//
|
|
340
|
+
// Without this, `hidden` is just another field on whichever copy has the
|
|
341
|
+
// newer date — so a machine that hasn't pulled the tombstone yet, holding an
|
|
342
|
+
// older un-hidden copy of the same text, resurrects it on its next
|
|
343
|
+
// merge/push. (The cleanup script sets `hidden` without touching `date`, so
|
|
344
|
+
// the tombstoned copy doesn't even win the date comparison.) Suppression has
|
|
345
|
+
// to be monotonic or it isn't suppression — you'd be re-hiding the same junk
|
|
346
|
+
// on every machine forever.
|
|
347
|
+
for (const [key, winner] of byText) {
|
|
348
|
+
if (winner.hidden) continue;
|
|
349
|
+
const tombstone = [...a, ...b].find(
|
|
350
|
+
(i) => i && i.text && i.text.trim().toLowerCase() === key && i.hidden
|
|
351
|
+
);
|
|
352
|
+
if (tombstone) {
|
|
353
|
+
byText.set(key, { ...winner, hidden: true, hidden_at: tombstone.hidden_at });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
258
357
|
return Array.from(byText.values())
|
|
259
358
|
.sort((x, y) => new Date(y[dateField] || 0) - new Date(x[dateField] || 0))
|
|
260
359
|
.slice(0, cap);
|
|
@@ -293,4 +392,5 @@ export const paths = {
|
|
|
293
392
|
config: CONFIG_DIR,
|
|
294
393
|
session: SESSION_PATH,
|
|
295
394
|
machineId: MACHINE_ID_PATH,
|
|
395
|
+
sessionLock: SESSION_LOCK_PATH,
|
|
296
396
|
};
|
package/src/utils/platform.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Single source of truth for OS-specific config locations.
|
|
2
|
-
//
|
|
3
|
-
// Why this exists: before this module, every adapter branched on a single
|
|
4
|
-
// `isWin` flag — `isWin ? <windows> : <macOS>`. On Linux, `process.platform`
|
|
5
|
-
// is `'linux'`, so those ternaries silently fell through to the *macOS* path
|
|
6
|
-
// (`~/Library/Application Support/...`), which doesn't exist on Linux. memoir
|
|
7
|
-
// would then detect zero tools, sync nothing, and the user would churn without
|
|
8
|
-
// any error — the "silent-zero-memory activation cliff."
|
|
9
|
-
//
|
|
10
|
-
// Every function is parameterized by { platform, env, home } so the three OS
|
|
11
|
-
// branches can be unit-tested from any machine, not just the target OS. Runtime
|
|
12
|
-
// callers omit the options and get the live platform.
|
|
13
|
-
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import os from 'node:os';
|
|
16
|
-
|
|
17
|
-
const HOME = os.homedir();
|
|
18
|
-
|
|
19
|
-
// VS Code-family per-user config base: <root>/<App>/User
|
|
20
|
-
// win32 : %APPDATA%/<App>/User
|
|
21
|
-
// darwin : ~/Library/Application Support/<App>/User
|
|
22
|
-
// linux : $XDG_CONFIG_HOME (or ~/.config)/<App>/User
|
|
23
|
-
export function vscodeUserDir(appName, { platform = process.platform, env = process.env, home = HOME } = {}) {
|
|
24
|
-
if (platform === 'win32') {
|
|
25
|
-
const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
26
|
-
return path.join(appData, appName, 'User');
|
|
27
|
-
}
|
|
28
|
-
if (platform === 'darwin') {
|
|
29
|
-
return path.join(home, 'Library', 'Application Support', appName, 'User');
|
|
30
|
-
}
|
|
31
|
-
// linux + anything else POSIX-y
|
|
32
|
-
const xdg = env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
33
|
-
return path.join(xdg, appName, 'User');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// A VS Code extension's globalStorage dir (e.g. Cline lives under the base
|
|
37
|
-
// "Code" install, not its own app dir).
|
|
38
|
-
export function vscodeGlobalStorage(extId, opts = {}) {
|
|
39
|
-
return path.join(vscodeUserDir('Code', opts), 'globalStorage', extId);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// XDG-aware ~/.config base, for non-VSCode tools that already store there
|
|
43
|
-
// (zed, github-copilot). Exposed so callers don't re-hardcode ~/.config and
|
|
44
|
-
// drift from XDG_CONFIG_HOME.
|
|
45
|
-
export function xdgConfigDir({ env = process.env, home = HOME } = {}) {
|
|
46
|
-
return env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
47
|
-
}
|