@nitsan-ai/ragsuite-test 0.1.0
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/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/ragsuite-test.js +21 -0
- package/package.json +31 -0
- package/src/commands/doctor.js +40 -0
- package/src/commands/init.js +255 -0
- package/src/commands/logs.js +67 -0
- package/src/commands/start.js +70 -0
- package/src/commands/stop.js +41 -0
- package/src/commands/update.js +101 -0
- package/src/commands/version.js +23 -0
- package/src/index.js +106 -0
- package/src/utils/args.js +152 -0
- package/src/utils/config.js +100 -0
- package/src/utils/distribution.js +39 -0
- package/src/utils/env-file.js +124 -0
- package/src/utils/log.js +15 -0
- package/src/utils/os-detect.js +12 -0
- package/src/utils/paths.js +124 -0
- package/src/utils/port.js +77 -0
- package/src/utils/prereqs.js +90 -0
- package/src/utils/prompt.js +57 -0
- package/src/utils/spawn.js +77 -0
- package/src/utils/zip-install.js +213 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { resolveRepoRoot } = require('../utils/paths');
|
|
6
|
+
const { readConfig } = require('../utils/config');
|
|
7
|
+
const { captureCommand } = require('../utils/spawn');
|
|
8
|
+
const { info, warn } = require('../utils/log');
|
|
9
|
+
|
|
10
|
+
const name = 'update';
|
|
11
|
+
const summary = 'Verify install path and print safe update steps';
|
|
12
|
+
|
|
13
|
+
function help() {
|
|
14
|
+
return `Usage: ragsuite-test update [options]
|
|
15
|
+
|
|
16
|
+
Verifies the install/repo path and prints safe update steps.
|
|
17
|
+
ZIP installs: download a newer ZIP and extract to a new dir (or backup + --force).
|
|
18
|
+
Checkout installs: optional git fetch + status (no reset).
|
|
19
|
+
|
|
20
|
+
Never runs docker compose down -v.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--repo-root <path> Monorepo / install root
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run(ctx) {
|
|
28
|
+
const repoRoot = resolveRepoRoot({
|
|
29
|
+
cwd: ctx.cwd,
|
|
30
|
+
repoRootFlag: ctx.globals.repoRoot,
|
|
31
|
+
env: ctx.env,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const cfg = readConfig(repoRoot);
|
|
35
|
+
const source = cfg && cfg.source === 'zip' ? 'zip' : 'checkout';
|
|
36
|
+
|
|
37
|
+
info(`Install path OK: ${repoRoot}`);
|
|
38
|
+
info(` source=${source}`);
|
|
39
|
+
info('');
|
|
40
|
+
|
|
41
|
+
if (source === 'zip') {
|
|
42
|
+
info('This install came from a ZIP. Safe update steps:');
|
|
43
|
+
info(' 1. ragsuite-test stop --repo-root ' + repoRoot);
|
|
44
|
+
info(' 2. Download a newer release ZIP (do not wipe Docker volumes)');
|
|
45
|
+
info(' 3. Extract to a NEW directory, e.g. --install-dir ~/ragsuite-test-v2');
|
|
46
|
+
info(' or: backup this dir, then init --from-zip … --force');
|
|
47
|
+
info(' 4. Copy your .env into the new install (do not commit secrets)');
|
|
48
|
+
info(' 5. ragsuite-test doctor --repo-root <new-dir>');
|
|
49
|
+
info(' 6. ragsuite-test start --repo-root <new-dir>');
|
|
50
|
+
info('');
|
|
51
|
+
info('Never: docker compose down -v (destroys Postgres/Chroma data).');
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
info('Safe update steps (checkout install):');
|
|
56
|
+
info(' 1. Stop the stack (ragsuite-test stop --repo-root …)');
|
|
57
|
+
info(' 2. Update sources (git pull when you have remote access)');
|
|
58
|
+
info(' 3. Docker: npm run rebuild or docker compose build');
|
|
59
|
+
info(' 4. Native: re-run backend/scripts/setup.sh if Python deps changed');
|
|
60
|
+
info(' 5. Start again (ragsuite-test start --repo-root …)');
|
|
61
|
+
info(' 6. Run doctor after upgrades');
|
|
62
|
+
info('');
|
|
63
|
+
|
|
64
|
+
const gitDir = path.join(repoRoot, '.git');
|
|
65
|
+
if (!fs.existsSync(gitDir)) {
|
|
66
|
+
warn('No .git directory — skip fetch/status (ZIP or packed tree).');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const remote = captureCommand('git', ['remote', '-v'], { cwd: repoRoot, env: ctx.env });
|
|
71
|
+
if (remote.status !== 0 || !String(remote.stdout || '').trim()) {
|
|
72
|
+
warn('No git remote configured — skip fetch.');
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
info('Git remotes:');
|
|
77
|
+
info(String(remote.stdout).trimEnd());
|
|
78
|
+
info('');
|
|
79
|
+
info('Running git fetch (non-destructive)…');
|
|
80
|
+
const fetch = captureCommand('git', ['fetch', '--all', '--prune'], {
|
|
81
|
+
cwd: repoRoot,
|
|
82
|
+
env: ctx.env,
|
|
83
|
+
});
|
|
84
|
+
if (fetch.status !== 0) {
|
|
85
|
+
warn(
|
|
86
|
+
`git fetch failed (exit ${fetch.status}). Private remotes need credentials. Continuing with status only.`,
|
|
87
|
+
);
|
|
88
|
+
if (fetch.stderr) warn(String(fetch.stderr).trimEnd());
|
|
89
|
+
} else {
|
|
90
|
+
info('git fetch OK');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const status = captureCommand('git', ['status', '-sb'], { cwd: repoRoot, env: ctx.env });
|
|
94
|
+
if (status.stdout) {
|
|
95
|
+
info('');
|
|
96
|
+
info(String(status.stdout).trimEnd());
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { name, summary, help, run };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { info } = require('../utils/log');
|
|
5
|
+
|
|
6
|
+
const name = 'version';
|
|
7
|
+
const summary = 'Print CLI package version';
|
|
8
|
+
|
|
9
|
+
function help() {
|
|
10
|
+
return `Usage: ragsuite-test version
|
|
11
|
+
|
|
12
|
+
Print @nitsan-ai/ragsuite-test version from package.json.
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run() {
|
|
17
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
18
|
+
const pkg = require(pkgPath);
|
|
19
|
+
info(`${pkg.name}@${pkg.version}`);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { name, summary, help, run };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { parseArgv } = require('./utils/args');
|
|
5
|
+
const { error, info } = require('./utils/log');
|
|
6
|
+
|
|
7
|
+
const commands = {
|
|
8
|
+
init: require('./commands/init'),
|
|
9
|
+
start: require('./commands/start'),
|
|
10
|
+
stop: require('./commands/stop'),
|
|
11
|
+
doctor: require('./commands/doctor'),
|
|
12
|
+
logs: require('./commands/logs'),
|
|
13
|
+
update: require('./commands/update'),
|
|
14
|
+
version: require('./commands/version'),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function globalHelp() {
|
|
18
|
+
const lines = [
|
|
19
|
+
'RAGSuite test CLI (experimental)',
|
|
20
|
+
'',
|
|
21
|
+
'Usage: ragsuite-test <command> [options]',
|
|
22
|
+
' (from cli/: node src/index.js <command> [options])',
|
|
23
|
+
'',
|
|
24
|
+
'Commands:',
|
|
25
|
+
];
|
|
26
|
+
for (const cmd of Object.values(commands)) {
|
|
27
|
+
lines.push(` ${cmd.name.padEnd(10)} ${cmd.summary}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push(
|
|
30
|
+
'',
|
|
31
|
+
'Global options:',
|
|
32
|
+
' --help, -h Show help',
|
|
33
|
+
' --repo-root <path> Monorepo / install root',
|
|
34
|
+
' --from-zip <path> init: extract app ZIP',
|
|
35
|
+
' --install-dir <path> init: ZIP target (default ~/ragsuite-test)',
|
|
36
|
+
' --from-release <tag> init: stub (not implemented)',
|
|
37
|
+
' --llm-api-key <key> init: CUSTOM_LLM_INTERNAL_API_KEY (--yes)',
|
|
38
|
+
' --force init: overwrite non-empty install dir / .env',
|
|
39
|
+
' --yes, -y Non-interactive',
|
|
40
|
+
' --mode <docker|native> Override .ragsuite-test/config.json / RAGSUITE_MODE',
|
|
41
|
+
' --dry-run Print actions without running (start/stop/doctor/logs)',
|
|
42
|
+
'',
|
|
43
|
+
'Env: RAGSUITE_REPO_ROOT or RAGSUITE_INSTALL_DIR',
|
|
44
|
+
'',
|
|
45
|
+
'Run ragsuite-test <command> --help for command details.',
|
|
46
|
+
);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main(argv = process.argv) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = parseArgv(argv);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error(err.message || String(err));
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { globals, command, commandArgs, commandHelp } = parsed;
|
|
60
|
+
|
|
61
|
+
if (!command) {
|
|
62
|
+
info(globalHelp());
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cmd = commands[command];
|
|
67
|
+
if (!cmd) {
|
|
68
|
+
error(`Unknown command: ${command}`);
|
|
69
|
+
info('');
|
|
70
|
+
info(globalHelp());
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (globals.help || commandHelp) {
|
|
75
|
+
info(cmd.help());
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ctx = {
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
env: process.env,
|
|
82
|
+
globals,
|
|
83
|
+
commandArgs,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = cmd.run(ctx);
|
|
88
|
+
const code = typeof result?.then === 'function' ? await result : result;
|
|
89
|
+
return typeof code === 'number' ? code : 0;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err && (err.code === 'NOT_REPO_ROOT' || err.code === 'USAGE' || err.code === 'ZIP_SLIP' || err.code === 'INSTALL_DIR_NONEMPTY' || err.code === 'ZIP_INVALID' || err.code === 'ZIP_MISSING' || err.code === 'ENV_EXISTS' || err.code === 'PORT_IN_USE' || err.code === 'LLM_KEY_INVALID')) {
|
|
92
|
+
error(err.message);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
error(err.message || String(err));
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (require.main === module) {
|
|
101
|
+
main().then((code) => {
|
|
102
|
+
process.exit(code);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { main, commands, globalHelp };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
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
|
+
*/
|
|
8
|
+
function parseArgv(argv) {
|
|
9
|
+
const tokens = argv.slice(2);
|
|
10
|
+
const globals = {
|
|
11
|
+
help: false,
|
|
12
|
+
repoRoot: null,
|
|
13
|
+
dryRun: false,
|
|
14
|
+
mode: null,
|
|
15
|
+
fromZip: null,
|
|
16
|
+
fromRelease: null,
|
|
17
|
+
installDir: null,
|
|
18
|
+
force: false,
|
|
19
|
+
yes: false,
|
|
20
|
+
llmApiKey: null,
|
|
21
|
+
};
|
|
22
|
+
const rest = [];
|
|
23
|
+
let command = null;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
26
|
+
const t = tokens[i];
|
|
27
|
+
|
|
28
|
+
if (t === '--help' || t === '-h') {
|
|
29
|
+
if (command) {
|
|
30
|
+
rest.push(t);
|
|
31
|
+
} else {
|
|
32
|
+
globals.help = true;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (t === '--dry-run') {
|
|
38
|
+
globals.dryRun = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (t === '--force') {
|
|
43
|
+
globals.force = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (t === '--yes' || t === '-y') {
|
|
48
|
+
globals.yes = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (t === '--repo-root' || t.startsWith('--repo-root=')) {
|
|
53
|
+
const value = t.startsWith('--repo-root=')
|
|
54
|
+
? t.slice('--repo-root='.length)
|
|
55
|
+
: tokens[++i];
|
|
56
|
+
if (!value || value.startsWith('-')) {
|
|
57
|
+
const err = new Error('--repo-root requires a path');
|
|
58
|
+
err.code = 'USAGE';
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
globals.repoRoot = value;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (t === '--mode' || t.startsWith('--mode=')) {
|
|
66
|
+
const value = t.startsWith('--mode=') ? t.slice('--mode='.length) : tokens[++i];
|
|
67
|
+
if (!value || String(value).startsWith('-')) {
|
|
68
|
+
const err = new Error('--mode requires docker or native');
|
|
69
|
+
err.code = 'USAGE';
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
globals.mode = normalizeMode(value);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (t === '--from-zip' || t.startsWith('--from-zip=')) {
|
|
77
|
+
const value = t.startsWith('--from-zip=')
|
|
78
|
+
? t.slice('--from-zip='.length)
|
|
79
|
+
: tokens[++i];
|
|
80
|
+
if (!value || value.startsWith('-')) {
|
|
81
|
+
const err = new Error('--from-zip requires a path to a .zip file');
|
|
82
|
+
err.code = 'USAGE';
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
globals.fromZip = value;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (t === '--from-release' || t.startsWith('--from-release=')) {
|
|
90
|
+
const value = t.startsWith('--from-release=')
|
|
91
|
+
? t.slice('--from-release='.length)
|
|
92
|
+
: tokens[++i];
|
|
93
|
+
if (!value || value.startsWith('-')) {
|
|
94
|
+
const err = new Error('--from-release requires a tag or "latest"');
|
|
95
|
+
err.code = 'USAGE';
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
globals.fromRelease = value;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (t === '--install-dir' || t.startsWith('--install-dir=')) {
|
|
103
|
+
const value = t.startsWith('--install-dir=')
|
|
104
|
+
? t.slice('--install-dir='.length)
|
|
105
|
+
: tokens[++i];
|
|
106
|
+
if (!value || value.startsWith('-')) {
|
|
107
|
+
const err = new Error('--install-dir requires a path');
|
|
108
|
+
err.code = 'USAGE';
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
globals.installDir = value;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (t === '--llm-api-key' || t.startsWith('--llm-api-key=')) {
|
|
116
|
+
const value = t.startsWith('--llm-api-key=')
|
|
117
|
+
? t.slice('--llm-api-key='.length)
|
|
118
|
+
: tokens[++i];
|
|
119
|
+
if (!value || value.startsWith('-')) {
|
|
120
|
+
const err = new Error('--llm-api-key requires a value');
|
|
121
|
+
err.code = 'USAGE';
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
globals.llmApiKey = value;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!command && !t.startsWith('-')) {
|
|
129
|
+
command = t;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
rest.push(t);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const commandHelp = rest.includes('--help') || rest.includes('-h');
|
|
137
|
+
const commandArgs = rest.filter((a) => a !== '--help' && a !== '-h');
|
|
138
|
+
|
|
139
|
+
return { globals, command, commandArgs, commandHelp };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeMode(value) {
|
|
143
|
+
const m = String(value).trim().toLowerCase();
|
|
144
|
+
if (m !== 'docker' && m !== 'native') {
|
|
145
|
+
const err = new Error(`Invalid mode "${value}" (use docker or native)`);
|
|
146
|
+
err.code = 'USAGE';
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
return m;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { parseArgv, normalizeMode };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const CONFIG_VERSION = 1;
|
|
7
|
+
const CONFIG_DIR = '.ragsuite-test';
|
|
8
|
+
|
|
9
|
+
function configPath(repoRoot) {
|
|
10
|
+
return path.join(repoRoot, CONFIG_DIR, 'config.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defaultConfig(repoRoot, mode = 'docker') {
|
|
14
|
+
const root = path.resolve(repoRoot);
|
|
15
|
+
return {
|
|
16
|
+
version: CONFIG_VERSION,
|
|
17
|
+
mode: mode === 'native' ? 'native' : 'docker',
|
|
18
|
+
source: 'checkout',
|
|
19
|
+
repoRoot: root,
|
|
20
|
+
installDir: root,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readConfig(repoRoot) {
|
|
25
|
+
const file = configPath(repoRoot);
|
|
26
|
+
if (!fs.existsSync(file)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
31
|
+
if (!raw || typeof raw !== 'object') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const source = raw.source === 'zip' ? 'zip' : 'checkout';
|
|
35
|
+
return {
|
|
36
|
+
version: Number(raw.version) || CONFIG_VERSION,
|
|
37
|
+
mode: raw.mode === 'native' ? 'native' : 'docker',
|
|
38
|
+
source,
|
|
39
|
+
repoRoot: path.resolve(String(raw.repoRoot || repoRoot)),
|
|
40
|
+
installDir: path.resolve(String(raw.installDir || raw.repoRoot || repoRoot)),
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeConfig(repoRoot, partial = {}) {
|
|
48
|
+
const dir = path.join(repoRoot, CONFIG_DIR);
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
const existing = readConfig(repoRoot) || defaultConfig(repoRoot);
|
|
51
|
+
const source =
|
|
52
|
+
partial.source === 'zip' || partial.source === 'checkout'
|
|
53
|
+
? partial.source
|
|
54
|
+
: existing.source || 'checkout';
|
|
55
|
+
const next = {
|
|
56
|
+
...existing,
|
|
57
|
+
...partial,
|
|
58
|
+
version: CONFIG_VERSION,
|
|
59
|
+
source,
|
|
60
|
+
repoRoot: path.resolve(partial.repoRoot || existing.repoRoot || repoRoot),
|
|
61
|
+
installDir: path.resolve(
|
|
62
|
+
partial.installDir || existing.installDir || partial.repoRoot || existing.repoRoot || repoRoot,
|
|
63
|
+
),
|
|
64
|
+
mode: (partial.mode || existing.mode) === 'native' ? 'native' : 'docker',
|
|
65
|
+
};
|
|
66
|
+
fs.writeFileSync(configPath(repoRoot), `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve runtime mode: CLI --mode > config > RAGSUITE_MODE > docker.
|
|
72
|
+
*/
|
|
73
|
+
function resolveMode({ flagMode, repoRoot, env = process.env } = {}) {
|
|
74
|
+
if (flagMode === 'docker' || flagMode === 'native') {
|
|
75
|
+
return flagMode;
|
|
76
|
+
}
|
|
77
|
+
if (repoRoot) {
|
|
78
|
+
const cfg = readConfig(repoRoot);
|
|
79
|
+
if (cfg && (cfg.mode === 'docker' || cfg.mode === 'native')) {
|
|
80
|
+
return cfg.mode;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const fromEnv = String(env.RAGSUITE_MODE || '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
if (fromEnv === 'native') {
|
|
87
|
+
return 'native';
|
|
88
|
+
}
|
|
89
|
+
return 'docker';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
CONFIG_VERSION,
|
|
94
|
+
CONFIG_DIR,
|
|
95
|
+
configPath,
|
|
96
|
+
defaultConfig,
|
|
97
|
+
readConfig,
|
|
98
|
+
writeConfig,
|
|
99
|
+
resolveMode,
|
|
100
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Distribution modes for the testing CLI.
|
|
5
|
+
*
|
|
6
|
+
* - local-checkout: --repo-root / env / walk-up (path C)
|
|
7
|
+
* - zip-bundle: init --from-zip (primary for private testing)
|
|
8
|
+
* Registry/GHCR compose remains stubbed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MODE_LOCAL = 'local-checkout';
|
|
12
|
+
const MODE_ZIP = 'zip-bundle';
|
|
13
|
+
const MODE_REGISTRY = 'registry-compose';
|
|
14
|
+
|
|
15
|
+
function assertSupportedInstall() {
|
|
16
|
+
// checkout + zip supported
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveComposeBundle() {
|
|
20
|
+
const err = new Error(
|
|
21
|
+
'Registry/compose (GHCR) install is not available yet. Use: ragsuite-test init --from-zip <app.zip> or --repo-root <checkout>.',
|
|
22
|
+
);
|
|
23
|
+
err.code = 'DIST_NOT_READY';
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function installHint() {
|
|
28
|
+
return 'Supported: ragsuite-test init --from-zip <app.zip> [--install-dir ~/ragsuite-test] OR --repo-root <checkout>. GitHub Release download (--from-release) is not ready yet.';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
MODE_LOCAL,
|
|
33
|
+
MODE_ZIP,
|
|
34
|
+
MODE_REGISTRY,
|
|
35
|
+
mode: MODE_ZIP,
|
|
36
|
+
assertSupportedInstall,
|
|
37
|
+
resolveComposeBundle,
|
|
38
|
+
installHint,
|
|
39
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { warn } = require('./log');
|
|
7
|
+
|
|
8
|
+
function generateJwtSecret() {
|
|
9
|
+
return crypto.randomBytes(32).toString('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateCiLlmKey() {
|
|
13
|
+
return `ci-test-llm-key-${crypto.randomBytes(8).toString('hex')}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isPlaceholderSecret(value) {
|
|
17
|
+
const v = String(value || '').trim();
|
|
18
|
+
return !v || v.startsWith('change-me');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build .env content from .env.example with overrides.
|
|
23
|
+
*/
|
|
24
|
+
function buildEnvFromExample(examplePath, overrides = {}) {
|
|
25
|
+
if (!fs.existsSync(examplePath)) {
|
|
26
|
+
const err = new Error(`Missing .env.example at ${examplePath}`);
|
|
27
|
+
err.code = 'ENV_EXAMPLE_MISSING';
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
const raw = fs.readFileSync(examplePath, 'utf8');
|
|
31
|
+
const lines = raw.split(/\r?\n/);
|
|
32
|
+
const keysSet = new Set(Object.keys(overrides));
|
|
33
|
+
const out = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
38
|
+
if (!m) {
|
|
39
|
+
out.push(line);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const key = m[1];
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(overrides, key)) {
|
|
44
|
+
out.push(`${key}=${overrides[key]}`);
|
|
45
|
+
seen.add(key);
|
|
46
|
+
} else {
|
|
47
|
+
out.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const key of keysSet) {
|
|
52
|
+
if (!seen.has(key)) {
|
|
53
|
+
out.push(`${key}=${overrides[key]}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${out.join('\n').replace(/\n+$/, '')}\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write .env. Refuse overwrite unless force.
|
|
62
|
+
* @returns {{ jwt: string, llmKey: string, smtpSkipped: boolean }}
|
|
63
|
+
*/
|
|
64
|
+
function writeInstallEnv(repoRoot, options = {}) {
|
|
65
|
+
const {
|
|
66
|
+
force = false,
|
|
67
|
+
jwtSecret = generateJwtSecret(),
|
|
68
|
+
llmApiKey,
|
|
69
|
+
smtpSkipped = true,
|
|
70
|
+
} = options;
|
|
71
|
+
|
|
72
|
+
const envPath = path.join(repoRoot, '.env');
|
|
73
|
+
const examplePath = path.join(repoRoot, '.env.example');
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(envPath) && !force) {
|
|
76
|
+
const err = new Error(
|
|
77
|
+
`.env already exists at ${envPath}. Pass --force to regenerate from .env.example (will overwrite).`,
|
|
78
|
+
);
|
|
79
|
+
err.code = 'ENV_EXISTS';
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isPlaceholderSecret(llmApiKey)) {
|
|
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
|
+
|
|
89
|
+
const overrides = {
|
|
90
|
+
JWT_SECRET_KEY: jwtSecret,
|
|
91
|
+
CUSTOM_LLM_INTERNAL_API_KEY: llmApiKey,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (smtpSkipped) {
|
|
95
|
+
// Backend refuses to start when SMTP_PASSWORD is empty (main.py lifespan guard).
|
|
96
|
+
// Use a non-empty smoke placeholder so Docker/native can boot; real email still fails.
|
|
97
|
+
overrides.SMTP_PASSWORD = 'ci-smoke-smtp-not-for-production';
|
|
98
|
+
warn('');
|
|
99
|
+
warn('WARNING: SMTP skipped for smoke testing (placeholder SMTP_PASSWORD).');
|
|
100
|
+
warn('Backend will start, but invites, password reset, and email 2FA will FAIL until real SMTP_* is set.');
|
|
101
|
+
warn('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = buildEnvFromExample(examplePath, overrides);
|
|
105
|
+
fs.writeFileSync(envPath, content, 'utf8');
|
|
106
|
+
return { jwt: jwtSecret, llmKey: llmApiKey, smtpSkipped: Boolean(smtpSkipped), envPath };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readEnvValue(envPath, key) {
|
|
110
|
+
if (!fs.existsSync(envPath)) return null;
|
|
111
|
+
const text = fs.readFileSync(envPath, 'utf8');
|
|
112
|
+
const re = new RegExp(`^${key}=(.*)$`, 'm');
|
|
113
|
+
const m = text.match(re);
|
|
114
|
+
return m ? m[1].trim() : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
generateJwtSecret,
|
|
119
|
+
generateCiLlmKey,
|
|
120
|
+
isPlaceholderSecret,
|
|
121
|
+
buildEnvFromExample,
|
|
122
|
+
writeInstallEnv,
|
|
123
|
+
readEnvValue,
|
|
124
|
+
};
|
package/src/utils/log.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function info(message) {
|
|
4
|
+
console.log(message);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function warn(message) {
|
|
8
|
+
console.error(`warn: ${message}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function error(message) {
|
|
12
|
+
console.error(`error: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { info, warn, error };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function detectOs() {
|
|
4
|
+
const platform = process.platform;
|
|
5
|
+
let label = platform;
|
|
6
|
+
if (platform === 'darwin') label = 'macOS';
|
|
7
|
+
else if (platform === 'linux') label = 'Linux';
|
|
8
|
+
else if (platform === 'win32') label = 'Windows';
|
|
9
|
+
return { platform, label };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = { detectOs };
|