create-memory-soda 0.1.0 → 0.2.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/README.md +9 -13
- package/index.js +159 -134
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-memory-soda
|
|
2
2
|
|
|
3
|
-
Scaffold a self-hosted [
|
|
3
|
+
Scaffold a self-hosted [Memory Soda](https://github.com/alagappan17/memory-soda)
|
|
4
4
|
instance — API, dashboard, SDK, and a Postgres to point them at.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
@@ -9,7 +9,9 @@ npm create memory-soda@latest
|
|
|
9
9
|
|
|
10
10
|
It asks for a folder name, a Gemini API key, your Postgres connection string,
|
|
11
11
|
which ports to use, and the dashboard admin login. Then it clones the repo,
|
|
12
|
-
writes `.env`, installs dependencies,
|
|
12
|
+
writes `.env`, installs dependencies, creates the database if it is missing,
|
|
13
|
+
and verifies pgvector is available. Clone and install run behind a spinner;
|
|
14
|
+
pass `--verbose` to stream their full output instead.
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
17
|
cd memory-soda
|
|
@@ -21,17 +23,11 @@ once. Dashboard on :3000, API on :3004.
|
|
|
21
23
|
|
|
22
24
|
## Postgres
|
|
23
25
|
|
|
24
|
-
You bring
|
|
25
|
-
port while you are still at the prompt, and
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
! database "memory_db" does not exist
|
|
32
|
-
createdb memory_db
|
|
33
|
-
Fix it and press enter to retry, or type a new DATABASE_URL (s to skip):
|
|
34
|
-
```
|
|
26
|
+
You bring the server; the installer creates the database. It asks for a
|
|
27
|
+
`DATABASE_URL`, probes the host and port while you are still at the prompt, and
|
|
28
|
+
after install connects for real. A missing database is created for you (the
|
|
29
|
+
role in the URL needs `CREATEDB`). Anything else — server down, bad password,
|
|
30
|
+
no pgvector — names the fix and lets you retry without starting over.
|
|
35
31
|
|
|
36
32
|
The role in the URL needs permission to `CREATE EXTENSION vector` — the first
|
|
37
33
|
migration creates the extension. On most installations that means a superuser.
|
package/index.js
CHANGED
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
existsSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
realpathSync,
|
|
10
|
-
} from 'node:fs';
|
|
2
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readdirSync, writeFileSync, rmSync, realpathSync } from 'node:fs';
|
|
11
4
|
import { resolve, basename, join } from 'node:path';
|
|
12
5
|
import { createRequire } from 'node:module';
|
|
13
6
|
import { connect } from 'node:net';
|
|
14
|
-
import {
|
|
7
|
+
import { exit } from 'node:process';
|
|
15
8
|
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import * as p from '@clack/prompts';
|
|
16
10
|
import chalk from 'chalk';
|
|
17
11
|
|
|
18
12
|
const REPO = 'https://github.com/alagappan17/memory-soda.git';
|
|
19
13
|
const DEFAULT_DB = 'postgresql://localhost:5432/memory_db';
|
|
20
|
-
|
|
21
|
-
const step = (msg) => console.log(`${chalk.cyan('→')} ${msg}`);
|
|
22
|
-
const warn = (msg) => console.log(`${chalk.yellow('!')} ${msg}`);
|
|
23
|
-
const note = (msg) => console.log(chalk.dim(` ${msg}`));
|
|
14
|
+
const VERBOSE = process.argv.includes('--verbose');
|
|
24
15
|
|
|
25
16
|
function die(msg) {
|
|
26
17
|
console.error(`\n${chalk.red('✗')} ${msg}\n`);
|
|
27
18
|
exit(1);
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
/** Bail if the user hit ctrl-c on a prompt. */
|
|
22
|
+
function guard(value) {
|
|
23
|
+
if (p.isCancel(value)) die('Cancelled.');
|
|
24
|
+
return value;
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
/** Run a command quietly, returning stdout — or null if it failed or is missing. */
|
|
@@ -40,19 +33,47 @@ function tryRun(cmd, args, opts = {}) {
|
|
|
40
33
|
}
|
|
41
34
|
}
|
|
42
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Run a long command behind a spinner. The spinner shows the command's last
|
|
38
|
+
* output line as it goes; the full log only appears if it fails (or with
|
|
39
|
+
* --verbose, where it streams straight through).
|
|
40
|
+
*/
|
|
41
|
+
function runWithSpinner(title, cmd, args, { cwd } = {}) {
|
|
42
|
+
if (VERBOSE) {
|
|
43
|
+
p.log.step(title);
|
|
44
|
+
return new Promise((done, fail) => {
|
|
45
|
+
spawn(cmd, args, { cwd, stdio: 'inherit' }).on('close', (code) =>
|
|
46
|
+
code === 0 ? done() : fail(new Error(`${cmd} exited with ${code}`)),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const s = p.spinner();
|
|
51
|
+
s.start(title);
|
|
52
|
+
return new Promise((done, fail) => {
|
|
53
|
+
let log = '';
|
|
54
|
+
const child = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
55
|
+
const onData = (chunk) => {
|
|
56
|
+
log += chunk;
|
|
57
|
+
const last = String(chunk).trim().split('\n').pop()?.trim();
|
|
58
|
+
if (last) s.message(`${title} ${chalk.dim(last.slice(0, 60))}`);
|
|
59
|
+
};
|
|
60
|
+
child.stdout.on('data', onData);
|
|
61
|
+
child.stderr.on('data', onData);
|
|
62
|
+
child.on('close', (code) => {
|
|
63
|
+
if (code === 0) return s.stop(title) ?? done();
|
|
64
|
+
s.stop(`${title} — failed`, 1);
|
|
65
|
+
p.log.error(log.trim().split('\n').slice(-20).join('\n'));
|
|
66
|
+
fail(new Error(`${cmd} ${args.join(' ')} exited with ${code}. Re-run with --verbose for the full log.`));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
/**
|
|
44
72
|
* Render the .env file. Only the values the installer collects are written
|
|
45
73
|
* explicitly — everything else stays on the defaults baked into config.ts, so
|
|
46
74
|
* this file does not drift every time a new option is added there.
|
|
47
75
|
*/
|
|
48
|
-
export function renderEnv({
|
|
49
|
-
databaseUrl,
|
|
50
|
-
geminiKey,
|
|
51
|
-
adminUser,
|
|
52
|
-
adminPassword,
|
|
53
|
-
apiPort,
|
|
54
|
-
dashboardPort,
|
|
55
|
-
}) {
|
|
76
|
+
export function renderEnv({ databaseUrl, geminiKey, adminUser, adminPassword, apiPort, dashboardPort }) {
|
|
56
77
|
const lines = [
|
|
57
78
|
'# Generated by create-memory-soda.',
|
|
58
79
|
'# Full reference: https://github.com/alagappan17/memory-soda',
|
|
@@ -60,15 +81,15 @@ export function renderEnv({
|
|
|
60
81
|
'# PostgreSQL — must have the pgvector extension available.',
|
|
61
82
|
`DATABASE_URL=${databaseUrl}`,
|
|
62
83
|
'',
|
|
63
|
-
'# Google Gemini —
|
|
84
|
+
'# Google Gemini — REQUIRED for extraction and search. Get a key at https://aistudio.google.com',
|
|
64
85
|
`GOOGLE_GENERATIVE_AI_API_KEY=${geminiKey}`,
|
|
65
86
|
'',
|
|
66
87
|
'# Ports. CORS_ORIGIN must match where the dashboard is served from, and',
|
|
67
|
-
|
|
88
|
+
"# VITE_API_URL is the browser's view of the API — not the server's.",
|
|
68
89
|
`PORT=${apiPort}`,
|
|
69
90
|
`DASHBOARD_PORT=${dashboardPort}`,
|
|
70
91
|
`CORS_ORIGIN=http://localhost:${dashboardPort}`,
|
|
71
|
-
`
|
|
92
|
+
`VITE_API_URL=http://localhost:${apiPort}`,
|
|
72
93
|
'',
|
|
73
94
|
'# Dashboard login, created once on first boot.',
|
|
74
95
|
`ADMIN_USERNAME=${adminUser}`,
|
|
@@ -89,8 +110,7 @@ export function isUsableTarget(dir) {
|
|
|
89
110
|
|
|
90
111
|
/**
|
|
91
112
|
* Pull the host and port out of a Postgres connection string, for the cheap
|
|
92
|
-
* reachability probe. Returns null for anything unparseable
|
|
93
|
-
* is the database check's problem to report, not the probe's.
|
|
113
|
+
* reachability probe. Returns null for anything unparseable.
|
|
94
114
|
*/
|
|
95
115
|
export function parsePostgresUrl(url) {
|
|
96
116
|
try {
|
|
@@ -117,6 +137,26 @@ function isReachable({ host, port }, timeout = 2000) {
|
|
|
117
137
|
});
|
|
118
138
|
}
|
|
119
139
|
|
|
140
|
+
function loadPg(target) {
|
|
141
|
+
const require = createRequire(pathToFileURL(join(target, 'package.json')));
|
|
142
|
+
return require('pg').Client;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Create the database named in the URL by connecting to the maintenance db on the same server. */
|
|
146
|
+
async function createDatabase(target, databaseUrl) {
|
|
147
|
+
const Client = loadPg(target);
|
|
148
|
+
const url = new URL(databaseUrl);
|
|
149
|
+
const db = decodeURIComponent(url.pathname.slice(1));
|
|
150
|
+
url.pathname = '/postgres';
|
|
151
|
+
const client = new Client({ connectionString: url.href });
|
|
152
|
+
try {
|
|
153
|
+
await client.connect();
|
|
154
|
+
await client.query(`CREATE DATABASE "${db.replaceAll('"', '""')}"`);
|
|
155
|
+
} finally {
|
|
156
|
+
await client.end().catch(() => {});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
120
160
|
/**
|
|
121
161
|
* Connect for real and confirm pgvector is installable, using the `pg` copy
|
|
122
162
|
* inside the freshly installed project so the installer needs no database
|
|
@@ -125,8 +165,7 @@ function isReachable({ host, port }, timeout = 2000) {
|
|
|
125
165
|
export async function checkDatabase(target, databaseUrl) {
|
|
126
166
|
let Client;
|
|
127
167
|
try {
|
|
128
|
-
|
|
129
|
-
({ Client } = require('pg'));
|
|
168
|
+
Client = loadPg(target);
|
|
130
169
|
} catch {
|
|
131
170
|
return { skipped: 'could not load pg from the new project' };
|
|
132
171
|
}
|
|
@@ -134,23 +173,18 @@ export async function checkDatabase(target, databaseUrl) {
|
|
|
134
173
|
const client = new Client({ connectionString: databaseUrl });
|
|
135
174
|
try {
|
|
136
175
|
await client.connect();
|
|
137
|
-
const { rows } = await client.query(
|
|
138
|
-
"SELECT 1 FROM pg_available_extensions WHERE name = 'vector'",
|
|
139
|
-
);
|
|
176
|
+
const { rows } = await client.query("SELECT 1 FROM pg_available_extensions WHERE name = 'vector'");
|
|
140
177
|
if (rows.length === 0) {
|
|
141
178
|
return {
|
|
142
179
|
problem: 'pgvector is not available on this server',
|
|
143
|
-
fix: 'Install it — brew install pgvector, or apt install postgresql-16-pgvector.',
|
|
180
|
+
fix: 'Install it — brew install pgvector, or apt install postgresql-16-pgvector — then retry.',
|
|
144
181
|
};
|
|
145
182
|
}
|
|
146
183
|
return null;
|
|
147
184
|
} catch (err) {
|
|
148
185
|
if (err.code === '3D000') {
|
|
149
186
|
const db = decodeURIComponent(new URL(databaseUrl).pathname.slice(1));
|
|
150
|
-
return {
|
|
151
|
-
problem: `database "${db}" does not exist`,
|
|
152
|
-
fix: `createdb ${db}`,
|
|
153
|
-
};
|
|
187
|
+
return { problem: `database "${db}" does not exist`, fix: `createdb ${db}`, missingDb: db };
|
|
154
188
|
}
|
|
155
189
|
if (err.code === '28P01' || err.code === '28000') {
|
|
156
190
|
return { problem: 'authentication failed', fix: 'Check the user and password in the URL.' };
|
|
@@ -167,119 +201,111 @@ export async function checkDatabase(target, databaseUrl) {
|
|
|
167
201
|
}
|
|
168
202
|
}
|
|
169
203
|
|
|
204
|
+
const validPort = (v) => {
|
|
205
|
+
if (!v) return; // blank takes the default
|
|
206
|
+
const n = Number(v);
|
|
207
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) return 'Enter a port between 1 and 65535.';
|
|
208
|
+
};
|
|
209
|
+
|
|
170
210
|
async function main() {
|
|
171
211
|
if (Number(process.versions.node.split('.')[0]) < 20) {
|
|
172
212
|
die(`Node 20 or newer is required — you are on ${process.versions.node}.`);
|
|
173
213
|
}
|
|
174
214
|
if (!tryRun('git', ['--version'])) die('git is required but was not found.');
|
|
175
215
|
|
|
176
|
-
console.log(
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
// Read answers off the line iterator rather than rl.question(): with a piped
|
|
181
|
-
// stdin, question() consumes only the first line of a buffered chunk and
|
|
182
|
-
// drops the rest, so `printf 'a\nb\n' | npm create memory-soda` would stall.
|
|
183
|
-
const rl = createInterface({ input: stdin });
|
|
184
|
-
const lines = rl[Symbol.asyncIterator]();
|
|
185
|
-
const ask = async (q, fallback = '') => {
|
|
186
|
-
const hint = fallback ? chalk.dim(` (${fallback})`) : '';
|
|
187
|
-
stdout.write(`${q}${hint}: `);
|
|
188
|
-
const { value, done } = await lines.next();
|
|
189
|
-
// EOF (a script that piped fewer answers than there are questions) takes
|
|
190
|
-
// the default rather than hanging.
|
|
191
|
-
if (done) stdout.write('\n');
|
|
192
|
-
return (value ?? '').trim() || fallback;
|
|
193
|
-
};
|
|
194
|
-
const askPort = async (q, fallback) => {
|
|
195
|
-
for (;;) {
|
|
196
|
-
const raw = await ask(q, String(fallback));
|
|
197
|
-
const port = Number(raw);
|
|
198
|
-
if (Number.isInteger(port) && port > 0 && port < 65536) return port;
|
|
199
|
-
warn(`${raw} is not a port number.`);
|
|
200
|
-
}
|
|
201
|
-
};
|
|
216
|
+
console.log();
|
|
217
|
+
p.intro(`${chalk.bgCyan.black(' Memory Soda ')} ${chalk.dim('self-hosted memory for AI agents')}`);
|
|
202
218
|
|
|
203
|
-
const
|
|
219
|
+
const dirArg = process.argv.slice(2).find((a) => !a.startsWith('-'));
|
|
220
|
+
const dirName =
|
|
221
|
+
dirArg ||
|
|
222
|
+
guard(
|
|
223
|
+
await p.text({
|
|
224
|
+
message: 'Where should we create your project?',
|
|
225
|
+
placeholder: 'memory-soda',
|
|
226
|
+
defaultValue: 'memory-soda',
|
|
227
|
+
validate: (v) =>
|
|
228
|
+
isUsableTarget(resolve(process.cwd(), v || 'memory-soda')) ? undefined : 'That folder exists and is not empty.',
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
204
231
|
const target = resolve(process.cwd(), dirName);
|
|
205
|
-
if (!isUsableTarget(target)) {
|
|
206
|
-
rl.close();
|
|
207
|
-
die(`${target} already exists and is not empty.`);
|
|
208
|
-
}
|
|
232
|
+
if (!isUsableTarget(target)) die(`${target} already exists and is not empty.`);
|
|
209
233
|
|
|
210
|
-
const geminiKey =
|
|
211
|
-
|
|
234
|
+
const geminiKey = guard(
|
|
235
|
+
await p.password({
|
|
236
|
+
message: `Gemini API key ${chalk.dim('(aistudio.google.com — required for the memory layer, leave blank to add to .env later)')}`,
|
|
237
|
+
mask: '•',
|
|
238
|
+
}),
|
|
239
|
+
) ?? '';
|
|
240
|
+
if (!geminiKey) p.log.warn('No key given. Memory extraction and search will not work until GOOGLE_GENERATIVE_AI_API_KEY is set in .env.');
|
|
212
241
|
|
|
213
|
-
|
|
214
|
-
|
|
242
|
+
let databaseUrl = guard(
|
|
243
|
+
await p.text({
|
|
244
|
+
message: `Postgres URL ${chalk.dim('(bring your own — pgvector must be installed; the database itself is created for you)')}`,
|
|
245
|
+
placeholder: DEFAULT_DB,
|
|
246
|
+
defaultValue: DEFAULT_DB,
|
|
247
|
+
validate: (v) => (v && !parsePostgresUrl(v) ? 'Expected a postgres:// URL.' : undefined),
|
|
248
|
+
}),
|
|
215
249
|
);
|
|
216
|
-
let databaseUrl = await ask('DATABASE_URL', DEFAULT_DB);
|
|
217
250
|
const addr = parsePostgresUrl(databaseUrl);
|
|
218
|
-
if (!addr) {
|
|
219
|
-
warn('That does not parse as a postgres:// URL — it is written as given.');
|
|
220
|
-
} else if (!(await isReachable(addr))) {
|
|
251
|
+
if (addr && !(await isReachable(addr))) {
|
|
221
252
|
// Only a warning: the server may legitimately be starting, or firewalled
|
|
222
253
|
// from here but reachable from wherever the app will actually run.
|
|
223
|
-
warn(`Nothing is listening on ${addr.host}:${addr.port} right now.`);
|
|
224
|
-
note('Continuing — the connection is checked again after install.');
|
|
225
|
-
} else {
|
|
226
|
-
note(`${addr.host}:${addr.port} is reachable.`);
|
|
254
|
+
p.log.warn(`Nothing is listening on ${addr.host}:${addr.port} right now — checked again after install.`);
|
|
227
255
|
}
|
|
228
256
|
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
console.log(`\n${chalk.bold('Dashboard login')}`);
|
|
234
|
-
const adminUser = await ask('Admin username', 'admin');
|
|
235
|
-
const adminPassword = await ask(
|
|
236
|
-
`Admin password ${chalk.dim('(blank = generate one)')}`,
|
|
257
|
+
const apiPort = Number(guard(await p.text({ message: 'API port', placeholder: '3004', defaultValue: '3004', validate: validPort })));
|
|
258
|
+
const dashboardPort = Number(
|
|
259
|
+
guard(await p.text({ message: 'Dashboard port', placeholder: '3000', defaultValue: '3000', validate: validPort })),
|
|
237
260
|
);
|
|
261
|
+
const adminUser = guard(await p.text({ message: 'Dashboard admin username', placeholder: 'admin', defaultValue: 'admin' }));
|
|
262
|
+
const adminPassword =
|
|
263
|
+
guard(await p.password({ message: `Dashboard admin password ${chalk.dim('(leave blank to generate one at first boot)')}`, mask: '•' })) ?? '';
|
|
238
264
|
|
|
239
|
-
|
|
240
|
-
step(`Cloning into ${chalk.bold(dirName)}`);
|
|
241
|
-
run('git', ['clone', '--depth', '1', REPO, target]);
|
|
265
|
+
await runWithSpinner(`Cloning into ${chalk.bold(dirName)}`, 'git', ['clone', '--depth', '1', REPO, target]);
|
|
242
266
|
rmSync(resolve(target, '.git'), { recursive: true, force: true });
|
|
243
267
|
|
|
244
268
|
const writeEnv = () =>
|
|
245
|
-
writeFileSync(
|
|
246
|
-
resolve(target, '.env'),
|
|
247
|
-
renderEnv({
|
|
248
|
-
databaseUrl,
|
|
249
|
-
geminiKey,
|
|
250
|
-
adminUser,
|
|
251
|
-
adminPassword,
|
|
252
|
-
apiPort,
|
|
253
|
-
dashboardPort,
|
|
254
|
-
}),
|
|
255
|
-
);
|
|
269
|
+
writeFileSync(resolve(target, '.env'), renderEnv({ databaseUrl, geminiKey, adminUser, adminPassword, apiPort, dashboardPort }));
|
|
256
270
|
writeEnv();
|
|
257
|
-
|
|
271
|
+
p.log.success('Wrote .env');
|
|
258
272
|
|
|
259
|
-
|
|
260
|
-
run('npm', ['install'], { cwd: target });
|
|
273
|
+
await runWithSpinner('Installing dependencies', 'npm', ['ci', '--no-audit', '--no-fund'], { cwd: target });
|
|
261
274
|
|
|
262
275
|
// Now that pg exists in the project, check the database for real. Looping
|
|
263
276
|
// here beats failing at first boot: the fix is usually one command away and
|
|
264
277
|
// the answer is still in front of the user.
|
|
265
278
|
for (;;) {
|
|
266
|
-
|
|
267
|
-
|
|
279
|
+
const s = p.spinner();
|
|
280
|
+
s.start('Checking the database');
|
|
281
|
+
let result = await checkDatabase(target, databaseUrl);
|
|
282
|
+
if (result?.missingDb) {
|
|
283
|
+
s.message(`Creating database ${result.missingDb}`);
|
|
284
|
+
try {
|
|
285
|
+
await createDatabase(target, databaseUrl);
|
|
286
|
+
result = await checkDatabase(target, databaseUrl);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
result = { problem: `could not create database "${result.missingDb}": ${err.message}`, fix: result.fix };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
268
291
|
if (result === null) {
|
|
269
|
-
|
|
292
|
+
s.stop('Database ready — connected, pgvector available');
|
|
270
293
|
break;
|
|
271
294
|
}
|
|
272
295
|
if (result.skipped) {
|
|
273
|
-
|
|
296
|
+
s.stop(`Database check skipped — ${result.skipped}`, 1);
|
|
274
297
|
break;
|
|
275
298
|
}
|
|
276
|
-
|
|
277
|
-
if (result.fix)
|
|
278
|
-
const retry =
|
|
279
|
-
|
|
280
|
-
|
|
299
|
+
s.stop(`Database: ${result.problem}`, 1);
|
|
300
|
+
if (result.fix) p.log.info(result.fix);
|
|
301
|
+
const retry = guard(
|
|
302
|
+
await p.text({
|
|
303
|
+
message: 'Fix it and press enter to retry, paste a new DATABASE_URL, or type s to skip',
|
|
304
|
+
placeholder: 'enter to retry',
|
|
305
|
+
}),
|
|
306
|
+
) ?? '';
|
|
281
307
|
if (retry.toLowerCase() === 's') {
|
|
282
|
-
|
|
308
|
+
p.log.warn('Skipped — the API will fail on boot until this is resolved.');
|
|
283
309
|
break;
|
|
284
310
|
}
|
|
285
311
|
if (retry) {
|
|
@@ -288,28 +314,27 @@ async function main() {
|
|
|
288
314
|
}
|
|
289
315
|
}
|
|
290
316
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
317
|
+
p.note(
|
|
318
|
+
[
|
|
319
|
+
`cd ${dirName}`,
|
|
320
|
+
'npm run dev',
|
|
321
|
+
'',
|
|
322
|
+
`Dashboard ${chalk.underline(`http://localhost:${dashboardPort}`)}`,
|
|
323
|
+
`API ${chalk.underline(`http://localhost:${apiPort}`)}`,
|
|
324
|
+
`Login ${adminUser} / ${adminPassword ? 'the password you chose' : 'password printed at first boot'}`,
|
|
325
|
+
'',
|
|
326
|
+
'First boot applies migrations and prints your API key once.',
|
|
327
|
+
...(geminiKey ? [] : ['Set GOOGLE_GENERATIVE_AI_API_KEY in .env before starting.']),
|
|
328
|
+
].join('\n'),
|
|
329
|
+
'Next steps',
|
|
298
330
|
);
|
|
299
|
-
|
|
300
|
-
adminPassword
|
|
301
|
-
? `Log in as ${adminUser} with the password you chose.`
|
|
302
|
-
: `Log in as ${adminUser} — the generated password is printed at first boot.`,
|
|
303
|
-
);
|
|
304
|
-
console.log();
|
|
331
|
+
p.outro(`${chalk.bold(basename(target))} is ready.`);
|
|
305
332
|
}
|
|
306
333
|
|
|
307
334
|
/**
|
|
308
335
|
* True when this file is the process entry point, false when it is imported.
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* as a symlink in `node_modules/.bin`, while `import.meta.url` is already the
|
|
312
|
-
* real path — comparing them raw makes every installed run a no-op.
|
|
336
|
+
* argv[1] is resolved through realpath: npm and npx invoke the bin as a
|
|
337
|
+
* symlink in node_modules/.bin, while import.meta.url is the real path.
|
|
313
338
|
*/
|
|
314
339
|
function isEntryPoint() {
|
|
315
340
|
if (!process.argv[1]) return false;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-memory-soda",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a self-hosted
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a self-hosted Memory Soda instance — API, dashboard, and Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"memory",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"bin": {
|
|
16
|
-
"create-memory-soda": "
|
|
16
|
+
"create-memory-soda": "index.js"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"index.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@clack/prompts": "^1.7.0",
|
|
39
40
|
"chalk": "^5.6.2"
|
|
40
41
|
}
|
|
41
42
|
}
|