@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 +39 -73
- package/package.json +2 -2
- package/src/commands/init.js +100 -59
- package/src/commands/update.js +84 -51
- package/src/index.js +2 -1
- package/src/utils/distribution.js +4 -4
- package/src/utils/git-install.js +12 -7
- package/src/utils/global-config.js +61 -0
- package/src/utils/images-install.js +7 -1
- package/src/utils/paths.js +48 -17
- package/src/utils/port.js +1 -1
package/README.md
CHANGED
|
@@ -1,103 +1,69 @@
|
|
|
1
1
|
# RAGSuite test CLI (`ragsuite-test`)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
#
|
|
25
|
-
#
|
|
10
|
+
# or without app source on disk:
|
|
11
|
+
# ragsuite-test init --from-images
|
|
26
12
|
|
|
27
|
-
ragsuite-test start
|
|
28
|
-
#
|
|
29
|
-
# ragsuite-test start --repo-root ~/ragsuite-test
|
|
13
|
+
ragsuite-test start
|
|
14
|
+
# API http://localhost:9090 · Web http://localhost:9091
|
|
30
15
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
57
|
-
ragsuite-test start
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
42
|
+
## Which first-time install?
|
|
75
43
|
|
|
76
|
-
|
|
|
77
|
-
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
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
|
-
|
|
50
|
+
---
|
|
84
51
|
|
|
85
|
-
##
|
|
52
|
+
## Commands
|
|
86
53
|
|
|
87
|
-
|
|
|
88
|
-
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
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
|
-
##
|
|
63
|
+
## Ports
|
|
96
64
|
|
|
97
|
-
|
|
65
|
+
API **9090** · Web **9091** (Docker/images) · Web **9191** (native) · Postgres **5436** · Redis **6382**
|
|
98
66
|
|
|
99
|
-
##
|
|
67
|
+
## Publish (maintainers)
|
|
100
68
|
|
|
101
|
-
|
|
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
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "RAGSuite testing CLI — init
|
|
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"
|
package/src/commands/init.js
CHANGED
|
@@ -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 = '
|
|
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
|
|
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:
|
|
47
|
+
info(' Web http://localhost:9191');
|
|
48
48
|
}
|
|
49
49
|
info('');
|
|
50
|
-
info('Next
|
|
51
|
-
|
|
52
|
-
info(
|
|
53
|
-
info('
|
|
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
|
-
|
|
81
|
+
First-time setup only (npm-style):
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
76
|
-
--from-git [url] Clone
|
|
77
|
-
--repo-root <path>
|
|
78
|
-
--install-dir <path>
|
|
79
|
-
--mode <docker|native> Git install only
|
|
80
|
-
--force
|
|
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.
|
|
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(`
|
|
136
|
-
info(`
|
|
165
|
+
info(`First-time install (git): ${DEFAULT_GIT_URL}`);
|
|
166
|
+
info(` → ${installDir}`);
|
|
137
167
|
}
|
|
138
|
-
|
|
139
168
|
if (fromImages) {
|
|
140
|
-
info(
|
|
141
|
-
info(` Target: ${path.resolve(installDir)}`);
|
|
169
|
+
info(`First-time install (images, no source): → ${installDir}`);
|
|
142
170
|
}
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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'
|
|
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(`
|
|
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
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -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 {
|
|
8
|
-
const {
|
|
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 = '
|
|
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
|
|
15
|
+
return `Usage: ragsuite-test update
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
Git
|
|
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>
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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) || '
|
|
49
|
+
const source = (cfg && cfg.source) || (isImagesInstall(repoRoot) ? 'images' : 'git');
|
|
50
|
+
const images = source === 'images' || isImagesInstall(repoRoot);
|
|
35
51
|
|
|
36
|
-
info(`
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 (
|
|
72
|
-
warn(
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
28
|
+
'Day-to-day:',
|
|
29
|
+
' ragsuite-test start',
|
|
30
|
+
' ragsuite-test update',
|
|
31
|
+
' ragsuite-test stop',
|
|
32
32
|
].join('\n');
|
|
33
33
|
}
|
|
34
34
|
|
package/src/utils/git-install.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/utils/paths.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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):',
|