@nitsan-ai/ragsuite-test 0.1.0 → 0.1.2
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 +47 -49
- package/package.json +2 -2
- package/src/commands/init.js +83 -128
- package/src/commands/update.js +7 -20
- package/src/index.js +11 -7
- package/src/utils/args.js +91 -30
- package/src/utils/distribution.js +14 -11
- 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/args.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Parse argv into global flags, command, and remaining args.
|
|
5
|
-
* Globals: --help/-h, --repo-root, --dry-run, --mode,
|
|
6
|
-
* --from-zip, --from-release, --install-dir, --force, --yes
|
|
7
5
|
*/
|
|
8
6
|
function parseArgv(argv) {
|
|
9
7
|
const tokens = argv.slice(2);
|
|
@@ -12,25 +10,33 @@ function parseArgv(argv) {
|
|
|
12
10
|
repoRoot: null,
|
|
13
11
|
dryRun: false,
|
|
14
12
|
mode: null,
|
|
15
|
-
|
|
13
|
+
fromGit: null,
|
|
16
14
|
fromRelease: null,
|
|
17
15
|
installDir: null,
|
|
18
16
|
force: false,
|
|
19
17
|
yes: false,
|
|
20
18
|
llmApiKey: null,
|
|
19
|
+
smtpHost: null,
|
|
20
|
+
smtpUser: null,
|
|
21
|
+
smtpPassword: null,
|
|
22
|
+
emailFrom: null,
|
|
21
23
|
};
|
|
22
24
|
const rest = [];
|
|
23
25
|
let command = null;
|
|
24
26
|
|
|
27
|
+
function takeValue(flag, t, i) {
|
|
28
|
+
const eq = `${flag}=`;
|
|
29
|
+
if (t.startsWith(eq)) return { value: t.slice(eq.length), nextI: i };
|
|
30
|
+
const value = tokens[i + 1];
|
|
31
|
+
return { value, nextI: i + 1 };
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
26
35
|
const t = tokens[i];
|
|
27
36
|
|
|
28
37
|
if (t === '--help' || t === '-h') {
|
|
29
|
-
if (command)
|
|
30
|
-
|
|
31
|
-
} else {
|
|
32
|
-
globals.help = true;
|
|
33
|
-
}
|
|
38
|
+
if (command) rest.push(t);
|
|
39
|
+
else globals.help = true;
|
|
34
40
|
continue;
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -50,9 +56,8 @@ function parseArgv(argv) {
|
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
if (t === '--repo-root' || t.startsWith('--repo-root=')) {
|
|
53
|
-
const value =
|
|
54
|
-
|
|
55
|
-
: tokens[++i];
|
|
59
|
+
const { value, nextI } = takeValue('--repo-root', t, i);
|
|
60
|
+
i = nextI;
|
|
56
61
|
if (!value || value.startsWith('-')) {
|
|
57
62
|
const err = new Error('--repo-root requires a path');
|
|
58
63
|
err.code = 'USAGE';
|
|
@@ -63,7 +68,8 @@ function parseArgv(argv) {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
if (t === '--mode' || t.startsWith('--mode=')) {
|
|
66
|
-
const value =
|
|
71
|
+
const { value, nextI } = takeValue('--mode', t, i);
|
|
72
|
+
i = nextI;
|
|
67
73
|
if (!value || String(value).startsWith('-')) {
|
|
68
74
|
const err = new Error('--mode requires docker or native');
|
|
69
75
|
err.code = 'USAGE';
|
|
@@ -73,23 +79,32 @@ function parseArgv(argv) {
|
|
|
73
79
|
continue;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
if (t === '--from-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (t === '--from-git' || t.startsWith('--from-git=')) {
|
|
83
|
+
if (t === '--from-git') {
|
|
84
|
+
const next = tokens[i + 1];
|
|
85
|
+
if (next && !next.startsWith('-')) {
|
|
86
|
+
globals.fromGit = next;
|
|
87
|
+
i += 1;
|
|
88
|
+
} else {
|
|
89
|
+
globals.fromGit = ''; // use default URL
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
globals.fromGit = t.slice('--from-git='.length);
|
|
84
93
|
}
|
|
85
|
-
globals.fromZip = value;
|
|
86
94
|
continue;
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
if (t === '--from-zip' || t.startsWith('--from-zip=')) {
|
|
98
|
+
const err = new Error(
|
|
99
|
+
'--from-zip was removed. Use --from-git (public deploy) or --repo-root <checkout>.',
|
|
100
|
+
);
|
|
101
|
+
err.code = 'USAGE';
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
if (t === '--from-release' || t.startsWith('--from-release=')) {
|
|
90
|
-
const value =
|
|
91
|
-
|
|
92
|
-
: tokens[++i];
|
|
106
|
+
const { value, nextI } = takeValue('--from-release', t, i);
|
|
107
|
+
i = nextI;
|
|
93
108
|
if (!value || value.startsWith('-')) {
|
|
94
109
|
const err = new Error('--from-release requires a tag or "latest"');
|
|
95
110
|
err.code = 'USAGE';
|
|
@@ -100,9 +115,8 @@ function parseArgv(argv) {
|
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
if (t === '--install-dir' || t.startsWith('--install-dir=')) {
|
|
103
|
-
const value =
|
|
104
|
-
|
|
105
|
-
: tokens[++i];
|
|
118
|
+
const { value, nextI } = takeValue('--install-dir', t, i);
|
|
119
|
+
i = nextI;
|
|
106
120
|
if (!value || value.startsWith('-')) {
|
|
107
121
|
const err = new Error('--install-dir requires a path');
|
|
108
122
|
err.code = 'USAGE';
|
|
@@ -113,9 +127,8 @@ function parseArgv(argv) {
|
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
if (t === '--llm-api-key' || t.startsWith('--llm-api-key=')) {
|
|
116
|
-
const value =
|
|
117
|
-
|
|
118
|
-
: tokens[++i];
|
|
130
|
+
const { value, nextI } = takeValue('--llm-api-key', t, i);
|
|
131
|
+
i = nextI;
|
|
119
132
|
if (!value || value.startsWith('-')) {
|
|
120
133
|
const err = new Error('--llm-api-key requires a value');
|
|
121
134
|
err.code = 'USAGE';
|
|
@@ -125,6 +138,54 @@ function parseArgv(argv) {
|
|
|
125
138
|
continue;
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
if (t === '--smtp-host' || t.startsWith('--smtp-host=')) {
|
|
142
|
+
const { value, nextI } = takeValue('--smtp-host', t, i);
|
|
143
|
+
i = nextI;
|
|
144
|
+
if (!value || value.startsWith('-')) {
|
|
145
|
+
const err = new Error('--smtp-host requires a value');
|
|
146
|
+
err.code = 'USAGE';
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
globals.smtpHost = value;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (t === '--smtp-user' || t.startsWith('--smtp-user=')) {
|
|
154
|
+
const { value, nextI } = takeValue('--smtp-user', t, i);
|
|
155
|
+
i = nextI;
|
|
156
|
+
if (!value || value.startsWith('-')) {
|
|
157
|
+
const err = new Error('--smtp-user requires a value');
|
|
158
|
+
err.code = 'USAGE';
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
globals.smtpUser = value;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (t === '--smtp-password' || t.startsWith('--smtp-password=')) {
|
|
166
|
+
const { value, nextI } = takeValue('--smtp-password', t, i);
|
|
167
|
+
i = nextI;
|
|
168
|
+
if (value === undefined || value === null || String(value).startsWith('-')) {
|
|
169
|
+
const err = new Error('--smtp-password requires a value');
|
|
170
|
+
err.code = 'USAGE';
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
globals.smtpPassword = value;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (t === '--email-from' || t.startsWith('--email-from=')) {
|
|
178
|
+
const { value, nextI } = takeValue('--email-from', t, i);
|
|
179
|
+
i = nextI;
|
|
180
|
+
if (!value || value.startsWith('-')) {
|
|
181
|
+
const err = new Error('--email-from requires a value');
|
|
182
|
+
err.code = 'USAGE';
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
globals.emailFrom = value;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
128
189
|
if (!command && !t.startsWith('-')) {
|
|
129
190
|
command = t;
|
|
130
191
|
continue;
|
|
@@ -3,36 +3,39 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Distribution modes for the testing CLI.
|
|
5
5
|
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* Registry/GHCR compose remains stubbed.
|
|
6
|
+
* - git-clone: default `ragsuite-test init` (public GitHub)
|
|
7
|
+
* - local-checkout: --repo-root (advanced / CI)
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
const MODE_LOCAL = 'local-checkout';
|
|
12
|
-
const
|
|
11
|
+
const MODE_GIT = 'git-clone';
|
|
13
12
|
const MODE_REGISTRY = 'registry-compose';
|
|
14
13
|
|
|
15
|
-
function assertSupportedInstall() {
|
|
16
|
-
// checkout + zip supported
|
|
17
|
-
}
|
|
14
|
+
function assertSupportedInstall() {}
|
|
18
15
|
|
|
19
16
|
function resolveComposeBundle() {
|
|
20
17
|
const err = new Error(
|
|
21
|
-
'Registry/compose (GHCR) install is not available yet.
|
|
18
|
+
'Registry/compose (GHCR) install is not available yet. Run: ragsuite-test init',
|
|
22
19
|
);
|
|
23
20
|
err.code = 'DIST_NOT_READY';
|
|
24
21
|
throw err;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
function installHint() {
|
|
28
|
-
return
|
|
25
|
+
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.',
|
|
31
|
+
].join('\n');
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
module.exports = {
|
|
32
35
|
MODE_LOCAL,
|
|
33
|
-
|
|
36
|
+
MODE_GIT,
|
|
34
37
|
MODE_REGISTRY,
|
|
35
|
-
mode:
|
|
38
|
+
mode: MODE_GIT,
|
|
36
39
|
assertSupportedInstall,
|
|
37
40
|
resolveComposeBundle,
|
|
38
41
|
installHint,
|
package/src/utils/env-file.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { warn } = require('./log');
|
|
6
|
+
const { info, warn } = require('./log');
|
|
7
7
|
|
|
8
8
|
function generateJwtSecret() {
|
|
9
9
|
return crypto.randomBytes(32).toString('hex');
|
|
@@ -15,7 +15,7 @@ function generateCiLlmKey() {
|
|
|
15
15
|
|
|
16
16
|
function isPlaceholderSecret(value) {
|
|
17
17
|
const v = String(value || '').trim();
|
|
18
|
-
return !v || v.startsWith('change-me');
|
|
18
|
+
return !v || v.startsWith('change-me') || v.startsWith('your-smtp-');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -57,16 +57,84 @@ function buildEnvFromExample(examplePath, overrides = {}) {
|
|
|
57
57
|
return `${out.join('\n').replace(/\n+$/, '')}\n`;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function parseEnvFile(envPath) {
|
|
61
|
+
const out = {};
|
|
62
|
+
if (!fs.existsSync(envPath)) return out;
|
|
63
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
|
64
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
65
|
+
if (m) out[m[1]] = m[2].trim();
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Short “how to get / fix” hints when a config value is missing or still a placeholder.
|
|
72
|
+
*/
|
|
73
|
+
function printConfigHints(issues) {
|
|
74
|
+
if (!issues.length) return;
|
|
75
|
+
warn('');
|
|
76
|
+
warn('Missing or placeholder configuration — how to fix:');
|
|
77
|
+
for (const issue of issues) {
|
|
78
|
+
warn(` ✗ ${issue.title}`);
|
|
79
|
+
for (const step of issue.steps) warn(` → ${step}`);
|
|
80
|
+
}
|
|
81
|
+
warn('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function collectEnvConfigIssues(envMap) {
|
|
85
|
+
const issues = [];
|
|
86
|
+
if (isPlaceholderSecret(envMap.JWT_SECRET_KEY)) {
|
|
87
|
+
issues.push({
|
|
88
|
+
title: 'JWT_SECRET_KEY',
|
|
89
|
+
steps: [
|
|
90
|
+
'Re-run: ragsuite-test init --force … (auto-generates JWT)',
|
|
91
|
+
'Or set JWT_SECRET_KEY in .env (never commit .env)',
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (isPlaceholderSecret(envMap.CUSTOM_LLM_INTERNAL_API_KEY)) {
|
|
96
|
+
issues.push({
|
|
97
|
+
title: 'CUSTOM_LLM_INTERNAL_API_KEY',
|
|
98
|
+
steps: [
|
|
99
|
+
'Edit .env and set CUSTOM_LLM_INTERNAL_API_KEY=…',
|
|
100
|
+
'Or: ragsuite-test init --force --llm-api-key YOUR_KEY',
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const smtpOk =
|
|
105
|
+
envMap.SMTP_HOST &&
|
|
106
|
+
envMap.SMTP_USER &&
|
|
107
|
+
envMap.SMTP_PASSWORD &&
|
|
108
|
+
!isPlaceholderSecret(envMap.SMTP_PASSWORD) &&
|
|
109
|
+
!isPlaceholderSecret(envMap.SMTP_USER) &&
|
|
110
|
+
(envMap.EMAIL_FROM || envMap.SMTP_USER);
|
|
111
|
+
if (!smtpOk) {
|
|
112
|
+
issues.push({
|
|
113
|
+
title: 'SMTP (real mail)',
|
|
114
|
+
steps: [
|
|
115
|
+
'Put real values only in .env (gitignored) — never in .env.example',
|
|
116
|
+
'Gmail: Google Account → Security → App passwords → Mail',
|
|
117
|
+
'Set SMTP_USER, SMTP_PASSWORD, EMAIL_FROM in .env, then restart',
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return issues;
|
|
122
|
+
}
|
|
123
|
+
|
|
60
124
|
/**
|
|
61
|
-
* Write .env
|
|
62
|
-
*
|
|
125
|
+
* Write .env from .env.example. Never prompts.
|
|
126
|
+
* - Always regenerates JWT
|
|
127
|
+
* - Keeps LLM default from example (or generates if placeholder)
|
|
128
|
+
* - If SMTP in example is still a placeholder, writes a non-empty smoke SMTP
|
|
129
|
+
* so the backend can start; real mail requires editing local .env
|
|
130
|
+
* - Optional flag overrides only
|
|
63
131
|
*/
|
|
64
132
|
function writeInstallEnv(repoRoot, options = {}) {
|
|
65
133
|
const {
|
|
66
134
|
force = false,
|
|
67
135
|
jwtSecret = generateJwtSecret(),
|
|
68
136
|
llmApiKey,
|
|
69
|
-
|
|
137
|
+
smtp,
|
|
70
138
|
} = options;
|
|
71
139
|
|
|
72
140
|
const envPath = path.join(repoRoot, '.env');
|
|
@@ -80,30 +148,62 @@ function writeInstallEnv(repoRoot, options = {}) {
|
|
|
80
148
|
throw err;
|
|
81
149
|
}
|
|
82
150
|
|
|
83
|
-
|
|
84
|
-
const err = new Error('CUSTOM_LLM_INTERNAL_API_KEY must be set and must not be a change-me* placeholder');
|
|
85
|
-
err.code = 'LLM_KEY_INVALID';
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
|
|
151
|
+
const example = parseEnvFile(examplePath);
|
|
89
152
|
const overrides = {
|
|
90
153
|
JWT_SECRET_KEY: jwtSecret,
|
|
91
|
-
CUSTOM_LLM_INTERNAL_API_KEY: llmApiKey,
|
|
92
154
|
};
|
|
93
155
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
overrides.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
if (llmApiKey && !isPlaceholderSecret(llmApiKey)) {
|
|
157
|
+
overrides.CUSTOM_LLM_INTERNAL_API_KEY = llmApiKey;
|
|
158
|
+
} else if (isPlaceholderSecret(example.CUSTOM_LLM_INTERNAL_API_KEY)) {
|
|
159
|
+
overrides.CUSTOM_LLM_INTERNAL_API_KEY = generateCiLlmKey();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (smtp) {
|
|
163
|
+
if (smtp.host) overrides.SMTP_HOST = smtp.host;
|
|
164
|
+
if (smtp.user) overrides.SMTP_USER = smtp.user;
|
|
165
|
+
if (smtp.password) overrides.SMTP_PASSWORD = smtp.password;
|
|
166
|
+
if (smtp.emailFrom) overrides.EMAIL_FROM = smtp.emailFrom;
|
|
167
|
+
} else {
|
|
168
|
+
// No prompts: if template SMTP is placeholder, use smoke values so API can boot.
|
|
169
|
+
// Real Gmail credentials must live only in local .env (never commit).
|
|
170
|
+
const smtpUser = example.SMTP_USER || '';
|
|
171
|
+
const smtpPass = example.SMTP_PASSWORD || '';
|
|
172
|
+
if (isPlaceholderSecret(smtpPass) || isPlaceholderSecret(smtpUser)) {
|
|
173
|
+
overrides.SMTP_HOST = example.SMTP_HOST || 'smtp.gmail.com';
|
|
174
|
+
overrides.SMTP_PORT = example.SMTP_PORT || '587';
|
|
175
|
+
overrides.SMTP_USER = 'smoke-smtp@localhost';
|
|
176
|
+
overrides.SMTP_PASSWORD = 'ci-smoke-smtp-not-for-production';
|
|
177
|
+
overrides.SMTP_USE_TLS = example.SMTP_USE_TLS || 'true';
|
|
178
|
+
overrides.EMAIL_FROM = 'smoke-smtp@localhost';
|
|
179
|
+
warn('SMTP template was placeholder — wrote smoke SMTP into .env so the app can start.');
|
|
180
|
+
warn('For real email: edit .env (SMTP_USER / SMTP_PASSWORD / EMAIL_FROM). Never commit .env.');
|
|
181
|
+
}
|
|
102
182
|
}
|
|
103
183
|
|
|
104
184
|
const content = buildEnvFromExample(examplePath, overrides);
|
|
105
185
|
fs.writeFileSync(envPath, content, 'utf8');
|
|
106
|
-
|
|
186
|
+
|
|
187
|
+
const written = parseEnvFile(envPath);
|
|
188
|
+
if (isPlaceholderSecret(written.JWT_SECRET_KEY) || isPlaceholderSecret(written.CUSTOM_LLM_INTERNAL_API_KEY)) {
|
|
189
|
+
printConfigHints(collectEnvConfigIssues(written));
|
|
190
|
+
const err = new Error('Install .env is incomplete (see hints above).');
|
|
191
|
+
err.code = 'ENV_INCOMPLETE';
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Soft hint only for smoke SMTP (do not fail init — backend can start)
|
|
196
|
+
if (written.SMTP_PASSWORD === 'ci-smoke-smtp-not-for-production') {
|
|
197
|
+
info('SMTP: smoke defaults in .env (app starts). Edit .env for real Gmail/app-password mail.');
|
|
198
|
+
} else {
|
|
199
|
+
info('SMTP + LLM loaded into .env (no interactive prompts).');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
jwt: written.JWT_SECRET_KEY,
|
|
204
|
+
llmKey: written.CUSTOM_LLM_INTERNAL_API_KEY,
|
|
205
|
+
envPath,
|
|
206
|
+
};
|
|
107
207
|
}
|
|
108
208
|
|
|
109
209
|
function readEnvValue(envPath, key) {
|
|
@@ -119,6 +219,9 @@ module.exports = {
|
|
|
119
219
|
generateCiLlmKey,
|
|
120
220
|
isPlaceholderSecret,
|
|
121
221
|
buildEnvFromExample,
|
|
222
|
+
parseEnvFile,
|
|
223
|
+
collectEnvConfigIssues,
|
|
224
|
+
printConfigHints,
|
|
122
225
|
writeInstallEnv,
|
|
123
226
|
readEnvValue,
|
|
124
227
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
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, looksLikeRepoRoot } = require('./paths');
|
|
8
|
+
|
|
9
|
+
/** Default public monorepo (when the GitHub repo is public). */
|
|
10
|
+
const DEFAULT_GIT_URL = 'https://github.com/nitsan-ai/RAGSUITE.git';
|
|
11
|
+
|
|
12
|
+
function defaultInstallDir() {
|
|
13
|
+
return path.join(os.homedir(), 'ragsuite-test');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireGit() {
|
|
17
|
+
const which = spawnSync('which', ['git'], { encoding: 'utf8' });
|
|
18
|
+
if (which.status !== 0) {
|
|
19
|
+
const err = new Error(
|
|
20
|
+
'git is required for --from-git. Install: macOS `xcode-select --install` or `brew install git`; Linux `sudo apt install git`',
|
|
21
|
+
);
|
|
22
|
+
err.code = 'MISSING_GIT';
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isDirEmptyOrMissing(dir) {
|
|
28
|
+
if (!fs.existsSync(dir)) return true;
|
|
29
|
+
const items = fs.readdirSync(dir).filter((n) => n !== '.DS_Store');
|
|
30
|
+
return items.length === 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shallow-clone a public (or authenticated) git repo into installDir.
|
|
35
|
+
* @returns {{ repoRoot: string, gitUrl: string }}
|
|
36
|
+
*/
|
|
37
|
+
function cloneGitToInstallDir(gitUrl, options = {}) {
|
|
38
|
+
const { installDir, force = false, depth = 1 } = options;
|
|
39
|
+
requireGit();
|
|
40
|
+
|
|
41
|
+
const url = String(gitUrl || DEFAULT_GIT_URL).trim();
|
|
42
|
+
if (!url) {
|
|
43
|
+
const err = new Error('--from-git requires a repository URL');
|
|
44
|
+
err.code = 'GIT_URL_INVALID';
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const target = path.resolve(installDir);
|
|
49
|
+
if (!isDirEmptyOrMissing(target)) {
|
|
50
|
+
if (!force) {
|
|
51
|
+
const err = new Error(
|
|
52
|
+
`Install directory is not empty: ${target}. Pass --force to remove and re-clone, or choose another --install-dir.`,
|
|
53
|
+
);
|
|
54
|
+
err.code = 'INSTALL_DIR_NONEMPTY';
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
60
|
+
|
|
61
|
+
const args = ['clone', `--depth=${depth}`, url, target];
|
|
62
|
+
const result = spawnSync('git', args, {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
65
|
+
});
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
const stderr = String(result.stderr || result.stdout || '').trim();
|
|
68
|
+
const err = new Error(
|
|
69
|
+
`git clone failed for ${url}: ${stderr || `exit ${result.status}`}. ` +
|
|
70
|
+
'Repo must be public (or you must be authenticated). Or clone manually and use init --repo-root.',
|
|
71
|
+
);
|
|
72
|
+
err.code = 'GIT_CLONE_FAILED';
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const repoRoot = target;
|
|
77
|
+
if (!looksLikeRepoRoot(repoRoot) && !looksLikeFullBundleRoot(repoRoot)) {
|
|
78
|
+
const err = new Error(
|
|
79
|
+
`Cloned repo at ${repoRoot} does not look like a RAGSuite monorepo (missing scripts/docker-start.sh).`,
|
|
80
|
+
);
|
|
81
|
+
err.code = 'GIT_INVALID_REPO';
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { repoRoot, gitUrl: url };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
DEFAULT_GIT_URL,
|
|
90
|
+
defaultInstallDir,
|
|
91
|
+
requireGit,
|
|
92
|
+
cloneGitToInstallDir,
|
|
93
|
+
};
|
package/src/utils/paths.js
CHANGED
|
@@ -91,7 +91,7 @@ function resolveRepoRoot({
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const err = new Error(
|
|
94
|
-
'Could not find RAGSuite repo root. Pass --repo-root <path>, set RAGSUITE_REPO_ROOT, or run: ragsuite-test init --from-
|
|
94
|
+
'Could not find RAGSuite repo root. Pass --repo-root <path>, set RAGSUITE_REPO_ROOT, or run: ragsuite-test init --from-git',
|
|
95
95
|
);
|
|
96
96
|
err.code = 'NOT_REPO_ROOT';
|
|
97
97
|
throw err;
|
package/src/utils/port.js
CHANGED
|
@@ -61,7 +61,21 @@ async function assertPortsFree(ports = DEFAULT_INSTALL_PORTS) {
|
|
|
61
61
|
}
|
|
62
62
|
if (busy.length) {
|
|
63
63
|
const err = new Error(
|
|
64
|
-
|
|
64
|
+
[
|
|
65
|
+
`Port(s) already in use: ${busy.join(', ')}.`,
|
|
66
|
+
'Free them, then retry. This CLI will not kill foreign processes.',
|
|
67
|
+
'',
|
|
68
|
+
'If a previous RAGSuite install is running:',
|
|
69
|
+
' ragsuite-test stop --repo-root ~/ragsuite-test',
|
|
70
|
+
' # or from a monorepo checkout: cd <checkout> && npm run down',
|
|
71
|
+
'',
|
|
72
|
+
'See what owns the ports (macOS/Linux):',
|
|
73
|
+
' lsof -nP -iTCP:9090 -sTCP:LISTEN',
|
|
74
|
+
' lsof -nP -iTCP:9091 -sTCP:LISTEN',
|
|
75
|
+
' lsof -nP -iTCP:5436 -sTCP:LISTEN',
|
|
76
|
+
' lsof -nP -iTCP:6382 -sTCP:LISTEN',
|
|
77
|
+
' docker ps',
|
|
78
|
+
].join('\n'),
|
|
65
79
|
);
|
|
66
80
|
err.code = 'PORT_IN_USE';
|
|
67
81
|
throw err;
|