@memrosetta/cli 0.4.1 → 0.4.3
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/dist/chunk-YXK6FDB6.js +42 -0
- package/dist/index.js +3 -3
- package/dist/status-HLKL32NP.js +184 -0
- package/dist/sync-7TONPJBY.js +351 -0
- package/dist/update-4F3YY2HU.js +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
function resolveCliVersion() {
|
|
7
|
+
const strategies = [
|
|
8
|
+
() => {
|
|
9
|
+
const require2 = createRequire(import.meta.url);
|
|
10
|
+
return require2("../../package.json").version;
|
|
11
|
+
},
|
|
12
|
+
() => {
|
|
13
|
+
const require2 = createRequire(import.meta.url);
|
|
14
|
+
return require2("@memrosetta/cli/package.json").version;
|
|
15
|
+
},
|
|
16
|
+
() => {
|
|
17
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
for (let d = dir, i = 0; i < 5; i++) {
|
|
19
|
+
const candidate = join(d, "package.json");
|
|
20
|
+
if (existsSync(candidate)) {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
22
|
+
if (pkg.name?.includes("memrosetta") && pkg.version) {
|
|
23
|
+
return pkg.version;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
d = dirname(d);
|
|
27
|
+
}
|
|
28
|
+
throw new Error("not found");
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
for (const strategy of strategies) {
|
|
32
|
+
try {
|
|
33
|
+
return strategy();
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
resolveCliVersion
|
|
42
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -165,7 +165,7 @@ async function main() {
|
|
|
165
165
|
break;
|
|
166
166
|
}
|
|
167
167
|
case "status": {
|
|
168
|
-
const mod = await import("./status-
|
|
168
|
+
const mod = await import("./status-HLKL32NP.js");
|
|
169
169
|
await mod.run(commandOptions);
|
|
170
170
|
break;
|
|
171
171
|
}
|
|
@@ -180,12 +180,12 @@ async function main() {
|
|
|
180
180
|
break;
|
|
181
181
|
}
|
|
182
182
|
case "update": {
|
|
183
|
-
const mod = await import("./update-
|
|
183
|
+
const mod = await import("./update-4F3YY2HU.js");
|
|
184
184
|
await mod.run();
|
|
185
185
|
break;
|
|
186
186
|
}
|
|
187
187
|
case "sync": {
|
|
188
|
-
const mod = await import("./sync-
|
|
188
|
+
const mod = await import("./sync-7TONPJBY.js");
|
|
189
189
|
await mod.run(commandOptions);
|
|
190
190
|
break;
|
|
191
191
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isClaudeCodeConfigured,
|
|
3
|
+
isCodexConfigured,
|
|
4
|
+
isCursorConfigured,
|
|
5
|
+
isGeminiConfigured,
|
|
6
|
+
isGenericMCPConfigured
|
|
7
|
+
} from "./chunk-IS4IKWPL.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveCliVersion
|
|
10
|
+
} from "./chunk-YXK6FDB6.js";
|
|
11
|
+
import {
|
|
12
|
+
getDefaultDbPath
|
|
13
|
+
} from "./chunk-72IW6TAV.js";
|
|
14
|
+
import {
|
|
15
|
+
output
|
|
16
|
+
} from "./chunk-ET6TNQOJ.js";
|
|
17
|
+
import {
|
|
18
|
+
getConfig
|
|
19
|
+
} from "./chunk-SEPYQK3J.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/status.ts
|
|
22
|
+
import { existsSync, statSync } from "fs";
|
|
23
|
+
async function run(options) {
|
|
24
|
+
const { format, db, noEmbeddings } = options;
|
|
25
|
+
const config = getConfig();
|
|
26
|
+
const dbPath = db ?? config.dbPath ?? getDefaultDbPath();
|
|
27
|
+
const exists = existsSync(dbPath);
|
|
28
|
+
let sizeBytes = 0;
|
|
29
|
+
let sizeFormatted = "0B";
|
|
30
|
+
let memoryCount = 0;
|
|
31
|
+
let userList = [];
|
|
32
|
+
let qualityFresh = 0;
|
|
33
|
+
let qualityInvalidated = 0;
|
|
34
|
+
let qualityWithRelations = 0;
|
|
35
|
+
let qualityAvgActivation = 0;
|
|
36
|
+
const embeddingsEnabled = !noEmbeddings && config.enableEmbeddings !== false;
|
|
37
|
+
if (exists) {
|
|
38
|
+
const stat = statSync(dbPath);
|
|
39
|
+
sizeBytes = stat.size;
|
|
40
|
+
sizeFormatted = formatSize(sizeBytes);
|
|
41
|
+
try {
|
|
42
|
+
const Database = (await import("better-sqlite3")).default;
|
|
43
|
+
const dbConn = new Database(dbPath);
|
|
44
|
+
dbConn.pragma("journal_mode = WAL");
|
|
45
|
+
const countRow = dbConn.prepare("SELECT COUNT(*) as count FROM memories").get();
|
|
46
|
+
memoryCount = countRow.count;
|
|
47
|
+
const userRows = dbConn.prepare("SELECT DISTINCT user_id FROM memories ORDER BY user_id").all();
|
|
48
|
+
userList = userRows.map((r) => r.user_id);
|
|
49
|
+
const freshRow = dbConn.prepare(
|
|
50
|
+
"SELECT COUNT(*) as c FROM memories WHERE is_latest = 1 AND invalidated_at IS NULL"
|
|
51
|
+
).get();
|
|
52
|
+
qualityFresh = freshRow.c;
|
|
53
|
+
const invalidatedRow = dbConn.prepare(
|
|
54
|
+
"SELECT COUNT(*) as c FROM memories WHERE invalidated_at IS NOT NULL"
|
|
55
|
+
).get();
|
|
56
|
+
qualityInvalidated = invalidatedRow.c;
|
|
57
|
+
const relationsRow = dbConn.prepare(
|
|
58
|
+
"SELECT COUNT(DISTINCT src_memory_id) + COUNT(DISTINCT dst_memory_id) as c FROM memory_relations"
|
|
59
|
+
).get();
|
|
60
|
+
qualityWithRelations = relationsRow.c;
|
|
61
|
+
const avgRow = dbConn.prepare(
|
|
62
|
+
"SELECT AVG(activation_score) as avg FROM memories WHERE is_latest = 1"
|
|
63
|
+
).get();
|
|
64
|
+
qualityAvgActivation = avgRow.avg ?? 0;
|
|
65
|
+
dbConn.close();
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const claudeCodeStatus = isClaudeCodeConfigured();
|
|
70
|
+
const cursorStatus = isCursorConfigured();
|
|
71
|
+
const codexStatus = isCodexConfigured();
|
|
72
|
+
const geminiStatus = isGeminiConfigured();
|
|
73
|
+
const mcpStatus = isGenericMCPConfigured();
|
|
74
|
+
if (format === "text") {
|
|
75
|
+
process.stdout.write("MemRosetta Status\n");
|
|
76
|
+
process.stdout.write(`${"=".repeat(40)}
|
|
77
|
+
|
|
78
|
+
`);
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
`Database: ${dbPath} (${exists ? `exists, ${sizeFormatted}` : "not found"})
|
|
81
|
+
`
|
|
82
|
+
);
|
|
83
|
+
process.stdout.write(`Memories: ${memoryCount}
|
|
84
|
+
`);
|
|
85
|
+
if (userList.length > 0) {
|
|
86
|
+
process.stdout.write(
|
|
87
|
+
`Users: ${userList.length} (${userList.join(", ")})
|
|
88
|
+
`
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
process.stdout.write("Users: 0\n");
|
|
92
|
+
}
|
|
93
|
+
const embeddingModelLabel = getEmbeddingModelLabel();
|
|
94
|
+
process.stdout.write(
|
|
95
|
+
`Embeddings: ${embeddingsEnabled ? `enabled (${embeddingModelLabel})` : "disabled"}
|
|
96
|
+
`
|
|
97
|
+
);
|
|
98
|
+
if (memoryCount > 0) {
|
|
99
|
+
process.stdout.write("\nQuality:\n");
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
` Fresh (is_latest=1): ${qualityFresh} / ${memoryCount}
|
|
102
|
+
`
|
|
103
|
+
);
|
|
104
|
+
process.stdout.write(` Invalidated: ${qualityInvalidated}
|
|
105
|
+
`);
|
|
106
|
+
process.stdout.write(` With relations: ${qualityWithRelations}
|
|
107
|
+
`);
|
|
108
|
+
process.stdout.write(
|
|
109
|
+
` Avg activation: ${qualityAvgActivation.toFixed(2)}
|
|
110
|
+
`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
process.stdout.write("\nIntegrations:\n");
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
` Claude Code: ${claudeCodeStatus ? "configured (hooks + MCP)" : "not configured"}
|
|
116
|
+
`
|
|
117
|
+
);
|
|
118
|
+
process.stdout.write(
|
|
119
|
+
` Cursor: ${cursorStatus ? "configured (MCP)" : "not configured"}
|
|
120
|
+
`
|
|
121
|
+
);
|
|
122
|
+
process.stdout.write(
|
|
123
|
+
` Codex: ${codexStatus ? "configured (MCP)" : "not configured"}
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
process.stdout.write(
|
|
127
|
+
` Gemini: ${geminiStatus ? "configured (MCP)" : "not configured"}
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
process.stdout.write(
|
|
131
|
+
` MCP (generic): ${mcpStatus ? "configured" : "not configured"}
|
|
132
|
+
`
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
output(
|
|
137
|
+
{
|
|
138
|
+
version: resolveCliVersion(),
|
|
139
|
+
database: {
|
|
140
|
+
path: dbPath,
|
|
141
|
+
exists,
|
|
142
|
+
sizeBytes,
|
|
143
|
+
sizeFormatted
|
|
144
|
+
},
|
|
145
|
+
memories: memoryCount,
|
|
146
|
+
users: userList,
|
|
147
|
+
quality: {
|
|
148
|
+
fresh: qualityFresh,
|
|
149
|
+
invalidated: qualityInvalidated,
|
|
150
|
+
withRelations: qualityWithRelations,
|
|
151
|
+
avgActivation: qualityAvgActivation
|
|
152
|
+
},
|
|
153
|
+
embeddings: embeddingsEnabled,
|
|
154
|
+
embeddingModel: getEmbeddingModelLabel(),
|
|
155
|
+
embeddingPreset: getConfig().embeddingPreset ?? "en",
|
|
156
|
+
integrations: {
|
|
157
|
+
claudeCode: claudeCodeStatus,
|
|
158
|
+
cursor: cursorStatus,
|
|
159
|
+
codex: codexStatus,
|
|
160
|
+
gemini: geminiStatus,
|
|
161
|
+
mcp: mcpStatus
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
format
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function formatSize(bytes) {
|
|
168
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
169
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
170
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
171
|
+
}
|
|
172
|
+
var PRESET_MODEL_LABELS = {
|
|
173
|
+
en: "bge-small-en-v1.5",
|
|
174
|
+
multilingual: "multilingual-e5-small",
|
|
175
|
+
ko: "ko-sroberta-multitask"
|
|
176
|
+
};
|
|
177
|
+
function getEmbeddingModelLabel() {
|
|
178
|
+
const config = getConfig();
|
|
179
|
+
const preset = config.embeddingPreset ?? "en";
|
|
180
|
+
return PRESET_MODEL_LABELS[preset] ?? preset;
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
run
|
|
184
|
+
};
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasFlag,
|
|
3
|
+
optionalOption,
|
|
4
|
+
requireOption
|
|
5
|
+
} from "./chunk-NU5ZJJXP.js";
|
|
6
|
+
import {
|
|
7
|
+
output,
|
|
8
|
+
outputError
|
|
9
|
+
} from "./chunk-ET6TNQOJ.js";
|
|
10
|
+
import {
|
|
11
|
+
getConfig,
|
|
12
|
+
getDefaultDbPath,
|
|
13
|
+
writeConfig
|
|
14
|
+
} from "./chunk-SEPYQK3J.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/sync.ts
|
|
17
|
+
import { randomUUID } from "crypto";
|
|
18
|
+
import { userInfo, platform } from "os";
|
|
19
|
+
import { createInterface } from "readline";
|
|
20
|
+
function parseSubcommand(args) {
|
|
21
|
+
const first = args[0];
|
|
22
|
+
if (!first || first.startsWith("--")) return null;
|
|
23
|
+
if (first === "enable" || first === "disable" || first === "status" || first === "now" || first === "device-id") {
|
|
24
|
+
return first;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
var CONTROL_CHAR_REGEX = /[\x00-\x1F\x7F]/;
|
|
29
|
+
function validateApiKey(key) {
|
|
30
|
+
const trimmed = key.trim();
|
|
31
|
+
if (trimmed.length === 0) {
|
|
32
|
+
throw new Error("API key is empty.");
|
|
33
|
+
}
|
|
34
|
+
if (CONTROL_CHAR_REGEX.test(trimmed)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"API key contains control characters. Your terminal likely does not support hidden input (e.g. Windows PowerShell). Pipe the key instead:\n echo <api-key> | memrosetta sync enable --server <url> --key-stdin"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
async function readHiddenInput(prompt) {
|
|
42
|
+
const stdin = process.stdin;
|
|
43
|
+
const stdout = process.stdout;
|
|
44
|
+
if (!stdin.isTTY) {
|
|
45
|
+
throw new Error("Interactive input requires a TTY. Use --key-stdin to pipe the key instead.");
|
|
46
|
+
}
|
|
47
|
+
stdout.write(prompt);
|
|
48
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
49
|
+
let muted = true;
|
|
50
|
+
stdout.write = (chunk, ...rest) => {
|
|
51
|
+
if (!muted) {
|
|
52
|
+
return originalWrite(chunk, ...rest);
|
|
53
|
+
}
|
|
54
|
+
const str = typeof chunk === "string" ? chunk : chunk?.toString?.("utf-8") ?? "";
|
|
55
|
+
if (str === "\n" || str === "\r\n" || str === "\r") {
|
|
56
|
+
return originalWrite(chunk, ...rest);
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const rl = createInterface({
|
|
61
|
+
input: stdin,
|
|
62
|
+
output: stdout,
|
|
63
|
+
terminal: true
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const answer = await new Promise((resolve, reject) => {
|
|
67
|
+
rl.once("close", () => {
|
|
68
|
+
reject(new Error("Aborted"));
|
|
69
|
+
});
|
|
70
|
+
rl.question("", (value) => {
|
|
71
|
+
resolve(value);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return answer;
|
|
75
|
+
} finally {
|
|
76
|
+
muted = false;
|
|
77
|
+
stdout.write = originalWrite;
|
|
78
|
+
rl.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function readStdinKey() {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
for await (const chunk of process.stdin) {
|
|
84
|
+
chunks.push(chunk);
|
|
85
|
+
}
|
|
86
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
87
|
+
}
|
|
88
|
+
async function testConnection(serverUrl, apiKey) {
|
|
89
|
+
const url = `${serverUrl.replace(/\/$/, "")}/sync/health`;
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(url, {
|
|
92
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
throw new Error(`Sync server health check failed: ${msg}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function withSyncClient(dbPath, config, fn) {
|
|
103
|
+
const Database = (await import("better-sqlite3")).default;
|
|
104
|
+
const { SyncClient, ensureSyncSchema } = await import("@memrosetta/sync-client");
|
|
105
|
+
if (!config.syncServerUrl || !config.syncApiKey || !config.syncDeviceId) {
|
|
106
|
+
throw new Error("Sync is not configured. Run: memrosetta sync enable --server <url>");
|
|
107
|
+
}
|
|
108
|
+
if (CONTROL_CHAR_REGEX.test(config.syncApiKey)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Stored API key is invalid (contains control characters from a previous terminal input). Re-run with --key-stdin to fix it:\n echo <api-key> | memrosetta sync enable --server " + config.syncServerUrl + " --key-stdin"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const db = new Database(dbPath);
|
|
114
|
+
try {
|
|
115
|
+
ensureSyncSchema(db);
|
|
116
|
+
const client = new SyncClient(db, {
|
|
117
|
+
serverUrl: config.syncServerUrl,
|
|
118
|
+
apiKey: config.syncApiKey,
|
|
119
|
+
deviceId: config.syncDeviceId,
|
|
120
|
+
userId: userInfo().username
|
|
121
|
+
});
|
|
122
|
+
return await fn(client, db);
|
|
123
|
+
} finally {
|
|
124
|
+
db.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function runEnable(options) {
|
|
128
|
+
const { args, format } = options;
|
|
129
|
+
let serverUrl;
|
|
130
|
+
try {
|
|
131
|
+
serverUrl = requireOption(args, "--server", "server URL");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
outputError(err instanceof Error ? err.message : String(err), format);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let rawApiKey = optionalOption(args, "--key");
|
|
138
|
+
if (hasFlag(args, "--key-stdin")) {
|
|
139
|
+
rawApiKey = await readStdinKey();
|
|
140
|
+
}
|
|
141
|
+
if (!rawApiKey) {
|
|
142
|
+
if (platform() === "win32") {
|
|
143
|
+
process.stdout.write(
|
|
144
|
+
"Note: Windows terminals do not always mask pasted input. If characters appear or the key is rejected, pipe it via --key-stdin instead.\n"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
rawApiKey = await readHiddenInput("API key: ");
|
|
149
|
+
} catch (err) {
|
|
150
|
+
outputError(err instanceof Error ? err.message : String(err), format);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!rawApiKey) {
|
|
156
|
+
outputError("API key is required", format);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let apiKey;
|
|
161
|
+
try {
|
|
162
|
+
apiKey = validateApiKey(rawApiKey);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
outputError(err instanceof Error ? err.message : String(err), format);
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const skipTest = hasFlag(args, "--no-test");
|
|
169
|
+
if (!skipTest) {
|
|
170
|
+
try {
|
|
171
|
+
await testConnection(serverUrl, apiKey);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
outputError(
|
|
174
|
+
`${err instanceof Error ? err.message : String(err)}
|
|
175
|
+
Use --no-test to skip the health check.`,
|
|
176
|
+
format
|
|
177
|
+
);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const existing = getConfig();
|
|
183
|
+
const deviceId = existing.syncDeviceId ?? `device-${randomUUID().slice(0, 8)}`;
|
|
184
|
+
writeConfig({
|
|
185
|
+
...existing,
|
|
186
|
+
syncEnabled: true,
|
|
187
|
+
syncServerUrl: serverUrl,
|
|
188
|
+
syncApiKey: apiKey,
|
|
189
|
+
syncDeviceId: deviceId
|
|
190
|
+
});
|
|
191
|
+
if (format === "text") {
|
|
192
|
+
process.stdout.write("Sync enabled.\n");
|
|
193
|
+
process.stdout.write(` Server: ${serverUrl}
|
|
194
|
+
`);
|
|
195
|
+
process.stdout.write(` DeviceId: ${deviceId}
|
|
196
|
+
`);
|
|
197
|
+
if (skipTest) {
|
|
198
|
+
process.stdout.write(" (health check skipped)\n");
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
output({ enabled: true, serverUrl, deviceId, healthCheckSkipped: skipTest }, format);
|
|
203
|
+
}
|
|
204
|
+
function runDisable(options) {
|
|
205
|
+
const { format } = options;
|
|
206
|
+
const existing = getConfig();
|
|
207
|
+
writeConfig({
|
|
208
|
+
...existing,
|
|
209
|
+
syncEnabled: false
|
|
210
|
+
});
|
|
211
|
+
if (format === "text") {
|
|
212
|
+
process.stdout.write("Sync disabled. (server URL and API key preserved for re-enable)\n");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
output({ enabled: false }, format);
|
|
216
|
+
}
|
|
217
|
+
async function runStatus(options) {
|
|
218
|
+
const { format, db } = options;
|
|
219
|
+
const config = getConfig();
|
|
220
|
+
const dbPath = db ?? config.dbPath ?? getDefaultDbPath();
|
|
221
|
+
if (!config.syncEnabled) {
|
|
222
|
+
if (format === "text") {
|
|
223
|
+
process.stdout.write("Sync: disabled\n");
|
|
224
|
+
if (config.syncServerUrl) {
|
|
225
|
+
process.stdout.write(` Server: ${config.syncServerUrl}
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
if (config.syncDeviceId) {
|
|
229
|
+
process.stdout.write(` DeviceId: ${config.syncDeviceId}
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
output(
|
|
235
|
+
{
|
|
236
|
+
enabled: false,
|
|
237
|
+
serverUrl: config.syncServerUrl ?? null,
|
|
238
|
+
deviceId: config.syncDeviceId ?? null
|
|
239
|
+
},
|
|
240
|
+
format
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const status = await withSyncClient(dbPath, config, async (client) => client.getStatus());
|
|
246
|
+
if (format === "text") {
|
|
247
|
+
process.stdout.write("Sync: enabled\n");
|
|
248
|
+
process.stdout.write(` Server: ${status.serverUrl}
|
|
249
|
+
`);
|
|
250
|
+
process.stdout.write(` DeviceId: ${status.deviceId}
|
|
251
|
+
`);
|
|
252
|
+
process.stdout.write(` Pending ops: ${status.pendingOps}
|
|
253
|
+
`);
|
|
254
|
+
process.stdout.write(` Current cursor: ${status.cursor}
|
|
255
|
+
`);
|
|
256
|
+
process.stdout.write(
|
|
257
|
+
` Last push: ${status.lastPush.successAt ?? "never"}` + (status.lastPush.attemptAt && status.lastPush.attemptAt !== status.lastPush.successAt ? ` (last attempt: ${status.lastPush.attemptAt})` : "") + "\n"
|
|
258
|
+
);
|
|
259
|
+
process.stdout.write(
|
|
260
|
+
` Last pull: ${status.lastPull.successAt ?? "never"}` + (status.lastPull.attemptAt && status.lastPull.attemptAt !== status.lastPull.successAt ? ` (last attempt: ${status.lastPull.attemptAt})` : "") + "\n"
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
output(status, format);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
outputError(err instanceof Error ? err.message : String(err), format);
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function runNow(options) {
|
|
271
|
+
const { args, format, db } = options;
|
|
272
|
+
const config = getConfig();
|
|
273
|
+
const dbPath = db ?? config.dbPath ?? getDefaultDbPath();
|
|
274
|
+
if (!config.syncEnabled) {
|
|
275
|
+
outputError("Sync is disabled. Run: memrosetta sync enable --server <url>", format);
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const pushOnly = hasFlag(args, "--push-only");
|
|
280
|
+
const pullOnly = hasFlag(args, "--pull-only");
|
|
281
|
+
try {
|
|
282
|
+
const result = await withSyncClient(dbPath, config, async (client) => {
|
|
283
|
+
let pushed = 0;
|
|
284
|
+
let pulled = 0;
|
|
285
|
+
if (!pullOnly) {
|
|
286
|
+
const pushResult = await client.push();
|
|
287
|
+
pushed = pushResult.pushed;
|
|
288
|
+
}
|
|
289
|
+
if (!pushOnly) {
|
|
290
|
+
pulled = await client.pull();
|
|
291
|
+
}
|
|
292
|
+
return { pushed, pulled };
|
|
293
|
+
});
|
|
294
|
+
if (format === "text") {
|
|
295
|
+
process.stdout.write(`Sync complete. pushed=${result.pushed} pulled=${result.pulled}
|
|
296
|
+
`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
output(result, format);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
outputError(err instanceof Error ? err.message : String(err), format);
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function runDeviceId(options) {
|
|
306
|
+
const { format } = options;
|
|
307
|
+
const config = getConfig();
|
|
308
|
+
if (!config.syncDeviceId) {
|
|
309
|
+
outputError("No deviceId set. Run: memrosetta sync enable --server <url>", format);
|
|
310
|
+
process.exitCode = 1;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (format === "text") {
|
|
314
|
+
process.stdout.write(`${config.syncDeviceId}
|
|
315
|
+
`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
output({ deviceId: config.syncDeviceId }, format);
|
|
319
|
+
}
|
|
320
|
+
async function run(options) {
|
|
321
|
+
const sub = parseSubcommand(options.args);
|
|
322
|
+
if (!sub) {
|
|
323
|
+
outputError(
|
|
324
|
+
"Usage: memrosetta sync <enable|disable|status|now|device-id>\n\n enable --server <url> [--key <key> | --key-stdin] [--no-test]\n disable\n status\n now [--push-only | --pull-only]\n device-id\n",
|
|
325
|
+
options.format
|
|
326
|
+
);
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const rest = { ...options, args: options.args.slice(1) };
|
|
331
|
+
switch (sub) {
|
|
332
|
+
case "enable":
|
|
333
|
+
await runEnable(rest);
|
|
334
|
+
return;
|
|
335
|
+
case "disable":
|
|
336
|
+
runDisable(rest);
|
|
337
|
+
return;
|
|
338
|
+
case "status":
|
|
339
|
+
await runStatus(rest);
|
|
340
|
+
return;
|
|
341
|
+
case "now":
|
|
342
|
+
await runNow(rest);
|
|
343
|
+
return;
|
|
344
|
+
case "device-id":
|
|
345
|
+
runDeviceId(rest);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export {
|
|
350
|
+
run
|
|
351
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveCliVersion
|
|
3
|
+
} from "./chunk-YXK6FDB6.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/update.ts
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
function parseNpmList(raw) {
|
|
8
|
+
const start = raw.indexOf("{");
|
|
9
|
+
if (start === -1) return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(raw.slice(start));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function getInstalledVersion(packageName) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = execSync(`npm list -g ${packageName} --depth=0 --json`, {
|
|
19
|
+
encoding: "utf-8",
|
|
20
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
21
|
+
});
|
|
22
|
+
const parsed = parseNpmList(raw);
|
|
23
|
+
const deps = parsed.dependencies ?? {};
|
|
24
|
+
return deps[packageName]?.version ?? null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function run() {
|
|
30
|
+
const runningVersion = resolveCliVersion();
|
|
31
|
+
const wrapperInstalled = getInstalledVersion("memrosetta");
|
|
32
|
+
const cliInstalled = getInstalledVersion("@memrosetta/cli");
|
|
33
|
+
let packageName;
|
|
34
|
+
let currentVersion;
|
|
35
|
+
if (wrapperInstalled) {
|
|
36
|
+
packageName = "memrosetta";
|
|
37
|
+
currentVersion = wrapperInstalled;
|
|
38
|
+
} else if (cliInstalled) {
|
|
39
|
+
packageName = "@memrosetta/cli";
|
|
40
|
+
currentVersion = cliInstalled;
|
|
41
|
+
} else {
|
|
42
|
+
packageName = "memrosetta";
|
|
43
|
+
currentVersion = runningVersion;
|
|
44
|
+
}
|
|
45
|
+
process.stdout.write(`Current version: ${currentVersion} (${packageName})
|
|
46
|
+
`);
|
|
47
|
+
if (currentVersion !== runningVersion && runningVersion !== "unknown") {
|
|
48
|
+
process.stdout.write(`Running binary: ${runningVersion}
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
process.stdout.write("Checking for updates...\n");
|
|
52
|
+
try {
|
|
53
|
+
const latest = execSync(`npm view ${packageName} version`, {
|
|
54
|
+
encoding: "utf-8"
|
|
55
|
+
}).trim();
|
|
56
|
+
if (latest === currentVersion) {
|
|
57
|
+
process.stdout.write(`Already up to date (${currentVersion}).
|
|
58
|
+
`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(`New version available: ${latest}
|
|
62
|
+
`);
|
|
63
|
+
process.stdout.write("Updating...\n");
|
|
64
|
+
execSync(`npm install -g ${packageName}@latest --force`, {
|
|
65
|
+
stdio: "inherit"
|
|
66
|
+
});
|
|
67
|
+
process.stdout.write(`
|
|
68
|
+
Updated: ${currentVersion} -> ${latest}
|
|
69
|
+
`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
process.stderr.write(`Update failed: ${message}
|
|
73
|
+
`);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
run
|
|
79
|
+
};
|