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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency detection utilities. Each function checks for a runtime dep
|
|
3
|
+
* and returns a result describing what's there or what's missing.
|
|
4
|
+
*
|
|
5
|
+
* Pure Node, no external deps. Uses child_process to shell out to system tools.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync, spawnSync } = require("node:child_process");
|
|
9
|
+
const os = require("node:os");
|
|
10
|
+
|
|
11
|
+
function getOS() {
|
|
12
|
+
const p = os.platform();
|
|
13
|
+
if (p === "darwin") return "mac";
|
|
14
|
+
if (p === "linux") return "linux";
|
|
15
|
+
if (p === "win32") return "windows";
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commandExists(cmd) {
|
|
20
|
+
const which = spawnSync(getOS() === "windows" ? "where" : "which", [cmd], {
|
|
21
|
+
stdio: "pipe",
|
|
22
|
+
});
|
|
23
|
+
return which.status === 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tryExec(cmd) {
|
|
27
|
+
try {
|
|
28
|
+
return execSync(cmd, { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Node.js version
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function detectNode() {
|
|
39
|
+
const version = process.version.replace(/^v/, "");
|
|
40
|
+
const major = parseInt(version.split(".")[0], 10);
|
|
41
|
+
return {
|
|
42
|
+
ok: major >= 20,
|
|
43
|
+
version,
|
|
44
|
+
major,
|
|
45
|
+
needed: 20,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// Postgres
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function detectPostgres() {
|
|
54
|
+
// Try common locations for psql binary
|
|
55
|
+
const candidates = [
|
|
56
|
+
"/opt/homebrew/opt/postgresql@17/bin/psql",
|
|
57
|
+
"/opt/homebrew/opt/postgresql@18/bin/psql",
|
|
58
|
+
"/usr/local/opt/postgresql@17/bin/psql",
|
|
59
|
+
"/usr/lib/postgresql/17/bin/psql",
|
|
60
|
+
"/usr/bin/psql",
|
|
61
|
+
"psql",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
let psqlPath = null;
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
const result = spawnSync(candidate, ["--version"], { stdio: "pipe" });
|
|
67
|
+
if (result.status === 0) {
|
|
68
|
+
psqlPath = candidate;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!psqlPath) {
|
|
74
|
+
return { ok: false, installed: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const versionStr = tryExec(`${psqlPath} --version`);
|
|
78
|
+
const versionMatch = versionStr?.match(/(\d+)\.(\d+)/);
|
|
79
|
+
const major = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
ok: major >= 17,
|
|
83
|
+
installed: true,
|
|
84
|
+
psqlPath,
|
|
85
|
+
version: versionStr,
|
|
86
|
+
major,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectPostgresService() {
|
|
91
|
+
// Try connecting to the default port
|
|
92
|
+
const result = spawnSync(
|
|
93
|
+
"pg_isready",
|
|
94
|
+
["-h", "localhost", "-p", "5432"],
|
|
95
|
+
{ stdio: "pipe" }
|
|
96
|
+
);
|
|
97
|
+
return { running: result.status === 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// pgvector extension (uses psql via shell to avoid pg dependency in the CLI)
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function detectPgvector(connectionString) {
|
|
105
|
+
try {
|
|
106
|
+
const result = spawnSync(
|
|
107
|
+
"psql",
|
|
108
|
+
[
|
|
109
|
+
connectionString,
|
|
110
|
+
"-tAc",
|
|
111
|
+
"SELECT 1 FROM pg_available_extensions WHERE name = 'vector'",
|
|
112
|
+
],
|
|
113
|
+
{ stdio: "pipe", encoding: "utf-8" }
|
|
114
|
+
);
|
|
115
|
+
if (result.status !== 0) {
|
|
116
|
+
return { ok: false, available: false, error: result.stderr?.trim() };
|
|
117
|
+
}
|
|
118
|
+
const available = result.stdout.trim() === "1";
|
|
119
|
+
return { ok: available, available };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { ok: false, available: false, error: err.message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// Ollama
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function detectOllama() {
|
|
130
|
+
const installed = commandExists("ollama");
|
|
131
|
+
if (!installed) return { ok: false, installed: false, running: false };
|
|
132
|
+
|
|
133
|
+
const version = tryExec("ollama --version");
|
|
134
|
+
|
|
135
|
+
// Check if the server is running
|
|
136
|
+
let running = false;
|
|
137
|
+
try {
|
|
138
|
+
execSync("curl -s -o /dev/null -w '%{http_code}' http://localhost:11434/api/version", {
|
|
139
|
+
stdio: "pipe",
|
|
140
|
+
});
|
|
141
|
+
running = true;
|
|
142
|
+
} catch {
|
|
143
|
+
running = false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ok: running, installed, running, version };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function detectOllamaModel(modelName) {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const http = require("node:http");
|
|
152
|
+
const req = http.request(
|
|
153
|
+
{
|
|
154
|
+
host: "localhost",
|
|
155
|
+
port: 11434,
|
|
156
|
+
path: "/api/tags",
|
|
157
|
+
method: "GET",
|
|
158
|
+
timeout: 3000,
|
|
159
|
+
},
|
|
160
|
+
(res) => {
|
|
161
|
+
let body = "";
|
|
162
|
+
res.on("data", (chunk) => (body += chunk));
|
|
163
|
+
res.on("end", () => {
|
|
164
|
+
try {
|
|
165
|
+
const data = JSON.parse(body);
|
|
166
|
+
const models = data.models || [];
|
|
167
|
+
const found = models.some(
|
|
168
|
+
(m) => m.name === modelName || m.name === `${modelName}:latest`
|
|
169
|
+
);
|
|
170
|
+
resolve({ ok: found, installed: found });
|
|
171
|
+
} catch {
|
|
172
|
+
resolve({ ok: false, installed: false });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
req.on("error", () => resolve({ ok: false, installed: false }));
|
|
178
|
+
req.on("timeout", () => {
|
|
179
|
+
req.destroy();
|
|
180
|
+
resolve({ ok: false, installed: false });
|
|
181
|
+
});
|
|
182
|
+
req.end();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
// Database existence (uses psql via shell to avoid pg dependency in the CLI)
|
|
188
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
function detectDatabase(connectionString) {
|
|
191
|
+
try {
|
|
192
|
+
const result = spawnSync("psql", [connectionString, "-tAc", "SELECT 1"], {
|
|
193
|
+
stdio: "pipe",
|
|
194
|
+
encoding: "utf-8",
|
|
195
|
+
});
|
|
196
|
+
if (result.status !== 0) {
|
|
197
|
+
return { ok: false, exists: false, error: result.stderr?.trim() };
|
|
198
|
+
}
|
|
199
|
+
return { ok: true, exists: true };
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return { ok: false, exists: false, error: err.message };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
getOS,
|
|
207
|
+
commandExists,
|
|
208
|
+
detectNode,
|
|
209
|
+
detectPostgres,
|
|
210
|
+
detectPostgresService,
|
|
211
|
+
detectPgvector,
|
|
212
|
+
detectOllama,
|
|
213
|
+
detectOllamaModel,
|
|
214
|
+
detectDatabase,
|
|
215
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-specific install hints. Printed when a dependency is missing.
|
|
3
|
+
* The user has to run these themselves -- we don't auto-install with sudo.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { color } = require("./output");
|
|
7
|
+
const { getOS } = require("./detect");
|
|
8
|
+
|
|
9
|
+
function postgresInstallHint() {
|
|
10
|
+
const os = getOS();
|
|
11
|
+
const lines = [
|
|
12
|
+
color.bold("Postgres is required (version 17+ with pgvector extension)."),
|
|
13
|
+
"",
|
|
14
|
+
];
|
|
15
|
+
if (os === "mac") {
|
|
16
|
+
lines.push("Install via Homebrew:");
|
|
17
|
+
lines.push(" brew install postgresql@17 pgvector");
|
|
18
|
+
lines.push(" brew services start postgresql@17");
|
|
19
|
+
} else if (os === "linux") {
|
|
20
|
+
lines.push("Install via apt (Ubuntu/Debian):");
|
|
21
|
+
lines.push(" sudo apt update");
|
|
22
|
+
lines.push(" sudo apt install -y postgresql-17 postgresql-17-pgvector");
|
|
23
|
+
lines.push(" sudo systemctl start postgresql");
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push("Or via dnf (Fedora):");
|
|
26
|
+
lines.push(" sudo dnf install postgresql17-server pgvector_17");
|
|
27
|
+
} else if (os === "windows") {
|
|
28
|
+
lines.push("Recommended: use WSL2 with Ubuntu and follow the Linux instructions.");
|
|
29
|
+
lines.push("Native Windows: download from https://www.postgresql.org/download/windows/");
|
|
30
|
+
lines.push("Then install pgvector separately.");
|
|
31
|
+
} else {
|
|
32
|
+
lines.push("See https://www.postgresql.org/download/ for your platform.");
|
|
33
|
+
}
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pgvectorInstallHint() {
|
|
38
|
+
const os = getOS();
|
|
39
|
+
const lines = [
|
|
40
|
+
color.bold("pgvector extension is required."),
|
|
41
|
+
"",
|
|
42
|
+
];
|
|
43
|
+
if (os === "mac") {
|
|
44
|
+
lines.push(" brew install pgvector");
|
|
45
|
+
} else if (os === "linux") {
|
|
46
|
+
lines.push(" sudo apt install postgresql-17-pgvector");
|
|
47
|
+
lines.push(" # or: https://github.com/pgvector/pgvector#installation");
|
|
48
|
+
} else {
|
|
49
|
+
lines.push("See https://github.com/pgvector/pgvector#installation");
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ollamaInstallHint() {
|
|
55
|
+
const os = getOS();
|
|
56
|
+
const lines = [
|
|
57
|
+
color.bold("Ollama is required for local LLMs."),
|
|
58
|
+
color.dim("(Skip this if you only want to use cloud providers like Claude or GPT.)"),
|
|
59
|
+
"",
|
|
60
|
+
];
|
|
61
|
+
if (os === "mac") {
|
|
62
|
+
lines.push("Install:");
|
|
63
|
+
lines.push(" brew install ollama");
|
|
64
|
+
lines.push(" brew services start ollama");
|
|
65
|
+
} else if (os === "linux") {
|
|
66
|
+
lines.push("Install:");
|
|
67
|
+
lines.push(" curl -fsSL https://ollama.com/install.sh | sh");
|
|
68
|
+
} else if (os === "windows") {
|
|
69
|
+
lines.push("Download from https://ollama.com/download/windows");
|
|
70
|
+
} else {
|
|
71
|
+
lines.push("See https://ollama.com/download");
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function nodeInstallHint() {
|
|
77
|
+
return [
|
|
78
|
+
color.bold("Node.js 20 or newer is required."),
|
|
79
|
+
"",
|
|
80
|
+
"Install via nvm (recommended):",
|
|
81
|
+
" curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash",
|
|
82
|
+
" nvm install 20",
|
|
83
|
+
"",
|
|
84
|
+
"Or via Homebrew (Mac): brew install node",
|
|
85
|
+
"Or download from https://nodejs.org",
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
postgresInstallHint,
|
|
91
|
+
pgvectorInstallHint,
|
|
92
|
+
ollamaInstallHint,
|
|
93
|
+
nodeInstallHint,
|
|
94
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install mode detection.
|
|
3
|
+
*
|
|
4
|
+
* The CLI supports three workflows that all use the same `recallmem` command:
|
|
5
|
+
*
|
|
6
|
+
* 1. "dev" mode - already inside a recallmem git checkout
|
|
7
|
+
* (cwd has package.json with name "recallmem", app/, lib/, migrations/)
|
|
8
|
+
* Use cwd. Don't clone anything. Hot reload works.
|
|
9
|
+
*
|
|
10
|
+
* 2. "user" mode - ~/.recallmem already exists from a previous run
|
|
11
|
+
* Use that. Skip the clone step.
|
|
12
|
+
*
|
|
13
|
+
* 3. "first-run" mode - neither of the above
|
|
14
|
+
* Clone the repo to ~/.recallmem, then proceed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const os = require("node:os");
|
|
20
|
+
const { execSync, spawnSync } = require("node:child_process");
|
|
21
|
+
|
|
22
|
+
const RECALLMEM_HOME =
|
|
23
|
+
process.env.RECALLMEM_HOME || path.join(os.homedir(), ".recallmem");
|
|
24
|
+
|
|
25
|
+
const REPO_URL =
|
|
26
|
+
process.env.RECALLMEM_REPO || "https://github.com/RealChrisSean/RecallMEM.git";
|
|
27
|
+
|
|
28
|
+
function isRecallmemCheckout(dir) {
|
|
29
|
+
const pkgPath = path.join(dir, "package.json");
|
|
30
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
33
|
+
if (pkg.name !== "recallmem") return false;
|
|
34
|
+
return (
|
|
35
|
+
fs.existsSync(path.join(dir, "app")) &&
|
|
36
|
+
fs.existsSync(path.join(dir, "lib")) &&
|
|
37
|
+
fs.existsSync(path.join(dir, "migrations"))
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectInstallMode() {
|
|
45
|
+
// 1. Are we already inside a recallmem checkout?
|
|
46
|
+
if (isRecallmemCheckout(process.cwd())) {
|
|
47
|
+
return { mode: "dev", path: process.cwd() };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Does ~/.recallmem already exist as a checkout?
|
|
51
|
+
if (isRecallmemCheckout(RECALLMEM_HOME)) {
|
|
52
|
+
return { mode: "user", path: RECALLMEM_HOME };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Need to clone
|
|
56
|
+
return { mode: "first-run", path: RECALLMEM_HOME };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function gitInstalled() {
|
|
60
|
+
try {
|
|
61
|
+
execSync("git --version", { stdio: "pipe" });
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clone the repo to ~/.recallmem and run npm install.
|
|
70
|
+
* Returns true on success, false on failure.
|
|
71
|
+
*/
|
|
72
|
+
function cloneAndInstall(targetPath) {
|
|
73
|
+
if (!gitInstalled()) {
|
|
74
|
+
return { ok: false, error: "git is not installed" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Make sure parent dir exists
|
|
78
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
79
|
+
|
|
80
|
+
// Clone (shallow, single branch, fast)
|
|
81
|
+
const cloneResult = spawnSync(
|
|
82
|
+
"git",
|
|
83
|
+
["clone", "--depth", "1", REPO_URL, targetPath],
|
|
84
|
+
{ stdio: "inherit" }
|
|
85
|
+
);
|
|
86
|
+
if (cloneResult.status !== 0) {
|
|
87
|
+
return { ok: false, error: "git clone failed" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Install npm dependencies inside the clone
|
|
91
|
+
const installResult = spawnSync("npm", ["install"], {
|
|
92
|
+
cwd: targetPath,
|
|
93
|
+
stdio: "inherit",
|
|
94
|
+
});
|
|
95
|
+
if (installResult.status !== 0) {
|
|
96
|
+
return { ok: false, error: "npm install failed" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pull latest changes inside an existing install. Used by `recallmem upgrade`.
|
|
104
|
+
*/
|
|
105
|
+
function gitPull(targetPath) {
|
|
106
|
+
if (!gitInstalled()) {
|
|
107
|
+
return { ok: false, error: "git is not installed" };
|
|
108
|
+
}
|
|
109
|
+
if (!fs.existsSync(path.join(targetPath, ".git"))) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: `${targetPath} is not a git repository (was it cloned?)`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const result = spawnSync("git", ["pull"], {
|
|
116
|
+
cwd: targetPath,
|
|
117
|
+
stdio: "inherit",
|
|
118
|
+
});
|
|
119
|
+
if (result.status !== 0) {
|
|
120
|
+
return { ok: false, error: "git pull failed" };
|
|
121
|
+
}
|
|
122
|
+
// Re-install in case dependencies changed
|
|
123
|
+
const installResult = spawnSync("npm", ["install"], {
|
|
124
|
+
cwd: targetPath,
|
|
125
|
+
stdio: "inherit",
|
|
126
|
+
});
|
|
127
|
+
if (installResult.status !== 0) {
|
|
128
|
+
return { ok: false, error: "npm install failed" };
|
|
129
|
+
}
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
RECALLMEM_HOME,
|
|
135
|
+
REPO_URL,
|
|
136
|
+
isRecallmemCheckout,
|
|
137
|
+
detectInstallMode,
|
|
138
|
+
gitInstalled,
|
|
139
|
+
cloneAndInstall,
|
|
140
|
+
gitPull,
|
|
141
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny zero-dependency terminal output helpers.
|
|
3
|
+
* No external chalk/picocolors dependency to keep the npm package light.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const isTTY = process.stdout.isTTY;
|
|
7
|
+
const noColor = process.env.NO_COLOR || !isTTY;
|
|
8
|
+
|
|
9
|
+
function wrap(start, end) {
|
|
10
|
+
return (s) => (noColor ? s : `\x1b[${start}m${s}\x1b[${end}m`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const color = {
|
|
14
|
+
bold: wrap(1, 22),
|
|
15
|
+
dim: wrap(2, 22),
|
|
16
|
+
red: wrap(31, 39),
|
|
17
|
+
green: wrap(32, 39),
|
|
18
|
+
yellow: wrap(33, 39),
|
|
19
|
+
blue: wrap(34, 39),
|
|
20
|
+
cyan: wrap(36, 39),
|
|
21
|
+
gray: wrap(90, 39),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const symbols = {
|
|
25
|
+
check: color.green("✓"),
|
|
26
|
+
cross: color.red("✗"),
|
|
27
|
+
warn: color.yellow("⚠"),
|
|
28
|
+
arrow: color.cyan("→"),
|
|
29
|
+
bullet: color.dim("•"),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function printHeader() {
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log(color.bold("RecallMEM") + color.dim(" - private local AI chatbot"));
|
|
35
|
+
console.log("");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function step(msg) {
|
|
39
|
+
console.log(` ${symbols.arrow} ${msg}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function success(msg) {
|
|
43
|
+
console.log(` ${symbols.check} ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function warn(msg) {
|
|
47
|
+
console.log(` ${symbols.warn} ${color.yellow(msg)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fail(msg) {
|
|
51
|
+
console.log(` ${symbols.cross} ${color.red(msg)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function info(msg) {
|
|
55
|
+
console.log(` ${color.dim(msg)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function section(title) {
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log(color.bold(title));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function blank() {
|
|
64
|
+
console.log("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
color,
|
|
69
|
+
symbols,
|
|
70
|
+
printHeader,
|
|
71
|
+
step,
|
|
72
|
+
success,
|
|
73
|
+
warn,
|
|
74
|
+
fail,
|
|
75
|
+
info,
|
|
76
|
+
section,
|
|
77
|
+
blank,
|
|
78
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny zero-dependency CLI prompt helpers (yes/no, list picker).
|
|
3
|
+
* Uses node:readline so we don't pull in inquirer/prompts as a dep.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require("node:readline");
|
|
7
|
+
|
|
8
|
+
function ask(question) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
});
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function confirm(question, defaultValue = true) {
|
|
22
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
23
|
+
const answer = await ask(question + suffix);
|
|
24
|
+
if (!answer) return defaultValue;
|
|
25
|
+
return /^y/i.test(answer);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function pick(question, choices) {
|
|
29
|
+
console.log(question);
|
|
30
|
+
choices.forEach((c, i) => {
|
|
31
|
+
console.log(` ${i + 1}) ${c.label}${c.detail ? ` — ${c.detail}` : ""}`);
|
|
32
|
+
});
|
|
33
|
+
while (true) {
|
|
34
|
+
const answer = await ask("Pick a number: ");
|
|
35
|
+
const n = parseInt(answer, 10);
|
|
36
|
+
if (n >= 1 && n <= choices.length) {
|
|
37
|
+
return choices[n - 1];
|
|
38
|
+
}
|
|
39
|
+
console.log(" invalid choice, try again");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { ask, confirm, pick };
|