@nitsan-ai/ragsuite-test 0.1.3 → 0.1.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/README.md CHANGED
@@ -1,103 +1,69 @@
1
1
  # RAGSuite test CLI (`ragsuite-test`)
2
2
 
3
- > **TESTING PACKAGE** temporary name before the final `ragsuite` CLI.
4
-
5
- Two ways to run. Pick one:
6
-
7
- | | **A) Git source** | **B) Docker images only** |
8
- |--|-------------------|---------------------------|
9
- | Command | `ragsuite-test init` | `ragsuite-test init --from-images` |
10
- | On disk | Full monorepo clone | Tiny compose bundle only |
11
- | App code visible? | Yes | **No** (runs in containers) |
12
- | Needs | git + Node; Docker *or* native Postgres/Redis | Node + **Docker** |
13
- | Best for | Dev, debugging, native mode, reading code | Testers / “just run the app” |
14
- | Pros | Native mode; full scripts; offline rebuild from source | No source leak; faster to start; smaller disk |
15
- | Cons | Source on disk; larger checkout | Needs published GHCR images; Docker required; no native mode |
16
-
17
- ---
18
-
19
- ## A) Git source (clone)
3
+ npm-style install: **init once**, then **start / update / stop** no `--repo-root` every time.
20
4
 
21
5
  ```bash
22
6
  npm install -g @nitsan-ai/ragsuite-test
7
+
8
+ # First time only
23
9
  ragsuite-test init
24
- # clones https://github.com/nitsan-ai/RAGSUITE.git ~/ragsuite-test
25
- # auto: docker if daemon up, else native
10
+ # or without app source on disk:
11
+ # ragsuite-test init --from-images
26
12
 
27
- ragsuite-test start --repo-root ~/ragsuite-test --detach # docker
28
- # or after init --mode native:
29
- # ragsuite-test start --repo-root ~/ragsuite-test
13
+ ragsuite-test start
14
+ # API http://localhost:9090 · Web http://localhost:9091
30
15
 
31
- # API http://localhost:9090
32
- # Web http://localhost:9091 (docker) or :9191 (native)
33
- ragsuite-test stop --repo-root ~/ragsuite-test
16
+ ragsuite-test stop
34
17
  ```
35
18
 
36
- ### Native (no Docker) git install only
37
-
38
- Needs Postgres `:5436` (`ragsuite_v3`) and Redis `:6382`:
39
-
40
- ```bash
41
- ragsuite-test init --mode native
42
- ragsuite-test start --repo-root ~/ragsuite-test
43
- ```
19
+ Active install is remembered in `~/.ragsuite-test/config.json` (default app dir: `~/ragsuite-test`).
44
20
 
45
21
  ---
46
22
 
47
- ## B) Docker images (no source on disk)
48
-
49
- **Maintainer (once):** publish images — Actions → **Publish images (GHCR)**
50
- (packages: `ghcr.io/nitsan-ai/ragsuite-backend`, `…/ragsuite-frontend`)
51
-
52
- **User:**
23
+ ## Upgrade (keeps data & `.env`)
53
24
 
54
25
  ```bash
55
- npm install -g @nitsan-ai/ragsuite-test
56
- ragsuite-test init --from-images
57
- ragsuite-test start --repo-root ~/ragsuite-test --detach
58
- # pulls images — does NOT clone app source
59
- ragsuite-test stop --repo-root ~/ragsuite-test
26
+ npm install -g @nitsan-ai/ragsuite-test@latest
27
+ ragsuite-test update
28
+ ragsuite-test start
60
29
  ```
61
30
 
62
- Optional pin:
31
+ | Install type | What `update` does |
32
+ |--------------|--------------------|
33
+ | Git (`init`) | `git pull` in `~/ragsuite-test` |
34
+ | Images (`init --from-images`) | `docker compose pull` + recreate containers |
63
35
 
64
- ```bash
65
- # in ~/ragsuite-test/.env
66
- IMAGE_TAG=1.0.0
67
- IMAGE_REGISTRY=ghcr.io/nitsan-ai
68
- ```
36
+ **Never** run `docker compose down -v` or `init --force` unless you mean to wipe data.
69
37
 
70
- If images are private on GHCR, make packages public or `docker login ghcr.io`.
38
+ If you already installed and run `init` again it says **Already installed** and exits OK (does not wipe).
71
39
 
72
40
  ---
73
41
 
74
- ## Prerequisites
42
+ ## Which first-time install?
75
43
 
76
- | Need | Git install | Images install |
77
- |------|-------------|----------------|
78
- | Node 18+ | Yes | Yes |
79
- | git | Yes | No |
80
- | Docker | Optional | **Required** |
81
- | Postgres/Redis on host | Only for `--mode native` | No (in compose) |
44
+ | | **Git source** `init` | **Images** `init --from-images` |
45
+ |--|----------------------|----------------------------------|
46
+ | App code on disk | Yes | No |
47
+ | Docker | Optional (`--mode native`) | Required |
48
+ | Best for | Dev / native | Testers / “just run it” |
82
49
 
