olly-molly 0.3.3 → 0.3.5
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 +83 -0
- package/bin/cli.js +434 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="#quick-start">Quick Start</a> •
|
|
13
|
+
<a href="#cli-options">CLI Options</a> •
|
|
13
14
|
<a href="#features">Features</a> •
|
|
14
15
|
<a href="#how-it-works">How It Works</a> •
|
|
15
16
|
<a href="#contributing">Contributing</a>
|
|
@@ -133,6 +134,88 @@ olly-molly-darwin-x64.tar.gz
|
|
|
133
134
|
olly-molly-win32-x64.tar.gz
|
|
134
135
|
```
|
|
135
136
|
|
|
137
|
+
### CLI Options
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
olly-molly [options]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Server Settings
|
|
144
|
+
|
|
145
|
+
| Flag | Short | Default | Description |
|
|
146
|
+
|------|-------|---------|-------------|
|
|
147
|
+
| `--port` | `-p` | `1234` | Server port |
|
|
148
|
+
| `--host` | `-H` | `localhost` | Binding host |
|
|
149
|
+
| `--no-open` | | | Disable auto browser open |
|
|
150
|
+
|
|
151
|
+
#### Data/Path Settings
|
|
152
|
+
|
|
153
|
+
| Flag | Short | Default | Description |
|
|
154
|
+
|------|-------|---------|-------------|
|
|
155
|
+
| `--data-dir` | `-d` | `~/.olly-molly` | App data directory |
|
|
156
|
+
| `--db-path` | | `<data-dir>/db` | Database path |
|
|
157
|
+
|
|
158
|
+
#### Development
|
|
159
|
+
|
|
160
|
+
| Flag | Short | Description |
|
|
161
|
+
|------|-------|-------------|
|
|
162
|
+
| `--dev` | | Run in development mode (`next dev`) |
|
|
163
|
+
| `--verbose` | `-v` | Enable verbose logging |
|
|
164
|
+
|
|
165
|
+
#### Advanced Options
|
|
166
|
+
|
|
167
|
+
| Flag | Description |
|
|
168
|
+
|------|-------------|
|
|
169
|
+
| `--reset` | Reset all app data (with confirmation prompt) |
|
|
170
|
+
| `--export-db <path>` | Export database to tar.gz file |
|
|
171
|
+
| `--import-db <path>` | Import database from tar.gz file |
|
|
172
|
+
|
|
173
|
+
#### Info
|
|
174
|
+
|
|
175
|
+
| Flag | Short | Description |
|
|
176
|
+
|------|-------|-------------|
|
|
177
|
+
| `--version` | `-V` | Show version and exit |
|
|
178
|
+
| `--help` | `-h` | Show help and exit |
|
|
179
|
+
|
|
180
|
+
#### Environment Variables
|
|
181
|
+
|
|
182
|
+
You can also configure Olly Molly using environment variables (CLI arguments take priority):
|
|
183
|
+
|
|
184
|
+
| Variable | Description |
|
|
185
|
+
|----------|-------------|
|
|
186
|
+
| `OLLY_MOLLY_PORT` | Server port |
|
|
187
|
+
| `OLLY_MOLLY_HOST` | Binding host |
|
|
188
|
+
| `OLLY_MOLLY_DATA_DIR` | App data directory |
|
|
189
|
+
| `OLLY_MOLLY_DB_PATH` | Database path |
|
|
190
|
+
|
|
191
|
+
#### Examples
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Start with defaults (port 1234, auto-open browser)
|
|
195
|
+
npx olly-molly
|
|
196
|
+
|
|
197
|
+
# Use custom port
|
|
198
|
+
npx olly-molly -p 3000
|
|
199
|
+
|
|
200
|
+
# Bind to all interfaces (for network access)
|
|
201
|
+
npx olly-molly --host 0.0.0.0
|
|
202
|
+
|
|
203
|
+
# Run in development mode with verbose logging
|
|
204
|
+
npx olly-molly --dev -v
|
|
205
|
+
|
|
206
|
+
# Disable auto browser open
|
|
207
|
+
npx olly-molly --no-open
|
|
208
|
+
|
|
209
|
+
# Export database for backup
|
|
210
|
+
npx olly-molly --export-db backup.tar.gz
|
|
211
|
+
|
|
212
|
+
# Import database from backup
|
|
213
|
+
npx olly-molly --import-db backup.tar.gz
|
|
214
|
+
|
|
215
|
+
# Reset all app data
|
|
216
|
+
npx olly-molly --reset
|
|
217
|
+
```
|
|
218
|
+
|
|
136
219
|
### Or install globally
|
|
137
220
|
|
|
138
221
|
```bash
|
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { spawn, execSync } = require('child_process');
|
|
4
|
+
const { parseArgs } = require('node:util');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const os = require('os');
|
|
@@ -8,12 +9,358 @@ const https = require('https');
|
|
|
8
9
|
|
|
9
10
|
const PACKAGE_NAME = 'olly-molly';
|
|
10
11
|
const REPO = 'ruucm/olly-molly';
|
|
11
|
-
const APP_DIR = path.join(os.homedir(), '.olly-molly');
|
|
12
|
-
const DB_DIR = path.join(APP_DIR, 'db');
|
|
13
12
|
const SOURCE_TARBALL_URL = `https://github.com/${REPO}/archive/refs/heads/main.tar.gz`;
|
|
14
13
|
const RELEASE_BASE_URL = `https://github.com/${REPO}/releases/download`;
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
// CLI argument definitions (no defaults for string options to allow env var fallback)
|
|
16
|
+
const CLI_OPTIONS = {
|
|
17
|
+
// Server settings
|
|
18
|
+
port: { type: 'string', short: 'p' },
|
|
19
|
+
host: { type: 'string', short: 'H' },
|
|
20
|
+
'no-open': { type: 'boolean', default: false },
|
|
21
|
+
// Data/path settings
|
|
22
|
+
'data-dir': { type: 'string', short: 'd' },
|
|
23
|
+
'db-path': { type: 'string' },
|
|
24
|
+
// Development/debugging
|
|
25
|
+
dev: { type: 'boolean', default: false },
|
|
26
|
+
verbose: { type: 'boolean', short: 'v', default: false },
|
|
27
|
+
version: { type: 'boolean', short: 'V', default: false },
|
|
28
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
29
|
+
// Advanced options
|
|
30
|
+
reset: { type: 'boolean', default: false },
|
|
31
|
+
'export-db': { type: 'string' },
|
|
32
|
+
'import-db': { type: 'string' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Parse CLI arguments
|
|
36
|
+
let args;
|
|
37
|
+
try {
|
|
38
|
+
const result = parseArgs({ options: CLI_OPTIONS, allowPositionals: false });
|
|
39
|
+
args = result.values;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`❌ ${err.message}`);
|
|
42
|
+
console.error('Run "olly-molly --help" for usage information.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get package version
|
|
47
|
+
function getPackageVersion() {
|
|
48
|
+
try {
|
|
49
|
+
return require('../package.json').version;
|
|
50
|
+
} catch {
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Show help message
|
|
56
|
+
function showHelp() {
|
|
57
|
+
console.log(`
|
|
58
|
+
🐙 Olly Molly - Your AI Development Team
|
|
59
|
+
|
|
60
|
+
USAGE
|
|
61
|
+
olly-molly [options]
|
|
62
|
+
|
|
63
|
+
SERVER SETTINGS
|
|
64
|
+
-p, --port <port> Server port (default: 1234)
|
|
65
|
+
-H, --host <host> Binding host (default: localhost)
|
|
66
|
+
--no-open Disable auto browser open
|
|
67
|
+
|
|
68
|
+
DATA/PATH SETTINGS
|
|
69
|
+
-d, --data-dir <path> App data directory (default: ~/.olly-molly)
|
|
70
|
+
--db-path <path> Database path (default: <data-dir>/db)
|
|
71
|
+
|
|
72
|
+
DEVELOPMENT
|
|
73
|
+
--dev Run in development mode (next dev)
|
|
74
|
+
-v, --verbose Enable verbose logging
|
|
75
|
+
|
|
76
|
+
ADVANCED OPTIONS
|
|
77
|
+
--reset Reset all app data (with confirmation)
|
|
78
|
+
--export-db <path> Export database to zip file
|
|
79
|
+
--import-db <path> Import database from zip file
|
|
80
|
+
|
|
81
|
+
INFO
|
|
82
|
+
-V, --version Show version and exit
|
|
83
|
+
-h, --help Show this help and exit
|
|
84
|
+
|
|
85
|
+
ENVIRONMENT VARIABLES
|
|
86
|
+
OLLY_MOLLY_PORT Server port (fallback)
|
|
87
|
+
OLLY_MOLLY_HOST Binding host (fallback)
|
|
88
|
+
OLLY_MOLLY_DATA_DIR App data directory (fallback)
|
|
89
|
+
OLLY_MOLLY_DB_PATH Database path (fallback)
|
|
90
|
+
|
|
91
|
+
EXAMPLES
|
|
92
|
+
olly-molly # Start with defaults
|
|
93
|
+
olly-molly -p 3000 # Use port 3000
|
|
94
|
+
olly-molly --host 0.0.0.0 # Bind to all interfaces
|
|
95
|
+
olly-molly --dev -v # Development mode with verbose
|
|
96
|
+
olly-molly --export-db backup.zip # Export database
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle immediate flags
|
|
101
|
+
if (args.help) {
|
|
102
|
+
showHelp();
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (args.version) {
|
|
107
|
+
console.log(`olly-molly v${getPackageVersion()}`);
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Configuration with priority: CLI args > env vars > defaults
|
|
112
|
+
function getConfig() {
|
|
113
|
+
const dataDir = args['data-dir']
|
|
114
|
+
|| process.env.OLLY_MOLLY_DATA_DIR
|
|
115
|
+
|| path.join(os.homedir(), '.olly-molly');
|
|
116
|
+
|
|
117
|
+
const port = args.port
|
|
118
|
+
|| process.env.OLLY_MOLLY_PORT
|
|
119
|
+
|| '1234';
|
|
120
|
+
|
|
121
|
+
// Validate port
|
|
122
|
+
const portNum = parseInt(port, 10);
|
|
123
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
124
|
+
console.error(`❌ Port must be a number between 1-65535, got: ${port}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const host = args.host
|
|
129
|
+
|| process.env.OLLY_MOLLY_HOST
|
|
130
|
+
|| 'localhost';
|
|
131
|
+
|
|
132
|
+
const dbPath = args['db-path']
|
|
133
|
+
|| process.env.OLLY_MOLLY_DB_PATH
|
|
134
|
+
|| path.join(dataDir, 'db');
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
APP_DIR: dataDir,
|
|
138
|
+
DB_DIR: dbPath,
|
|
139
|
+
CUSTOM_PROFILES_DIR: path.join(dataDir, 'custom-profiles'),
|
|
140
|
+
PORT: port,
|
|
141
|
+
HOST: host,
|
|
142
|
+
OPEN_BROWSER: !args['no-open'],
|
|
143
|
+
DEV_MODE: args.dev,
|
|
144
|
+
VERBOSE: args.verbose,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Verbose logging utility
|
|
149
|
+
function verbose(config, ...messages) {
|
|
150
|
+
if (config.VERBOSE) {
|
|
151
|
+
console.log('🔍', ...messages);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle --reset
|
|
156
|
+
async function handleReset(config) {
|
|
157
|
+
const readline = require('readline');
|
|
158
|
+
const rl = readline.createInterface({
|
|
159
|
+
input: process.stdin,
|
|
160
|
+
output: process.stdout,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
console.log(`\n⚠️ This will delete all data in: ${config.APP_DIR}`);
|
|
165
|
+
console.log(' Including: database, custom profiles, and application files.\n');
|
|
166
|
+
|
|
167
|
+
rl.question('Are you sure? Type "yes" to confirm: ', (answer) => {
|
|
168
|
+
rl.close();
|
|
169
|
+
if (answer.toLowerCase() === 'yes') {
|
|
170
|
+
if (fs.existsSync(config.APP_DIR)) {
|
|
171
|
+
fs.rmSync(config.APP_DIR, { recursive: true, force: true });
|
|
172
|
+
console.log('✅ All data has been reset.');
|
|
173
|
+
} else {
|
|
174
|
+
console.log('ℹ️ No data directory found.');
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.log('❌ Reset cancelled.');
|
|
178
|
+
}
|
|
179
|
+
resolve();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle --export-db
|
|
185
|
+
async function handleExportDb(exportPath, config) {
|
|
186
|
+
const absPath = path.resolve(exportPath);
|
|
187
|
+
const parentDir = path.dirname(absPath);
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(parentDir)) {
|
|
190
|
+
console.error(`❌ Export directory does not exist: ${parentDir}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!fs.existsSync(config.DB_DIR) && !fs.existsSync(config.CUSTOM_PROFILES_DIR)) {
|
|
195
|
+
console.error('❌ No data to export. Database and profiles directories do not exist.');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('📦 Exporting database...');
|
|
200
|
+
verbose(config, `DB_DIR: ${config.DB_DIR}`);
|
|
201
|
+
verbose(config, `CUSTOM_PROFILES_DIR: ${config.CUSTOM_PROFILES_DIR}`);
|
|
202
|
+
|
|
203
|
+
// Create tar.gz using native tar command
|
|
204
|
+
const tmpDir = path.join(os.tmpdir(), `olly-molly-export-${Date.now()}`);
|
|
205
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// Copy data to temp directory
|
|
209
|
+
if (fs.existsSync(config.DB_DIR)) {
|
|
210
|
+
fs.cpSync(config.DB_DIR, path.join(tmpDir, 'db'), { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
if (fs.existsSync(config.CUSTOM_PROFILES_DIR)) {
|
|
213
|
+
fs.cpSync(config.CUSTOM_PROFILES_DIR, path.join(tmpDir, 'custom-profiles'), { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create tar.gz
|
|
217
|
+
execSync(`tar -czf "${absPath}" -C "${tmpDir}" .`, { stdio: 'pipe' });
|
|
218
|
+
console.log(`✅ Database exported to: ${absPath}`);
|
|
219
|
+
} finally {
|
|
220
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle --import-db
|
|
225
|
+
async function handleImportDb(importPath, config) {
|
|
226
|
+
const absPath = path.resolve(importPath);
|
|
227
|
+
|
|
228
|
+
if (!fs.existsSync(absPath)) {
|
|
229
|
+
console.error(`❌ Import file not found: ${absPath}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('📥 Importing database...');
|
|
234
|
+
verbose(config, `Import from: ${absPath}`);
|
|
235
|
+
|
|
236
|
+
// Backup existing data
|
|
237
|
+
const backupDir = path.join(os.tmpdir(), `olly-molly-import-backup-${Date.now()}`);
|
|
238
|
+
let hasBackup = false;
|
|
239
|
+
|
|
240
|
+
if (fs.existsSync(config.DB_DIR) || fs.existsSync(config.CUSTOM_PROFILES_DIR)) {
|
|
241
|
+
console.log('📋 Backing up existing data...');
|
|
242
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
243
|
+
if (fs.existsSync(config.DB_DIR)) {
|
|
244
|
+
fs.cpSync(config.DB_DIR, path.join(backupDir, 'db'), { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
if (fs.existsSync(config.CUSTOM_PROFILES_DIR)) {
|
|
247
|
+
fs.cpSync(config.CUSTOM_PROFILES_DIR, path.join(backupDir, 'custom-profiles'), { recursive: true });
|
|
248
|
+
}
|
|
249
|
+
hasBackup = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
// Extract to temp dir first to validate
|
|
254
|
+
const tmpDir = path.join(os.tmpdir(), `olly-molly-import-${Date.now()}`);
|
|
255
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
256
|
+
execSync(`tar -xzf "${absPath}" -C "${tmpDir}"`, { stdio: 'pipe' });
|
|
257
|
+
|
|
258
|
+
// Check if valid export
|
|
259
|
+
const hasDb = fs.existsSync(path.join(tmpDir, 'db'));
|
|
260
|
+
const hasProfiles = fs.existsSync(path.join(tmpDir, 'custom-profiles'));
|
|
261
|
+
|
|
262
|
+
if (!hasDb && !hasProfiles) {
|
|
263
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
264
|
+
console.error('❌ Invalid import file: no db or custom-profiles found.');
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Remove existing and copy new data
|
|
269
|
+
if (hasDb) {
|
|
270
|
+
if (fs.existsSync(config.DB_DIR)) {
|
|
271
|
+
fs.rmSync(config.DB_DIR, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
fs.mkdirSync(path.dirname(config.DB_DIR), { recursive: true });
|
|
274
|
+
fs.cpSync(path.join(tmpDir, 'db'), config.DB_DIR, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (hasProfiles) {
|
|
278
|
+
if (fs.existsSync(config.CUSTOM_PROFILES_DIR)) {
|
|
279
|
+
fs.rmSync(config.CUSTOM_PROFILES_DIR, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
fs.mkdirSync(path.dirname(config.CUSTOM_PROFILES_DIR), { recursive: true });
|
|
282
|
+
fs.cpSync(path.join(tmpDir, 'custom-profiles'), config.CUSTOM_PROFILES_DIR, { recursive: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
286
|
+
|
|
287
|
+
// Clean backup on success
|
|
288
|
+
if (hasBackup) {
|
|
289
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log('✅ Database imported successfully.');
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// Restore backup on failure
|
|
295
|
+
if (hasBackup) {
|
|
296
|
+
console.log('⚠️ Import failed, restoring backup...');
|
|
297
|
+
if (fs.existsSync(path.join(backupDir, 'db'))) {
|
|
298
|
+
fs.cpSync(path.join(backupDir, 'db'), config.DB_DIR, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
if (fs.existsSync(path.join(backupDir, 'custom-profiles'))) {
|
|
301
|
+
fs.cpSync(path.join(backupDir, 'custom-profiles'), config.CUSTOM_PROFILES_DIR, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Handle --dev mode
|
|
310
|
+
async function handleDevMode(config) {
|
|
311
|
+
// Check if source exists (not just prebuilt)
|
|
312
|
+
const packageJsonPath = path.join(config.APP_DIR, 'package.json');
|
|
313
|
+
const srcExists = fs.existsSync(path.join(config.APP_DIR, 'app')) ||
|
|
314
|
+
fs.existsSync(path.join(config.APP_DIR, 'src'));
|
|
315
|
+
|
|
316
|
+
if (!fs.existsSync(packageJsonPath) || !srcExists) {
|
|
317
|
+
console.log('📥 Downloading source for development...');
|
|
318
|
+
if (fs.existsSync(config.APP_DIR)) {
|
|
319
|
+
// Backup user data
|
|
320
|
+
const backupDir = backupUserData(config);
|
|
321
|
+
fs.rmSync(config.APP_DIR, { recursive: true, force: true });
|
|
322
|
+
await download(SOURCE_TARBALL_URL, config.APP_DIR, { stripComponents: 1 });
|
|
323
|
+
restoreUserData(backupDir, config);
|
|
324
|
+
} else {
|
|
325
|
+
await download(SOURCE_TARBALL_URL, config.APP_DIR, { stripComponents: 1 });
|
|
326
|
+
}
|
|
327
|
+
console.log('✅ Source downloaded\n');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Install full dependencies (not --omit=dev)
|
|
331
|
+
if (!fs.existsSync(path.join(config.APP_DIR, 'node_modules'))) {
|
|
332
|
+
console.log('📦 Installing dependencies...\n');
|
|
333
|
+
execSync('npm install', { cwd: config.APP_DIR, stdio: 'inherit' });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const displayHost = config.HOST === '0.0.0.0' ? 'localhost' : config.HOST;
|
|
337
|
+
console.log(`\n🚀 http://${displayHost}:${config.PORT}\n`);
|
|
338
|
+
|
|
339
|
+
// Auto-open browser
|
|
340
|
+
if (config.OPEN_BROWSER) {
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
const url = `http://${displayHost}:${config.PORT}`;
|
|
343
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
344
|
+
: process.platform === 'win32' ? 'start'
|
|
345
|
+
: 'xdg-open';
|
|
346
|
+
try {
|
|
347
|
+
execSync(`${cmd} ${url}`, { stdio: 'ignore' });
|
|
348
|
+
} catch {}
|
|
349
|
+
}, 3000);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Run next dev
|
|
353
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
354
|
+
const server = spawn(npxCmd, ['next', 'dev', '--port', config.PORT, '--hostname', config.HOST], {
|
|
355
|
+
cwd: config.APP_DIR,
|
|
356
|
+
stdio: 'inherit',
|
|
357
|
+
shell: false,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
server.on('close', (code) => process.exit(code || 0));
|
|
361
|
+
process.on('SIGINT', () => server.kill('SIGINT'));
|
|
362
|
+
process.on('SIGTERM', () => server.kill('SIGTERM'));
|
|
363
|
+
}
|
|
17
364
|
|
|
18
365
|
// Get latest version from npm registry
|
|
19
366
|
function getNpmVersion() {
|
|
@@ -80,103 +427,136 @@ function download(url, destDir, { allowNotFound = false, stripComponents = 1 } =
|
|
|
80
427
|
});
|
|
81
428
|
}
|
|
82
429
|
|
|
83
|
-
function getLocalVersion() {
|
|
430
|
+
function getLocalVersion(appDir) {
|
|
84
431
|
try {
|
|
85
|
-
return JSON.parse(fs.readFileSync(path.join(
|
|
432
|
+
return JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).version;
|
|
86
433
|
} catch { return null; }
|
|
87
434
|
}
|
|
88
435
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
function hasProductionBuild(standaloneServerPath) {
|
|
92
|
-
return fs.existsSync(path.join(APP_DIR, '.next', 'BUILD_ID')) ||
|
|
436
|
+
function hasProductionBuild(appDir, standaloneServerPath) {
|
|
437
|
+
return fs.existsSync(path.join(appDir, '.next', 'BUILD_ID')) ||
|
|
93
438
|
fs.existsSync(standaloneServerPath);
|
|
94
439
|
}
|
|
95
440
|
|
|
96
|
-
function backupUserData() {
|
|
441
|
+
function backupUserData(config) {
|
|
97
442
|
const backupDir = path.join(os.tmpdir(), 'olly-molly-backup');
|
|
98
443
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
99
|
-
|
|
444
|
+
|
|
100
445
|
// Backup database
|
|
101
|
-
if (fs.existsSync(DB_DIR)) {
|
|
446
|
+
if (fs.existsSync(config.DB_DIR)) {
|
|
102
447
|
const dbBackupDir = path.join(backupDir, 'db');
|
|
103
|
-
fs.cpSync(DB_DIR, dbBackupDir, { recursive: true });
|
|
448
|
+
fs.cpSync(config.DB_DIR, dbBackupDir, { recursive: true });
|
|
104
449
|
}
|
|
105
|
-
|
|
450
|
+
|
|
106
451
|
// Backup custom profile images
|
|
107
|
-
if (fs.existsSync(CUSTOM_PROFILES_DIR)) {
|
|
452
|
+
if (fs.existsSync(config.CUSTOM_PROFILES_DIR)) {
|
|
108
453
|
const profilesBackupDir = path.join(backupDir, 'custom-profiles');
|
|
109
|
-
fs.cpSync(CUSTOM_PROFILES_DIR, profilesBackupDir, { recursive: true });
|
|
454
|
+
fs.cpSync(config.CUSTOM_PROFILES_DIR, profilesBackupDir, { recursive: true });
|
|
110
455
|
}
|
|
111
|
-
|
|
456
|
+
|
|
112
457
|
return backupDir;
|
|
113
458
|
}
|
|
114
459
|
|
|
115
|
-
function restoreUserData(backupDir) {
|
|
460
|
+
function restoreUserData(backupDir, config) {
|
|
116
461
|
if (!backupDir || !fs.existsSync(backupDir)) return;
|
|
117
|
-
|
|
462
|
+
|
|
118
463
|
// Restore database
|
|
119
464
|
const dbBackupDir = path.join(backupDir, 'db');
|
|
120
465
|
if (fs.existsSync(dbBackupDir)) {
|
|
121
|
-
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
466
|
+
fs.mkdirSync(config.DB_DIR, { recursive: true });
|
|
122
467
|
for (const file of fs.readdirSync(dbBackupDir)) {
|
|
123
468
|
if (file.includes('.sqlite')) {
|
|
124
|
-
fs.copyFileSync(path.join(dbBackupDir, file), path.join(DB_DIR, file));
|
|
469
|
+
fs.copyFileSync(path.join(dbBackupDir, file), path.join(config.DB_DIR, file));
|
|
125
470
|
}
|
|
126
471
|
}
|
|
127
472
|
}
|
|
128
|
-
|
|
473
|
+
|
|
129
474
|
// Restore custom profile images
|
|
130
475
|
const profilesBackupDir = path.join(backupDir, 'custom-profiles');
|
|
131
476
|
if (fs.existsSync(profilesBackupDir)) {
|
|
132
|
-
fs.mkdirSync(CUSTOM_PROFILES_DIR, { recursive: true });
|
|
477
|
+
fs.mkdirSync(config.CUSTOM_PROFILES_DIR, { recursive: true });
|
|
133
478
|
for (const file of fs.readdirSync(profilesBackupDir)) {
|
|
134
|
-
fs.copyFileSync(path.join(profilesBackupDir, file), path.join(CUSTOM_PROFILES_DIR, file));
|
|
479
|
+
fs.copyFileSync(path.join(profilesBackupDir, file), path.join(config.CUSTOM_PROFILES_DIR, file));
|
|
135
480
|
}
|
|
136
481
|
}
|
|
137
|
-
|
|
482
|
+
|
|
138
483
|
// Clean up backup
|
|
139
484
|
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
140
485
|
}
|
|
141
486
|
|
|
142
487
|
async function main() {
|
|
488
|
+
console.log('\n🐙 Olly Molly\n');
|
|
489
|
+
|
|
490
|
+
const config = getConfig();
|
|
491
|
+
|
|
492
|
+
verbose(config, 'Configuration:');
|
|
493
|
+
verbose(config, ` APP_DIR: ${config.APP_DIR}`);
|
|
494
|
+
verbose(config, ` DB_DIR: ${config.DB_DIR}`);
|
|
495
|
+
verbose(config, ` PORT: ${config.PORT}`);
|
|
496
|
+
verbose(config, ` HOST: ${config.HOST}`);
|
|
497
|
+
|
|
498
|
+
// Handle mutually exclusive operations
|
|
499
|
+
if (args.reset) {
|
|
500
|
+
await handleReset(config);
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (args['export-db']) {
|
|
505
|
+
await handleExportDb(args['export-db'], config);
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (args['import-db']) {
|
|
510
|
+
await handleImportDb(args['import-db'], config);
|
|
511
|
+
process.exit(0);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Handle dev mode
|
|
515
|
+
if (config.DEV_MODE) {
|
|
516
|
+
await handleDevMode(config);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Regular startup flow
|
|
143
521
|
let needsInstall = false;
|
|
144
522
|
let needsBuild = false;
|
|
145
523
|
let usedPrebuilt = false;
|
|
146
524
|
|
|
147
|
-
const localVersion = getLocalVersion();
|
|
525
|
+
const localVersion = getLocalVersion(config.APP_DIR);
|
|
148
526
|
const npmVersion = await getNpmVersion();
|
|
149
527
|
const prebuiltUrl = getPrebuiltUrl(npmVersion);
|
|
150
|
-
const standaloneServerPath = path.join(APP_DIR, '.next', 'standalone', 'server.js');
|
|
528
|
+
const standaloneServerPath = path.join(config.APP_DIR, '.next', 'standalone', 'server.js');
|
|
151
529
|
const prebuiltOnDisk = () => fs.existsSync(standaloneServerPath);
|
|
152
530
|
|
|
153
531
|
async function downloadApp() {
|
|
154
532
|
if (prebuiltUrl) {
|
|
155
|
-
|
|
533
|
+
verbose(config, `Trying prebuilt: ${prebuiltUrl}`);
|
|
534
|
+
const ok = await download(prebuiltUrl, config.APP_DIR, { allowNotFound: true, stripComponents: 0 });
|
|
156
535
|
if (ok) {
|
|
157
536
|
usedPrebuilt = true;
|
|
158
537
|
return;
|
|
159
538
|
}
|
|
160
539
|
}
|
|
161
|
-
|
|
540
|
+
verbose(config, `Downloading source: ${SOURCE_TARBALL_URL}`);
|
|
541
|
+
await download(SOURCE_TARBALL_URL, config.APP_DIR, { stripComponents: 1 });
|
|
162
542
|
usedPrebuilt = false;
|
|
163
543
|
}
|
|
164
544
|
|
|
165
545
|
// Update if npm version is newer
|
|
166
546
|
if (localVersion && npmVersion && localVersion !== npmVersion) {
|
|
167
547
|
console.log(`🔄 Updating ${localVersion} → ${npmVersion}\n`);
|
|
168
|
-
const userDataBackup = backupUserData();
|
|
169
|
-
fs.rmSync(APP_DIR, { recursive: true, force: true });
|
|
548
|
+
const userDataBackup = backupUserData(config);
|
|
549
|
+
fs.rmSync(config.APP_DIR, { recursive: true, force: true });
|
|
170
550
|
console.log('📥 Downloading...');
|
|
171
551
|
await downloadApp();
|
|
172
552
|
console.log('✅ Downloaded\n');
|
|
173
|
-
restoreUserData(userDataBackup);
|
|
553
|
+
restoreUserData(userDataBackup, config);
|
|
174
554
|
needsInstall = !usedPrebuilt;
|
|
175
555
|
needsBuild = !usedPrebuilt;
|
|
176
556
|
}
|
|
177
557
|
|
|
178
558
|
// First time
|
|
179
|
-
if (!fs.existsSync(APP_DIR)) {
|
|
559
|
+
if (!fs.existsSync(config.APP_DIR)) {
|
|
180
560
|
console.log('📥 Downloading...');
|
|
181
561
|
await downloadApp();
|
|
182
562
|
console.log('✅ Downloaded\n');
|
|
@@ -189,9 +569,9 @@ async function main() {
|
|
|
189
569
|
}
|
|
190
570
|
|
|
191
571
|
// Install
|
|
192
|
-
if (needsInstall || !fs.existsSync(path.join(APP_DIR, 'node_modules'))) {
|
|
572
|
+
if (needsInstall || !fs.existsSync(path.join(config.APP_DIR, 'node_modules'))) {
|
|
193
573
|
console.log('📦 Installing...\n');
|
|
194
|
-
execSync('npm install --omit=dev', { cwd: APP_DIR, stdio: 'inherit' });
|
|
574
|
+
execSync('npm install --omit=dev', { cwd: config.APP_DIR, stdio: 'inherit' });
|
|
195
575
|
}
|
|
196
576
|
|
|
197
577
|
if (usedPrebuilt && !prebuiltOnDisk()) {
|
|
@@ -201,36 +581,39 @@ async function main() {
|
|
|
201
581
|
}
|
|
202
582
|
|
|
203
583
|
// Build
|
|
204
|
-
if (needsBuild || !hasProductionBuild(standaloneServerPath)) {
|
|
584
|
+
if (needsBuild || !hasProductionBuild(config.APP_DIR, standaloneServerPath)) {
|
|
205
585
|
console.log('\n🔨 Building...\n');
|
|
206
|
-
execSync('npm run build', { cwd: APP_DIR, stdio: 'inherit' });
|
|
586
|
+
execSync('npm run build', { cwd: config.APP_DIR, stdio: 'inherit' });
|
|
207
587
|
}
|
|
208
588
|
|
|
209
|
-
|
|
589
|
+
const displayHost = config.HOST === '0.0.0.0' ? 'localhost' : config.HOST;
|
|
590
|
+
console.log(`\n🚀 http://${displayHost}:${config.PORT}\n`);
|
|
210
591
|
|
|
211
592
|
// Auto-open browser after a short delay
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
593
|
+
if (config.OPEN_BROWSER) {
|
|
594
|
+
setTimeout(() => {
|
|
595
|
+
const url = `http://${displayHost}:${config.PORT}`;
|
|
596
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
597
|
+
: process.platform === 'win32' ? 'start'
|
|
598
|
+
: 'xdg-open';
|
|
599
|
+
try {
|
|
600
|
+
execSync(`${cmd} ${url}`, { stdio: 'ignore' });
|
|
601
|
+
} catch {}
|
|
602
|
+
}, 2000);
|
|
603
|
+
}
|
|
221
604
|
|
|
222
605
|
let server;
|
|
223
606
|
if (usedPrebuilt) {
|
|
224
607
|
server = spawn('node', [standaloneServerPath], {
|
|
225
|
-
cwd: APP_DIR,
|
|
608
|
+
cwd: config.APP_DIR,
|
|
226
609
|
stdio: 'inherit',
|
|
227
|
-
env: { ...process.env, PORT:
|
|
610
|
+
env: { ...process.env, PORT: config.PORT, HOSTNAME: config.HOST },
|
|
228
611
|
shell: false
|
|
229
612
|
});
|
|
230
613
|
} else {
|
|
231
614
|
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
232
|
-
server = spawn(npxCmd, ['next', 'start', '--port', '
|
|
233
|
-
cwd: APP_DIR, stdio: 'inherit', shell: false
|
|
615
|
+
server = spawn(npxCmd, ['next', 'start', '--port', config.PORT, '--hostname', config.HOST], {
|
|
616
|
+
cwd: config.APP_DIR, stdio: 'inherit', shell: false
|
|
234
617
|
});
|
|
235
618
|
}
|
|
236
619
|
|