@paparats/cli 0.3.1 → 0.4.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/dist/commands/edit.d.ts +30 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +131 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/groups.d.ts.map +1 -1
- package/dist/commands/groups.js +1 -1
- package/dist/commands/groups.js.map +1 -1
- package/dist/commands/install.d.ts +64 -21
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +337 -294
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/lifecycle.d.ts +17 -0
- package/dist/commands/lifecycle.d.ts.map +1 -0
- package/dist/commands/lifecycle.js +86 -0
- package/dist/commands/lifecycle.js.map +1 -0
- package/dist/commands/projects.d.ts +81 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +321 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/docker-compose-generator.d.ts +30 -24
- package/dist/docker-compose-generator.d.ts.map +1 -1
- package/dist/docker-compose-generator.js +58 -81
- package/dist/docker-compose-generator.js.map +1 -1
- package/dist/index.js +19 -7
- package/dist/index.js.map +1 -1
- package/dist/projects-yml.d.ts +95 -0
- package/dist/projects-yml.d.ts.map +1 -0
- package/dist/projects-yml.js +157 -0
- package/dist/projects-yml.js.map +1 -0
- package/package.json +2 -2
- package/dist/commands/index-cmd.d.ts +0 -75
- package/dist/commands/index-cmd.d.ts.map +0 -1
- package/dist/commands/index-cmd.js +0 -240
- package/dist/commands/index-cmd.js.map +0 -1
- package/dist/commands/init.d.ts +0 -28
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -316
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -33
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -358
- package/dist/commands/watch.js.map +0 -1
- package/dist/lsp-installers.d.ts +0 -28
- package/dist/lsp-installers.d.ts.map +0 -1
- package/dist/lsp-installers.js +0 -125
- package/dist/lsp-installers.js.map +0 -1
package/dist/commands/install.js
CHANGED
|
@@ -5,10 +5,10 @@ import os from 'os';
|
|
|
5
5
|
import { execSync, spawn } from 'child_process';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
|
-
import { confirm, input } from '@inquirer/prompts';
|
|
8
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
9
9
|
import { createTimeoutSignal } from '../abort.js';
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import { generateCompose } from '../docker-compose-generator.js';
|
|
11
|
+
import { PAPARATS_HOME, COMPOSE_YML, PROJECTS_YML, migrateLegacyProjectsFile, readProjectsFile, writeProjectsFile, writeInstallState, localProjectsFor, } from '../projects-yml.js';
|
|
12
12
|
const MODELS_DIR = path.join(PAPARATS_HOME, 'models');
|
|
13
13
|
const OLLAMA_MODEL_NAME = 'jina-code-embeddings';
|
|
14
14
|
const GGUF_URL = 'https://huggingface.co/jinaai/jina-code-embeddings-1.5b-GGUF/resolve/main/jina-code-embeddings-1.5b-Q8_0.gguf';
|
|
@@ -203,9 +203,145 @@ async function ensureLocalOllama(deps, cleanupTasks) {
|
|
|
203
203
|
throw err;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
// ──
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
// ── Migration ───────────────────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Detects a v1 install: legacy compose has `paparats-mcp` container without
|
|
209
|
+
* `paparats-indexer` service.
|
|
210
|
+
*/
|
|
211
|
+
export function detectLegacyInstall(composeContent) {
|
|
212
|
+
if (!composeContent)
|
|
213
|
+
return null;
|
|
214
|
+
if (composeContent.includes('container_name: paparats-indexer'))
|
|
215
|
+
return null;
|
|
216
|
+
if (composeContent.includes('container_name: paparats-mcp') ||
|
|
217
|
+
composeContent.includes("container_name: 'paparats-mcp'")) {
|
|
218
|
+
return 'legacy compose has paparats-mcp without paparats-indexer';
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get the user's consent to migrate from a v1 install. Does NOT touch any
|
|
224
|
+
* file on disk — the actual tear-down happens later, after the replacement
|
|
225
|
+
* compose has been generated and written. This split prevents the
|
|
226
|
+
* "interrupt-mid-install" hole where we used to delete the legacy compose
|
|
227
|
+
* before knowing whether we could write a successor.
|
|
228
|
+
*/
|
|
229
|
+
async function confirmMigration(resolvedDeps, opts) {
|
|
230
|
+
console.log(chalk.yellow.bold('\nLegacy install detected.\n') +
|
|
231
|
+
'Paparats has switched to a single global install with one docker-compose.yml and a\n' +
|
|
232
|
+
'project list at ~/.paparats/projects.yml. Per-project `paparats init` and the\n' +
|
|
233
|
+
'`developer` / `server` install modes are no longer used.\n\n' +
|
|
234
|
+
'Existing data volumes (qdrant_data, paparats_data, indexer_repos) are\n' +
|
|
235
|
+
'preserved — your indexed projects survive. The legacy compose and .env will be\n' +
|
|
236
|
+
'backed up to *.legacy.bak and replaced once the new compose is ready.\n');
|
|
237
|
+
if (opts.force)
|
|
238
|
+
return true;
|
|
239
|
+
if (opts.nonInteractive) {
|
|
240
|
+
throw new Error('Migration prompt required but --non-interactive set; pass --force to proceed.');
|
|
241
|
+
}
|
|
242
|
+
const promptMigrate = resolvedDeps.promptMigrate ??
|
|
243
|
+
(() => confirm({ message: 'Continue migration?', default: false }));
|
|
244
|
+
const proceed = await promptMigrate();
|
|
245
|
+
if (!proceed) {
|
|
246
|
+
console.log(chalk.dim('Migration aborted, no changes made.'));
|
|
247
|
+
}
|
|
248
|
+
return proceed;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Tear down the legacy stack and back up its compose+env. Backups stay on
|
|
252
|
+
* disk as `<name>.legacy.bak` so the user has a recovery path if anything
|
|
253
|
+
* downstream goes wrong; we only clean them up after a fully successful
|
|
254
|
+
* install. Returns the backup paths that were actually created.
|
|
255
|
+
*/
|
|
256
|
+
export function tearDownAndBackupLegacy(composePath, envPath, resolvedDeps, opts) {
|
|
257
|
+
// Best-effort: stop the legacy stack. A failure here doesn't block the
|
|
258
|
+
// upgrade — the user may already have stopped it manually, or the daemon
|
|
259
|
+
// may be unreachable; either way, the new compose can still be written.
|
|
260
|
+
try {
|
|
261
|
+
resolvedDeps.execSync(`${resolvedDeps.getDockerComposeCommand()} -f "${composePath}" down`, {
|
|
262
|
+
stdio: opts.verbose ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
263
|
+
timeout: 60_000,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// user may have already stopped it; ignore
|
|
268
|
+
}
|
|
269
|
+
let composeBak = null;
|
|
270
|
+
let envBak = null;
|
|
271
|
+
if (resolvedDeps.existsSync(composePath)) {
|
|
272
|
+
composeBak = `${composePath}.legacy.bak`;
|
|
273
|
+
resolvedDeps.renameSync(composePath, composeBak);
|
|
274
|
+
}
|
|
275
|
+
if (resolvedDeps.existsSync(envPath)) {
|
|
276
|
+
envBak = `${envPath}.legacy.bak`;
|
|
277
|
+
resolvedDeps.renameSync(envPath, envBak);
|
|
278
|
+
}
|
|
279
|
+
return { composeBak, envBak };
|
|
280
|
+
}
|
|
281
|
+
export async function decideOllamaMode(opts, deps) {
|
|
282
|
+
if (opts.ollamaUrl) {
|
|
283
|
+
return { mode: 'external', ollamaUrl: opts.ollamaUrl, setupHostOllama: false };
|
|
284
|
+
}
|
|
285
|
+
if (opts.ollamaMode === 'docker') {
|
|
286
|
+
return { mode: 'docker', setupHostOllama: false };
|
|
287
|
+
}
|
|
288
|
+
if (opts.ollamaMode === 'native') {
|
|
289
|
+
return { mode: 'native', setupHostOllama: true };
|
|
290
|
+
}
|
|
291
|
+
const platform = deps.platform();
|
|
292
|
+
if (platform === 'darwin') {
|
|
293
|
+
if (deps.commandExists('ollama')) {
|
|
294
|
+
console.log(chalk.green('✓ Native Ollama detected on macOS\n'));
|
|
295
|
+
return { mode: 'native', setupHostOllama: true };
|
|
296
|
+
}
|
|
297
|
+
console.log(chalk.yellow('Ollama is not installed.\n') +
|
|
298
|
+
chalk.dim('Recommendation for macOS: install Ollama natively. Running Ollama in Docker on\n' +
|
|
299
|
+
'macOS is significantly slower because the Docker VM cannot use Apple Silicon\n' +
|
|
300
|
+
'GPU acceleration. Native Ollama uses Metal directly.\n'));
|
|
301
|
+
if (opts.nonInteractive) {
|
|
302
|
+
throw new Error('Ollama not found. Install with `brew install ollama`, or pass --ollama-mode docker / --ollama-url <url>.');
|
|
303
|
+
}
|
|
304
|
+
const choice = deps.promptOllamaChoiceMacOs
|
|
305
|
+
? await deps.promptOllamaChoiceMacOs()
|
|
306
|
+
: await select({
|
|
307
|
+
message: 'How should Paparats reach Ollama?',
|
|
308
|
+
choices: [
|
|
309
|
+
{ name: 'Install natively via brew install ollama (recommended)', value: 'brew' },
|
|
310
|
+
{ name: 'Use a remote Ollama URL', value: 'remote' },
|
|
311
|
+
{ name: 'Run Ollama in Docker (slower on macOS)', value: 'docker' },
|
|
312
|
+
],
|
|
313
|
+
default: 'brew',
|
|
314
|
+
});
|
|
315
|
+
if (choice === 'brew') {
|
|
316
|
+
if (!deps.commandExists('brew')) {
|
|
317
|
+
throw new Error('Homebrew not found. Install brew first (https://brew.sh) or pass --ollama-mode docker / --ollama-url.');
|
|
318
|
+
}
|
|
319
|
+
const spinner = ora('Installing ollama via brew...').start();
|
|
320
|
+
try {
|
|
321
|
+
deps.execSync('brew install ollama', {
|
|
322
|
+
stdio: opts.verbose ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
323
|
+
timeout: 180_000,
|
|
324
|
+
});
|
|
325
|
+
spinner.succeed('ollama installed');
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
spinner.fail('brew install ollama failed');
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
return { mode: 'native', setupHostOllama: true };
|
|
332
|
+
}
|
|
333
|
+
if (choice === 'remote') {
|
|
334
|
+
const url = deps.promptRemoteOllamaUrl
|
|
335
|
+
? await deps.promptRemoteOllamaUrl()
|
|
336
|
+
: await input({ message: 'Remote Ollama URL:' });
|
|
337
|
+
return { mode: 'external', ollamaUrl: url, setupHostOllama: false };
|
|
338
|
+
}
|
|
339
|
+
return { mode: 'docker', setupHostOllama: false };
|
|
340
|
+
}
|
|
341
|
+
return { mode: 'docker', setupHostOllama: false };
|
|
342
|
+
}
|
|
343
|
+
// ── Unified install ─────────────────────────────────────────────────────────
|
|
344
|
+
async function runUnifiedInstall(opts, deps) {
|
|
209
345
|
const cleanupTasks = [];
|
|
210
346
|
if (deps.signal) {
|
|
211
347
|
deps.signal.addEventListener('abort', () => {
|
|
@@ -221,216 +357,172 @@ async function runDeveloperInstall(opts, deps) {
|
|
|
221
357
|
process.exit(130);
|
|
222
358
|
});
|
|
223
359
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
];
|
|
228
|
-
if (ollamaMode === 'local' && !opts.ollamaUrl) {
|
|
229
|
-
checks.push({ cmd: 'ollama', name: 'Ollama', install: 'https://ollama.com' });
|
|
360
|
+
// 1. Prerequisites
|
|
361
|
+
if (!deps.commandExists('docker')) {
|
|
362
|
+
throw new Error('Docker not found. Install from https://docker.com');
|
|
230
363
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
364
|
+
deps.getDockerComposeCommand();
|
|
365
|
+
console.log(chalk.green('✓ Docker + docker compose found\n'));
|
|
366
|
+
// 2. Migration check
|
|
367
|
+
deps.mkdirSync(PAPARATS_HOME);
|
|
368
|
+
const composePath = path.join(PAPARATS_HOME, COMPOSE_YML);
|
|
369
|
+
const envPath = path.join(PAPARATS_HOME, '.env');
|
|
370
|
+
const composeContent = deps.existsSync(composePath)
|
|
371
|
+
? deps.readFileSync(composePath, 'utf8')
|
|
372
|
+
: null;
|
|
373
|
+
const legacyTrigger = detectLegacyInstall(composeContent);
|
|
374
|
+
let needsLegacyTeardown = false;
|
|
375
|
+
if (legacyTrigger) {
|
|
376
|
+
const proceeded = await confirmMigration(deps, opts);
|
|
377
|
+
if (!proceeded)
|
|
378
|
+
return;
|
|
379
|
+
// Defer the actual tear-down until we have the new compose generated
|
|
380
|
+
// and ready to write — see step 7c below.
|
|
381
|
+
needsLegacyTeardown = true;
|
|
382
|
+
}
|
|
383
|
+
// 3. Ollama decision
|
|
384
|
+
const ollamaDecision = await decideOllamaMode(opts, deps);
|
|
385
|
+
// 4. Qdrant decision
|
|
386
|
+
if (!opts.qdrantUrl && !opts.nonInteractive) {
|
|
387
|
+
const promptExternal = deps.promptUseExternalQdrant ??
|
|
388
|
+
(() => confirm({
|
|
389
|
+
message: 'Use an external Qdrant instance? (skip Qdrant Docker container)',
|
|
390
|
+
default: false,
|
|
391
|
+
}));
|
|
392
|
+
const useExternal = await promptExternal();
|
|
393
|
+
if (useExternal) {
|
|
394
|
+
const promptUrl = deps.promptQdrantUrl ??
|
|
395
|
+
(() => input({
|
|
396
|
+
message: 'Qdrant URL:',
|
|
397
|
+
default: 'http://localhost:6333',
|
|
398
|
+
validate: (value) => {
|
|
399
|
+
try {
|
|
400
|
+
new URL(value);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return 'Please enter a valid URL';
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
}));
|
|
408
|
+
opts.qdrantUrl = await promptUrl();
|
|
234
409
|
}
|
|
235
410
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
// Write .env for docker-compose variable substitution (API key, etc.)
|
|
275
|
-
if (opts.qdrantApiKey) {
|
|
276
|
-
const envPath = path.join(PAPARATS_HOME, '.env');
|
|
277
|
-
const envLines = [];
|
|
278
|
-
envLines.push(`QDRANT_API_KEY=${opts.qdrantApiKey}`);
|
|
279
|
-
deps.writeFileSync(envPath, envLines.join('\n') + '\n');
|
|
280
|
-
}
|
|
281
|
-
spinner.text = 'Starting Docker containers...';
|
|
282
|
-
const composeCmd = deps.getDockerComposeCommand();
|
|
283
|
-
const fullCmd = `${composeCmd} -f "${composeDest}" up -d`;
|
|
284
|
-
try {
|
|
285
|
-
execSync(fullCmd, {
|
|
286
|
-
stdio: opts.verbose ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
287
|
-
timeout: 120_000,
|
|
288
|
-
});
|
|
289
|
-
spinner.succeed('Docker containers started');
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
spinner.fail('Failed to start Docker containers');
|
|
293
|
-
throw err;
|
|
294
|
-
}
|
|
295
|
-
const qdrantReady = await deps.waitForHealth(qdrantHealthUrl(opts.qdrantUrl), 'Qdrant');
|
|
296
|
-
if (!qdrantReady) {
|
|
297
|
-
console.log(chalk.yellow(' Qdrant is not responding yet. Continuing with remaining setup.\n' +
|
|
298
|
-
' Check Docker logs: docker compose -f ~/.paparats/docker-compose.yml logs qdrant'));
|
|
299
|
-
}
|
|
300
|
-
const mcpReady = await deps.waitForHealth('http://localhost:9876/health', 'MCP server');
|
|
301
|
-
if (!mcpReady) {
|
|
302
|
-
console.log(chalk.yellow(' MCP server is not responding yet. Continuing with remaining setup.\n' +
|
|
303
|
-
' Check Docker logs: docker compose -f ~/.paparats/docker-compose.yml logs paparats'));
|
|
411
|
+
if (opts.qdrantUrl && !opts.qdrantApiKey && !opts.nonInteractive) {
|
|
412
|
+
const promptApiKey = deps.promptQdrantApiKey ??
|
|
413
|
+
(() => input({ message: 'Qdrant API key (leave empty if none):', default: '' }));
|
|
414
|
+
const key = await promptApiKey();
|
|
415
|
+
if (key)
|
|
416
|
+
opts.qdrantApiKey = key;
|
|
417
|
+
}
|
|
418
|
+
// 5. Ensure projects.yml exists. Migrate the legacy paparats-indexer.yml
|
|
419
|
+
// in place if present so users coming from paparats < 0.4 don't lose
|
|
420
|
+
// their project list.
|
|
421
|
+
if (migrateLegacyProjectsFile(PAPARATS_HOME)) {
|
|
422
|
+
console.log(chalk.yellow(`Renamed legacy paparats-indexer.yml → ${PROJECTS_YML} (one-time migration).`));
|
|
423
|
+
}
|
|
424
|
+
const projectsYmlPath = path.join(PAPARATS_HOME, PROJECTS_YML);
|
|
425
|
+
if (!deps.existsSync(projectsYmlPath)) {
|
|
426
|
+
writeProjectsFile({ repos: [] }, PAPARATS_HOME);
|
|
427
|
+
console.log(chalk.dim(`Created empty ${projectsYmlPath}`));
|
|
428
|
+
}
|
|
429
|
+
// 6. Generate compose using current projects list
|
|
430
|
+
const projectsFile = readProjectsFile(PAPARATS_HOME);
|
|
431
|
+
const newComposeContent = deps.generateCompose({
|
|
432
|
+
ollamaMode: ollamaDecision.mode,
|
|
433
|
+
...(ollamaDecision.ollamaUrl !== undefined ? { ollamaUrl: ollamaDecision.ollamaUrl } : {}),
|
|
434
|
+
...(opts.qdrantUrl !== undefined ? { qdrantUrl: opts.qdrantUrl } : {}),
|
|
435
|
+
...(opts.qdrantApiKey !== undefined ? { qdrantApiKey: opts.qdrantApiKey } : {}),
|
|
436
|
+
paparatsHome: PAPARATS_HOME,
|
|
437
|
+
localProjects: localProjectsFor(projectsFile),
|
|
438
|
+
});
|
|
439
|
+
// 7. Tear down the legacy stack and back up its compose+env now that we
|
|
440
|
+
// have a validated replacement in memory. Backups stay on disk under
|
|
441
|
+
// *.legacy.bak so the user has a recovery path; we keep them around
|
|
442
|
+
// even after success — `paparats install` is idempotent and a stray
|
|
443
|
+
// pair of bak files is cheaper than a missed rollback opportunity.
|
|
444
|
+
if (needsLegacyTeardown) {
|
|
445
|
+
const { composeBak, envBak } = tearDownAndBackupLegacy(composePath, envPath, deps, opts);
|
|
446
|
+
if (composeBak) {
|
|
447
|
+
console.log(chalk.dim(`Backed up legacy compose to ${composeBak}` + (envBak ? ` and .env to ${envBak}` : '')));
|
|
304
448
|
}
|
|
305
449
|
}
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.log(chalk.dim(' 1. cd <your-project>'));
|
|
315
|
-
console.log(chalk.dim(' 2. paparats init'));
|
|
316
|
-
console.log(chalk.dim(' 3. paparats index'));
|
|
317
|
-
console.log(chalk.dim(' 4. Connect your IDE (see README)'));
|
|
318
|
-
console.log('');
|
|
319
|
-
console.log(chalk.dim('To scope searches to specific projects, set PAPARATS_PROJECTS in your'));
|
|
320
|
-
console.log(chalk.dim('MCP client config (e.g. "billing,tracking"). Project names are directory'));
|
|
321
|
-
console.log(chalk.dim('basenames, not org/repo format.\n'));
|
|
322
|
-
}
|
|
323
|
-
// ── Server mode ─────────────────────────────────────────────────────────────
|
|
324
|
-
async function runServerInstall(opts, deps) {
|
|
325
|
-
const ollamaMode = opts.ollamaMode ?? 'docker';
|
|
326
|
-
const needsLocalOllama = ollamaMode === 'local' && !opts.ollamaUrl;
|
|
327
|
-
// Check prerequisites
|
|
328
|
-
const checks = [
|
|
329
|
-
{ cmd: 'docker', name: 'Docker', install: 'https://docker.com' },
|
|
330
|
-
];
|
|
331
|
-
if (needsLocalOllama) {
|
|
332
|
-
checks.push({ cmd: 'ollama', name: 'Ollama', install: 'https://ollama.com' });
|
|
333
|
-
}
|
|
334
|
-
for (const check of checks) {
|
|
335
|
-
if (!deps.commandExists(check.cmd)) {
|
|
336
|
-
throw new Error(`${check.name} not found. Install from ${check.install}`);
|
|
450
|
+
// 7a. Compose write with overwrite confirmation (default N)
|
|
451
|
+
const existingCompose = deps.existsSync(composePath)
|
|
452
|
+
? deps.readFileSync(composePath, 'utf8')
|
|
453
|
+
: null;
|
|
454
|
+
let writeCompose = true;
|
|
455
|
+
if (existingCompose !== null && existingCompose !== newComposeContent) {
|
|
456
|
+
if (opts.force) {
|
|
457
|
+
writeCompose = true;
|
|
337
458
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
cron: opts.cron,
|
|
351
|
-
group: opts.group,
|
|
352
|
-
});
|
|
353
|
-
const composeDest = path.join(PAPARATS_HOME, 'docker-compose.yml');
|
|
354
|
-
if (deps.existsSync(composeDest)) {
|
|
355
|
-
const existing = deps.readFileSync(composeDest, 'utf8');
|
|
356
|
-
if (existing !== composeContent) {
|
|
357
|
-
const overwrite = process.stdin.isTTY
|
|
358
|
-
? await confirm({
|
|
359
|
-
message: `${composeDest} already exists and differs. Overwrite?`,
|
|
360
|
-
default: true,
|
|
361
|
-
})
|
|
362
|
-
: true;
|
|
363
|
-
if (!overwrite) {
|
|
364
|
-
console.log(chalk.dim('Keeping existing docker-compose.yml'));
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
deps.writeFileSync(composeDest, composeContent);
|
|
459
|
+
else if (opts.nonInteractive) {
|
|
460
|
+
throw new Error('docker-compose.yml differs; pass --force to overwrite or run without --non-interactive.');
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
const promptOverwrite = deps.promptOverwriteCompose ??
|
|
464
|
+
(() => confirm({
|
|
465
|
+
message: `${composePath} has been hand-edited. Overwrite?`,
|
|
466
|
+
default: false,
|
|
467
|
+
}));
|
|
468
|
+
writeCompose = await promptOverwrite();
|
|
469
|
+
if (!writeCompose) {
|
|
470
|
+
console.log(chalk.dim('Existing compose preserved. Run `paparats install --force` to regenerate.\n'));
|
|
368
471
|
}
|
|
369
472
|
}
|
|
370
473
|
}
|
|
371
|
-
|
|
372
|
-
deps.writeFileSync(
|
|
373
|
-
}
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
console.log(chalk.dim(`Created ${envPath}`));
|
|
390
|
-
}
|
|
391
|
-
// Start containers
|
|
392
|
-
const spinner = ora('Starting Docker containers (this may take a while on first run)...').start();
|
|
474
|
+
if (writeCompose) {
|
|
475
|
+
deps.writeFileSync(composePath, newComposeContent);
|
|
476
|
+
}
|
|
477
|
+
// 7b. Persist install state so `paparats add | remove | edit projects` can
|
|
478
|
+
// regenerate compose later with the same flags (ollama mode, qdrant
|
|
479
|
+
// credentials, cron). Without this, those commands have no idea which
|
|
480
|
+
// services should be in the compose.
|
|
481
|
+
writeInstallState({
|
|
482
|
+
ollamaMode: ollamaDecision.mode,
|
|
483
|
+
...(ollamaDecision.ollamaUrl !== undefined ? { ollamaUrl: ollamaDecision.ollamaUrl } : {}),
|
|
484
|
+
...(opts.qdrantUrl !== undefined ? { qdrantUrl: opts.qdrantUrl } : {}),
|
|
485
|
+
...(opts.qdrantApiKey !== undefined ? { qdrantApiKey: opts.qdrantApiKey } : {}),
|
|
486
|
+
}, PAPARATS_HOME);
|
|
487
|
+
// 8. .env file
|
|
488
|
+
if (opts.qdrantApiKey) {
|
|
489
|
+
deps.writeFileSync(envPath, `QDRANT_API_KEY=${opts.qdrantApiKey}\n`);
|
|
490
|
+
}
|
|
491
|
+
// 9. Bring up the stack
|
|
393
492
|
const composeCmd = deps.getDockerComposeCommand();
|
|
394
|
-
const
|
|
493
|
+
const upSpinner = ora('Starting Docker containers...').start();
|
|
395
494
|
try {
|
|
396
|
-
execSync(
|
|
495
|
+
execSync(`${composeCmd} -f "${composePath}" up -d`, {
|
|
397
496
|
stdio: opts.verbose ? 'inherit' : ['pipe', 'pipe', 'pipe'],
|
|
398
|
-
timeout:
|
|
497
|
+
timeout: 180_000,
|
|
399
498
|
});
|
|
400
|
-
|
|
499
|
+
upSpinner.succeed('Docker containers started');
|
|
401
500
|
}
|
|
402
501
|
catch (err) {
|
|
403
|
-
|
|
502
|
+
upSpinner.fail('Failed to start Docker containers');
|
|
404
503
|
throw err;
|
|
405
504
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (!mcpReady) {
|
|
413
|
-
console.log(chalk.yellow(' MCP server is not responding yet. Continuing with remaining setup.\n' +
|
|
414
|
-
' Check Docker logs: docker compose -f ~/.paparats/docker-compose.yml logs paparats'));
|
|
415
|
-
}
|
|
416
|
-
// Ollama setup (local mode only, skip when external URL or Docker Ollama)
|
|
417
|
-
if (needsLocalOllama) {
|
|
418
|
-
await ensureLocalOllama(deps, []);
|
|
419
|
-
}
|
|
420
|
-
console.log(chalk.bold.green('\n\u2713 Server installation complete!\n'));
|
|
421
|
-
console.log('MCP endpoints:');
|
|
422
|
-
console.log(chalk.dim(' Coding: http://localhost:9876/mcp'));
|
|
423
|
-
console.log(chalk.dim(' Support: http://localhost:9876/support/mcp'));
|
|
424
|
-
console.log(chalk.dim(' Health: http://localhost:9876/health'));
|
|
425
|
-
if (opts.repos) {
|
|
426
|
-
console.log(chalk.dim(`\nIndexer will process repos on schedule: ${opts.cron ?? '0 */6 * * *'}`));
|
|
427
|
-
console.log(chalk.dim(' Trigger now: curl -X POST http://localhost:9877/trigger'));
|
|
428
|
-
console.log(chalk.dim(' Status: curl http://localhost:9877/health'));
|
|
505
|
+
await deps.waitForHealth(qdrantHealthUrl(opts.qdrantUrl), 'Qdrant');
|
|
506
|
+
await deps.waitForHealth('http://localhost:9876/health', 'MCP server');
|
|
507
|
+
await deps.waitForHealth('http://localhost:9877/health', 'Indexer');
|
|
508
|
+
// 10. Ollama model registration on host (when native)
|
|
509
|
+
if (ollamaDecision.setupHostOllama) {
|
|
510
|
+
await ensureLocalOllama(deps, cleanupTasks);
|
|
429
511
|
}
|
|
512
|
+
// 11. MCP-client wiring
|
|
513
|
+
configureCursorMcp('http://localhost:9876/mcp', deps);
|
|
514
|
+
// 12. Final summary
|
|
515
|
+
console.log(chalk.bold.green('\n✓ Installation complete!\n'));
|
|
516
|
+
console.log('Next steps:');
|
|
517
|
+
console.log(chalk.dim(` • Add a project: paparats add <path-or-repo>`));
|
|
518
|
+
console.log(chalk.dim(` • List projects: paparats list`));
|
|
519
|
+
console.log(chalk.dim(` • Edit project list: paparats edit projects`));
|
|
520
|
+
console.log(chalk.dim(` • Edit compose: paparats edit compose`));
|
|
521
|
+
console.log(chalk.dim(` • Stack lifecycle: paparats start | stop | restart\n`));
|
|
522
|
+
console.log(chalk.dim('MCP endpoints:'));
|
|
523
|
+
console.log(chalk.dim(' http://localhost:9876/mcp'));
|
|
524
|
+
console.log(chalk.dim(' http://localhost:9876/support/mcp'));
|
|
430
525
|
console.log('');
|
|
431
|
-
console.log(chalk.dim('To scope searches to specific projects, set PAPARATS_PROJECTS env var'));
|
|
432
|
-
console.log(chalk.dim('on the MCP server (e.g. PAPARATS_PROJECTS=billing,tracking). Project'));
|
|
433
|
-
console.log(chalk.dim('names are directory basenames, not org/repo format.\n'));
|
|
434
526
|
}
|
|
435
527
|
// ── Support mode ────────────────────────────────────────────────────────────
|
|
436
528
|
async function runSupportInstall(opts, deps) {
|
|
@@ -497,114 +589,65 @@ function configureCursorMcp(mcpUrl, deps, serverName = 'paparats') {
|
|
|
497
589
|
}
|
|
498
590
|
// ── Main entry point ────────────────────────────────────────────────────────
|
|
499
591
|
export async function runInstall(opts, deps) {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
catch {
|
|
535
|
-
return 'Please enter a valid URL (e.g. http://localhost:6333)';
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
}));
|
|
539
|
-
opts.qdrantUrl = await promptUrl();
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
// Prompt for API key when using external Qdrant (via prompt or --qdrant-url flag)
|
|
543
|
-
if (opts.qdrantUrl && !opts.qdrantApiKey) {
|
|
544
|
-
const promptApiKey = deps?.promptQdrantApiKey ??
|
|
545
|
-
(() => input({
|
|
546
|
-
message: 'Qdrant API key (leave empty if none):',
|
|
547
|
-
default: '',
|
|
548
|
-
}));
|
|
549
|
-
const apiKey = await promptApiKey();
|
|
550
|
-
if (apiKey) {
|
|
551
|
-
opts.qdrantApiKey = apiKey;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
const resolvedDeps = {
|
|
555
|
-
commandExists: cmdExists,
|
|
556
|
-
getDockerComposeCommand: getCompose,
|
|
557
|
-
ollamaModelExists: modelExists,
|
|
558
|
-
isOllamaRunning: ollamaRunning,
|
|
559
|
-
waitForHealth: waitHealth,
|
|
560
|
-
downloadFile: download,
|
|
561
|
-
generateDockerCompose: genCompose,
|
|
562
|
-
generateServerCompose: genServerCompose,
|
|
563
|
-
mkdirSync: mkdir,
|
|
564
|
-
readFileSync: readFile,
|
|
565
|
-
writeFileSync: writeFile,
|
|
566
|
-
existsSync: exists,
|
|
567
|
-
unlinkSync: unlink,
|
|
568
|
-
signal,
|
|
592
|
+
const resolved = {
|
|
593
|
+
commandExists: deps?.commandExists ?? commandExists,
|
|
594
|
+
getDockerComposeCommand: deps?.getDockerComposeCommand ?? getDockerComposeCommand,
|
|
595
|
+
ollamaModelExists: deps?.ollamaModelExists ?? ollamaModelExists,
|
|
596
|
+
isOllamaRunning: deps?.isOllamaRunning ?? isOllamaRunning,
|
|
597
|
+
waitForHealth: deps?.waitForHealth ?? waitForHealth,
|
|
598
|
+
downloadFile: deps?.downloadFile ?? downloadFile,
|
|
599
|
+
generateCompose: deps?.generateCompose ?? generateCompose,
|
|
600
|
+
mkdirSync: deps?.mkdirSync ?? ((p) => fs.mkdirSync(p, { recursive: true })),
|
|
601
|
+
readFileSync: deps?.readFileSync ?? ((p) => fs.readFileSync(p, 'utf8')),
|
|
602
|
+
writeFileSync: deps?.writeFileSync ?? fs.writeFileSync.bind(fs),
|
|
603
|
+
existsSync: deps?.existsSync ?? fs.existsSync.bind(fs),
|
|
604
|
+
unlinkSync: deps?.unlinkSync ?? fs.unlinkSync.bind(fs),
|
|
605
|
+
renameSync: deps?.renameSync ?? fs.renameSync.bind(fs),
|
|
606
|
+
platform: deps?.platform ?? (() => process.platform),
|
|
607
|
+
execSync: deps?.execSync ?? execSync,
|
|
608
|
+
...(deps?.signal !== undefined ? { signal: deps.signal } : {}),
|
|
609
|
+
...(deps?.promptUseExternalQdrant !== undefined
|
|
610
|
+
? { promptUseExternalQdrant: deps.promptUseExternalQdrant }
|
|
611
|
+
: {}),
|
|
612
|
+
...(deps?.promptQdrantUrl !== undefined ? { promptQdrantUrl: deps.promptQdrantUrl } : {}),
|
|
613
|
+
...(deps?.promptQdrantApiKey !== undefined
|
|
614
|
+
? { promptQdrantApiKey: deps.promptQdrantApiKey }
|
|
615
|
+
: {}),
|
|
616
|
+
...(deps?.promptOllamaChoiceMacOs !== undefined
|
|
617
|
+
? { promptOllamaChoiceMacOs: deps.promptOllamaChoiceMacOs }
|
|
618
|
+
: {}),
|
|
619
|
+
...(deps?.promptRemoteOllamaUrl !== undefined
|
|
620
|
+
? { promptRemoteOllamaUrl: deps.promptRemoteOllamaUrl }
|
|
621
|
+
: {}),
|
|
622
|
+
...(deps?.promptOverwriteCompose !== undefined
|
|
623
|
+
? { promptOverwriteCompose: deps.promptOverwriteCompose }
|
|
624
|
+
: {}),
|
|
625
|
+
...(deps?.promptMigrate !== undefined ? { promptMigrate: deps.promptMigrate } : {}),
|
|
569
626
|
};
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
await runServerInstall(opts, resolvedDeps);
|
|
576
|
-
break;
|
|
577
|
-
case 'support':
|
|
578
|
-
await runSupportInstall(opts, resolvedDeps);
|
|
579
|
-
break;
|
|
580
|
-
default:
|
|
581
|
-
throw new Error(`Unknown install mode: ${mode}`);
|
|
627
|
+
const mode = opts.mode ?? 'developer';
|
|
628
|
+
console.log(chalk.bold(`\npaparats install${mode === 'support' ? ' --mode support' : ''}\n`));
|
|
629
|
+
if (mode === 'support') {
|
|
630
|
+
await runSupportInstall(opts, resolved);
|
|
631
|
+
return;
|
|
582
632
|
}
|
|
633
|
+
await runUnifiedInstall(opts, resolved);
|
|
583
634
|
}
|
|
584
635
|
export const installCommand = new Command('install')
|
|
585
|
-
.description('Set up Paparats — Docker
|
|
586
|
-
.option('--mode <mode>', '
|
|
587
|
-
.option('--ollama-mode <mode>', 'Ollama
|
|
588
|
-
.option('--ollama-url <url>', 'External Ollama URL (
|
|
589
|
-
.option('--
|
|
590
|
-
.option('--skip-ollama', 'Skip Ollama model setup (developer mode)')
|
|
591
|
-
.option('--qdrant-url <url>', 'External Qdrant URL (skip Qdrant Docker container)')
|
|
636
|
+
.description('Set up Paparats — Docker stack, Ollama, MCP wiring')
|
|
637
|
+
.option('--mode <mode>', 'support to wire up an MCP client only; otherwise the unified install runs', 'developer')
|
|
638
|
+
.option('--ollama-mode <mode>', 'Force Ollama mode: native | docker (default: native on macOS, docker elsewhere)')
|
|
639
|
+
.option('--ollama-url <url>', 'External Ollama URL (skips both native and docker Ollama)')
|
|
640
|
+
.option('--qdrant-url <url>', 'External Qdrant URL (skips Qdrant Docker container)')
|
|
592
641
|
.option('--qdrant-api-key <key>', 'Qdrant API key for authenticated access')
|
|
593
|
-
.option('--repos <repos>', 'Comma-separated repos to index (server mode)')
|
|
594
|
-
.option('--github-token <token>', 'GitHub token for private repos (server mode)')
|
|
595
|
-
.option('--cron <expression>', 'Cron schedule for indexing (server mode)')
|
|
596
|
-
.option('--group <name>', 'Shared Qdrant group — all repos in one collection (server mode)')
|
|
597
642
|
.option('--server <url>', 'Server URL to connect to (support mode)', 'http://localhost:9876')
|
|
643
|
+
.option('--force', 'Skip overwrite/migration prompts (always overwrite)')
|
|
644
|
+
.option('--non-interactive', 'Fail on any prompt instead of asking')
|
|
598
645
|
.option('-v, --verbose', 'Show detailed output')
|
|
599
646
|
.action(async (opts) => {
|
|
600
647
|
const controller = new AbortController();
|
|
601
648
|
process.on('SIGINT', () => controller.abort());
|
|
602
649
|
try {
|
|
603
|
-
await runInstall({
|
|
604
|
-
...opts,
|
|
605
|
-
// --ollama-url implies local mode (no Docker Ollama)
|
|
606
|
-
ollamaMode: opts.ollamaUrl ? 'local' : (opts.ollamaMode ?? 'local'),
|
|
607
|
-
}, { signal: controller.signal });
|
|
650
|
+
await runInstall({ ...opts, ollamaMode: opts.ollamaMode }, { signal: controller.signal });
|
|
608
651
|
}
|
|
609
652
|
catch (err) {
|
|
610
653
|
console.error(chalk.red(err.message));
|