83
- SMTP/LLM: no init prompts. Real SMTP only in local `.env` (gitignored).
50
+ ---
84
51
 
85
- ## Ports
52
+ ## Commands
86
53
 
87
- | Service | Port |
88
- |---------|------|
89
- | API | 9090 |
90
- | Web (Docker/images) | 9091 |
91
- | Web (native Expo) | 9191 |
92
- | Postgres | 5436 |
93
- | Redis | 6382 |
54
+ | Command | Purpose |
55
+ |---------|---------|
56
+ | `init` | First-time install |
57
+ | `start` | Start app (uses saved install path) |
58
+ | `stop` | Stop app (volumes kept) |
59
+ | `update` | Upgrade code/images in place |
60
+ | `doctor` | Health / prereqs |
61
+ | `version` | CLI version |
94
62
 
95
- ## Publish CLI
63
+ ## Ports
96
64
 
97
- See [PUBLISH.md](./PUBLISH.md). Version bump then Actions **CLI publish**.
65
+ API **9090** · Web **9091** (Docker/images) · Web **9191** (native) · Postgres **5436** · Redis **6382**
98
66
 
99
- ## Tests
67
+ ## Publish (maintainers)
100
68
 
101
- ```bash
102
- cd cli && npm test && npm run prepublish:check
103
- ```
69
+ See [PUBLISH.md](./PUBLISH.md). Also publish GHCR images before `--from-images` works for others.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nitsan-ai/ragsuite-test",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
- "description": "RAGSuite testing CLI — init (git source) or init --from-images (pull-only, no source).",
5
+ "description": "RAGSuite testing CLI — init once, then start/update/stop (npm-style).",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
8
  "ragsuite-test": "./bin/ragsuite-test.js"
@@ -1,12 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const { writeConfig, CONFIG_DIR } = require('../utils/config');
4
+ const { writeConfig, CONFIG_DIR, readConfig } = require('../utils/config');
5
5
  const {
6
6
  ensureRagsuiteDir,
7
7
  resolveRepoRoot,
8
8
  isImagesInstall,
9
+ looksLikeRepoRoot,
10
+ isDirEmptyOrMissing,
9
11
  } = require('../utils/paths');
12
+ const { setActiveInstall } = require('../utils/global-config');
10
13
  const { assertSupportedInstall, installHint } = require('../utils/distribution');
