@nitsan-ai/ragsuite-test 0.1.2 → 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.
@@ -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
@@ -31,8 +31,9 @@ function globalHelp() {
31
31
  'Global options:',
32
32
  ' --help, -h Show help',
33
33
  ' --repo-root <path> Monorepo / install root',
34
- ' --from-git [url] init: clone this URL (default: public nitsan-ai/RAGSUITE)',
35
- ' --install-dir <path> init: clone target (default ~/ragsuite-test)',
34
+ ' --from-git [url] init: clone source (default public repo)',
35
+ ' --from-images init: Docker images only (no app source on disk)',
36
+ ' --install-dir <path> init: install target (default ~/ragsuite-test)',
36
37
  ' --llm-api-key <key> init: optional override for CUSTOM_LLM_INTERNAL_API_KEY',
37
38
  ' --smtp-host <host> init: optional SMTP override',
38
39
  ' --smtp-user <user> init: optional SMTP override',
@@ -41,8 +42,9 @@ function globalHelp() {
41
42
  ' --force init: overwrite non-empty install dir / .env',
42
43
  ' --yes, -y Non-interactive (same defaults as plain init)',
43
44
  ' --mode <docker|native> Override config (Docker optional)',
44
- ' --dry-run Print actions without running (start/stop/doctor/logs)',
45
+ ' --dry-run Print actions without running (start/stop/doctor/logs/update)',
45
46
  '',
47
+ 'Active install: ~/.ragsuite-test/config.json (set by init; used by start/update/stop)',
46
48
  'Env: RAGSUITE_REPO_ROOT or RAGSUITE_INSTALL_DIR',
47
49
  ' RAGSUITE_TEST_LLM_API_KEY, RAGSUITE_TEST_SMTP_* , RAGSUITE_TEST_EMAIL_FROM',
48
50
  '',
package/src/utils/args.js CHANGED
@@ -11,6 +11,7 @@ function parseArgv(argv) {
11
11
  dryRun: false,
12
12
  mode: null,
13
13
  fromGit: null,
14
+ fromImages: false,
14
15
  fromRelease: null,
15
16
  installDir: null,
16
17
  force: false,
@@ -79,6 +80,11 @@ function parseArgv(argv) {
79
80
  continue;
80
81
  }
81
82
 
83
+ if (t === '--from-images') {
84
+ globals.fromImages = true;
85
+ continue;
86
+ }
87
+
82
88
  if (t === '--from-git' || t.startsWith('--from-git=')) {
83
89
  if (t === '--from-git') {
84
90
  const next = tokens[i + 1];
@@ -21,6 +21,12 @@ function defaultConfig(repoRoot, mode = 'docker') {
21
21
  };
22
22
  }
23
23
 
24
+ function normalizeSource(raw) {
25
+ const s = String(raw || '').trim();
26
+ if (s === 'images' || s === 'git' || s === 'zip' || s === 'checkout') return s;
27
+ return 'checkout';
28
+ }
29
+
24
30
  function readConfig(repoRoot) {
25
31
  const file = configPath(repoRoot);
26
32
  if (!fs.existsSync(file)) {
@@ -31,11 +37,10 @@ function readConfig(repoRoot) {
31
37
  if (!raw || typeof raw !== 'object') {
32
38
  return null;
33
39
  }
34
- const source = raw.source === 'zip' ? 'zip' : 'checkout';
35
40
  return {
36
41
  version: Number(raw.version) || CONFIG_VERSION,
37
42
  mode: raw.mode === 'native' ? 'native' : 'docker',
38
- source,
43
+ source: normalizeSource(raw.source),
39
44
  repoRoot: path.resolve(String(raw.repoRoot || repoRoot)),
40
45
  installDir: path.resolve(String(raw.installDir || raw.repoRoot || repoRoot)),
41
46
  };
@@ -48,10 +53,11 @@ function writeConfig(repoRoot, partial = {}) {
48
53
  const dir = path.join(repoRoot, CONFIG_DIR);
49
54
  fs.mkdirSync(dir, { recursive: true });
50
55
  const existing = readConfig(repoRoot) || defaultConfig(repoRoot);
51
- const source =
52
- partial.source === 'zip' || partial.source === 'checkout'
56
+ const source = normalizeSource(
57
+ partial.source !== undefined && partial.source !== null
53
58
  ? partial.source
54
- : existing.source || 'checkout';
59
+ : existing.source || 'checkout',
60
+ );
55
61
  const next = {
56
62
  ...existing,
57
63
  ...partial,
@@ -3,19 +3,21 @@
3
3
  /**
4
4
  * Distribution modes for the testing CLI.
5
5
  *
6
- * - git-clone: default `ragsuite-test init` (public GitHub)
7
- * - local-checkout: --repo-root (advanced / CI)
6
+ * - git-clone: `ragsuite-test init` (source on disk)
7
+ * - images: `ragsuite-test init --from-images` (pull GHCR, no app source)
8
+ * - local-checkout: --repo-root
8
9
  */
9
10
 
10
11
  const MODE_LOCAL = 'local-checkout';
11
12
  const MODE_GIT = 'git-clone';
13
+ const MODE_IMAGES = 'images';
12
14
  const MODE_REGISTRY = 'registry-compose';
13
15
 
14
16
  function assertSupportedInstall() {}
15
17
 
16
18
  function resolveComposeBundle() {
17
19
  const err = new Error(
18
- 'Registry/compose (GHCR) install is not available yet. Run: ragsuite-test init',
20
+ 'Use: ragsuite-test init --from-images (pull-only) OR ragsuite-test init (git source)',
19
21
  );
20
22
  err.code = 'DIST_NOT_READY';
21
23
  throw err;
@@ -23,17 +25,17 @@ function resolveComposeBundle() {
23
25
 
24
26
  function installHint() {
25
27
  return [
26
- 'Simple deploy:',
27
- ' npm install -g @nitsan-ai/ragsuite-test',
28
- ' ragsuite-test init',
29
- ' ragsuite-test start --repo-root ~/ragsuite-test --detach',
30
- 'Docker is optional — init picks native automatically if Docker is not running.',
28
+ 'Day-to-day:',
29
+ ' ragsuite-test start',
30
+ ' ragsuite-test update',
31
+ ' ragsuite-test stop',
31
32
  ].join('\n');
32
33
  }
33
34
 
34
35
  module.exports = {
35
36
  MODE_LOCAL,
36
37
  MODE_GIT,
38
+ MODE_IMAGES,
37
39
  MODE_REGISTRY,
38
40
  mode: MODE_GIT,
39
41
  assertSupportedInstall,
@@ -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
+ };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { defaultInstallDir } = require('./git-install');
6
+
7
+ function templatesRoot() {
8
+ return path.join(__dirname, '..', '..', 'templates', 'images');
9
+ }
10
+
11
+ function isDirEmptyOrMissing(dir) {
12
+ if (!fs.existsSync(dir)) return true;
13
+ const items = fs.readdirSync(dir).filter((n) => n !== '.DS_Store');
14
+ return items.length === 0;
15
+ }
16
+
17
+ function copyFile(src, dest) {
18
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
19
+ fs.copyFileSync(src, dest);
20
+ }
21
+
22
+ /**
23
+ * Materialize a pull-only install dir (compose + scripts + .env.example).
24
+ * No monorepo source.
25
+ */
26
+ function materializeImagesInstall(options = {}) {
27
+ const { installDir = defaultInstallDir(), force = false } = options;
28
+ const srcRoot = templatesRoot();
29
+ if (!fs.existsSync(path.join(srcRoot, 'docker-compose.yml'))) {
30
+ const err = new Error(
31
+ `Images templates missing in CLI package (${srcRoot}). Reinstall @nitsan-ai/ragsuite-test.`,
32
+ );
33
+ err.code = 'IMAGES_TEMPLATE_MISSING';
34
+ throw err;
35
+ }
36
+
37
+ const target = path.resolve(installDir);
38
+ if (!isDirEmptyOrMissing(target)) {
39
+ if (!force) {
40
+ const err = new Error(
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'),
48
+ );
49
+ err.code = 'INSTALL_DIR_NONEMPTY';
50
+ throw err;
51
+ }
52
+ fs.rmSync(target, { recursive: true, force: true });
53
+ }
54
+
55
+ fs.mkdirSync(target, { recursive: true });
56
+ copyFile(
57
+ path.join(srcRoot, 'docker-compose.yml'),
58
+ path.join(target, 'docker-compose.yml'),
59
+ );
60
+ copyFile(
61
+ path.join(srcRoot, '.env.example'),
62
+ path.join(target, '.env.example'),
63
+ );
64
+ copyFile(
65
+ path.join(srcRoot, 'scripts', 'docker-start-images.sh'),
66
+ path.join(target, 'scripts', 'docker-start-images.sh'),
67
+ );
68
+ copyFile(
69
+ path.join(srcRoot, 'scripts', 'docker-stop-images.sh'),
70
+ path.join(target, 'scripts', 'docker-stop-images.sh'),
71
+ );
72
+ copyFile(
73
+ path.join(srcRoot, 'scripts', 'docker-doctor-images.sh'),
74
+ path.join(target, 'scripts', 'docker-doctor-images.sh'),
75
+ );
76
+ try {
77
+ fs.chmodSync(path.join(target, 'scripts', 'docker-start-images.sh'), 0o755);
78
+ fs.chmodSync(path.join(target, 'scripts', 'docker-stop-images.sh'), 0o755);
79
+ fs.chmodSync(path.join(target, 'scripts', 'docker-doctor-images.sh'), 0o755);
80
+ } catch {
81
+ /* windows */
82
+ }
83
+
84
+ // Minimal package.json so tooling can identify the install dir
85
+ fs.writeFileSync(
86
+ path.join(target, 'package.json'),
87
+ `${JSON.stringify(
88
+ {
89
+ name: 'ragsuite-images-install',
90
+ private: true,
91
+ description: 'RAGSuite pull-only images install (no source tree)',
92
+ },
93
+ null,
94
+ 2,
95
+ )}\n`,
96
+ 'utf8',
97
+ );
98
+
99
+ return { repoRoot: target };
100
+ }
101
+
102
+ module.exports = {
103
+ templatesRoot,
104
+ materializeImagesInstall,
105
+ defaultInstallDir,
106
+ };