vimo-oss 2.2.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/README.md +61 -0
- package/bin/vimo.mjs +656 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# vimo-oss — one-command VIMO
|
|
2
|
+
|
|
3
|
+
VIMO is a marketing-operations platform with its own AI. This package is a thin
|
|
4
|
+
launcher that installs and runs VIMO locally so anyone can start it without
|
|
5
|
+
knowing how the pieces fit together. No git, no build tools, no API keys.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g vimo-oss
|
|
11
|
+
vimo
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
On Windows you can type `VIMO` (any case) in cmd or PowerShell; on macOS and
|
|
15
|
+
Linux both `vimo` and `VIMO` work after install.
|
|
16
|
+
|
|
17
|
+
On first run it downloads VIMO, installs its components, builds the app, starts
|
|
18
|
+
it on a free port, and opens your browser. First run takes a few minutes;
|
|
19
|
+
every later run is fast. Press `Ctrl+C` to stop.
|
|
20
|
+
|
|
21
|
+
## From this repo (developers)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install
|
|
25
|
+
cd packages/cli && npm link
|
|
26
|
+
vimo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or run the script directly: `node packages/cli/bin/vimo.mjs`. When run inside a
|
|
30
|
+
VIMO checkout, it uses that checkout instead of downloading one — but note it
|
|
31
|
+
starts the **built production app** (`npm run build:app` first if needed), not
|
|
32
|
+
the dev servers. Use `npm run dev` at the repo root for development with hot reload.
|
|
33
|
+
|
|
34
|
+
## Flags
|
|
35
|
+
|
|
36
|
+
| Flag | What it does |
|
|
37
|
+
| ------------ | ------------------------------------------------------------------ |
|
|
38
|
+
| `--repo` | Use a specific VIMO checkout instead of auto-detecting |
|
|
39
|
+
| `--port` | Preferred port (default 3000; busy ports are skipped automatically) |
|
|
40
|
+
| `--no-open` | Start without opening a browser |
|
|
41
|
+
| `--reset` | Reinstall dependencies and rebuild from scratch |
|
|
42
|
+
| `--update` | Refresh VIMO to the latest version (keeps your data and settings) |
|
|
43
|
+
| `--doctor` | Check whether this computer is ready to run VIMO |
|
|
44
|
+
| `--version` | Print the version |
|
|
45
|
+
| `--help` | Show usage |
|
|
46
|
+
|
|
47
|
+
## Where does VIMO live?
|
|
48
|
+
|
|
49
|
+
1. `--repo <path>` if given,
|
|
50
|
+
2. `VIMO_HOME` environment variable if set,
|
|
51
|
+
3. the current directory, when it is a VIMO checkout,
|
|
52
|
+
4. otherwise `~/.vimo` (downloaded automatically on first run).
|
|
53
|
+
|
|
54
|
+
Your content, settings, and connected accounts live in `<install>/data`
|
|
55
|
+
(SQLite). `--update` preserves them.
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Node.js 20–22 LTS ("Current" versions may need extra build tools for native modules).
|
|
60
|
+
- Internet access for the first download.
|
|
61
|
+
- Nothing else — no git, no Docker, no compilers on supported Node versions.
|
package/bin/vimo.mjs
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VIMO — one-command launcher.
|
|
4
|
+
*
|
|
5
|
+
* Built for people who have a brand to grow, not a DevOps team. From nothing
|
|
6
|
+
* to a running VIMO in your browser:
|
|
7
|
+
*
|
|
8
|
+
* npm i -g @vimo-oss/cli
|
|
9
|
+
* vimo (or: VIMO)
|
|
10
|
+
*
|
|
11
|
+
* What it does:
|
|
12
|
+
* 1. Checks Node.js (>=20) and explains how to get it if missing.
|
|
13
|
+
* 2. Finds the VIMO app code (cwd if you're in the repo, ~/.vimo otherwise).
|
|
14
|
+
* No git required — it downloads a ready-made archive.
|
|
15
|
+
* 3. First run: downloads, installs dependencies and builds (~a few minutes,
|
|
16
|
+
* automatic). Later runs start fast.
|
|
17
|
+
* 4. Starts VIMO on a free port and opens your browser.
|
|
18
|
+
* 5. Press Ctrl+C to stop everything cleanly.
|
|
19
|
+
*
|
|
20
|
+
* Flags:
|
|
21
|
+
* --repo <path> Use a specific VIMO checkout instead of auto-detecting.
|
|
22
|
+
* --port <n> Preferred port to serve on (default 3000; busy ports are skipped).
|
|
23
|
+
* --no-open Start without opening a browser.
|
|
24
|
+
* --reset Reinstall dependencies and rebuild from scratch.
|
|
25
|
+
* --update Refresh VIMO to the latest version (your data is kept).
|
|
26
|
+
* --doctor Check this computer is ready to run VIMO.
|
|
27
|
+
* --version Print the version.
|
|
28
|
+
* --help Show this help.
|
|
29
|
+
*/
|
|
30
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
31
|
+
import crypto from 'node:crypto';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import net from 'node:net';
|
|
34
|
+
import os from 'node:os';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import process from 'node:process';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
|
|
39
|
+
const REPO_TARBALL_URL = 'https://github.com/Krish-1507/VIMO_OSS/archive/refs/heads/main.tar.gz';
|
|
40
|
+
const TARBALL_ROOT_DIR = 'VIMO_OSS-main'; // top-level folder inside the archive
|
|
41
|
+
const DEFAULT_PORT = 3000;
|
|
42
|
+
const PORT_ATTEMPTS = 50;
|
|
43
|
+
const HEALTH_TIMEOUT_MS = 120_000;
|
|
44
|
+
const MIN_NODE_MAJOR = 20;
|
|
45
|
+
|
|
46
|
+
const args = process.argv.slice(2);
|
|
47
|
+
const getArg = (name) => {
|
|
48
|
+
const i = args.indexOf(name);
|
|
49
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let pkgVersion = '0.0.0';
|
|
53
|
+
try {
|
|
54
|
+
pkgVersion = JSON.parse(
|
|
55
|
+
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
56
|
+
).version;
|
|
57
|
+
} catch {
|
|
58
|
+
// keep fallback version
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* -------------------------------------------------------------------------- */
|
|
62
|
+
/* Output helpers */
|
|
63
|
+
/* -------------------------------------------------------------------------- */
|
|
64
|
+
|
|
65
|
+
const CYAN = '\x1b[36m';
|
|
66
|
+
const GREEN = '\x1b[32m';
|
|
67
|
+
const YELLOW = '\x1b[33m';
|
|
68
|
+
const RED = '\x1b[31m';
|
|
69
|
+
const DIM = '\x1b[2m';
|
|
70
|
+
const RESET = '\x1b[0m';
|
|
71
|
+
|
|
72
|
+
function log(msg) {
|
|
73
|
+
console.log(`${CYAN}[vimo]${RESET} ${msg}`);
|
|
74
|
+
}
|
|
75
|
+
function ok(msg) {
|
|
76
|
+
console.log(`${GREEN}[vimo]${RESET} ${msg}`);
|
|
77
|
+
}
|
|
78
|
+
function warn(msg) {
|
|
79
|
+
console.warn(`${YELLOW}[vimo]${RESET} ${msg}`);
|
|
80
|
+
}
|
|
81
|
+
function fail(msg) {
|
|
82
|
+
console.error(`\n${RED}[vimo] ${msg}${RESET}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function printBanner() {
|
|
87
|
+
// Hand-set block letters spelling V I M O (verified in monospace).
|
|
88
|
+
console.log(
|
|
89
|
+
`\n${CYAN}` +
|
|
90
|
+
'__ __ _ __ __ ___ \n' +
|
|
91
|
+
'\\ \\ / / | | | \\/ | / _ \\ \n' +
|
|
92
|
+
' \\ V / | | | |\\/| | | (_) |\n' +
|
|
93
|
+
' \\_/ |_| |_| |_| \\___/ \n' +
|
|
94
|
+
`${RESET}`,
|
|
95
|
+
);
|
|
96
|
+
console.log(` ${GREEN}VIMO OSS${RESET} ${DIM}- Vibe Marketing Operations - v${pkgVersion}${RESET}\n`);
|
|
97
|
+
console.log(' Your marketing operations app is starting up.\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* -------------------------------------------------------------------------- */
|
|
101
|
+
/* Flags */
|
|
102
|
+
/* -------------------------------------------------------------------------- */
|
|
103
|
+
|
|
104
|
+
if (args.includes('--version')) {
|
|
105
|
+
console.log(`vimo ${pkgVersion}`);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
110
|
+
console.log(`vimo ${pkgVersion}
|
|
111
|
+
|
|
112
|
+
Starts VIMO locally and opens it in your browser.
|
|
113
|
+
|
|
114
|
+
Usage: vimo [flags]
|
|
115
|
+
|
|
116
|
+
Flags:
|
|
117
|
+
--repo <path> Use a specific VIMO checkout.
|
|
118
|
+
--port <n> Preferred port (default ${DEFAULT_PORT}; busy ports are skipped automatically).
|
|
119
|
+
--no-open Start without opening a browser.
|
|
120
|
+
--reset Reinstall dependencies and rebuild from scratch.
|
|
121
|
+
--update Refresh VIMO to the latest version (your data is kept).
|
|
122
|
+
--doctor Check whether this computer is ready to run VIMO.
|
|
123
|
+
--version Print the version.
|
|
124
|
+
--help Show this help.
|
|
125
|
+
|
|
126
|
+
First run downloads and builds VIMO automatically — no other tools needed.`);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const portBase = Number(getArg('--port') || DEFAULT_PORT);
|
|
131
|
+
const noOpen = args.includes('--no-open');
|
|
132
|
+
const doReset = args.includes('--reset');
|
|
133
|
+
const doUpdate = args.includes('--update');
|
|
134
|
+
|
|
135
|
+
/* -------------------------------------------------------------------------- */
|
|
136
|
+
/* Environment checks */
|
|
137
|
+
/* -------------------------------------------------------------------------- */
|
|
138
|
+
|
|
139
|
+
function nodeMajor() {
|
|
140
|
+
return Number(process.versions.node.split('.')[0]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function checkNode() {
|
|
144
|
+
const major = nodeMajor();
|
|
145
|
+
if (major < MIN_NODE_MAJOR) {
|
|
146
|
+
fail(
|
|
147
|
+
`VIMO needs Node.js version ${MIN_NODE_MAJOR} or newer, but this computer has ` +
|
|
148
|
+
`${process.versions.node}.\n` +
|
|
149
|
+
`Please install the current Node.js from https://nodejs.org/en/download ` +
|
|
150
|
+
`(choose the "LTS" version), then run \`vimo\` again.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// Very fresh Node versions may lack prebuilt binaries for some of VIMO's
|
|
154
|
+
// native modules, which can make the first install slow or fail.
|
|
155
|
+
if (major > 22) {
|
|
156
|
+
warn(
|
|
157
|
+
`You are using Node.js ${process.versions.node}. VIMO works best on Node 20–22 ` +
|
|
158
|
+
`(the "LTS" release). If the install below fails, switching to LTS fixes it.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isWindows() {
|
|
164
|
+
return process.platform === 'win32';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function npmCmd() {
|
|
168
|
+
return isWindows() ? 'npm.cmd' : 'npm';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Locate the OS "tar" tool. Present by default on Windows 10+, macOS, Linux. */
|
|
172
|
+
function findTar() {
|
|
173
|
+
if (isWindows()) {
|
|
174
|
+
const sysTar = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe');
|
|
175
|
+
if (fs.existsSync(sysTar)) return sysTar;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const res = spawnSync('tar', ['--version'], { stdio: 'ignore' });
|
|
179
|
+
return res.error ? null : 'tar';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function runLive(cmd, cmdArgs, opts, label) {
|
|
183
|
+
log(label);
|
|
184
|
+
const res = spawnSync(cmd, cmdArgs, { stdio: 'inherit', ...opts });
|
|
185
|
+
if (res.status !== 0 || res.error) {
|
|
186
|
+
fail(`${label.replace(/\.\.\.$/, '')} didn't finish. Check the messages above, then run \`vimo\` again.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* -------------------------------------------------------------------------- */
|
|
191
|
+
/* Repo resolution & download */
|
|
192
|
+
/* -------------------------------------------------------------------------- */
|
|
193
|
+
|
|
194
|
+
function isVimoRepo(dir) {
|
|
195
|
+
try {
|
|
196
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
197
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
198
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
199
|
+
return (
|
|
200
|
+
pkg.name === 'vimo' &&
|
|
201
|
+
Array.isArray(pkg.workspaces) &&
|
|
202
|
+
fs.existsSync(path.join(dir, 'packages'))
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function homeDir() {
|
|
210
|
+
return path.join(os.homedir(), '.vimo');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function downloadFile(url, dest) {
|
|
214
|
+
let res;
|
|
215
|
+
try {
|
|
216
|
+
res = await fetch(url, { redirect: 'follow' });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
fail(
|
|
219
|
+
`Couldn't reach the internet to download VIMO (${err.message}).\n` +
|
|
220
|
+
'Check your connection and run `vimo` again.',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (!res.ok || !res.body) {
|
|
224
|
+
fail(`Download failed (HTTP ${res.status}). Please try again in a minute.`);
|
|
225
|
+
}
|
|
226
|
+
const total = Number(res.headers.get('content-length') || 0);
|
|
227
|
+
let received = 0;
|
|
228
|
+
let lastPct = -10;
|
|
229
|
+
|
|
230
|
+
const out = fs.createWriteStream(dest);
|
|
231
|
+
const drained = () =>
|
|
232
|
+
new Promise((resolve) => {
|
|
233
|
+
out.once('drain', resolve);
|
|
234
|
+
});
|
|
235
|
+
for await (const chunk of res.body) {
|
|
236
|
+
received += chunk.length;
|
|
237
|
+
if (!out.write(chunk)) await drained();
|
|
238
|
+
if (total > 0) {
|
|
239
|
+
const pct = Math.floor((received / total) * 100);
|
|
240
|
+
if (pct >= lastPct + 10 && pct < 100) {
|
|
241
|
+
lastPct = pct;
|
|
242
|
+
log(`Downloading VIMO… ${pct}%`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await new Promise((resolve, reject) => {
|
|
247
|
+
out.end(resolve);
|
|
248
|
+
out.on('error', reject);
|
|
249
|
+
});
|
|
250
|
+
log('Downloading VIMO… done.');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function rmRf(dir) {
|
|
254
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Extract the archive into destDir; returns path to the extracted repo folder. */
|
|
258
|
+
function extractArchive(archivePath, destDir, tarBin) {
|
|
259
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
260
|
+
const res = spawnSync(tarBin, ['-xzf', archivePath, '-C', destDir], { stdio: 'ignore' });
|
|
261
|
+
if (res.status !== 0 || res.error) {
|
|
262
|
+
fail("Couldn't unpack the downloaded files. Run `vimo` again — if it keeps failing, run `vimo doctor`.");
|
|
263
|
+
}
|
|
264
|
+
const extracted = path.join(destDir, TARBALL_ROOT_DIR);
|
|
265
|
+
if (!isVimoRepo(extracted)) {
|
|
266
|
+
fail('The downloaded package looks damaged. Delete it and run `vimo` again.');
|
|
267
|
+
}
|
|
268
|
+
return extracted;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Download the latest code into targetDir. If oldRepo exists, `.env` and the
|
|
273
|
+
* `data/` folder (your content, settings and connected accounts) are carried over.
|
|
274
|
+
*/
|
|
275
|
+
async function downloadLatest(targetDir, oldRepo) {
|
|
276
|
+
const home = path.dirname(targetDir); // parent used for temp artifacts
|
|
277
|
+
const marker = `${targetDir}.incomplete`;
|
|
278
|
+
const staging = path.join(home, `.vimo-staging-${Date.now()}`);
|
|
279
|
+
const archive = path.join(home, `.vimo-download-${Date.now()}.tar.gz`);
|
|
280
|
+
|
|
281
|
+
fs.mkdirSync(home, { recursive: true });
|
|
282
|
+
fs.writeFileSync(marker, String(Date.now()));
|
|
283
|
+
try {
|
|
284
|
+
await downloadFile(REPO_TARBALL_URL, archive);
|
|
285
|
+
const extracted = extractArchive(archive, staging, findTar());
|
|
286
|
+
|
|
287
|
+
if (oldRepo && fs.existsSync(oldRepo)) {
|
|
288
|
+
for (const keep of ['.env', 'data']) {
|
|
289
|
+
const src = path.join(oldRepo, keep);
|
|
290
|
+
const dst = path.join(extracted, keep);
|
|
291
|
+
if (fs.existsSync(src)) {
|
|
292
|
+
fs.cpSync(src, dst, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
rmRf(targetDir);
|
|
298
|
+
fs.renameSync(extracted, targetDir);
|
|
299
|
+
ok('VIMO downloaded.');
|
|
300
|
+
} finally {
|
|
301
|
+
try {
|
|
302
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
303
|
+
} catch {}
|
|
304
|
+
try {
|
|
305
|
+
fs.rmSync(archive, { force: true });
|
|
306
|
+
} catch {}
|
|
307
|
+
try {
|
|
308
|
+
fs.rmSync(marker, { force: true });
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveRepo() {
|
|
314
|
+
const fromFlag = getArg('--repo');
|
|
315
|
+
if (fromFlag) {
|
|
316
|
+
const p = path.resolve(fromFlag);
|
|
317
|
+
if (!isVimoRepo(p)) fail(`--repo ${fromFlag} is not a VIMO checkout.`);
|
|
318
|
+
return p;
|
|
319
|
+
}
|
|
320
|
+
if (process.env.VIMO_HOME) {
|
|
321
|
+
const p = path.resolve(process.env.VIMO_HOME);
|
|
322
|
+
if (!isVimoRepo(p)) fail(`VIMO_HOME (${process.env.VIMO_HOME}) is not a VIMO checkout.`);
|
|
323
|
+
return p;
|
|
324
|
+
}
|
|
325
|
+
if (isVimoRepo(process.cwd())) {
|
|
326
|
+
log(`Using the VIMO copy in ${process.cwd()}`);
|
|
327
|
+
return process.cwd();
|
|
328
|
+
}
|
|
329
|
+
const home = homeDir();
|
|
330
|
+
const marker = `${home}.incomplete`;
|
|
331
|
+
const staleMarker = fs.existsSync(marker) && !fs.existsSync(home);
|
|
332
|
+
if (staleMarker) {
|
|
333
|
+
// A previous download was interrupted before anything was installed.
|
|
334
|
+
try {
|
|
335
|
+
fs.rmSync(marker, { force: true });
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
return home; // may not exist yet — ensureRepo handles it
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function ensureRepo(repo) {
|
|
342
|
+
if (doUpdate && repo === homeDir()) {
|
|
343
|
+
log('Checking for a newer version of VIMO…');
|
|
344
|
+
await downloadLatest(repo, repo);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (fs.existsSync(repo)) {
|
|
348
|
+
if (!isVimoRepo(repo)) {
|
|
349
|
+
fail(
|
|
350
|
+
`${repo} already exists but isn't a VIMO installation.\n` +
|
|
351
|
+
'If you don\'t recognize it, rename or delete that folder and run `vimo` again.',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
log('Welcome! Setting up VIMO on this computer (first run only).');
|
|
357
|
+
await downloadLatest(repo, null);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* -------------------------------------------------------------------------- */
|
|
361
|
+
/* Configuration (.env) */
|
|
362
|
+
/* -------------------------------------------------------------------------- */
|
|
363
|
+
|
|
364
|
+
function upsertEnvLine(content, key, value) {
|
|
365
|
+
const re = new RegExp(`^#?\\s*${key}=.*$`, 'm');
|
|
366
|
+
if (re.test(content)) return content.replace(re, `${key}=${value}`);
|
|
367
|
+
return `${content.replace(/\s*$/, '')}\n${key}=${value}\n`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ensureEnv(repo, managedHome) {
|
|
371
|
+
const envFile = path.join(repo, '.env');
|
|
372
|
+
const example = path.join(repo, '.env.example');
|
|
373
|
+
let content = '';
|
|
374
|
+
let created = false;
|
|
375
|
+
if (fs.existsSync(envFile)) {
|
|
376
|
+
content = fs.readFileSync(envFile, 'utf8');
|
|
377
|
+
} else if (fs.existsSync(example)) {
|
|
378
|
+
content = fs.readFileSync(example, 'utf8');
|
|
379
|
+
created = true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const keyRe = /^#?\s*ENCRYPTION_KEY=(.*)$/m;
|
|
383
|
+
const currentKey = (content.match(keyRe) || [])[1] || '';
|
|
384
|
+
const keyLooksBad =
|
|
385
|
+
currentKey.trim().length < 32 ||
|
|
386
|
+
currentKey.includes('your-') ||
|
|
387
|
+
currentKey.includes('change-me') ||
|
|
388
|
+
currentKey.includes('example');
|
|
389
|
+
if (keyLooksBad) {
|
|
390
|
+
content = upsertEnvLine(content, 'ENCRYPTION_KEY', crypto.randomBytes(32).toString('hex'));
|
|
391
|
+
created = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Only the launcher-managed install (~/.vimo) is forced into production mode;
|
|
395
|
+
// developers pointing --repo at their own checkout keep their environment.
|
|
396
|
+
if (managedHome) {
|
|
397
|
+
content = upsertEnvLine(content, 'NODE_ENV', 'production');
|
|
398
|
+
}
|
|
399
|
+
content = upsertEnvLine(content, 'DB_PATH', './data/vimo.db');
|
|
400
|
+
|
|
401
|
+
fs.writeFileSync(envFile, content);
|
|
402
|
+
if (created) log('Created settings file with secure defaults.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* -------------------------------------------------------------------------- */
|
|
406
|
+
/* Dependencies & build */
|
|
407
|
+
/* -------------------------------------------------------------------------- */
|
|
408
|
+
|
|
409
|
+
function depsInstalled(repo) {
|
|
410
|
+
return (
|
|
411
|
+
fs.existsSync(path.join(repo, 'node_modules', '.bin')) &&
|
|
412
|
+
fs.existsSync(path.join(repo, 'node_modules', 'better-sqlite3'))
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function ensureDeps(repo) {
|
|
417
|
+
if (!doReset && depsInstalled(repo)) return;
|
|
418
|
+
if (doReset) {
|
|
419
|
+
warn('Resetting: reinstalling everything from scratch (this can take a few minutes).');
|
|
420
|
+
}
|
|
421
|
+
runLive(npmCmd(), ['install', '--no-fund', '--no-audit'], { cwd: repo }, 'Installing components (first run only)…');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function appBuilt(repo) {
|
|
425
|
+
return (
|
|
426
|
+
fs.existsSync(path.join(repo, 'packages', 'backend', 'dist', 'backend', 'src', 'index.js')) &&
|
|
427
|
+
fs.existsSync(path.join(repo, 'packages', 'frontend', 'dist', 'index.html'))
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function ensureBuild(repo) {
|
|
432
|
+
if (!doReset && appBuilt(repo)) return;
|
|
433
|
+
runLive(npmCmd(), ['run', 'build:app'], { cwd: repo }, 'Building VIMO (first run only)…');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* -------------------------------------------------------------------------- */
|
|
437
|
+
/* Networking helpers */
|
|
438
|
+
/* -------------------------------------------------------------------------- */
|
|
439
|
+
|
|
440
|
+
function probeFreePort(base) {
|
|
441
|
+
return new Promise((resolve) => {
|
|
442
|
+
const attempt = (p, left) => {
|
|
443
|
+
if (left <= 0) return resolve(null);
|
|
444
|
+
const srv = net.createServer();
|
|
445
|
+
srv.once('error', () => {
|
|
446
|
+
try {
|
|
447
|
+
srv.close();
|
|
448
|
+
} catch {}
|
|
449
|
+
attempt(p + 1, left - 1);
|
|
450
|
+
});
|
|
451
|
+
srv.listen(p, '127.0.0.1', () => {
|
|
452
|
+
srv.close(() => resolve(p));
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
attempt(base, PORT_ATTEMPTS);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function waitForHealth(port) {
|
|
460
|
+
const startedAt = Date.now();
|
|
461
|
+
// Probe 127.0.0.1 explicitly: Node's fetch can resolve `localhost` to ::1,
|
|
462
|
+
// which refuses connections when the server is bound to IPv4 loopback.
|
|
463
|
+
// Browsers fall back between families automatically; Node's fetch may not.
|
|
464
|
+
const targets = [
|
|
465
|
+
`http://127.0.0.1:${port}/api/health`,
|
|
466
|
+
`http://localhost:${port}/api/health`,
|
|
467
|
+
];
|
|
468
|
+
while (Date.now() - startedAt < HEALTH_TIMEOUT_MS) {
|
|
469
|
+
for (const url of targets) {
|
|
470
|
+
try {
|
|
471
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1500) });
|
|
472
|
+
if (res.ok) return true;
|
|
473
|
+
} catch {
|
|
474
|
+
// not up yet — keep waiting
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function openBrowser(url) {
|
|
483
|
+
try {
|
|
484
|
+
if (isWindows()) {
|
|
485
|
+
spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
|
|
486
|
+
} else if (process.platform === 'darwin') {
|
|
487
|
+
spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
488
|
+
} else {
|
|
489
|
+
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
warn(`Could not open your browser automatically. Please open ${url}.`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function killTree(pid) {
|
|
497
|
+
try {
|
|
498
|
+
if (isWindows()) {
|
|
499
|
+
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
500
|
+
} else {
|
|
501
|
+
try {
|
|
502
|
+
process.kill(-pid, 'SIGTERM');
|
|
503
|
+
} catch {
|
|
504
|
+
process.kill(pid, 'SIGTERM');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
// already gone
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* -------------------------------------------------------------------------- */
|
|
513
|
+
/* Doctor */
|
|
514
|
+
/* -------------------------------------------------------------------------- */
|
|
515
|
+
|
|
516
|
+
function doctor() {
|
|
517
|
+
const results = [];
|
|
518
|
+
const push = (name, good, note) => results.push({ name, good, note });
|
|
519
|
+
|
|
520
|
+
const major = nodeMajor();
|
|
521
|
+
push(
|
|
522
|
+
'Node.js',
|
|
523
|
+
major >= MIN_NODE_MAJOR,
|
|
524
|
+
`found v${process.versions.node}` + (major > 22 ? ' (LTS 20–22 recommended)' : ''),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const npmRes = spawnSync(npmCmd(), ['--version'], { encoding: 'utf8', shell: isWindows() });
|
|
528
|
+
push('npm', !npmRes.error && npmRes.status === 0, npmRes.error ? 'not found' : `v${(npmRes.stdout || '').trim()}`);
|
|
529
|
+
|
|
530
|
+
push('Unpack tool (tar)', !!findTar(), isWindows() ? 'C:\\Windows\\System32\\tar.exe' : 'system tar');
|
|
531
|
+
|
|
532
|
+
const home = homeDir();
|
|
533
|
+
const installed = isVimoRepo(home);
|
|
534
|
+
push('VIMO app files', installed, installed ? home : `not downloaded yet (will go to ${home})`);
|
|
535
|
+
|
|
536
|
+
push('Components installed', installed && depsInstalled(home), installed ? '' : 'run `vimo` once to set up');
|
|
537
|
+
push('App built', installed && appBuilt(home), installed ? '' : 'run `vimo` once to set up');
|
|
538
|
+
|
|
539
|
+
let keyOk = false;
|
|
540
|
+
if (installed) {
|
|
541
|
+
try {
|
|
542
|
+
const envText = fs.readFileSync(path.join(home, '.env'), 'utf8');
|
|
543
|
+
const k = (envText.match(/^ENCRYPTION_KEY=(.*)$/m) || [])[1] || '';
|
|
544
|
+
keyOk = k.trim().length >= 32 && !k.includes('your-');
|
|
545
|
+
} catch {}
|
|
546
|
+
}
|
|
547
|
+
push('Security key configured', keyOk, keyOk ? '' : 'created automatically on first start');
|
|
548
|
+
|
|
549
|
+
console.log(`\n${CYAN}[vimo]${RESET} Environment check:\n`);
|
|
550
|
+
for (const r of results) {
|
|
551
|
+
const mark = r.good ? `${GREEN}OK${RESET}` : `${YELLOW}--${RESET}`;
|
|
552
|
+
console.log(` ${mark} ${r.name.padEnd(24)} ${r.note || ''}`);
|
|
553
|
+
}
|
|
554
|
+
const blockers = results.filter((r) => !r.good);
|
|
555
|
+
if (blockers.length) {
|
|
556
|
+
console.log(`\nItems marked "${YELLOW}--${RESET}" are fixed automatically the first time you run \`vimo\`.`);
|
|
557
|
+
} else {
|
|
558
|
+
console.log(`\n${GREEN}Everything looks good — just type \`vimo\` to start.${RESET}`);
|
|
559
|
+
}
|
|
560
|
+
process.exit(0);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* -------------------------------------------------------------------------- */
|
|
564
|
+
/* Main */
|
|
565
|
+
/* -------------------------------------------------------------------------- */
|
|
566
|
+
|
|
567
|
+
async function main() {
|
|
568
|
+
const startedAt = Date.now();
|
|
569
|
+
printBanner();
|
|
570
|
+
checkNode();
|
|
571
|
+
|
|
572
|
+
if (args.includes('--doctor')) doctor();
|
|
573
|
+
|
|
574
|
+
const repo = resolveRepo();
|
|
575
|
+
await ensureRepo(repo);
|
|
576
|
+
const managedHome = repo === homeDir();
|
|
577
|
+
ensureEnv(repo, managedHome);
|
|
578
|
+
|
|
579
|
+
ensureDeps(repo);
|
|
580
|
+
ensureBuild(repo);
|
|
581
|
+
|
|
582
|
+
const port = await probeFreePort(portBase);
|
|
583
|
+
if (!port) {
|
|
584
|
+
fail(
|
|
585
|
+
`No free port found between ${portBase} and ${portBase + PORT_ATTEMPTS - 1}.\n` +
|
|
586
|
+
'Close some apps and try again.',
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (port !== portBase) {
|
|
590
|
+
log(`Port ${portBase} was busy — VIMO will use ${port} instead.`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const entry = path.join(repo, 'packages', 'backend', 'dist', 'backend', 'src', 'index.js');
|
|
594
|
+
if (!fs.existsSync(entry)) fail('VIMO is missing its built app files. Try running `vimo --reset`.');
|
|
595
|
+
|
|
596
|
+
const url = `http://localhost:${port}`;
|
|
597
|
+
log(`Starting VIMO on ${url} …`);
|
|
598
|
+
log('Keep this window open. Press Ctrl+C here when you want to stop VIMO.');
|
|
599
|
+
|
|
600
|
+
const child = spawn(process.execPath, [entry], {
|
|
601
|
+
cwd: repo,
|
|
602
|
+
env: { ...process.env, PORT: String(port), NODE_ENV: 'production' },
|
|
603
|
+
stdio: 'inherit',
|
|
604
|
+
detached: !isWindows(),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
let exiting = false;
|
|
608
|
+
function shutdown(signal) {
|
|
609
|
+
if (exiting) return;
|
|
610
|
+
exiting = true;
|
|
611
|
+
if (signal) log('Stopping VIMO… see you soon!');
|
|
612
|
+
if (child && child.pid) killTree(child.pid);
|
|
613
|
+
setTimeout(() => process.exit(0), 400);
|
|
614
|
+
}
|
|
615
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
616
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
617
|
+
child.on('exit', (code) => {
|
|
618
|
+
if (!exiting) {
|
|
619
|
+
exiting = true;
|
|
620
|
+
if (code && code !== 0) {
|
|
621
|
+
console.error(`\n${RED}[vimo] VIMO stopped unexpectedly (code ${code}).${RESET}`);
|
|
622
|
+
console.error(`${RED}[vimo] Try \`vimo --reset\`, or run \`vimo doctor\` for a health check.${RESET}`);
|
|
623
|
+
}
|
|
624
|
+
process.exit(code ?? 0);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const healthy = await waitForHealth(port);
|
|
629
|
+
const seconds = Math.round((Date.now() - startedAt) / 1000);
|
|
630
|
+
if (!healthy) {
|
|
631
|
+
warn(`VIMO didn't become reachable on ${url} within ${Math.round(HEALTH_TIMEOUT_MS / 1000)}s.`);
|
|
632
|
+
warn('Check the messages above for an error, then run `vimo` again.');
|
|
633
|
+
// Don't leave a half-started app running behind the user's back.
|
|
634
|
+
if (child && child.pid) killTree(child.pid);
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
console.log(`
|
|
639
|
+
──────────────────────────────────────────────
|
|
640
|
+
${GREEN}VIMO is ready! (${seconds}s)${RESET}
|
|
641
|
+
|
|
642
|
+
Open: ${url}
|
|
643
|
+
Stop: press Ctrl+C in this window
|
|
644
|
+
Data: ${managedHome ? path.join(repo, 'data') : path.join(repo, 'data')}
|
|
645
|
+
Update: run \`vimo --update\`
|
|
646
|
+
|
|
647
|
+
Next time, just type ${CYAN}vimo${RESET}.
|
|
648
|
+
──────────────────────────────────────────────
|
|
649
|
+
`);
|
|
650
|
+
|
|
651
|
+
if (!noOpen) openBrowser(url);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
main().catch((err) => {
|
|
655
|
+
fail(err && err.message ? err.message : String(err));
|
|
656
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vimo-oss",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "VIMO — one-command start for the VIBE Marketing Operations platform. Installs, builds and launches VIMO locally; type `vimo` (or `VIMO`) anywhere to open it in your browser.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vimo": "./bin/vimo.mjs",
|
|
8
|
+
"VIMO": "./bin/vimo.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"lint": "node --check bin/vimo.mjs && node --check tests/smoke.test.mjs",
|
|
18
|
+
"build": "node --check bin/vimo.mjs",
|
|
19
|
+
"test": "node --test tests/*.test.mjs"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"vimo",
|
|
23
|
+
"marketing",
|
|
24
|
+
"ai",
|
|
25
|
+
"local",
|
|
26
|
+
"self-hosted",
|
|
27
|
+
"launcher"
|
|
28
|
+
]
|
|
29
|
+
}
|