11
14
  const {
12
15
  DEFAULT_GIT_URL,
@@ -25,10 +28,10 @@ const {
25
28
  writeInstallEnv,
26
29
  } = require('../utils/env-file');
27
30
  const { assertPortsFree } = require('../utils/port');
28
- const { info, error } = require('../utils/log');
31
+ const { info, warn, error } = require('../utils/log');
29
32
 
30
33
  const name = 'init';
31
- const summary = 'Install: git clone (source) or pull-only Docker images';
34
+ const summary = 'First-time install (git or images). Safe if already installed.';
32
35
 
33
36
  function printSuccess(repoRoot, mode, source) {
34
37
  info('');
@@ -36,50 +39,65 @@ function printSuccess(repoRoot, mode, source) {
36
39
  info(` Install dir : ${repoRoot}`);
37
40
  info(` Source : ${source}`);
38
41
  info(` Mode : ${mode}`);
39
- info(` Config : ${path.join(repoRoot, CONFIG_DIR, 'config.json')}`);
40
42
  info('');
41
43
  info(' API http://localhost:9090');
42
- if (source === 'images') {
43
- info(' Web http://localhost:9091 (images / Docker nginx)');
44
- } else if (mode === 'native') {
45
- info(' Web http://localhost:9191 (Expo web — native mode)');
44
+ if (source === 'images' || mode === 'docker') {
45
+ info(' Web http://localhost:9091');
46
46
  } else {
47
- info(' Web http://localhost:9091 (Docker nginx)');
47
+ info(' Web http://localhost:9191');
48
48
  }
49
49
  info('');
50
- info('Next steps:');
51
- const detach = mode === 'docker' || source === 'images' ? ' --detach' : '';
52
- info(` 1. ragsuite-test start --repo-root ${repoRoot}${detach}`);
53
- info(' 2. Open the Web URL above and bootstrap the first admin');
54
- info(` 3. ragsuite-test doctor --repo-root ${repoRoot}`);
50
+ info('Next (no --repo-root needed):');
51
+ info(' ragsuite-test start');
52
+ info(' ragsuite-test stop');
53
+ info(' ragsuite-test update # later upgrades, keeps data');
55
54
  info('');
56
55
  info(installHint());
57
56
  }
58
57
 
58
+ function printAlreadyInstalled(repoRoot) {
59
+ const cfg = readConfig(repoRoot);
60
+ setActiveInstall(repoRoot);
61
+ info('');
62
+ info('=== Already installed ===');
63
+ info(` ${repoRoot}`);
64
+ if (cfg) info(` source=${cfg.source} mode=${cfg.mode}`);
65
+ info('');
66
+ info('Use:');
67
+ info(' ragsuite-test start # run the app');
68
+ info(' ragsuite-test update # upgrade code/images (keeps .env + data)');
69
+ info(' ragsuite-test stop');
70
+ info('');
71
+ info('Do NOT run init --force unless you intend to wipe and reinstall.');
72
+ }
73
+
74
+ function isInitializedInstall(dir) {
75
+ return looksLikeRepoRoot(dir) && Boolean(readConfig(dir));
76
+ }
77
+
59
78
  function help() {
60
79
  return `Usage: ragsuite-test init [options]
61
80
 
62
- Two install styles:
81
+ First-time setup only (npm-style):
63
82
 
64
- A) Source on disk (git) — default
65
- ragsuite-test init
66
- Clones ${DEFAULT_GIT_URL} → ~/ragsuite-test
67
- Supports --mode docker|native
83
+ ragsuite-test init # git clone ~/ragsuite-test
84
+ ragsuite-test init --from-images # Docker images only (no app source)
68
85
 
69
- B) No source (Docker images only)
70
- ragsuite-test init --from-images
71
- Copies a tiny compose bundle → ~/ragsuite-test
72
- Pulls ghcr.io/nitsan-ai/ragsuite-backend|frontend (no git clone of app code)
86
+ If already installed, init exits OK and tells you to start/update.
87
+
88
+ Day-to-day:
89
+ ragsuite-test start
90
+ ragsuite-test update
91
+ ragsuite-test stop
73
92
 
74
93
  Options:
75
- --from-images Pull-only images install (no monorepo source)
76
- --from-git [url] Clone this URL (default public repo)
77
- --repo-root <path> Configure an existing git checkout
78
- --install-dir <path> Target dir (default: ~/ragsuite-test)
79
- --mode <docker|native> Git install only (images install is always docker)
80
- --force Overwrite non-empty install dir / existing .env
94
+ --from-images Images-only install
95
+ --from-git [url] Clone URL (default public repo)
96
+ --repo-root <path> Point at an existing checkout
97
+ --install-dir <path> Default: ~/ragsuite-test
98
+ --mode <docker|native> Git install only
99
+ --force Wipe install dir and reinstall (destructive)
81
100
  --yes, -y Non-interactive
82
- --llm-api-key / --smtp-* Optional overrides
83
101
  `;
84
102
  }
85
103
 
@@ -113,7 +131,7 @@ async function run(ctx) {
113
131
  const g = ctx.globals;
114
132
 
115
133
  if (g.fromRelease) {
116
- error('--from-release is not supported. Use init, --from-images, --from-git, or --repo-root.');
134
+ error('--from-release is not supported.');
117
135
  return 1;
118
136
  }
119
137
 
@@ -128,25 +146,55 @@ async function run(ctx) {
128
146
  let fromGit = g.fromGit;
129
147
  let fromImages = Boolean(g.fromImages);
130
148
  let repoRootFlag = g.repoRoot;
131
- const installDir = g.installDir || defaultInstallDir();
149
+ const installDir = path.resolve(g.installDir || defaultInstallDir());
150
+
151
+ // Existing configured install → friendly no-op (like other npm CLIs)
152
+ if (!g.force) {
153
+ if (repoRootFlag && isInitializedInstall(path.resolve(repoRootFlag))) {
154
+ printAlreadyInstalled(path.resolve(repoRootFlag));
155
+ return 0;
156
+ }
157
+ if (!repoRootFlag && isInitializedInstall(installDir)) {
158
+ printAlreadyInstalled(installDir);
159
+ return 0;
160
+ }
161
+ }
132
162
 
133
163
  if (!fromImages && fromGit === null && !repoRootFlag) {
134
164
  fromGit = '';
135
- info(`Install style: git clone (source on disk)`);
136
- info(` ${DEFAULT_GIT_URL} → ${path.resolve(installDir)}`);
165
+ info(`First-time install (git): ${DEFAULT_GIT_URL}`);
166
+ info(` → ${installDir}`);
137
167
  }
138
-
139
168
  if (fromImages) {
140
- info('Install style: Docker images only (no app source on disk)');
141
- info(` Target: ${path.resolve(installDir)}`);
169
+ info(`First-time install (images, no source): ${installDir}`);
142
170
  }
143
171
 
144
- let mode;
145
- if (fromImages) {
146
- mode = 'docker';
147
- } else {
148
- mode = g.mode || resolvePreferredMode(null);
172
+ // Checkout already on disk but never inited → wire config/.env, do not re-clone
173
+ if (
174
+ !g.force &&
175
+ !repoRootFlag &&
176
+ !fromImages &&
177
+ looksLikeRepoRoot(installDir) &&
178
+ !readConfig(installDir)
179
+ ) {
180
+ info(`Found existing source at ${installDir} — finishing setup (no re-clone).`);
181
+ repoRootFlag = installDir;
182
+ fromGit = null;
183
+ }
184
+
185
+ if (!g.force && !isDirEmptyOrMissing(installDir) && !looksLikeRepoRoot(installDir) && !repoRootFlag) {
186
+ error(
187
+ [
188
+ `Directory exists but is not a RAGSuite install: ${installDir}`,
189
+ ' Fix leftover files, or:',
190
+ ' ragsuite-test init --force # wipe & reinstall (destructive)',
191
+ ' ragsuite-test init --install-dir ~/ragsuite-test-2',
192
+ ].join('\n'),
193
+ );
194
+ return 1;
149
195
  }
196
+
197
+ let mode = fromImages ? 'docker' : g.mode || resolvePreferredMode(null);
150
198
  info(`Run mode: ${mode}`);
151
199
 
152
200
  const needGit = !fromImages && fromGit !== null && fromGit !== undefined;
@@ -157,7 +205,6 @@ async function run(ctx) {
157
205
 
158
206
  const jwtSecret = generateJwtSecret();
159
207
  info(`JWT_SECRET_KEY will be auto-generated (…${jwtSecret.slice(-8)})`);
160
- info('SMTP + LLM: from .env.example into .env (no prompts).');
161
208
 
162
209
  const llmKey = resolveLlmOverride(g, ctx.env);
163
210
  const smtp = optionalSmtpOverrides(g, ctx.env);
@@ -176,23 +223,14 @@ async function run(ctx) {
176
223
 
177
224
  try {
178
225
  if (fromImages) {
179
- const result = materializeImagesInstall({
180
- installDir,
181
- force: g.force,
182
- });
226
+ const result = materializeImagesInstall({ installDir, force: g.force });
183
227
  repoRoot = result.repoRoot;
184
228
  source = 'images';
185
- info(`Images bundle installed at ${repoRoot}`);
186
229
  } else if (fromGit !== null && fromGit !== undefined) {
187
230
  const url = fromGit === '' ? DEFAULT_GIT_URL : fromGit;
188
- info(`Cloning ${url} ${path.resolve(installDir)}`);
189
- const result = cloneGitToInstallDir(url, {
190
- installDir,
191
- force: g.force,
192
- });
231
+ const result = cloneGitToInstallDir(url, { installDir, force: g.force });
193
232
  repoRoot = result.repoRoot;
194
233
  source = 'git';
195
- info(`Installed app at ${repoRoot}`);
196
234
  } else {
197
235
  repoRoot = resolveRepoRoot({
198
236
  cwd: ctx.cwd,
@@ -213,6 +251,7 @@ async function run(ctx) {
213
251
  repoRoot,
214
252
  installDir: repoRoot,
215
253
  });
254
+ setActiveInstall(repoRoot);
216
255
 
217
256
  try {
218
257
  writeInstallEnv(repoRoot, {
@@ -222,16 +261,18 @@ async function run(ctx) {
222
261
  smtp,
223
262
  });
224
263
  } catch (err) {
225
- if (err.code === 'ENV_EXISTS' || err.code === 'ENV_INCOMPLETE') {
264
+ if (err.code === 'ENV_EXISTS') {
265
+ warn('.env already present — keeping it (use --force only if you want to regenerate).');
266
+ } else if (err.code === 'ENV_INCOMPLETE') {
226
267
  error(err.message);
227
268
  return 1;
269
+ } else {
270
+ error(err.message || String(err));
271
+ return 1;
228
272
  }
229
- error(err.message || String(err));
230
- return 1;
231
273
  }
232
274
 
233
- info(`Wrote ${path.join(repoRoot, CONFIG_DIR, 'config.json')}`);
234
- info(` mode=${cfg.mode} source=${cfg.source}`);
275
+ info(`Config: mode=${cfg.mode} source=${cfg.source}`);
235
276
  printSuccess(repoRoot, mode, source);
236
277
  return 0;
237
278
  }
@@ -2,86 +2,119 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { resolveRepoRoot } = require('../utils/paths');
5
+ const { resolveRepoRoot, isImagesInstall, looksLikeGitCheckout } = require('../utils/paths');
6
6
  const { readConfig } = require('../utils/config');
7
- const { captureCommand } = require('../utils/spawn');
8
- const { info, warn } = require('../utils/log');
7
+ const { setActiveInstall } = require('../utils/global-config');
8
+ const { captureCommand, runScript } = require('../utils/spawn');
9
+ const { info, warn, error } = require('../utils/log');
9
10
 
10
11
  const name = 'update';
11
- const summary = 'Verify install path and print safe update steps';
12
+ const summary = 'Update app in place (git pull or image pull) — keeps data & .env';
12
13
 
13
14
  function help() {
14
- return `Usage: ragsuite-test update [options]
15
+ return `Usage: ragsuite-test update
15
16
 
16
- Verifies the install/repo path and prints safe update steps.
17
- Git installs: git fetch + pull guidance (no destructive reset).
17
+ Upgrades the active install without wiping data:
18
+ Git install → git pull (keeps .env + Docker volumes)
19
+ • Images install → docker compose pull (keeps .env + volumes)
18
20
 
19
21
  Never runs docker compose down -v.
20
22
 
23
+ Typical flow after a new release:
24
+ npm install -g @nitsan-ai/ragsuite-test@latest
25
+ ragsuite-test update
26
+ ragsuite-test start
27
+
21
28
  Options:
22
- --repo-root <path> Monorepo / install root
29
+ --repo-root <path> Override active install
30
+ --dry-run Print actions only
23
31
  `;
24
32
  }
25
33
 
26
34
  function run(ctx) {
27
- const repoRoot = resolveRepoRoot({
28
- cwd: ctx.cwd,
29
- repoRootFlag: ctx.globals.repoRoot,
30
- env: ctx.env,
31
- });
35
+ let repoRoot;
36
+ try {
37
+ repoRoot = resolveRepoRoot({
38
+ cwd: ctx.cwd,
39
+ repoRootFlag: ctx.globals.repoRoot,
40
+ env: ctx.env,
41
+ });
42
+ } catch (err) {
43
+ error(err.message);
44
+ return 1;
45
+ }
32
46
 
47
+ setActiveInstall(repoRoot);
33
48
  const cfg = readConfig(repoRoot);
34
- const source = (cfg && cfg.source) || 'checkout';
49
+ const source = (cfg && cfg.source) || (isImagesInstall(repoRoot) ? 'images' : 'git');
50
+ const images = source === 'images' || isImagesInstall(repoRoot);
35
51
 
36
- info(`Install path OK: ${repoRoot}`);
52
+ info(`Updating install: ${repoRoot}`);
37
53
  info(` source=${source}`);
54
+ info(' (.env and Docker volumes are kept — never wiped)');
38
55
  info('');
39
56
 
40
- info('Safe update steps:');
41
- info(' 1. Stop the stack (ragsuite-test stop --repo-root )');
42
- info(' 2. Update sources (git pull)');
43
- info(' 3. Docker: npm run rebuild or docker compose build');
44
- info(' 4. Native: re-run backend/scripts/setup.sh if Python deps changed');
45
- info(' 5. Start again (ragsuite-test start --repo-root …)');
46
- info(' 6. Run doctor after upgrades');
47
- info('');
48
- info('Never: docker compose down -v (destroys Postgres/Chroma data).');
49
- info('');
50
-
51
- const gitDir = path.join(repoRoot, '.git');
52
- if (!fs.existsSync(gitDir)) {
53
- warn('No .git directory — skip fetch/status. Re-init with --from-git if needed.');
57
+ if (ctx.globals.dryRun) {
58
+ info(`[dry-run] would update ${images ? 'images (compose pull)' : 'git (pull)'}`);
54
59
  return 0;
55
60
  }
56
61
 
57
- const remote = captureCommand('git', ['remote', '-v'], { cwd: repoRoot, env: ctx.env });
58
- if (remote.status !== 0 || !String(remote.stdout || '').trim()) {
59
- warn('No git remote configured — skip fetch.');
60
- return 0;
62
+ if (images) {
63
+ info('Pulling newer Docker images…');
64
+ const pull = captureCommand(
65
+ 'docker',
66
+ ['compose', '-f', 'docker-compose.yml', 'pull'],
67
+ { cwd: repoRoot, env: ctx.env },
68
+ );
69
+ if (pull.status !== 0) {
70
+ error(`Image pull failed (exit ${pull.status}).`);
71
+ if (pull.stderr) error(String(pull.stderr).trimEnd());
72
+ error('Publish GHCR images / check IMAGE_TAG in .env, then retry.');
73
+ return 1;
74
+ }
75
+ info('Images pulled.');
76
+ info('');
77
+ info('Apply update:');
78
+ info(' ragsuite-test stop');
79
+ info(' ragsuite-test start');
80
+ info('(or: ragsuite-test start — compose recreate with new images)');
81
+ // Recreate containers with new images without wiping volumes
82
+ info('');
83
+ info('Recreating containers with new images (volumes preserved)…');
84
+ return runScript(repoRoot, path.join('scripts', 'docker-start-images.sh'), ['--detach'], {
85
+ dryRun: false,
86
+ env: { ...ctx.env, RAGSUITE_MODE: 'docker' },
87
+ });
61
88
  }
62
89
 
63
- info('Git remotes:');
64
- info(String(remote.stdout).trimEnd());
65
- info('');
66
- info('Running git fetch (non-destructive)…');
67
- const fetch = captureCommand('git', ['fetch', '--all', '--prune'], {
90
+ // Git / checkout install
91
+ if (!looksLikeGitCheckout(repoRoot) && !fs.existsSync(path.join(repoRoot, '.git'))) {
92
+ error('This install has no .git directory. Re-run: ragsuite-test init --force');
93
+ return 1;
94
+ }
95
+
96
+ info('Fetching and pulling latest from git (non-destructive)…');
97
+ const pull = captureCommand('git', ['pull', '--ff-only'], {
68
98
  cwd: repoRoot,
69
99
  env: ctx.env,
70
100
  });
71
- if (fetch.status !== 0) {
72
- warn(
73
- `git fetch failed (exit ${fetch.status}). Private remotes need credentials. Continuing with status only.`,
74
- );
75
- if (fetch.stderr) warn(String(fetch.stderr).trimEnd());
76
- } else {
77
- info('git fetch OK');
78
- }
79
-
80
- const status = captureCommand('git', ['status', '-sb'], { cwd: repoRoot, env: ctx.env });
81
- if (status.stdout) {
82
- info('');
83
- info(String(status.stdout).trimEnd());
101
+ if (pull.status !== 0) {
102
+ warn(`git pull --ff-only failed (exit ${pull.status}). Trying fetch + status…`);
103
+ if (pull.stderr) warn(String(pull.stderr).trimEnd());
104
+ captureCommand('git', ['fetch', '--all', '--prune'], { cwd: repoRoot, env: ctx.env });
105
+ const st = captureCommand('git', ['status', '-sb'], { cwd: repoRoot, env: ctx.env });
106
+ if (st.stdout) info(String(st.stdout).trimEnd());
107
+ error('Could not fast-forward. Fix local commits or stash, then: cd install && git pull');
108
+ return 1;
84
109
  }
110
+ if (pull.stdout) info(String(pull.stdout).trimEnd());
111
+ info('git pull OK.');
112
+ info('');
113
+ info('Restart to apply:');
114
+ info(' ragsuite-test stop');
115
+ info(' ragsuite-test start');
116
+ info('');
117
+ info('Never: docker compose down -v (would delete Postgres/Chroma data)');
85
118
  return 0;
86
119
  }
87
120
 
package/src/index.js CHANGED
@@ -42,8 +42,9 @@ function globalHelp() {
42
42
  ' --force init: overwrite non-empty install dir / .env',
43
43
  ' --yes, -y Non-interactive (same defaults as plain init)',
44
44
  ' --mode <docker|native> Override config (Docker optional)',
45
- ' --dry-run Print actions without running (start/stop/doctor/logs)',
45
+ ' --dry-run Print actions without running (start/stop/doctor/logs/update)',
46
46
  '',
47
+ 'Active install: ~/.ragsuite-test/config.json (set by init; used by start/update/stop)',
47
48
  'Env: RAGSUITE_REPO_ROOT or RAGSUITE_INSTALL_DIR',
48
49
  ' RAGSUITE_TEST_LLM_API_KEY, RAGSUITE_TEST_SMTP_* , RAGSUITE_TEST_EMAIL_FROM',
49
50
  '',
@@ -25,10 +25,10 @@ function resolveComposeBundle() {
25
25
 
26
26
  function installHint() {
27
27
  return [
28
- 'Choose an install style:',
29
- ' Source on disk: ragsuite-test init',
30
- ' No source: ragsuite-test init --from-images',
31
- 'Then: ragsuite-test start --repo-root ~/ragsuite-test --detach',
28
+ 'Day-to-day:',
29
+ ' ragsuite-test start',
30
+ ' ragsuite-test update',
31
+ ' ragsuite-test stop',
32
32
  ].join('\n');
33
33
  }
34
34
 
@@ -1,18 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const os = require('os');
5
4
  const path = require('path');
6
5
  const { spawnSync } = require('child_process');
7
- const { looksLikeFullBundleRoot, looksLikeRepoRoot } = require('./paths');
6
+ const {
7
+ defaultInstallDir,
8
+ looksLikeFullBundleRoot,
9
+ looksLikeRepoRoot,
10
+ } = require('./paths');
8
11
 
9
12
  /** Default public monorepo (when the GitHub repo is public). */
10
13
  const DEFAULT_GIT_URL = 'https://github.com/nitsan-ai/RAGSUITE.git';
11
14
 
12
- function defaultInstallDir() {
13
- return path.join(os.homedir(), 'ragsuite-test');
14
- }
15
-
16
15
  function requireGit() {
17
16
  const which = spawnSync('which', ['git'], { encoding: 'utf8' });
18
17
  if (which.status !== 0) {
@@ -49,7 +48,13 @@ function cloneGitToInstallDir(gitUrl, options = {}) {
49
48
  if (!isDirEmptyOrMissing(target)) {
50
49
  if (!force) {
51
50
  const err = new Error(
52
- `Install directory is not empty: ${target}. Pass --force to remove and re-clone, or choose another --install-dir.`,
51
+ [
52
+ `Install directory is not empty: ${target}`,
53
+ ' If already installed: ragsuite-test start',
54
+ ' To upgrade in place: ragsuite-test update',
55
+ ' To wipe & reinstall: ragsuite-test init --force',
56
+ ' Or use another path: ragsuite-test init --install-dir <path>',
57
+ ].join('\n'),
53
58
  );
54
59
  err.code = 'INSTALL_DIR_NONEMPTY';
55
60
  throw err;
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ /** User-level CLI state (not inside the app install). Like other npm CLIs. */
8
+ function globalConfigDir() {
9
+ return path.join(os.homedir(), '.ragsuite-test');
10
+ }
11
+
12
+ function globalConfigPath() {
13
+ return path.join(globalConfigDir(), 'config.json');
14
+ }
15
+
16
+ function readGlobalConfig() {
17
+ const file = globalConfigPath();
18
+ if (!fs.existsSync(file)) return null;
19
+ try {
20
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ if (!raw || typeof raw !== 'object') return null;
22
+ return {
23
+ installDir: raw.installDir ? path.resolve(String(raw.installDir)) : null,
24
+ updatedAt: raw.updatedAt || null,
25
+ };
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function writeGlobalConfig(partial = {}) {
32
+ const dir = globalConfigDir();
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ const existing = readGlobalConfig() || {};
35
+ const next = {
36
+ ...existing,
37
+ ...partial,
38
+ updatedAt: new Date().toISOString(),
39
+ };
40
+ if (next.installDir) next.installDir = path.resolve(String(next.installDir));
41
+ fs.writeFileSync(globalConfigPath(), `${JSON.stringify(next, null, 2)}\n`, 'utf8');
42
+ return next;
43
+ }
44
+
45
+ function setActiveInstall(installDir) {
46
+ return writeGlobalConfig({ installDir: path.resolve(installDir) });
47
+ }
48
+
49
+ function getActiveInstall() {
50
+ const g = readGlobalConfig();
51
+ return g && g.installDir ? g.installDir : null;
52
+ }
53
+
54
+ module.exports = {
55
+ globalConfigDir,
56
+ globalConfigPath,
57
+ readGlobalConfig,
58
+ writeGlobalConfig,
59
+ setActiveInstall,
60
+ getActiveInstall,
61
+ };
@@ -38,7 +38,13 @@ function materializeImagesInstall(options = {}) {
38
38
  if (!isDirEmptyOrMissing(target)) {
39
39
  if (!force) {
40
40
  const err = new Error(
41
- `Install directory is not empty: ${target}. Pass --force to replace, or choose another --install-dir.`,
41
+ [
42
+ `Install directory is not empty: ${target}`,
43
+ ' If already installed: ragsuite-test start',
44
+ ' To upgrade in place: ragsuite-test update',
45
+ ' To wipe & reinstall: ragsuite-test init --from-images --force',
46
+ ' Or use another path: ragsuite-test init --from-images --install-dir <path>',
47
+ ].join('\n'),
42
48
  );
43
49
  err.code = 'INSTALL_DIR_NONEMPTY';
44
50
  throw err;
@@ -1,8 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { readConfig, CONFIG_DIR } = require('./config');
7
+ const { getActiveInstall, setActiveInstall } = require('./global-config');
8
+
9
+ /** Default app install dir (npm-CLI style home install). */
10
+ function defaultInstallDir() {
11
+ return path.join(os.homedir(), 'ragsuite-test');
12
+ }
6
13
 
7
14
  function looksLikeGitCheckout(dir) {
8
15
  const start = path.join(dir, 'scripts', 'docker-start.sh');
@@ -23,7 +30,6 @@ function looksLikeImagesInstall(dir) {
23
30
  /* fall through */
24
31
  }
25
32
  }
26
- // Before config is written, or force-detect by scripts present without docker-start.sh
27
33
  return !fs.existsSync(path.join(dir, 'scripts', 'docker-start.sh'));
28
34
  }
29
35
 
@@ -33,36 +39,32 @@ function looksLikeRepoRoot(dir) {
33
39
 
34
40
  function looksLikeFullBundleRoot(dir) {
35
41
  if (!looksLikeGitCheckout(dir)) return false;
36
- const compose = path.join(dir, 'docker-compose.yml');
37
- return fs.existsSync(compose);
42
+ return fs.existsSync(path.join(dir, 'docker-compose.yml'));
38
43
  }
39
44
 
40
45
  function walkUpForRepoRoot(startDir) {
41
46
  let current = path.resolve(startDir);
42
47
  for (;;) {
43
- if (looksLikeRepoRoot(current)) {
44
- return current;
45
- }
48
+ if (looksLikeRepoRoot(current)) return current;
46
49
  const parent = path.dirname(current);
47
- if (parent === current) {
48
- return null;
49
- }
50
+ if (parent === current) return null;
50
51
  current = parent;
51
52
  }
52
53
  }
53
54
 
54
55
  function envRepoRoot(env = process.env) {
55
- const candidates = [env.RAGSUITE_REPO_ROOT, env.RAGSUITE_INSTALL_DIR];
56
- for (const raw of candidates) {
56
+ for (const raw of [env.RAGSUITE_REPO_ROOT, env.RAGSUITE_INSTALL_DIR]) {
57
57
  if (!raw || !String(raw).trim()) continue;
58
58
  const root = path.resolve(String(raw).trim());
59
- if (looksLikeRepoRoot(root)) {
60
- return root;
61
- }
59
+ if (looksLikeRepoRoot(root)) return root;
62
60
  }
63
61
  return null;
64
62
  }
65
63
 
64
+ /**
65
+ * Resolve install root (npm-CLI style):
66
+ * --repo-root → env → cwd walk-up → ~/.ragsuite-test active install → ~/ragsuite-test
67
+ */
66
68
  function resolveRepoRoot({
67
69
  cwd = process.cwd(),
68
70
  repoRootFlag,
@@ -73,16 +75,18 @@ function resolveRepoRoot({
73
75
  const root = path.resolve(repoRootFlag);
74
76
  if (!looksLikeRepoRoot(root)) {
75
77
  const err = new Error(
76
- `Not a RAGSuite install root: ${root} (need git checkout or images install)`,
78
+ `Not a RAGSuite install: ${root}. Run: ragsuite-test init`,
77
79
  );
78
80
  err.code = 'NOT_REPO_ROOT';
79
81
  throw err;
80
82
  }
83
+ setActiveInstall(root);
81
84
  return root;
82
85
  }
83
86
 
84
87
  const fromEnv = envRepoRoot(env);
85
88
  if (fromEnv) {
89
+ setActiveInstall(fromEnv);
86
90
  return fromEnv;
87
91
  }
88
92
 
@@ -94,21 +98,41 @@ function resolveRepoRoot({
94
98
  for (const key of ['repoRoot', 'installDir']) {
95
99
  const candidate = cfg[key];
96
100
  if (candidate && looksLikeRepoRoot(candidate)) {
97
- return path.resolve(candidate);
101
+ const resolved = path.resolve(candidate);
102
+ setActiveInstall(resolved);
103
+ return resolved;
98
104
  }
99
105
  }
100
106
  }
107
+ setActiveInstall(walked);
101
108
  return walked;
102
109
  }
103
110
  }
104
111
 
105
112
  const walked = walkUpForRepoRoot(cwd);
106
113
  if (walked) {
114
+ setActiveInstall(walked);
107
115
  return walked;
108
116
  }
109
117
 
118
+ const active = getActiveInstall();
119
+ if (active && looksLikeRepoRoot(active)) {
120
+ return active;
121
+ }
122
+
123
+ const fallback = defaultInstallDir();
124
+ if (looksLikeRepoRoot(fallback)) {
125
+ setActiveInstall(fallback);
126
+ return fallback;
127
+ }
128
+
110
129
  const err = new Error(
111
- 'Could not find RAGSuite install. Run: ragsuite-test init OR ragsuite-test init --from-images',
130
+ [
131
+ 'No RAGSuite install found.',
132
+ ' First time: ragsuite-test init',
133
+ ' Or images: ragsuite-test init --from-images',
134
+ ' Then: ragsuite-test start',
135
+ ].join('\n'),
112
136
  );
113
137
  err.code = 'NOT_REPO_ROOT';
114
138
  throw err;
@@ -134,7 +158,13 @@ function isImagesInstall(repoRoot) {
134
158
  return looksLikeImagesInstall(repoRoot) && !looksLikeGitCheckout(repoRoot);
135
159
  }
136
160
 
161
+ function isDirEmptyOrMissing(dir) {
162
+ if (!fs.existsSync(dir)) return true;
163
+ return fs.readdirSync(dir).filter((n) => n !== '.DS_Store').length === 0;
164
+ }
165
+
137
166
  module.exports = {
167
+ defaultInstallDir,
138
168
  looksLikeRepoRoot,
139
169
  looksLikeGitCheckout,
140
170
  looksLikeImagesInstall,
@@ -146,4 +176,5 @@ module.exports = {
146
176
  ensureRagsuiteDir,
147
177
  nativeLogDir,
148
178
  isImagesInstall,
179
+ isDirEmptyOrMissing,
149
180
  };
package/src/utils/port.js CHANGED
@@ -66,7 +66,7 @@ async function assertPortsFree(ports = DEFAULT_INSTALL_PORTS) {
66
66
  'Free them, then retry. This CLI will not kill foreign processes.',
67
67
  '',
68
68
  'If a previous RAGSuite install is running:',
69
- ' ragsuite-test stop --repo-root ~/ragsuite-test',
69
+ ' ragsuite-test stop',
70
70
  ' # or from a monorepo checkout: cd <checkout> && npm run down',
71
71
  '',
72
72
  'See what owns the ports (macOS/Linux):',