memoire-ai 0.3.2 → 0.3.4
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/cli.js +6 -17
- package/dist/cli.js.map +1 -1
- package/dist/configure.d.ts +1 -0
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +7 -7
- package/dist/configure.js.map +1 -1
- package/dist/detect.d.ts +1 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +0 -95
- package/dist/detect.js.map +1 -1
- package/dist/setup-wizard.d.ts +1 -0
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +195 -410
- package/dist/setup-wizard.js.map +1 -1
- package/package.json +6 -6
package/dist/setup-wizard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn
|
|
2
|
-
import { existsSync, writeFileSync
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { homedir, platform } from 'node:os';
|
|
@@ -10,16 +10,11 @@ import { init, install } from './configure.js';
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Constants
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
|
-
const CLOUD_API_URL = '
|
|
13
|
+
const CLOUD_API_URL = 'http://esprit-prod-alb-1025392673.us-east-1.elb.amazonaws.com';
|
|
14
14
|
const DEFAULT_API_PORT = 3100;
|
|
15
15
|
const DEFAULT_DASHBOARD_PORT = 4310;
|
|
16
|
-
/** Agent IDs that have working install() / init() support
|
|
17
|
-
const
|
|
18
|
-
/** All agents we show in the wizard (detected even if no install logic yet). */
|
|
19
|
-
const ALL_WIZARD_AGENTS = [
|
|
20
|
-
'cursor', 'claude', 'codex', 'amp', 'windsurf',
|
|
21
|
-
'opencode', 'zed', 'aider', 'continue', 'cline', 'copilot', 'trae', 'void',
|
|
22
|
-
];
|
|
16
|
+
/** Agent IDs that have working install() / init() support. */
|
|
17
|
+
const SUPPORTED_AGENTS = ['cursor', 'claude', 'codex', 'amp'];
|
|
23
18
|
// ---------------------------------------------------------------------------
|
|
24
19
|
// Helpers
|
|
25
20
|
// ---------------------------------------------------------------------------
|
|
@@ -83,149 +78,6 @@ function cancelAndExit(value, message = 'Setup cancelled.') {
|
|
|
83
78
|
return false;
|
|
84
79
|
}
|
|
85
80
|
// ---------------------------------------------------------------------------
|
|
86
|
-
// Local infrastructure helpers
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
const MEMOIRE_REPO = 'https://github.com/esprit-cli/Memoire.git';
|
|
89
|
-
const MEMOIRE_DOCKER_IMAGE = 'ghcr.io/esprit-cli/memoire';
|
|
90
|
-
function runCmd(cmd, args, opts) {
|
|
91
|
-
return execFileSync(cmd, args, {
|
|
92
|
-
cwd: opts?.cwd,
|
|
93
|
-
env: { ...process.env, ...(opts?.env ?? {}) },
|
|
94
|
-
encoding: 'utf-8',
|
|
95
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
-
timeout: 300_000,
|
|
97
|
-
}).trim();
|
|
98
|
-
}
|
|
99
|
-
/** Clone the Memoire repo, install deps, build, and return the root path. */
|
|
100
|
-
async function cloneAndBuild(s, isJson) {
|
|
101
|
-
const installDir = join(homedir(), '.memoire', 'server');
|
|
102
|
-
if (existsSync(join(installDir, 'apps', 'api', 'dist', 'index.js'))) {
|
|
103
|
-
if (!isJson)
|
|
104
|
-
p.log.info('Memoire server already installed, pulling latest...');
|
|
105
|
-
try {
|
|
106
|
-
runCmd('git', ['pull', '--ff-only'], { cwd: installDir });
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// Pull failed — that's OK, use what we have
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
if (!isJson)
|
|
114
|
-
s.start('Cloning Memoire repository...');
|
|
115
|
-
mkdirSync(join(homedir(), '.memoire'), { recursive: true });
|
|
116
|
-
runCmd('git', ['clone', '--depth', '1', MEMOIRE_REPO, installDir]);
|
|
117
|
-
if (!isJson)
|
|
118
|
-
s.stop('Repository cloned');
|
|
119
|
-
}
|
|
120
|
-
// Install deps
|
|
121
|
-
if (!isJson)
|
|
122
|
-
s.start('Installing dependencies (this may take a minute)...');
|
|
123
|
-
runCmd('pnpm', ['install', '--frozen-lockfile'], { cwd: installDir });
|
|
124
|
-
if (!isJson)
|
|
125
|
-
s.stop('Dependencies installed');
|
|
126
|
-
// Build
|
|
127
|
-
if (!isJson)
|
|
128
|
-
s.start('Building Memoire...');
|
|
129
|
-
runCmd('pnpm', ['--filter', '@memoire-ai/shared', 'build'], { cwd: installDir });
|
|
130
|
-
runCmd('pnpm', ['--filter', '@memoire-ai/sdk', 'build'], { cwd: installDir });
|
|
131
|
-
runCmd('pnpm', ['--filter', '@memoire-ai/api', 'build'], { cwd: installDir });
|
|
132
|
-
runCmd('pnpm', ['--filter', '@memoire-ai/dashboard', 'build'], { cwd: installDir });
|
|
133
|
-
if (!isJson)
|
|
134
|
-
s.stop('Build complete');
|
|
135
|
-
return installDir;
|
|
136
|
-
}
|
|
137
|
-
/** Start Memoire via Docker Compose. */
|
|
138
|
-
async function startWithDocker(s, isJson, dbUrl, dbUrlDirect, apiPort, dashboardPort, openaiKey, anthropicKey) {
|
|
139
|
-
const installDir = join(homedir(), '.memoire', 'server');
|
|
140
|
-
// We need the docker-compose.yml from the repo
|
|
141
|
-
if (!existsSync(join(installDir, 'docker-compose.yml'))) {
|
|
142
|
-
if (!isJson)
|
|
143
|
-
s.start('Downloading Memoire Docker config...');
|
|
144
|
-
mkdirSync(join(homedir(), '.memoire'), { recursive: true });
|
|
145
|
-
runCmd('git', ['clone', '--depth', '1', MEMOIRE_REPO, installDir]);
|
|
146
|
-
if (!isJson)
|
|
147
|
-
s.stop('Docker config ready');
|
|
148
|
-
}
|
|
149
|
-
// Write .env for Docker Compose
|
|
150
|
-
const envLines = [
|
|
151
|
-
`DATABASE_URL=${dbUrl}`,
|
|
152
|
-
`DATABASE_URL_DIRECT=${dbUrlDirect}`,
|
|
153
|
-
`API_PORT=${apiPort}`,
|
|
154
|
-
`DASHBOARD_PORT=${dashboardPort}`,
|
|
155
|
-
];
|
|
156
|
-
if (openaiKey)
|
|
157
|
-
envLines.push(`OPENAI_API_KEY=${openaiKey}`);
|
|
158
|
-
if (anthropicKey)
|
|
159
|
-
envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
|
|
160
|
-
writeFileSync(join(installDir, '.env'), envLines.join('\n') + '\n');
|
|
161
|
-
if (!isJson)
|
|
162
|
-
s.start('Starting Memoire with Docker Compose...');
|
|
163
|
-
runCmd('docker', ['compose', 'up', '-d'], { cwd: installDir });
|
|
164
|
-
if (!isJson)
|
|
165
|
-
s.stop('Docker containers started');
|
|
166
|
-
return { memoireRoot: installDir, started: true };
|
|
167
|
-
}
|
|
168
|
-
/** Start API + push schema from a local monorepo clone. */
|
|
169
|
-
async function startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey) {
|
|
170
|
-
const apiDir = join(memoireRoot, 'apps', 'api');
|
|
171
|
-
// Write .env
|
|
172
|
-
const envLines = [
|
|
173
|
-
`DATABASE_URL="${dbUrl}"`,
|
|
174
|
-
`DATABASE_URL_DIRECT="${dbUrlDirect}"`,
|
|
175
|
-
`PORT=${apiPort}`,
|
|
176
|
-
];
|
|
177
|
-
if (openaiKey)
|
|
178
|
-
envLines.push(`OPENAI_API_KEY="${openaiKey}"`);
|
|
179
|
-
if (anthropicKey)
|
|
180
|
-
envLines.push(`ANTHROPIC_API_KEY="${anthropicKey}"`);
|
|
181
|
-
writeFileSync(join(apiDir, '.env'), envLines.join('\n') + '\n');
|
|
182
|
-
// Push schema
|
|
183
|
-
if (!isJson)
|
|
184
|
-
s.start('Pushing database schema...');
|
|
185
|
-
try {
|
|
186
|
-
runCmd('npx', ['drizzle-kit', 'push', '--force'], {
|
|
187
|
-
cwd: apiDir,
|
|
188
|
-
env: { DATABASE_URL_DIRECT: dbUrlDirect },
|
|
189
|
-
});
|
|
190
|
-
if (!isJson)
|
|
191
|
-
s.stop('Schema pushed');
|
|
192
|
-
}
|
|
193
|
-
catch (err) {
|
|
194
|
-
if (!isJson)
|
|
195
|
-
s.stop('Schema push failed — you may need to run it manually');
|
|
196
|
-
}
|
|
197
|
-
// Start API
|
|
198
|
-
if (!isJson)
|
|
199
|
-
s.start('Starting API server...');
|
|
200
|
-
const env = {
|
|
201
|
-
...process.env,
|
|
202
|
-
DATABASE_URL: dbUrl,
|
|
203
|
-
PORT: String(apiPort),
|
|
204
|
-
};
|
|
205
|
-
if (openaiKey)
|
|
206
|
-
env.OPENAI_API_KEY = openaiKey;
|
|
207
|
-
if (anthropicKey)
|
|
208
|
-
env.ANTHROPIC_API_KEY = anthropicKey;
|
|
209
|
-
const child = spawn('node', ['dist/index.js'], {
|
|
210
|
-
cwd: apiDir,
|
|
211
|
-
detached: true,
|
|
212
|
-
stdio: 'ignore',
|
|
213
|
-
env,
|
|
214
|
-
});
|
|
215
|
-
child.unref();
|
|
216
|
-
// Wait for health
|
|
217
|
-
for (let i = 0; i < 15; i++) {
|
|
218
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
219
|
-
if (await checkApiHealth(`http://localhost:${apiPort}`)) {
|
|
220
|
-
if (!isJson)
|
|
221
|
-
s.stop(`API running at http://localhost:${apiPort}`);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (!isJson)
|
|
226
|
-
s.stop('API started (health check timed out — may still be loading)');
|
|
227
|
-
}
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
81
|
// Main wizard
|
|
230
82
|
// ---------------------------------------------------------------------------
|
|
231
83
|
export async function runSetupWizard(flags = {}) {
|
|
@@ -244,7 +96,7 @@ export async function runSetupWizard(flags = {}) {
|
|
|
244
96
|
const allAgents = await detectAllAgents();
|
|
245
97
|
if (!isJson)
|
|
246
98
|
s.stop('System scan complete');
|
|
247
|
-
const detectedAgents = allAgents.filter((a) => a.detected);
|
|
99
|
+
const detectedAgents = allAgents.filter((a) => a.detected && SUPPORTED_AGENTS.includes(a.id));
|
|
248
100
|
if (!isJson) {
|
|
249
101
|
const lines = [
|
|
250
102
|
`Platform: ${systemInfo.platform} / ${systemInfo.arch}`,
|
|
@@ -257,101 +109,53 @@ export async function runSetupWizard(flags = {}) {
|
|
|
257
109
|
p.note(lines.join('\n'), 'System Information');
|
|
258
110
|
}
|
|
259
111
|
// -----------------------------------------------------------------------
|
|
260
|
-
// Phase 2:
|
|
112
|
+
// Phase 2: Hosting mode
|
|
261
113
|
// -----------------------------------------------------------------------
|
|
262
|
-
let
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
isCloud = apiUrl === CLOUD_API_URL;
|
|
269
|
-
}
|
|
270
|
-
else if (flags.cloud) {
|
|
271
|
-
apiUrl = CLOUD_API_URL;
|
|
272
|
-
isCloud = true;
|
|
114
|
+
let isCloud;
|
|
115
|
+
if (flags.cloud !== undefined) {
|
|
116
|
+
isCloud = flags.cloud;
|
|
117
|
+
}
|
|
118
|
+
else if (flags.apiUrl) {
|
|
119
|
+
isCloud = flags.apiUrl === CLOUD_API_URL;
|
|
273
120
|
}
|
|
274
121
|
else if (isNonInteractive) {
|
|
275
|
-
|
|
276
|
-
apiUrl = `http://localhost:${flags.apiPort ?? DEFAULT_API_PORT}`;
|
|
122
|
+
isCloud = true;
|
|
277
123
|
}
|
|
278
124
|
else {
|
|
279
|
-
const
|
|
280
|
-
message: '
|
|
125
|
+
const mode = await p.select({
|
|
126
|
+
message: 'How would you like to host Memoire?',
|
|
281
127
|
options: [
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
label: 'Run locally',
|
|
285
|
-
hint: `starts API on localhost:${flags.apiPort ?? DEFAULT_API_PORT} — uses your Supabase DB`,
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
value: 'custom',
|
|
289
|
-
label: 'Custom URL',
|
|
290
|
-
hint: 'connect to an API already running somewhere',
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
value: 'cloud',
|
|
294
|
-
label: 'Memoire Cloud',
|
|
295
|
-
hint: 'api.memoire.dev — coming soon',
|
|
296
|
-
},
|
|
128
|
+
{ value: 'cloud', label: 'Memoire Cloud', hint: 'Fastest — no infrastructure to manage' },
|
|
129
|
+
{ value: 'self-host', label: 'Self-hosted', hint: 'Your own database and servers' },
|
|
297
130
|
],
|
|
298
131
|
});
|
|
299
|
-
cancelAndExit(
|
|
300
|
-
|
|
301
|
-
apiUrl = CLOUD_API_URL;
|
|
302
|
-
isCloud = true;
|
|
303
|
-
}
|
|
304
|
-
else if (apiChoice === 'custom') {
|
|
305
|
-
const urlInput = await p.text({
|
|
306
|
-
message: 'API URL:',
|
|
307
|
-
placeholder: 'https://your-api.example.com',
|
|
308
|
-
validate: (v) => {
|
|
309
|
-
if (!v?.startsWith('http://') && !v?.startsWith('https://'))
|
|
310
|
-
return 'Must be an HTTP(S) URL';
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
cancelAndExit(urlInput);
|
|
314
|
-
apiUrl = urlInput.replace(/\/$/, '');
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
// localhost
|
|
318
|
-
apiUrl = `http://localhost:${flags.apiPort ?? DEFAULT_API_PORT}`;
|
|
319
|
-
needsLocalStart = true;
|
|
320
|
-
}
|
|
132
|
+
cancelAndExit(mode);
|
|
133
|
+
isCloud = mode === 'cloud';
|
|
321
134
|
}
|
|
322
|
-
|
|
323
|
-
// Phase 3: Database URL (needed for localhost + custom without running API)
|
|
324
|
-
// -----------------------------------------------------------------------
|
|
325
|
-
let dbUrl = flags.dbUrl;
|
|
326
|
-
let dbUrlDirect = flags.dbUrlDirect;
|
|
327
|
-
// Check if the API is already reachable — if so, skip DB prompt
|
|
328
|
-
const apiAlreadyRunning = await checkApiHealth(apiUrl);
|
|
135
|
+
let apiUrl;
|
|
329
136
|
if (isCloud) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
p.log.warn('Memoire Cloud (api.memoire.dev) is coming soon. Your agent configs will be ready when it launches.');
|
|
336
|
-
}
|
|
337
|
-
}
|
|
137
|
+
apiUrl = flags.apiUrl ?? CLOUD_API_URL;
|
|
138
|
+
if (!isJson)
|
|
139
|
+
p.log.info(`Using Memoire Cloud at ${apiUrl}`);
|
|
338
140
|
}
|
|
339
|
-
else
|
|
340
|
-
//
|
|
341
|
-
|
|
141
|
+
else {
|
|
142
|
+
// ------------------------------------------------------------------
|
|
143
|
+
// Phase 3: Database URL (self-host only)
|
|
144
|
+
// ------------------------------------------------------------------
|
|
145
|
+
apiUrl = flags.apiUrl ?? `http://localhost:${flags.apiPort ?? DEFAULT_API_PORT}`;
|
|
146
|
+
let dbUrl = flags.dbUrl;
|
|
147
|
+
let dbUrlDirect = flags.dbUrlDirect;
|
|
342
148
|
if (!dbUrl) {
|
|
343
149
|
if (isNonInteractive) {
|
|
344
|
-
p.log.error('
|
|
150
|
+
p.log.error('Self-hosted mode requires --db-url in non-interactive mode.');
|
|
345
151
|
process.exit(1);
|
|
346
152
|
}
|
|
347
|
-
p.note('Memoire needs a PostgreSQL database (Supabase recommended).\n' +
|
|
348
|
-
'Get a free one at supabase.com/dashboard → Settings → Database → Connection string.', 'Database Setup');
|
|
349
153
|
const dbInput = await p.text({
|
|
350
|
-
message: '
|
|
351
|
-
placeholder: 'postgresql://
|
|
154
|
+
message: 'PostgreSQL connection string (pooler / transaction mode):',
|
|
155
|
+
placeholder: 'postgresql://user:pass@host:6543/memoire',
|
|
352
156
|
validate: (v) => {
|
|
353
157
|
if (!v?.startsWith('postgresql://') && !v?.startsWith('postgres://')) {
|
|
354
|
-
return 'Must be a PostgreSQL connection string';
|
|
158
|
+
return 'Must be a valid PostgreSQL connection string';
|
|
355
159
|
}
|
|
356
160
|
},
|
|
357
161
|
});
|
|
@@ -361,9 +165,9 @@ export async function runSetupWizard(flags = {}) {
|
|
|
361
165
|
if (!dbUrlDirect) {
|
|
362
166
|
if (!isNonInteractive) {
|
|
363
167
|
const directInput = await p.text({
|
|
364
|
-
message: 'Direct URL for migrations
|
|
365
|
-
placeholder: dbUrl
|
|
366
|
-
defaultValue: dbUrl
|
|
168
|
+
message: 'Direct database URL (for migrations, optional):',
|
|
169
|
+
placeholder: dbUrl,
|
|
170
|
+
defaultValue: dbUrl,
|
|
367
171
|
});
|
|
368
172
|
cancelAndExit(directInput);
|
|
369
173
|
dbUrlDirect = directInput || dbUrl;
|
|
@@ -372,19 +176,16 @@ export async function runSetupWizard(flags = {}) {
|
|
|
372
176
|
dbUrlDirect = dbUrl;
|
|
373
177
|
}
|
|
374
178
|
}
|
|
179
|
+
// Store for later use in .env writing
|
|
180
|
+
flags._resolvedDbUrl = dbUrl;
|
|
181
|
+
flags._resolvedDbUrlDirect = dbUrlDirect;
|
|
375
182
|
}
|
|
376
|
-
else if (apiAlreadyRunning && !isJson) {
|
|
377
|
-
p.log.success(`API already running at ${apiUrl}`);
|
|
378
|
-
}
|
|
379
|
-
// Store resolved DB URLs for later phases
|
|
380
|
-
flags._resolvedDbUrl = dbUrl;
|
|
381
|
-
flags._resolvedDbUrlDirect = dbUrlDirect;
|
|
382
183
|
// -----------------------------------------------------------------------
|
|
383
184
|
// Phase 4: Optional API keys
|
|
384
185
|
// -----------------------------------------------------------------------
|
|
385
186
|
let openaiKey = flags.openaiKey;
|
|
386
187
|
let anthropicKey = flags.anthropicKey;
|
|
387
|
-
if (
|
|
188
|
+
if (!isCloud && !isNonInteractive) {
|
|
388
189
|
if (!openaiKey) {
|
|
389
190
|
const wantOpenai = await p.confirm({
|
|
390
191
|
message: 'Add an OpenAI API key? (used for embeddings)',
|
|
@@ -432,38 +233,30 @@ export async function runSetupWizard(flags = {}) {
|
|
|
432
233
|
selectedAgentIds = flags.agents
|
|
433
234
|
.split(',')
|
|
434
235
|
.map((a) => a.trim())
|
|
435
|
-
.filter((id) =>
|
|
236
|
+
.filter((id) => SUPPORTED_AGENTS.includes(id));
|
|
436
237
|
}
|
|
437
238
|
else if (isNonInteractive) {
|
|
438
|
-
|
|
439
|
-
selectedAgentIds = detectedAgents
|
|
440
|
-
.filter((a) => INSTALL_READY_AGENTS.includes(a.id))
|
|
441
|
-
.map((a) => a.id);
|
|
239
|
+
selectedAgentIds = detectedAgents.map((a) => a.id);
|
|
442
240
|
if (selectedAgentIds.length === 0) {
|
|
443
|
-
p.log.error('No
|
|
241
|
+
p.log.error('No supported agents detected. Use --agents to specify agents in non-interactive mode.');
|
|
444
242
|
process.exit(1);
|
|
445
243
|
}
|
|
446
244
|
}
|
|
447
245
|
else {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
p.log.warn('No agents found. Please install an AI coding agent first.');
|
|
246
|
+
const supportedDetected = allAgents.filter((a) => SUPPORTED_AGENTS.includes(a.id));
|
|
247
|
+
if (supportedDetected.length === 0) {
|
|
248
|
+
p.log.warn('No supported agents detected. Please install Cursor, Claude Code, Codex, or Amp first.');
|
|
452
249
|
process.exit(1);
|
|
453
250
|
}
|
|
454
|
-
const agentChoices =
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return { value: a.id, label: a.name, hint };
|
|
460
|
-
});
|
|
251
|
+
const agentChoices = supportedDetected.map((a) => ({
|
|
252
|
+
value: a.id,
|
|
253
|
+
label: a.name,
|
|
254
|
+
hint: a.detected ? 'detected' : 'not found',
|
|
255
|
+
}));
|
|
461
256
|
const chosen = await p.multiselect({
|
|
462
257
|
message: 'Which agents should Memoire connect to?',
|
|
463
258
|
options: agentChoices,
|
|
464
|
-
initialValues: detectedAgents
|
|
465
|
-
.filter((a) => INSTALL_READY_AGENTS.includes(a.id))
|
|
466
|
-
.map((a) => a.id),
|
|
259
|
+
initialValues: detectedAgents.map((a) => a.id),
|
|
467
260
|
required: true,
|
|
468
261
|
});
|
|
469
262
|
cancelAndExit(chosen);
|
|
@@ -477,45 +270,124 @@ export async function runSetupWizard(flags = {}) {
|
|
|
477
270
|
p.log.info(`Selected agents: ${selectedAgentIds.join(', ')}`);
|
|
478
271
|
}
|
|
479
272
|
// -----------------------------------------------------------------------
|
|
480
|
-
// Phase 6: Workspace info
|
|
273
|
+
// Phase 6: Workspace info (or invite code for cloud join)
|
|
481
274
|
// -----------------------------------------------------------------------
|
|
482
275
|
let orgName = flags.orgName;
|
|
483
276
|
let projectName = flags.projectName;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
277
|
+
let joinInviteCode = flags.inviteCode;
|
|
278
|
+
if (isCloud && !isNonInteractive && !joinInviteCode && !flags.inviteToken) {
|
|
279
|
+
const joinOrCreate = await p.select({
|
|
280
|
+
message: 'How would you like to get started?',
|
|
281
|
+
options: [
|
|
282
|
+
{ value: 'join', label: 'Join an existing workspace (I have an invite code)' },
|
|
283
|
+
{ value: 'create', label: 'Create a new workspace' },
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
cancelAndExit(joinOrCreate);
|
|
287
|
+
if (joinOrCreate === 'join') {
|
|
288
|
+
const code = await p.text({
|
|
289
|
+
message: 'Enter your invite code:',
|
|
290
|
+
placeholder: 'ABCD-1234',
|
|
489
291
|
validate: (v) => {
|
|
490
292
|
if (!v?.trim())
|
|
491
|
-
return '
|
|
293
|
+
return 'Invite code is required';
|
|
492
294
|
},
|
|
493
295
|
});
|
|
494
|
-
cancelAndExit(
|
|
495
|
-
|
|
296
|
+
cancelAndExit(code);
|
|
297
|
+
joinInviteCode = code;
|
|
496
298
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
299
|
+
}
|
|
300
|
+
if (!joinInviteCode && !flags.inviteToken) {
|
|
301
|
+
if (!isNonInteractive) {
|
|
302
|
+
if (!orgName) {
|
|
303
|
+
const org = await p.text({
|
|
304
|
+
message: 'Organization name:',
|
|
305
|
+
placeholder: 'My Team',
|
|
306
|
+
validate: (v) => {
|
|
307
|
+
if (!v?.trim())
|
|
308
|
+
return 'Organization name is required';
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
cancelAndExit(org);
|
|
312
|
+
orgName = org;
|
|
313
|
+
}
|
|
314
|
+
if (!projectName) {
|
|
315
|
+
const proj = await p.text({
|
|
316
|
+
message: 'Project name:',
|
|
317
|
+
placeholder: 'my-project',
|
|
318
|
+
validate: (v) => {
|
|
319
|
+
if (!v?.trim())
|
|
320
|
+
return 'Project name is required';
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
cancelAndExit(proj);
|
|
324
|
+
projectName = proj;
|
|
325
|
+
}
|
|
508
326
|
}
|
|
509
327
|
}
|
|
510
328
|
// -----------------------------------------------------------------------
|
|
511
|
-
// Phase 7
|
|
512
|
-
// Set up database, start API server — via Docker or clone+build
|
|
329
|
+
// Phase 7: Write .env and push DB schema (self-host only)
|
|
513
330
|
// -----------------------------------------------------------------------
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
331
|
+
if (!isCloud) {
|
|
332
|
+
const memoireRoot = findMemoireRoot();
|
|
333
|
+
if (memoireRoot) {
|
|
334
|
+
const apiDir = join(memoireRoot, 'apps', 'api');
|
|
335
|
+
const envPath = join(apiDir, '.env');
|
|
336
|
+
const dbUrl = flags._resolvedDbUrl;
|
|
337
|
+
const dbUrlDirect = flags._resolvedDbUrlDirect;
|
|
338
|
+
const envLines = [
|
|
339
|
+
`DATABASE_URL="${dbUrl}"`,
|
|
340
|
+
`DATABASE_URL_DIRECT="${dbUrlDirect}"`,
|
|
341
|
+
`PORT=${flags.apiPort ?? DEFAULT_API_PORT}`,
|
|
342
|
+
];
|
|
343
|
+
if (openaiKey)
|
|
344
|
+
envLines.push(`OPENAI_API_KEY="${openaiKey}"`);
|
|
345
|
+
if (anthropicKey)
|
|
346
|
+
envLines.push(`ANTHROPIC_API_KEY="${anthropicKey}"`);
|
|
347
|
+
writeFileSync(envPath, envLines.join('\n') + '\n');
|
|
348
|
+
if (!isJson)
|
|
349
|
+
p.log.success(`Environment file written to ${envPath}`);
|
|
350
|
+
// Push database schema
|
|
351
|
+
if (!isJson)
|
|
352
|
+
s.start('Pushing database schema...');
|
|
353
|
+
try {
|
|
354
|
+
const { execFileSync } = await import('node:child_process');
|
|
355
|
+
execFileSync('npx', ['drizzle-kit', 'push', '--force'], {
|
|
356
|
+
cwd: apiDir,
|
|
357
|
+
stdio: 'pipe',
|
|
358
|
+
env: { ...process.env, DATABASE_URL_DIRECT: dbUrlDirect },
|
|
359
|
+
});
|
|
360
|
+
if (!isJson)
|
|
361
|
+
s.stop('Database schema pushed');
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
if (!isJson)
|
|
365
|
+
s.stop('Failed to push database schema');
|
|
366
|
+
p.log.warn(`Could not push schema: ${err instanceof Error ? err.message : 'unknown error'}. You may need to run "npx drizzle-kit push --force" manually in apps/api.`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
p.log.warn('Could not locate Memoire monorepo root. Skipping .env and schema push.');
|
|
371
|
+
}
|
|
517
372
|
}
|
|
518
|
-
|
|
373
|
+
// -----------------------------------------------------------------------
|
|
374
|
+
// Phase 8: Start API server (self-host only)
|
|
375
|
+
// -----------------------------------------------------------------------
|
|
376
|
+
let apiReachable = false;
|
|
377
|
+
if (isCloud) {
|
|
378
|
+
if (!isJson)
|
|
379
|
+
s.start('Checking Memoire Cloud...');
|
|
380
|
+
apiReachable = await checkApiHealth(apiUrl);
|
|
381
|
+
if (!isJson) {
|
|
382
|
+
if (apiReachable) {
|
|
383
|
+
s.stop('Memoire Cloud is reachable');
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
s.stop('Memoire Cloud is not reachable — proceeding anyway');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (!flags.skipStart) {
|
|
519
391
|
const apiPort = flags.apiPort ?? DEFAULT_API_PORT;
|
|
520
392
|
const portFree = await isPortAvailable(apiPort);
|
|
521
393
|
if (!portFree) {
|
|
@@ -524,80 +396,37 @@ export async function runSetupWizard(flags = {}) {
|
|
|
524
396
|
apiReachable = await checkApiHealth(apiUrl);
|
|
525
397
|
}
|
|
526
398
|
else {
|
|
527
|
-
|
|
528
|
-
const dbUrl = flags._resolvedDbUrl;
|
|
529
|
-
const dbUrlDirect = flags._resolvedDbUrlDirect;
|
|
530
|
-
// Check if we're already in the monorepo
|
|
531
|
-
let memoireRoot = findMemoireRoot();
|
|
399
|
+
const memoireRoot = findMemoireRoot();
|
|
532
400
|
if (memoireRoot) {
|
|
533
|
-
// We're in the monorepo — use it directly
|
|
534
401
|
if (!isJson)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
message: 'How should Memoire run locally?',
|
|
543
|
-
options: [
|
|
544
|
-
...(systemInfo.hasDocker
|
|
545
|
-
? [{ value: 'docker', label: 'Docker Compose', hint: 'recommended — fastest' }]
|
|
546
|
-
: []),
|
|
547
|
-
...(systemInfo.hasPnpm
|
|
548
|
-
? [{ value: 'clone', label: 'Clone & build from source', hint: `installs to ~/.memoire/server` }]
|
|
549
|
-
: []),
|
|
550
|
-
{ value: 'skip', label: 'Skip — I\'ll start the API myself', hint: 'manual setup' },
|
|
551
|
-
],
|
|
402
|
+
s.start('Starting API server...');
|
|
403
|
+
const apiDir = join(memoireRoot, 'apps', 'api');
|
|
404
|
+
const child = spawn('node', ['dist/index.js'], {
|
|
405
|
+
cwd: apiDir,
|
|
406
|
+
detached: true,
|
|
407
|
+
stdio: 'ignore',
|
|
408
|
+
env: { ...process.env, PORT: String(apiPort) },
|
|
552
409
|
});
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// Wait for API health via Docker
|
|
558
|
-
if (!isJson)
|
|
559
|
-
s.start('Waiting for API to be ready...');
|
|
560
|
-
for (let i = 0; i < 30; i++) {
|
|
561
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
562
|
-
apiReachable = await checkApiHealth(apiUrl);
|
|
563
|
-
if (apiReachable)
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
if (!isJson) {
|
|
567
|
-
if (apiReachable)
|
|
568
|
-
s.stop('API is healthy');
|
|
569
|
-
else
|
|
570
|
-
s.stop('API may still be starting — Docker containers are running');
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
else if (startMethod === 'clone') {
|
|
574
|
-
memoireRoot = await cloneAndBuild(s, isJson);
|
|
575
|
-
await startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey);
|
|
410
|
+
child.unref();
|
|
411
|
+
// Wait briefly for the server to start
|
|
412
|
+
for (let i = 0; i < 10; i++) {
|
|
413
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
576
414
|
apiReachable = await checkApiHealth(apiUrl);
|
|
415
|
+
if (apiReachable)
|
|
416
|
+
break;
|
|
577
417
|
}
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
|
|
418
|
+
if (!isJson) {
|
|
419
|
+
if (apiReachable) {
|
|
420
|
+
s.stop(`API server running on port ${apiPort}`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
s.stop('API server may still be starting');
|
|
424
|
+
p.log.warn('Could not verify API health. Continuing anyway.');
|
|
425
|
+
}
|
|
581
426
|
}
|
|
582
427
|
}
|
|
583
428
|
else {
|
|
584
|
-
|
|
585
|
-
if (systemInfo.hasDocker) {
|
|
586
|
-
const dbUrl = flags._resolvedDbUrl;
|
|
587
|
-
const dbUrlDirect = flags._resolvedDbUrlDirect;
|
|
588
|
-
await startWithDocker(s, isJson, dbUrl, dbUrlDirect, apiPort, flags.dashboardPort ?? DEFAULT_DASHBOARD_PORT, openaiKey, anthropicKey);
|
|
589
|
-
for (let i = 0; i < 30; i++) {
|
|
590
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
591
|
-
apiReachable = await checkApiHealth(apiUrl);
|
|
592
|
-
if (apiReachable)
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
else if (systemInfo.hasPnpm) {
|
|
597
|
-
memoireRoot = await cloneAndBuild(s, isJson);
|
|
598
|
-
await startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey);
|
|
599
|
-
apiReachable = await checkApiHealth(apiUrl);
|
|
600
|
-
}
|
|
429
|
+
p.log.warn('Could not locate Memoire monorepo root. Skipping API start.');
|
|
601
430
|
}
|
|
602
431
|
}
|
|
603
432
|
}
|
|
@@ -662,6 +491,7 @@ export async function runSetupWizard(flags = {}) {
|
|
|
662
491
|
apiUrl,
|
|
663
492
|
setupToken: flags.setupToken,
|
|
664
493
|
inviteToken: flags.inviteToken,
|
|
494
|
+
inviteCode: joinInviteCode ?? flags.inviteCode,
|
|
665
495
|
orgName,
|
|
666
496
|
orgSlug: flags.orgSlug,
|
|
667
497
|
projectName,
|
|
@@ -681,43 +511,10 @@ export async function runSetupWizard(flags = {}) {
|
|
|
681
511
|
s.stop('Workspace bootstrapped');
|
|
682
512
|
}
|
|
683
513
|
catch (err) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
s.stop('Workspace already exists');
|
|
689
|
-
if (existingConfig?.api_key && existingConfig?.org_id) {
|
|
690
|
-
if (!isJson)
|
|
691
|
-
p.log.info('Using existing workspace credentials.');
|
|
692
|
-
bootstrapResult = {
|
|
693
|
-
org_id: existingConfig.org_id,
|
|
694
|
-
project_id: existingConfig.project_id ?? '',
|
|
695
|
-
token: existingConfig.api_key,
|
|
696
|
-
user_id: existingConfig.user_id ?? '',
|
|
697
|
-
};
|
|
698
|
-
// Install for the first agent
|
|
699
|
-
await install({
|
|
700
|
-
client: firstAgent,
|
|
701
|
-
apiKey: bootstrapResult.token,
|
|
702
|
-
apiUrl,
|
|
703
|
-
orgId: bootstrapResult.org_id,
|
|
704
|
-
projectId: bootstrapResult.project_id || undefined,
|
|
705
|
-
userName: flags.userName,
|
|
706
|
-
userEmail: flags.userEmail,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
if (!isJson)
|
|
711
|
-
p.log.error('Workspace exists but no local credentials found. Run `npx memoire-ai login` to authenticate.');
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
if (!isJson)
|
|
717
|
-
s.stop('Bootstrap failed');
|
|
718
|
-
p.log.error(`Failed to bootstrap workspace: ${msg}`);
|
|
719
|
-
process.exit(1);
|
|
720
|
-
}
|
|
514
|
+
if (!isJson)
|
|
515
|
+
s.stop('Bootstrap failed');
|
|
516
|
+
p.log.error(`Failed to bootstrap workspace: ${err instanceof Error ? err.message : 'unknown error'}`);
|
|
517
|
+
process.exit(1);
|
|
721
518
|
}
|
|
722
519
|
}
|
|
723
520
|
// -----------------------------------------------------------------------
|
|
@@ -750,10 +547,9 @@ export async function runSetupWizard(flags = {}) {
|
|
|
750
547
|
}
|
|
751
548
|
// -----------------------------------------------------------------------
|
|
752
549
|
// Phase 11: Start dashboard (self-host only)
|
|
753
|
-
// Docker users already have dashboard running from docker compose up.
|
|
754
550
|
// -----------------------------------------------------------------------
|
|
755
551
|
let dashboardUrl = null;
|
|
756
|
-
if (!isCloud && !flags.skipStart
|
|
552
|
+
if (!isCloud && !flags.skipStart) {
|
|
757
553
|
const dashPort = flags.dashboardPort ?? DEFAULT_DASHBOARD_PORT;
|
|
758
554
|
const dashPortFree = await isPortAvailable(dashPort);
|
|
759
555
|
if (!dashPortFree) {
|
|
@@ -762,34 +558,23 @@ export async function runSetupWizard(flags = {}) {
|
|
|
762
558
|
p.log.info(`Dashboard already running at ${dashboardUrl}`);
|
|
763
559
|
}
|
|
764
560
|
else {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const dashboardBuild = join(memoireRoot, 'apps', 'dashboard', 'dist', 'server.js');
|
|
768
|
-
if (existsSync(dashboardBuild)) {
|
|
561
|
+
const memoireRoot = findMemoireRoot();
|
|
562
|
+
if (memoireRoot) {
|
|
769
563
|
if (!isJson)
|
|
770
564
|
s.start('Starting dashboard...');
|
|
771
|
-
const
|
|
772
|
-
|
|
565
|
+
const configPath = join(homedir(), '.memoire', 'config.json');
|
|
566
|
+
const child = spawn('npx', ['memoire', 'dashboard', '--port', String(dashPort), '--config-path', configPath], {
|
|
567
|
+
cwd: memoireRoot,
|
|
773
568
|
detached: true,
|
|
774
569
|
stdio: 'ignore',
|
|
775
|
-
env: {
|
|
776
|
-
...process.env,
|
|
777
|
-
MEMOIRE_API_URL: apiUrl,
|
|
778
|
-
MEMOIRE_DASHBOARD_DEFAULT_API_URL: apiUrl,
|
|
779
|
-
PORT: String(dashPort),
|
|
780
|
-
NODE_ENV: 'production',
|
|
781
|
-
},
|
|
782
570
|
});
|
|
783
571
|
child.unref();
|
|
784
572
|
dashboardUrl = `http://localhost:${dashPort}`;
|
|
785
|
-
|
|
573
|
+
// Wait briefly for dashboard to start
|
|
574
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
786
575
|
if (!isJson)
|
|
787
576
|
s.stop(`Dashboard starting at ${dashboardUrl}`);
|
|
788
577
|
}
|
|
789
|
-
else {
|
|
790
|
-
if (!isJson)
|
|
791
|
-
p.log.info('Dashboard not built — run `pnpm --filter @memoire-ai/dashboard build` to enable it');
|
|
792
|
-
}
|
|
793
578
|
}
|
|
794
579
|
}
|
|
795
580
|
else if (isCloud) {
|