recallmem 0.1.0
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 +201 -0
- package/NOTICE +28 -0
- package/README.md +735 -0
- package/bin/commands/doctor.js +129 -0
- package/bin/commands/setup.js +296 -0
- package/bin/commands/start.js +59 -0
- package/bin/commands/upgrade.js +77 -0
- package/bin/lib/detect.js +215 -0
- package/bin/lib/install-hints.js +94 -0
- package/bin/lib/install-mode.js +141 -0
- package/bin/lib/output.js +78 -0
- package/bin/lib/prompt.js +43 -0
- package/bin/recallmem.js +196 -0
- package/package.json +40 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recallmem doctor
|
|
3
|
+
*
|
|
4
|
+
* Diagnostic command. Checks every dependency and prints a status report.
|
|
5
|
+
* Doesn't try to fix anything -- just tells you what's working and what's not.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
detectNode,
|
|
13
|
+
detectPostgres,
|
|
14
|
+
detectPostgresService,
|
|
15
|
+
detectPgvector,
|
|
16
|
+
detectOllama,
|
|
17
|
+
detectOllamaModel,
|
|
18
|
+
detectDatabase,
|
|
19
|
+
} = require("../lib/detect");
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
color,
|
|
23
|
+
symbols,
|
|
24
|
+
printHeader,
|
|
25
|
+
section,
|
|
26
|
+
success,
|
|
27
|
+
fail,
|
|
28
|
+
warn,
|
|
29
|
+
info,
|
|
30
|
+
blank,
|
|
31
|
+
} = require("../lib/output");
|
|
32
|
+
|
|
33
|
+
const PROJECT_ROOT = process.cwd();
|
|
34
|
+
const ENV_PATH = path.join(PROJECT_ROOT, ".env.local");
|
|
35
|
+
|
|
36
|
+
function readEnv() {
|
|
37
|
+
if (!fs.existsSync(ENV_PATH)) return {};
|
|
38
|
+
const text = fs.readFileSync(ENV_PATH, "utf-8");
|
|
39
|
+
const env = {};
|
|
40
|
+
for (const line of text.split("\n")) {
|
|
41
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
42
|
+
if (m) env[m[1]] = m[2];
|
|
43
|
+
}
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function doctorCommand() {
|
|
48
|
+
printHeader();
|
|
49
|
+
section("System");
|
|
50
|
+
|
|
51
|
+
// Node
|
|
52
|
+
const node = detectNode();
|
|
53
|
+
if (node.ok) success(`Node.js ${node.version}`);
|
|
54
|
+
else fail(`Node.js ${node.version} (need ${node.needed}+)`);
|
|
55
|
+
|
|
56
|
+
// Postgres
|
|
57
|
+
const pg = detectPostgres();
|
|
58
|
+
if (pg.ok) success(`Postgres ${pg.major} (${pg.psqlPath})`);
|
|
59
|
+
else if (pg.installed) fail(`Postgres ${pg.major} - too old, need 17+`);
|
|
60
|
+
else fail("Postgres not installed");
|
|
61
|
+
|
|
62
|
+
// Service
|
|
63
|
+
if (pg.installed) {
|
|
64
|
+
const svc = detectPostgresService();
|
|
65
|
+
if (svc.running) success("Postgres service running on localhost:5432");
|
|
66
|
+
else fail("Postgres installed but not running");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Env file
|
|
70
|
+
section("Configuration");
|
|
71
|
+
const envExists = fs.existsSync(ENV_PATH);
|
|
72
|
+
if (envExists) success(`.env.local found`);
|
|
73
|
+
else warn(".env.local not found - run `npx recallmem init` to create it");
|
|
74
|
+
|
|
75
|
+
const env = readEnv();
|
|
76
|
+
if (env.DATABASE_URL) {
|
|
77
|
+
info(`DATABASE_URL=${env.DATABASE_URL.replace(/:[^@]*@/, ":***@")}`);
|
|
78
|
+
}
|
|
79
|
+
if (env.OLLAMA_URL) info(`OLLAMA_URL=${env.OLLAMA_URL}`);
|
|
80
|
+
if (env.OLLAMA_CHAT_MODEL) info(`OLLAMA_CHAT_MODEL=${env.OLLAMA_CHAT_MODEL}`);
|
|
81
|
+
if (env.OLLAMA_FAST_MODEL) info(`OLLAMA_FAST_MODEL=${env.OLLAMA_FAST_MODEL}`);
|
|
82
|
+
if (env.OLLAMA_EMBED_MODEL) info(`OLLAMA_EMBED_MODEL=${env.OLLAMA_EMBED_MODEL}`);
|
|
83
|
+
|
|
84
|
+
// Database connectivity
|
|
85
|
+
section("Database");
|
|
86
|
+
if (env.DATABASE_URL && pg.installed) {
|
|
87
|
+
const dbCheck = await detectDatabase(env.DATABASE_URL);
|
|
88
|
+
if (dbCheck.exists) {
|
|
89
|
+
success("Database reachable");
|
|
90
|
+
const pgvec = await detectPgvector(env.DATABASE_URL);
|
|
91
|
+
if (pgvec.available) success("pgvector extension available");
|
|
92
|
+
else fail("pgvector extension not available");
|
|
93
|
+
} else {
|
|
94
|
+
fail(`Cannot connect to database: ${dbCheck.error}`);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
info("Skipped (DATABASE_URL not set)");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ollama
|
|
101
|
+
section("LLM runtime");
|
|
102
|
+
const ollama = detectOllama();
|
|
103
|
+
if (ollama.running) {
|
|
104
|
+
success(`Ollama running ${ollama.version || ""}`);
|
|
105
|
+
|
|
106
|
+
// Required model
|
|
107
|
+
const embed = await detectOllamaModel("embeddinggemma");
|
|
108
|
+
if (embed.installed) success("embeddinggemma installed (required)");
|
|
109
|
+
else fail("embeddinggemma not installed - run: ollama pull embeddinggemma");
|
|
110
|
+
|
|
111
|
+
// Optional models
|
|
112
|
+
const models = ["gemma4:26b", "gemma4:31b", "gemma4:e4b", "gemma4:e2b"];
|
|
113
|
+
for (const m of models) {
|
|
114
|
+
const r = await detectOllamaModel(m);
|
|
115
|
+
if (r.installed) success(`${m} installed`);
|
|
116
|
+
}
|
|
117
|
+
} else if (ollama.installed) {
|
|
118
|
+
warn("Ollama installed but not running");
|
|
119
|
+
info("Try: brew services start ollama (Mac) or ollama serve");
|
|
120
|
+
} else {
|
|
121
|
+
warn("Ollama not installed (optional - cloud providers still work)");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
blank();
|
|
125
|
+
console.log(color.dim("Done. Run `npx recallmem init` to fix any issues."));
|
|
126
|
+
blank();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { doctorCommand };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recallmem init / setup
|
|
3
|
+
*
|
|
4
|
+
* Idempotent setup pipeline:
|
|
5
|
+
* 1. Check Node.js version
|
|
6
|
+
* 2. Check Postgres is installed and running
|
|
7
|
+
* 3. Check pgvector is available
|
|
8
|
+
* 4. Create the database if missing
|
|
9
|
+
* 5. Run migrations
|
|
10
|
+
* 6. Check Ollama (optional - skip if user wants cloud-only)
|
|
11
|
+
* 7. Pull embeddinggemma (required, ~600MB)
|
|
12
|
+
* 8. Offer to pull gemma4:26b (recommended chat model, ~18GB)
|
|
13
|
+
* 9. Generate .env.local with sensible defaults
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("node:fs");
|
|
17
|
+
const path = require("node:path");
|
|
18
|
+
const { spawn, spawnSync, execSync } = require("node:child_process");
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
getOS,
|
|
22
|
+
detectNode,
|
|
23
|
+
detectPostgres,
|
|
24
|
+
detectPostgresService,
|
|
25
|
+
detectPgvector,
|
|
26
|
+
detectOllama,
|
|
27
|
+
detectOllamaModel,
|
|
28
|
+
detectDatabase,
|
|
29
|
+
} = require("../lib/detect");
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
postgresInstallHint,
|
|
33
|
+
pgvectorInstallHint,
|
|
34
|
+
ollamaInstallHint,
|
|
35
|
+
nodeInstallHint,
|
|
36
|
+
} = require("../lib/install-hints");
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
color,
|
|
40
|
+
step,
|
|
41
|
+
success,
|
|
42
|
+
warn,
|
|
43
|
+
fail,
|
|
44
|
+
info,
|
|
45
|
+
section,
|
|
46
|
+
blank,
|
|
47
|
+
} = require("../lib/output");
|
|
48
|
+
|
|
49
|
+
const { confirm } = require("../lib/prompt");
|
|
50
|
+
|
|
51
|
+
const DEFAULT_DB_NAME = "recallmem";
|
|
52
|
+
|
|
53
|
+
function defaultConnectionString() {
|
|
54
|
+
const user = process.env.USER || process.env.USERNAME || "postgres";
|
|
55
|
+
return `postgres://${user}@localhost:5432/${DEFAULT_DB_NAME}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readEnv(envPath) {
|
|
59
|
+
if (!fs.existsSync(envPath)) return {};
|
|
60
|
+
const text = fs.readFileSync(envPath, "utf-8");
|
|
61
|
+
const env = {};
|
|
62
|
+
for (const line of text.split("\n")) {
|
|
63
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
64
|
+
if (m) env[m[1]] = m[2];
|
|
65
|
+
}
|
|
66
|
+
return env;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeEnv(envPath, env) {
|
|
70
|
+
const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
71
|
+
fs.writeFileSync(envPath, lines.join("\n") + "\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function setupCommand(opts = {}) {
|
|
75
|
+
const {
|
|
76
|
+
silent = false,
|
|
77
|
+
skipIfDone = false,
|
|
78
|
+
installPath = process.cwd(),
|
|
79
|
+
devMode = false,
|
|
80
|
+
} = opts;
|
|
81
|
+
const ENV_PATH = path.join(installPath, ".env.local");
|
|
82
|
+
|
|
83
|
+
// ─── Step 1: Node.js ───────────────────────────────────────────────────
|
|
84
|
+
if (!silent) section("Checking dependencies");
|
|
85
|
+
const node = detectNode();
|
|
86
|
+
if (!node.ok) {
|
|
87
|
+
fail(`Node.js ${node.version} is too old (need ${node.needed}+)`);
|
|
88
|
+
blank();
|
|
89
|
+
console.log(nodeInstallHint());
|
|
90
|
+
return { ok: false };
|
|
91
|
+
}
|
|
92
|
+
success(`Node.js ${node.version}`);
|
|
93
|
+
|
|
94
|
+
// ─── Step 2: Postgres ──────────────────────────────────────────────────
|
|
95
|
+
const pg = detectPostgres();
|
|
96
|
+
if (!pg.installed) {
|
|
97
|
+
fail("Postgres not found");
|
|
98
|
+
blank();
|
|
99
|
+
console.log(postgresInstallHint());
|
|
100
|
+
blank();
|
|
101
|
+
info("Once installed, re-run: npx recallmem");
|
|
102
|
+
return { ok: false };
|
|
103
|
+
}
|
|
104
|
+
if (!pg.ok) {
|
|
105
|
+
fail(`Postgres ${pg.major} found, but version 17+ is required`);
|
|
106
|
+
blank();
|
|
107
|
+
console.log(postgresInstallHint());
|
|
108
|
+
return { ok: false };
|
|
109
|
+
}
|
|
110
|
+
success(`Postgres ${pg.major}`);
|
|
111
|
+
|
|
112
|
+
// ─── Step 3: Postgres service running ──────────────────────────────────
|
|
113
|
+
const pgService = detectPostgresService();
|
|
114
|
+
if (!pgService.running) {
|
|
115
|
+
warn("Postgres is installed but not running");
|
|
116
|
+
if (getOS() === "mac") {
|
|
117
|
+
info("Try: brew services start postgresql@17");
|
|
118
|
+
} else if (getOS() === "linux") {
|
|
119
|
+
info("Try: sudo systemctl start postgresql");
|
|
120
|
+
}
|
|
121
|
+
return { ok: false };
|
|
122
|
+
}
|
|
123
|
+
success("Postgres service running on localhost:5432");
|
|
124
|
+
|
|
125
|
+
// ─── Step 4: env file (we need DATABASE_URL before checking pgvector) ──
|
|
126
|
+
const env = readEnv(ENV_PATH);
|
|
127
|
+
let connectionString = env.DATABASE_URL;
|
|
128
|
+
|
|
129
|
+
if (!connectionString) {
|
|
130
|
+
connectionString = defaultConnectionString();
|
|
131
|
+
step(`No .env.local found, will create one with default DATABASE_URL`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Step 5: Database exists ───────────────────────────────────────────
|
|
135
|
+
// Extract the database name from the connection string for accurate messages
|
|
136
|
+
const dbNameMatch = connectionString.match(/\/([^/?]+)(\?|$)/);
|
|
137
|
+
const dbName = dbNameMatch ? dbNameMatch[1] : DEFAULT_DB_NAME;
|
|
138
|
+
|
|
139
|
+
const dbCheck = await detectDatabase(connectionString);
|
|
140
|
+
if (!dbCheck.exists) {
|
|
141
|
+
step(`Database '${dbName}' not found, creating...`);
|
|
142
|
+
try {
|
|
143
|
+
execSync(`${pg.psqlPath.replace(/psql$/, "createdb")} ${dbName}`, {
|
|
144
|
+
stdio: "pipe",
|
|
145
|
+
});
|
|
146
|
+
success(`Created database '${dbName}'`);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
fail(`Failed to create database: ${err.message}`);
|
|
149
|
+
info(`Try manually: createdb ${dbName}`);
|
|
150
|
+
return { ok: false };
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
success(`Database '${dbName}' exists`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Step 6: pgvector extension available ──────────────────────────────
|
|
157
|
+
const pgvec = await detectPgvector(connectionString);
|
|
158
|
+
if (!pgvec.available) {
|
|
159
|
+
fail("pgvector extension is not installed in Postgres");
|
|
160
|
+
blank();
|
|
161
|
+
console.log(pgvectorInstallHint());
|
|
162
|
+
return { ok: false };
|
|
163
|
+
}
|
|
164
|
+
success("pgvector extension available");
|
|
165
|
+
|
|
166
|
+
// ─── Step 7: Run migrations ────────────────────────────────────────────
|
|
167
|
+
step("Running migrations...");
|
|
168
|
+
try {
|
|
169
|
+
process.env.DATABASE_URL = connectionString;
|
|
170
|
+
const migrateResult = spawnSync("npx", ["tsx", "scripts/migrate.ts"], {
|
|
171
|
+
cwd: installPath,
|
|
172
|
+
stdio: silent ? "pipe" : "inherit",
|
|
173
|
+
env: { ...process.env, DATABASE_URL: connectionString },
|
|
174
|
+
});
|
|
175
|
+
if (migrateResult.status !== 0) {
|
|
176
|
+
fail("Migration failed");
|
|
177
|
+
return { ok: false };
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
fail(`Migration failed: ${err.message}`);
|
|
181
|
+
return { ok: false };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Step 8: Ollama (optional) ─────────────────────────────────────────
|
|
185
|
+
section("Checking LLM runtime");
|
|
186
|
+
const ollama = detectOllama();
|
|
187
|
+
let ollamaUrl = env.OLLAMA_URL || "http://localhost:11434";
|
|
188
|
+
|
|
189
|
+
if (!ollama.installed) {
|
|
190
|
+
warn("Ollama not installed (optional - you can use cloud providers instead)");
|
|
191
|
+
blank();
|
|
192
|
+
console.log(ollamaInstallHint());
|
|
193
|
+
blank();
|
|
194
|
+
info("Continuing without Ollama. You can add Claude/OpenAI as a provider in the app.");
|
|
195
|
+
blank();
|
|
196
|
+
} else if (!ollama.running) {
|
|
197
|
+
warn("Ollama is installed but not running");
|
|
198
|
+
if (getOS() === "mac") {
|
|
199
|
+
info("Try: brew services start ollama");
|
|
200
|
+
} else {
|
|
201
|
+
info("Try: ollama serve");
|
|
202
|
+
}
|
|
203
|
+
blank();
|
|
204
|
+
} else {
|
|
205
|
+
success(`Ollama running (${ollama.version || "unknown version"})`);
|
|
206
|
+
|
|
207
|
+
// ─── Step 9: Required model: embeddinggemma ──────────────────────────
|
|
208
|
+
const embedModel = await detectOllamaModel("embeddinggemma");
|
|
209
|
+
if (!embedModel.installed) {
|
|
210
|
+
step("Pulling embeddinggemma (~600MB, required for vector search)...");
|
|
211
|
+
try {
|
|
212
|
+
execSync("ollama pull embeddinggemma", { stdio: "inherit" });
|
|
213
|
+
success("Pulled embeddinggemma");
|
|
214
|
+
} catch (err) {
|
|
215
|
+
fail(`Failed to pull embeddinggemma: ${err.message}`);
|
|
216
|
+
return { ok: false };
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
success("embeddinggemma installed");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Step 10: Recommended model: gemma4:26b ──────────────────────────
|
|
223
|
+
const chatModel = await detectOllamaModel("gemma4:26b");
|
|
224
|
+
if (!chatModel.installed && !skipIfDone) {
|
|
225
|
+
blank();
|
|
226
|
+
info("Recommended chat model: gemma4:26b (~18GB)");
|
|
227
|
+
info("Optional - you can use cloud providers (Claude, GPT) instead.");
|
|
228
|
+
const wantsPull = await confirm("Pull gemma4:26b now?", false);
|
|
229
|
+
if (wantsPull) {
|
|
230
|
+
try {
|
|
231
|
+
execSync("ollama pull gemma4:26b", { stdio: "inherit" });
|
|
232
|
+
success("Pulled gemma4:26b");
|
|
233
|
+
} catch (err) {
|
|
234
|
+
fail(`Failed to pull gemma4:26b: ${err.message}`);
|
|
235
|
+
info("You can pull it later with: ollama pull gemma4:26b");
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
info("Skipped. You can pull it later with: ollama pull gemma4:26b");
|
|
239
|
+
}
|
|
240
|
+
} else if (chatModel.installed) {
|
|
241
|
+
success("gemma4:26b installed");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Step 11: Write .env.local ─────────────────────────────────────────
|
|
246
|
+
section("Writing config");
|
|
247
|
+
const finalEnv = {
|
|
248
|
+
DATABASE_URL: env.DATABASE_URL || connectionString,
|
|
249
|
+
OLLAMA_URL: env.OLLAMA_URL || ollamaUrl,
|
|
250
|
+
OLLAMA_CHAT_MODEL: env.OLLAMA_CHAT_MODEL || "gemma4:26b",
|
|
251
|
+
OLLAMA_FAST_MODEL: env.OLLAMA_FAST_MODEL || "gemma4:e4b",
|
|
252
|
+
OLLAMA_EMBED_MODEL: env.OLLAMA_EMBED_MODEL || "embeddinggemma",
|
|
253
|
+
};
|
|
254
|
+
const existedBefore = fs.existsSync(ENV_PATH);
|
|
255
|
+
writeEnv(ENV_PATH, finalEnv);
|
|
256
|
+
success(`.env.local ${existedBefore ? "updated" : "created"}`);
|
|
257
|
+
|
|
258
|
+
// ─── Step 12: Production build (skipped in dev mode) ──────────────────
|
|
259
|
+
// End users get a production build for speed and to avoid dev-mode hot-reload
|
|
260
|
+
// hydration warnings. Developers in their own checkout (devMode=true) skip the
|
|
261
|
+
// build so they get hot reload via `next dev`.
|
|
262
|
+
if (!devMode) {
|
|
263
|
+
const hasBuild = fs.existsSync(
|
|
264
|
+
path.join(installPath, ".next", "BUILD_ID")
|
|
265
|
+
);
|
|
266
|
+
if (!hasBuild) {
|
|
267
|
+
section("Building app for production");
|
|
268
|
+
info("This takes about 30-60 seconds, but only on the first install.");
|
|
269
|
+
try {
|
|
270
|
+
const buildResult = spawnSync("npx", ["next", "build"], {
|
|
271
|
+
cwd: installPath,
|
|
272
|
+
stdio: silent ? "pipe" : "inherit",
|
|
273
|
+
env: { ...process.env, ...finalEnv },
|
|
274
|
+
});
|
|
275
|
+
if (buildResult.status !== 0) {
|
|
276
|
+
warn("Production build failed, will fall back to dev mode at runtime");
|
|
277
|
+
} else {
|
|
278
|
+
success("Production build complete");
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
warn(`Build failed: ${err.message}`);
|
|
282
|
+
info("Falling back to dev mode at runtime");
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
success("Production build already exists");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
blank();
|
|
290
|
+
success(color.bold("Setup complete!"));
|
|
291
|
+
blank();
|
|
292
|
+
|
|
293
|
+
return { ok: true };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = { setupCommand };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recallmem start
|
|
3
|
+
*
|
|
4
|
+
* Starts the Next.js server. In dev mode (no .next build), runs `next dev`.
|
|
5
|
+
* In production mode (built), runs `next start`.
|
|
6
|
+
*
|
|
7
|
+
* Opens the browser when the server is ready.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const { spawn } = require("node:child_process");
|
|
13
|
+
const { color, section, success, info } = require("../lib/output");
|
|
14
|
+
|
|
15
|
+
function openBrowser(url) {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
const cmd =
|
|
18
|
+
platform === "darwin" ? "open" :
|
|
19
|
+
platform === "win32" ? "start" :
|
|
20
|
+
"xdg-open";
|
|
21
|
+
try {
|
|
22
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
23
|
+
} catch {
|
|
24
|
+
// best effort
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function startCommand(opts = {}) {
|
|
29
|
+
const { installPath = process.cwd() } = opts;
|
|
30
|
+
section("Starting RecallMEM");
|
|
31
|
+
|
|
32
|
+
const hasBuild = fs.existsSync(path.join(installPath, ".next", "BUILD_ID"));
|
|
33
|
+
const command = hasBuild ? "start" : "dev";
|
|
34
|
+
|
|
35
|
+
info(hasBuild ? "Production build detected, running next start" : "No build found, running next dev");
|
|
36
|
+
info("Opening http://localhost:3000 in your browser...");
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log(color.dim(" (Press Ctrl+C to stop)"));
|
|
39
|
+
console.log("");
|
|
40
|
+
|
|
41
|
+
// Open the browser shortly after starting (give Next a moment to be ready)
|
|
42
|
+
setTimeout(() => openBrowser("http://localhost:3000"), 2000);
|
|
43
|
+
|
|
44
|
+
const child = spawn("npx", ["next", command], {
|
|
45
|
+
cwd: installPath,
|
|
46
|
+
stdio: "inherit",
|
|
47
|
+
env: process.env,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
child.on("exit", (code) => {
|
|
52
|
+
if (code === 0 || code === null) resolve();
|
|
53
|
+
else reject(new Error(`Server exited with code ${code}`));
|
|
54
|
+
});
|
|
55
|
+
child.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { startCommand };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recallmem upgrade
|
|
3
|
+
*
|
|
4
|
+
* Pulls the latest code (git pull), reinstalls dependencies, runs pending
|
|
5
|
+
* migrations, and rebuilds the production bundle so the next start picks up
|
|
6
|
+
* the new code. Doesn't restart the server itself -- assumes you're not
|
|
7
|
+
* running it, or you'll restart manually.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("node:fs");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const { spawnSync } = require("node:child_process");
|
|
13
|
+
const { gitPull, detectInstallMode } = require("../lib/install-mode");
|
|
14
|
+
const { color, section, success, fail, warn, info, blank } = require("../lib/output");
|
|
15
|
+
|
|
16
|
+
async function upgradeCommand() {
|
|
17
|
+
const mode = detectInstallMode();
|
|
18
|
+
|
|
19
|
+
if (mode.mode === "first-run") {
|
|
20
|
+
fail("Nothing to upgrade -- no install found.");
|
|
21
|
+
info("Run `npx recallmem` first to install.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
section(`Upgrading ${mode.path}`);
|
|
26
|
+
|
|
27
|
+
// Pull latest code
|
|
28
|
+
const pullResult = gitPull(mode.path);
|
|
29
|
+
if (!pullResult.ok) {
|
|
30
|
+
fail(`git pull failed: ${pullResult.error}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Run pending migrations
|
|
35
|
+
section("Running migrations");
|
|
36
|
+
const migrateResult = spawnSync("npx", ["tsx", "scripts/migrate.ts"], {
|
|
37
|
+
cwd: mode.path,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
env: process.env,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (migrateResult.status !== 0) {
|
|
43
|
+
fail("Migration failed");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rebuild the production bundle so the next start picks up new code.
|
|
48
|
+
// Skip in dev mode since developers run `next dev` (which compiles on demand).
|
|
49
|
+
if (mode.mode !== "dev") {
|
|
50
|
+
section("Rebuilding production bundle");
|
|
51
|
+
info("This takes about 30-60 seconds.");
|
|
52
|
+
|
|
53
|
+
// Wipe old build first so we always get fresh output
|
|
54
|
+
const nextDir = path.join(mode.path, ".next");
|
|
55
|
+
if (fs.existsSync(nextDir)) {
|
|
56
|
+
fs.rmSync(nextDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const buildResult = spawnSync("npx", ["next", "build"], {
|
|
60
|
+
cwd: mode.path,
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
env: process.env,
|
|
63
|
+
});
|
|
64
|
+
if (buildResult.status !== 0) {
|
|
65
|
+
warn("Build failed, will fall back to dev mode at runtime");
|
|
66
|
+
} else {
|
|
67
|
+
success("Production build complete");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
blank();
|
|
72
|
+
success("Up to date");
|
|
73
|
+
info("Restart with: npx recallmem");
|
|
74
|
+
blank();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { upgradeCommand };
|