@nitsan-ai/ragsuite-test 0.1.0 → 0.1.1
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 +56 -39
- package/package.json +1 -1
- package/src/commands/init.js +123 -94
- package/src/commands/update.js +7 -20
- package/src/index.js +10 -6
- package/src/utils/args.js +91 -30
- package/src/utils/distribution.js +13 -8
- package/src/utils/env-file.js +124 -21
- package/src/utils/git-install.js +93 -0
- package/src/utils/paths.js +1 -1
- package/src/utils/port.js +15 -1
- package/src/utils/prereqs.js +78 -16
- package/src/utils/zip-install.js +0 -213
package/src/utils/prereqs.js
CHANGED
|
@@ -22,62 +22,122 @@ function nodeMajorOk() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* How to install a missing prerequisite (shown in doctor/init).
|
|
26
26
|
*/
|
|
27
|
-
function
|
|
27
|
+
function installHintFor(issueKey) {
|
|
28
|
+
const hints = {
|
|
29
|
+
node: 'Install Node 18+: https://nodejs.org/ or `brew install node` / `sudo apt install nodejs`',
|
|
30
|
+
docker:
|
|
31
|
+
'Docker is optional. Install Docker Desktop, OR use --mode native (Postgres :5436 + Redis :6382 on the host).',
|
|
32
|
+
'docker-compose': 'Install Docker Desktop (includes compose) or the docker compose plugin.',
|
|
33
|
+
python3: 'Install Python 3: `brew install python` / `sudo apt install python3 python3-venv`',
|
|
34
|
+
yarn: 'Optional: `corepack enable && corepack prepare yarn@stable --activate` or `npm i -g yarn`',
|
|
35
|
+
git: 'Install git: macOS `xcode-select --install` / `brew install git`; Linux `sudo apt install git`',
|
|
36
|
+
postgres:
|
|
37
|
+
'Native mode: Postgres on localhost:5436, db ragsuite_v3. Example: `brew install postgresql@15` then create DB/user.',
|
|
38
|
+
redis: 'Native mode: Redis on localhost:6382. Example: `brew install redis` then `redis-server --port 6382`.',
|
|
39
|
+
};
|
|
40
|
+
return hints[issueKey] || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pick run mode when user did not pass --mode.
|
|
45
|
+
* Docker is never required — prefer native if Docker is unavailable.
|
|
46
|
+
*/
|
|
47
|
+
function resolvePreferredMode(flagMode) {
|
|
48
|
+
if (flagMode === 'docker' || flagMode === 'native') return flagMode;
|
|
49
|
+
if (commandExists('docker') && dockerDaemonRunning()) return 'docker';
|
|
50
|
+
return 'native';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check prerequisites for install mode. Returns { ok, issues[], notes[], hints[] }.
|
|
55
|
+
*/
|
|
56
|
+
function checkPrereqs(mode = 'native', options = {}) {
|
|
57
|
+
const { needGit = false } = options;
|
|
28
58
|
const issues = [];
|
|
29
59
|
const notes = [];
|
|
60
|
+
const hints = [];
|
|
61
|
+
|
|
62
|
+
function pushIssue(msg, hintKey) {
|
|
63
|
+
issues.push(msg);
|
|
64
|
+
const h = installHintFor(hintKey);
|
|
65
|
+
if (h) hints.push(h);
|
|
66
|
+
}
|
|
30
67
|
|
|
31
68
|
if (!nodeMajorOk()) {
|
|
32
|
-
|
|
69
|
+
pushIssue(`Node.js 18+ required (current: ${process.version})`, 'node');
|
|
33
70
|
} else {
|
|
34
71
|
notes.push(`Node.js ${process.version}`);
|
|
35
72
|
}
|
|
36
73
|
|
|
74
|
+
if (needGit) {
|
|
75
|
+
if (!commandExists('git')) {
|
|
76
|
+
pushIssue('git not found (required for --from-git)', 'git');
|
|
77
|
+
} else {
|
|
78
|
+
notes.push('git present');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
37
82
|
if (mode === 'docker') {
|
|
38
83
|
if (!commandExists('docker')) {
|
|
39
|
-
|
|
84
|
+
pushIssue('Docker CLI not found', 'docker');
|
|
40
85
|
} else if (!dockerDaemonRunning()) {
|
|
41
|
-
|
|
86
|
+
pushIssue('Docker daemon not running — start Docker Desktop / dockerd', 'docker');
|
|
42
87
|
} else {
|
|
43
88
|
notes.push('Docker CLI + daemon OK');
|
|
44
|
-
}
|
|
45
|
-
if (!commandExists('docker')) {
|
|
46
|
-
/* already noted */
|
|
47
|
-
} else {
|
|
48
89
|
const compose = spawnSync('docker', ['compose', 'version'], { encoding: 'utf8' });
|
|
49
90
|
if (compose.status !== 0) {
|
|
50
|
-
|
|
91
|
+
pushIssue('docker compose plugin not available', 'docker-compose');
|
|
51
92
|
} else {
|
|
52
93
|
notes.push('docker compose OK');
|
|
53
94
|
}
|
|
54
95
|
}
|
|
55
96
|
} else {
|
|
97
|
+
notes.push('Mode=native — Docker is NOT required');
|
|
56
98
|
if (!commandExists('python3')) {
|
|
57
|
-
|
|
99
|
+
pushIssue('python3 not found (required for native mode)', 'python3');
|
|
58
100
|
} else {
|
|
59
101
|
notes.push('python3 present');
|
|
60
102
|
}
|
|
61
103
|
if (!commandExists('yarn')) {
|
|
62
|
-
notes.push('yarn missing —
|
|
104
|
+
notes.push('yarn missing — enable with corepack or npm i -g yarn');
|
|
105
|
+
const h = installHintFor('yarn');
|
|
106
|
+
if (h) hints.push(h);
|
|
63
107
|
} else {
|
|
64
108
|
notes.push('yarn present');
|
|
65
109
|
}
|
|
66
|
-
notes.push('
|
|
110
|
+
notes.push('Need Postgres :5436 (ragsuite_v3) and Redis :6382 on this machine');
|
|
111
|
+
hints.push(installHintFor('postgres'));
|
|
112
|
+
hints.push(installHintFor('redis'));
|
|
67
113
|
}
|
|
68
114
|
|
|
69
115
|
if (process.platform === 'win32') {
|
|
70
116
|
notes.push('Windows: use WSL or Git Bash — start/stop spawn bash scripts');
|
|
71
117
|
}
|
|
72
118
|
|
|
73
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
ok: issues.length === 0,
|
|
121
|
+
issues,
|
|
122
|
+
notes,
|
|
123
|
+
hints: [...new Set(hints.filter(Boolean))],
|
|
124
|
+
};
|
|
74
125
|
}
|
|
75
126
|
|
|
76
|
-
function printPrereqs(mode) {
|
|
77
|
-
const { ok, issues, notes } = checkPrereqs(mode);
|
|
127
|
+
function printPrereqs(mode, options = {}) {
|
|
128
|
+
const { ok, issues, notes, hints } = checkPrereqs(mode, options);
|
|
78
129
|
info(`Prerequisites (mode=${mode}):`);
|
|
79
130
|
for (const n of notes) info(` ✓ ${n}`);
|
|
80
131
|
for (const i of issues) error(` ✗ ${i}`);
|
|
132
|
+
if (hints.length) {
|
|
133
|
+
info('');
|
|
134
|
+
info('How to get missing pieces:');
|
|
135
|
+
for (const h of hints) info(` → ${h}`);
|
|
136
|
+
}
|
|
137
|
+
if (!ok && mode === 'docker') {
|
|
138
|
+
warn('');
|
|
139
|
+
warn('Docker is optional. Re-run with --mode native if you prefer host Postgres/Redis (no Docker).');
|
|
140
|
+
}
|
|
81
141
|
return ok;
|
|
82
142
|
}
|
|
83
143
|
|
|
@@ -85,6 +145,8 @@ module.exports = {
|
|
|
85
145
|
commandExists,
|
|
86
146
|
dockerDaemonRunning,
|
|
87
147
|
nodeMajorOk,
|
|
148
|
+
installHintFor,
|
|
149
|
+
resolvePreferredMode,
|
|
88
150
|
checkPrereqs,
|
|
89
151
|
printPrereqs,
|
|
90
152
|
};
|
package/src/utils/zip-install.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
7
|
-
const { looksLikeFullBundleRoot } = require('./paths');
|
|
8
|
-
|
|
9
|
-
function defaultInstallDir() {
|
|
10
|
-
return path.join(os.homedir(), 'ragsuite-test');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function requireUnzip() {
|
|
14
|
-
const which = spawnSync('which', ['unzip'], { encoding: 'utf8' });
|
|
15
|
-
if (which.status !== 0) {
|
|
16
|
-
const err = new Error('unzip is required for --from-zip (install unzip / use macOS or Linux)');
|
|
17
|
-
err.code = 'MISSING_UNZIP';
|
|
18
|
-
throw err;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* List zip entries; reject zip-slip (absolute paths, .. segments).
|
|
24
|
-
* @returns {string[]}
|
|
25
|
-
*/
|
|
26
|
-
function listZipEntries(zipPath) {
|
|
27
|
-
requireUnzip();
|
|
28
|
-
const opts = { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 };
|
|
29
|
-
const result = spawnSync('unzip', ['-Z1', zipPath], opts);
|
|
30
|
-
if (result.status === 0) {
|
|
31
|
-
return String(result.stdout || '')
|
|
32
|
-
.split('\n')
|
|
33
|
-
.map((l) => l.trim())
|
|
34
|
-
.filter(Boolean);
|
|
35
|
-
}
|
|
36
|
-
const alt = spawnSync('unzip', ['-l', zipPath], opts);
|
|
37
|
-
if (alt.status !== 0) {
|
|
38
|
-
const err = new Error(
|
|
39
|
-
`Failed to list zip: ${zipPath} (${(result.stderr || alt.stderr || result.error || '').toString().trim() || 'unzip -Z1/-l failed'})`,
|
|
40
|
-
);
|
|
41
|
-
err.code = 'ZIP_LIST';
|
|
42
|
-
throw err;
|
|
43
|
-
}
|
|
44
|
-
return parseUnzipL(alt.stdout);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseUnzipL(stdout) {
|
|
48
|
-
const lines = String(stdout || '').split('\n');
|
|
49
|
-
const entries = [];
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
// typical: " Length Date Time Name" then data lines ending with name
|
|
52
|
-
const m = line.match(/^\s*\d+\s+\d{2}-\d{2}-\d{4}\s+\d{2}:\d{2}\s+(.+)$/);
|
|
53
|
-
if (m) entries.push(m[1].trim());
|
|
54
|
-
}
|
|
55
|
-
return entries.filter(Boolean);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function assertNoZipSlip(entries) {
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const normalized = entry.replace(/\\/g, '/');
|
|
61
|
-
if (normalized.startsWith('/') || /^[A-Za-z]:/.test(normalized)) {
|
|
62
|
-
const err = new Error(`Refusing zip with absolute path entry: ${entry}`);
|
|
63
|
-
err.code = 'ZIP_SLIP';
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
const parts = normalized.split('/');
|
|
67
|
-
if (parts.some((p) => p === '..')) {
|
|
68
|
-
const err = new Error(`Refusing zip with path traversal entry: ${entry}`);
|
|
69
|
-
err.code = 'ZIP_SLIP';
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isDirEmptyOrMissing(dir) {
|
|
76
|
-
if (!fs.existsSync(dir)) return true;
|
|
77
|
-
const items = fs.readdirSync(dir).filter((n) => n !== '.DS_Store');
|
|
78
|
-
return items.length === 0;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function findBundleRoot(extractRoot) {
|
|
82
|
-
if (looksLikeFullBundleRoot(extractRoot)) {
|
|
83
|
-
return extractRoot;
|
|
84
|
-
}
|
|
85
|
-
const kids = fs
|
|
86
|
-
.readdirSync(extractRoot)
|
|
87
|
-
.map((n) => path.join(extractRoot, n))
|
|
88
|
-
.filter((p) => fs.statSync(p).isDirectory());
|
|
89
|
-
if (kids.length === 1 && looksLikeFullBundleRoot(kids[0])) {
|
|
90
|
-
return kids[0];
|
|
91
|
-
}
|
|
92
|
-
for (const kid of kids) {
|
|
93
|
-
if (looksLikeFullBundleRoot(kid)) return kid;
|
|
94
|
-
}
|
|
95
|
-
const err = new Error(
|
|
96
|
-
'ZIP does not contain a RAGSuite monorepo root (need package.json, scripts/docker-start.sh, docker-compose.yml)',
|
|
97
|
-
);
|
|
98
|
-
err.code = 'ZIP_INVALID';
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function copyDirRecursive(src, dest) {
|
|
103
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
104
|
-
for (const name of fs.readdirSync(src)) {
|
|
105
|
-
const from = path.join(src, name);
|
|
106
|
-
const to = path.join(dest, name);
|
|
107
|
-
const st = fs.statSync(from);
|
|
108
|
-
if (st.isDirectory()) {
|
|
109
|
-
copyDirRecursive(from, to);
|
|
110
|
-
} else {
|
|
111
|
-
fs.copyFileSync(from, to);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function rmRecursive(dir) {
|
|
117
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Extract zip safely into installDir.
|
|
122
|
-
* @returns {{ installDir: string, repoRoot: string }}
|
|
123
|
-
*/
|
|
124
|
-
function extractZipToInstallDir(zipPath, options = {}) {
|
|
125
|
-
const absZip = path.resolve(zipPath);
|
|
126
|
-
if (!fs.existsSync(absZip)) {
|
|
127
|
-
const err = new Error(`ZIP not found: ${absZip}`);
|
|
128
|
-
err.code = 'ZIP_MISSING';
|
|
129
|
-
throw err;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const installDir = path.resolve(options.installDir || defaultInstallDir());
|
|
133
|
-
const force = Boolean(options.force);
|
|
134
|
-
|
|
135
|
-
if (!isDirEmptyOrMissing(installDir) && !force) {
|
|
136
|
-
const err = new Error(
|
|
137
|
-
`Install dir is not empty: ${installDir}. Pass --force to overwrite, or choose another --install-dir.`,
|
|
138
|
-
);
|
|
139
|
-
err.code = 'INSTALL_DIR_NONEMPTY';
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const entries = listZipEntries(absZip);
|
|
144
|
-
if (entries.length === 0) {
|
|
145
|
-
const err = new Error('ZIP is empty');
|
|
146
|
-
err.code = 'ZIP_EMPTY';
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
assertNoZipSlip(entries);
|
|
150
|
-
|
|
151
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ragsuite-zip-'));
|
|
152
|
-
try {
|
|
153
|
-
requireUnzip();
|
|
154
|
-
const unzip = spawnSync('unzip', ['-q', '-o', absZip, '-d', tmp], {
|
|
155
|
-
encoding: 'utf8',
|
|
156
|
-
});
|
|
157
|
-
if (unzip.status !== 0) {
|
|
158
|
-
const err = new Error(`unzip failed: ${unzip.stderr || unzip.stdout || 'unknown'}`);
|
|
159
|
-
err.code = 'ZIP_EXTRACT';
|
|
160
|
-
throw err;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const bundleRoot = findBundleRoot(tmp);
|
|
164
|
-
|
|
165
|
-
if (force && fs.existsSync(installDir)) {
|
|
166
|
-
rmRecursive(installDir);
|
|
167
|
-
}
|
|
168
|
-
fs.mkdirSync(path.dirname(installDir), { recursive: true });
|
|
169
|
-
if (fs.existsSync(installDir) && isDirEmptyOrMissing(installDir)) {
|
|
170
|
-
rmRecursive(installDir);
|
|
171
|
-
}
|
|
172
|
-
// Move/copy bundle into installDir
|
|
173
|
-
try {
|
|
174
|
-
fs.renameSync(bundleRoot, installDir);
|
|
175
|
-
} catch {
|
|
176
|
-
copyDirRecursive(bundleRoot, installDir);
|
|
177
|
-
if (bundleRoot !== tmp) {
|
|
178
|
-
rmRecursive(bundleRoot);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} catch (err) {
|
|
182
|
-
try {
|
|
183
|
-
rmRecursive(tmp);
|
|
184
|
-
} catch {
|
|
185
|
-
/* ignore */
|
|
186
|
-
}
|
|
187
|
-
throw err;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// tmp may still have empty wrapper dirs
|
|
191
|
-
try {
|
|
192
|
-
if (fs.existsSync(tmp)) rmRecursive(tmp);
|
|
193
|
-
} catch {
|
|
194
|
-
/* ignore */
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!looksLikeFullBundleRoot(installDir)) {
|
|
198
|
-
const err = new Error(`Extracted install is not a valid bundle: ${installDir}`);
|
|
199
|
-
err.code = 'ZIP_INVALID';
|
|
200
|
-
throw err;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return { installDir, repoRoot: installDir };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
module.exports = {
|
|
207
|
-
defaultInstallDir,
|
|
208
|
-
listZipEntries,
|
|
209
|
-
assertNoZipSlip,
|
|
210
|
-
extractZipToInstallDir,
|
|
211
|
-
findBundleRoot,
|
|
212
|
-
isDirEmptyOrMissing,
|
|
213
|
-
};
|