9drive 1.0.1 → 1.0.3

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/bin/9drive.js CHANGED
@@ -1,320 +1,423 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * 9Drive CLI Launcher
5
- * Zero-config Local-First Storage Cloud Server
6
- */
7
-
8
- const fs = require('fs')
9
- const path = require('path')
10
- const os = require('os')
11
- const crypto = require('crypto')
12
- const { spawn, execSync } = require('child_process')
13
-
14
- // Version from root package.json
15
- let version = '1.0.0'
16
- try {
17
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
18
- if (pkg.version) version = pkg.version
19
- } catch {}
20
-
21
- const BANNER = `
22
- \x1b[36m ___ ___ _ \x1b[0m
23
- \x1b[36m / _ \\ | \\ _ _(_)_ _____ \x1b[0m
24
- \x1b[36m | (_) || |) | '_| \\ V / -_) \x1b[0m
25
- \x1b[36m \\__\\_\\|___/|_| |_|\\_/\\___| \x1b[33mv${version}\x1b[0m
26
- \x1b[90m Cloud Storage & Universal API Gateway\x1b[0m
27
- `
28
-
29
- function printHelp() {
30
- console.log(BANNER)
31
- console.log(`
32
- Usage:
33
- 9drive [command] [options]
34
-
35
- Commands:
36
- start Start the 9Drive local server (default)
37
- status Show 9Drive data directory, database, and configuration
38
- backup Create an instant backup of the local database
39
- open Open the 9Drive Web UI in your default browser
40
-
41
- Options:
42
- --port <num> Port number to run 9Drive on (default: 9999 or from config)
43
- --host <str> Host to bind server (default: localhost)
44
- --db <url> Custom database URL (SQLite file:... or MySQL mysql://...)
45
- --no-open Do not open the browser automatically upon launch
46
- --help, -h Show this help message
47
- --version, -v Show version number
48
-
49
- Examples:
50
- npx 9drive
51
- npx 9drive start --port 8080
52
- npx 9drive status
53
- npx 9drive backup
54
- `)
55
- }
56
-
57
- // Parse command line arguments
58
- const args = process.argv.slice(2)
59
- let command = 'start'
60
- let portArg = null
61
- let dbArg = null
62
- let noOpen = false
63
-
64
- for (let i = 0; i < args.length; i++) {
65
- const arg = args[i]
66
- if (arg === '--help' || arg === '-h') {
67
- printHelp()
68
- process.exit(0)
69
- }
70
- if (arg === '--version' || arg === '-v') {
71
- console.log(`9Drive v${version}`)
72
- process.exit(0)
73
- }
74
- if (arg === '--port') {
75
- portArg = Number(args[++i])
76
- } else if (arg === '--db') {
77
- dbArg = args[++i]
78
- } else if (arg === '--no-open') {
79
- noOpen = true
80
- } else if (!arg.startsWith('-') && i === 0) {
81
- command = arg
82
- }
83
- }
84
-
85
- // Setup user data directory: ~/.9drive/
86
- const homeDir = os.homedir()
87
- const dataDir = process.env.NINEDRIVE_DATA_DIR || path.join(homeDir, '.9drive')
88
- const backupsDir = path.join(dataDir, 'backups')
89
- const storageDir = path.join(dataDir, 'storage')
90
- const configPath = path.join(dataDir, 'config.json')
91
- const sqliteDbPath = path.join(dataDir, '9drive.db')
92
-
93
- function ensureDirectories() {
94
- if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
95
- if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true })
96
- if (!fs.existsSync(storageDir)) fs.mkdirSync(storageDir, { recursive: true })
97
- }
98
-
99
- function loadOrCreateConfig() {
100
- ensureDirectories()
101
- let config = {}
102
- if (fs.existsSync(configPath)) {
103
- try {
104
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
105
- } catch {}
106
- }
107
-
108
- const defaultPort = portArg || config.APP_PORT || 9999
109
- const defaultDatabaseUrl = dbArg || config.DATABASE_URL || `file:${sqliteDbPath.replace(/\\/g, '/')}`
110
- const defaultFrontendUrl = config.FRONTEND_URL || `http://localhost:${defaultPort}`
111
- const defaultJwtSecret = config.JWT_ACCESS_SECRET || crypto.randomBytes(32).toString('hex')
112
- const defaultEncryptionKey = config.TOKEN_ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex')
113
-
114
- const updatedConfig = {
115
- APP_PORT: defaultPort,
116
- DATABASE_URL: defaultDatabaseUrl,
117
- FRONTEND_URL: defaultFrontendUrl,
118
- JWT_ACCESS_SECRET: defaultJwtSecret,
119
- TOKEN_ENCRYPTION_KEY: defaultEncryptionKey,
120
- MAX_UPLOAD_BYTES: config.MAX_UPLOAD_BYTES || 5 * 1024 * 1024 * 1024,
121
- ...config,
122
- }
123
-
124
- if (portArg) updatedConfig.APP_PORT = portArg
125
- if (dbArg) updatedConfig.DATABASE_URL = dbArg
126
-
127
- fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2))
128
- return updatedConfig
129
- }
130
-
131
- function openBrowser(url) {
132
- const start =
133
- process.platform === 'darwin'
134
- ? 'open'
135
- : process.platform === 'win32'
136
- ? 'start'
137
- : 'xdg-open'
138
- try {
139
- execSync(`${start} ${url}`, { stdio: 'ignore' })
140
- } catch {}
141
- }
142
-
143
- function handleStatus() {
144
- ensureDirectories()
145
- const config = loadOrCreateConfig()
146
- console.log(BANNER)
147
- console.log('\x1b[32m[9Drive Status]\x1b[0m')
148
- console.log(` Version: v${version}`)
149
- console.log(` Data Directory: ${dataDir}`)
150
- console.log(` Config File: ${configPath}`)
151
- console.log(` Database URL: ${config.DATABASE_URL}`)
152
- console.log(` Configured Port: ${config.APP_PORT}`)
153
-
154
- if (fs.existsSync(sqliteDbPath)) {
155
- const stats = fs.statSync(sqliteDbPath)
156
- console.log(` SQLite DB Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`)
157
- } else {
158
- console.log(` SQLite DB Size: Not initialized yet`)
159
- }
160
-
161
- const backups = fs.existsSync(backupsDir) ? fs.readdirSync(backupsDir) : []
162
- console.log(` Total Backups: ${backups.length} snapshot(s)`)
163
- }
164
-
165
- function handleBackup() {
166
- ensureDirectories()
167
- if (!fs.existsSync(sqliteDbPath)) {
168
- console.log('\x1b[33m[!] No SQLite database found to backup yet.\x1b[0m')
169
- return
170
- }
171
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
172
- const destPath = path.join(backupsDir, `9drive-backup-${timestamp}.db`)
173
- fs.copyFileSync(sqliteDbPath, destPath)
174
- console.log(`\x1b[32m[✓] Database backup created:\x1b[0m ${destPath}`)
175
- }
176
-
177
- async function handleStart() {
178
- ensureDirectories()
179
- const config = loadOrCreateConfig()
180
-
181
- console.log(BANNER)
182
- console.log(` \x1b[32m[✓]\x1b[0m Data directory: \x1b[90m${dataDir}\x1b[0m`)
183
-
184
- const isSqlite = config.DATABASE_URL.startsWith('file:') || config.DATABASE_URL.startsWith('sqlite:')
185
-
186
- // Auto-backup database on startup if existing SQLite database found
187
- if (isSqlite && fs.existsSync(sqliteDbPath)) {
188
- try {
189
- const stats = fs.statSync(sqliteDbPath)
190
- if (stats.size > 0) {
191
- const timestamp = new Date().toISOString().slice(0, 10)
192
- const autoBackup = path.join(backupsDir, `auto-backup-${timestamp}.db`)
193
- fs.copyFileSync(sqliteDbPath, autoBackup)
194
- }
195
- } catch {}
196
- }
197
-
198
- // Resolve backend root and prisma
199
- const backendDir = path.resolve(__dirname, '../backend')
200
- const rootDir = path.resolve(__dirname, '..')
201
-
202
- // Sync SQLite schema automatically if in SQLite mode
203
- if (isSqlite) {
204
- const sqliteSchemaPath = path.join(backendDir, 'prisma/schema.sqlite.prisma')
205
- const sqliteClientIndex = path.join(backendDir, 'prisma/generated/sqlite-client/index.js')
206
- const prismaCli = [
207
- path.join(rootDir, 'node_modules/prisma/build/index.js'),
208
- path.join(backendDir, 'node_modules/prisma/build/index.js'),
209
- ].find((p) => fs.existsSync(p)) || null
210
-
211
- if (fs.existsSync(sqliteSchemaPath) && prismaCli) {
212
- if (!fs.existsSync(sqliteClientIndex)) {
213
- process.stdout.write(' [⧗] Generating SQLite database client... ')
214
- try {
215
- execSync(`node "${prismaCli}" generate --schema="${sqliteSchemaPath}"`, {
216
- stdio: 'ignore',
217
- env: { ...process.env, DATABASE_URL: config.DATABASE_URL },
218
- })
219
- console.log('\x1b[32m[Ready]\x1b[0m')
220
- } catch {
221
- console.log('\x1b[33m[Notice: Generator skipped]\x1b[0m')
222
- }
223
- }
224
-
225
- process.stdout.write(' [⧗] Checking database schema... ')
226
- try {
227
- execSync(
228
- `node "${prismaCli}" db push --schema="${sqliteSchemaPath}" --skip-generate`,
229
- {
230
- stdio: 'ignore',
231
- env: {
232
- ...process.env,
233
- DATABASE_URL: config.DATABASE_URL,
234
- },
235
- }
236
- )
237
- console.log('\x1b[32m[Ready]\x1b[0m')
238
- } catch (err) {
239
- console.log('\x1b[33m[Notice: Using existing schema]\x1b[0m')
240
- }
241
- }
242
- }
243
-
244
- // Locate server entry file (dist/server.js or src/server.ts)
245
- let serverEntry = path.join(backendDir, 'dist/server.js')
246
- let useTsx = false
247
-
248
- if (!fs.existsSync(serverEntry)) {
249
- serverEntry = path.join(backendDir, 'src/server.ts')
250
- useTsx = true
251
- }
252
-
253
- const serverUrl = `http://localhost:${config.APP_PORT}`
254
- console.log(` \x1b[32m[✓]\x1b[0m Web Dashboard: \x1b[36m\x1b[4m${serverUrl}\x1b[0m`)
255
- console.log(` \x1b[90m[i] Press Ctrl+C to stop the server.\x1b[0m\n`)
256
-
257
- const nodePath = [
258
- path.join(backendDir, 'node_modules'),
259
- path.join(rootDir, 'node_modules'),
260
- process.env.NODE_PATH || '',
261
- ].filter(Boolean).join(path.delimiter)
262
-
263
- const envVars = {
264
- ...process.env,
265
- NODE_PATH: nodePath,
266
- NINEDRIVE_CLI: 'true',
267
- NINEDRIVE_DATA_DIR: dataDir,
268
- APP_PORT: String(config.APP_PORT),
269
- DATABASE_URL: config.DATABASE_URL,
270
- FRONTEND_URL: config.FRONTEND_URL || serverUrl,
271
- JWT_ACCESS_SECRET: config.JWT_ACCESS_SECRET,
272
- TOKEN_ENCRYPTION_KEY: config.TOKEN_ENCRYPTION_KEY,
273
- SERVE_FRONTEND: 'true',
274
- }
275
-
276
- const runner = useTsx
277
- ? path.join(backendDir, 'node_modules/tsx/dist/cli.mjs')
278
- : null
279
-
280
- const child = runner
281
- ? spawn('node', [runner, serverEntry], {
282
- cwd: backendDir,
283
- env: envVars,
284
- stdio: 'inherit',
285
- })
286
- : spawn('node', [serverEntry], {
287
- cwd: backendDir,
288
- env: envVars,
289
- stdio: 'inherit',
290
- })
291
-
292
- // Open browser after short delay
293
- if (!noOpen) {
294
- setTimeout(() => {
295
- openBrowser(serverUrl)
296
- }, 1500)
297
- }
298
-
299
- child.on('exit', (code) => {
300
- process.exit(code || 0)
301
- })
302
- }
303
-
304
- // Route commands
305
- switch (command) {
306
- case 'status':
307
- handleStatus()
308
- break
309
- case 'backup':
310
- handleBackup()
311
- break
312
- case 'open':
313
- const conf = loadOrCreateConfig()
314
- openBrowser(`http://localhost:${conf.APP_PORT}`)
315
- break
316
- case 'start':
317
- default:
318
- handleStart()
319
- break
320
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 9Drive CLI Launcher
5
+ * Zero-config Local-First Storage Cloud Server
6
+ */
7
+
8
+ const fs = require('fs')
9
+ const path = require('path')
10
+ const os = require('os')
11
+ const crypto = require('crypto')
12
+ const { spawn, execSync } = require('child_process')
13
+
14
+ // Version from root package.json
15
+ let version = '1.0.0'
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
18
+ if (pkg.version) version = pkg.version
19
+ } catch {}
20
+
21
+ const BANNER = `
22
+ \x1b[36m _ ___ ____ _ \x1b[0m
23
+ \x1b[36m / | / (_)___ ___ / __ \\_____(_) _____ \x1b[0m
24
+ \x1b[36m / |/ / / __ \\/ _ \\/ / / / ___/ / | / / _ \\\x1b[0m
25
+ \x1b[36m / /| / / / / / __/ /_/ / / / /| |/ / __/\x1b[0m
26
+ \x1b[36m/_/ |_/_/_/ /_/\\___/_____/_/ /_/ |___/\\___/ \x1b[33mv${version}\x1b[0m
27
+ \x1b[90m Cloud Storage & Universal API Gateway\x1b[0m
28
+ `
29
+
30
+ function printHelp() {
31
+ console.log(BANNER)
32
+ console.log(`
33
+ Usage:
34
+ 9drive [command] [options]
35
+
36
+ Commands:
37
+ start Start the 9Drive local server (default)
38
+ status Show 9Drive data directory, database, and configuration
39
+ shortcut Create a Desktop shortcut / launcher (Windows, macOS, Linux)
40
+ backup Create an instant backup of the local database
41
+ open Open the 9Drive Web UI in your default browser
42
+
43
+ Options:
44
+ --port <num> Port number to run 9Drive on (default: 9999 or from config)
45
+ --host <str> Host to bind server (default: localhost)
46
+ --db <url> Custom database URL (SQLite file:... or MySQL mysql://...)
47
+ --no-open Do not open the browser automatically upon launch
48
+ --help, -h Show this help message
49
+ --version, -v Show version number
50
+
51
+ Examples:
52
+ npx 9drive
53
+ npm i -g 9drive && 9drive shortcut
54
+ 9drive start --port 8080
55
+ 9drive status
56
+ 9drive shortcut
57
+ `)
58
+ }
59
+
60
+ // Parse command line arguments
61
+ const args = process.argv.slice(2)
62
+ let command = 'start'
63
+ let portArg = null
64
+ let dbArg = null
65
+ let noOpen = false
66
+
67
+ for (let i = 0; i < args.length; i++) {
68
+ const arg = args[i]
69
+ if (arg === '--help' || arg === '-h') {
70
+ printHelp()
71
+ process.exit(0)
72
+ }
73
+ if (arg === '--version' || arg === '-v') {
74
+ console.log(`9Drive v${version}`)
75
+ process.exit(0)
76
+ }
77
+ if (arg === '--port') {
78
+ portArg = Number(args[++i])
79
+ } else if (arg === '--db') {
80
+ dbArg = args[++i]
81
+ } else if (arg === '--no-open') {
82
+ noOpen = true
83
+ } else if (!arg.startsWith('-') && i === 0) {
84
+ command = arg
85
+ }
86
+ }
87
+
88
+ // Setup user data directory: ~/.9drive/
89
+ const homeDir = os.homedir()
90
+ const dataDir = process.env.NINEDRIVE_DATA_DIR || path.join(homeDir, '.9drive')
91
+ const backupsDir = path.join(dataDir, 'backups')
92
+ const storageDir = path.join(dataDir, 'storage')
93
+ const configPath = path.join(dataDir, 'config.json')
94
+ const sqliteDbPath = path.join(dataDir, '9drive.db')
95
+
96
+ function ensureDirectories() {
97
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
98
+ if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true })
99
+ if (!fs.existsSync(storageDir)) fs.mkdirSync(storageDir, { recursive: true })
100
+ }
101
+
102
+ function loadOrCreateConfig() {
103
+ ensureDirectories()
104
+ let config = {}
105
+ if (fs.existsSync(configPath)) {
106
+ try {
107
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
108
+ } catch {}
109
+ }
110
+
111
+ const defaultPort = portArg || config.APP_PORT || 9999
112
+ const defaultDatabaseUrl = dbArg || config.DATABASE_URL || `file:${sqliteDbPath.replace(/\\/g, '/')}`
113
+ const defaultFrontendUrl = config.FRONTEND_URL || `http://localhost:${defaultPort}`
114
+ const defaultJwtSecret = config.JWT_ACCESS_SECRET || crypto.randomBytes(32).toString('hex')
115
+ const defaultEncryptionKey = config.TOKEN_ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex')
116
+
117
+ const updatedConfig = {
118
+ APP_PORT: defaultPort,
119
+ DATABASE_URL: defaultDatabaseUrl,
120
+ FRONTEND_URL: defaultFrontendUrl,
121
+ JWT_ACCESS_SECRET: defaultJwtSecret,
122
+ TOKEN_ENCRYPTION_KEY: defaultEncryptionKey,
123
+ MAX_UPLOAD_BYTES: config.MAX_UPLOAD_BYTES || 5 * 1024 * 1024 * 1024,
124
+ ...config,
125
+ }
126
+
127
+ if (portArg) updatedConfig.APP_PORT = portArg
128
+ if (dbArg) updatedConfig.DATABASE_URL = dbArg
129
+
130
+ fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2))
131
+ return updatedConfig
132
+ }
133
+
134
+ function openBrowser(url) {
135
+ const start =
136
+ process.platform === 'darwin'
137
+ ? 'open'
138
+ : process.platform === 'win32'
139
+ ? 'start'
140
+ : 'xdg-open'
141
+ try {
142
+ execSync(`${start} ${url}`, { stdio: 'ignore' })
143
+ } catch {}
144
+ }
145
+
146
+ function handleStatus() {
147
+ ensureDirectories()
148
+ const config = loadOrCreateConfig()
149
+ console.log(BANNER)
150
+ console.log('\x1b[32m[9Drive Status]\x1b[0m')
151
+ console.log(` Version: v${version}`)
152
+ console.log(` Data Directory: ${dataDir}`)
153
+ console.log(` Config File: ${configPath}`)
154
+ console.log(` Database URL: ${config.DATABASE_URL}`)
155
+ console.log(` Configured Port: ${config.APP_PORT}`)
156
+
157
+ if (fs.existsSync(sqliteDbPath)) {
158
+ const stats = fs.statSync(sqliteDbPath)
159
+ console.log(` SQLite DB Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`)
160
+ } else {
161
+ console.log(` SQLite DB Size: Not initialized yet`)
162
+ }
163
+
164
+ const backups = fs.existsSync(backupsDir) ? fs.readdirSync(backupsDir) : []
165
+ console.log(` Total Backups: ${backups.length} snapshot(s)`)
166
+ }
167
+
168
+ function handleBackup() {
169
+ ensureDirectories()
170
+ if (!fs.existsSync(sqliteDbPath)) {
171
+ console.log('\x1b[33m[!] No SQLite database found to backup yet.\x1b[0m')
172
+ return
173
+ }
174
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
175
+ const destPath = path.join(backupsDir, `9drive-backup-${timestamp}.db`)
176
+ fs.copyFileSync(sqliteDbPath, destPath)
177
+ console.log(`\x1b[32m[✓] Database backup created:\x1b[0m ${destPath}`)
178
+ }
179
+
180
+ function handleShortcut() {
181
+ console.log(BANNER)
182
+ const home = os.homedir()
183
+ const platform = process.platform
184
+
185
+ if (platform === 'win32') {
186
+ const iconPath = path.join(__dirname, '../backend/public/favicon.ico')
187
+
188
+ const vbsScript = `
189
+ Set oWS = WScript.CreateObject("WScript.Shell")
190
+ sDesktop = oWS.SpecialFolders("Desktop")
191
+ sPrograms = oWS.SpecialFolders("Programs")
192
+
193
+ sLinkFile = sDesktop & "\\9Drive.lnk"
194
+ Set oLink = oWS.CreateShortcut(sLinkFile)
195
+ oLink.TargetPath = "cmd.exe"
196
+ oLink.Arguments = "/c 9drive"
197
+ oLink.Description = "9Drive Personal Cloud Storage"
198
+ oLink.WorkingDirectory = "${home.replace(/\\/g, '\\\\')}"
199
+ ${fs.existsSync(iconPath) ? `oLink.IconLocation = "${iconPath.replace(/\\/g, '\\\\')}"` : ''}
200
+ oLink.Save
201
+
202
+ On Error Resume Next
203
+ sLinkFile2 = sPrograms & "\\9Drive.lnk"
204
+ Set oLink2 = oWS.CreateShortcut(sLinkFile2)
205
+ oLink2.TargetPath = "cmd.exe"
206
+ oLink2.Arguments = "/c 9drive"
207
+ oLink2.Description = "9Drive Personal Cloud Storage"
208
+ oLink2.WorkingDirectory = "${home.replace(/\\/g, '\\\\')}"
209
+ ${fs.existsSync(iconPath) ? `oLink2.IconLocation = "${iconPath.replace(/\\/g, '\\\\')}"` : ''}
210
+ oLink2.Save
211
+
212
+ WScript.Echo sDesktop
213
+ `
214
+
215
+ const tempVbs = path.join(os.tmpdir(), `create-9drive-shortcut-${Date.now()}.vbs`)
216
+ try {
217
+ fs.writeFileSync(tempVbs, vbsScript)
218
+ const output = execSync(`cscript //nologo "${tempVbs}"`, { encoding: 'utf8' }).trim()
219
+ try { fs.unlinkSync(tempVbs) } catch {}
220
+ console.log(' \x1b[32m[✓]\x1b[0m Shortcut Desktop Windows berhasil dipasang!')
221
+ console.log(` Lokasi Desktop: \x1b[36m${output}\\9Drive.lnk\x1b[0m`)
222
+ console.log(' Sekarang Anda cukup double-click icon "9Drive" di Desktop untuk membuka aplikasi!\n')
223
+ } catch (err) {
224
+ console.log(' \x1b[31m[x]\x1b[0m Gagal membuat shortcut:', err.message)
225
+ }
226
+ } else if (platform === 'darwin') {
227
+ const desktopPath = path.join(home, 'Desktop')
228
+ const commandPath = path.join(desktopPath, '9Drive.command')
229
+ const scriptContent = `#!/bin/bash\n9drive\n`
230
+ try {
231
+ fs.writeFileSync(commandPath, scriptContent)
232
+ fs.chmodSync(commandPath, '755')
233
+ console.log(' \x1b[32m[✓]\x1b[0m macOS Desktop launcher created successfully!')
234
+ console.log(` Location: \x1b[36m${commandPath}\x1b[0m`)
235
+ console.log(' Double-click the 9Drive.command file to launch!\n')
236
+ } catch (err) {
237
+ console.log(' \x1b[31m[x]\x1b[0m Failed to create shortcut:', err.message)
238
+ }
239
+ } else {
240
+ const desktopDirs = [
241
+ path.join(home, '.local/share/applications'),
242
+ path.join(home, 'Desktop'),
243
+ ]
244
+ const desktopContent = `[Desktop Entry]
245
+ Name=9Drive
246
+ Comment=Personal Cloud Storage & Universal API Gateway
247
+ Exec=9drive
248
+ Terminal=false
249
+ Type=Application
250
+ Categories=Network;FileTransfer;Utility;
251
+ `
252
+ let created = false
253
+ for (const dir of desktopDirs) {
254
+ if (fs.existsSync(dir)) {
255
+ const file = path.join(dir, '9drive.desktop')
256
+ try {
257
+ fs.writeFileSync(file, desktopContent)
258
+ fs.chmodSync(file, '755')
259
+ created = true
260
+ console.log(` \x1b[32m[✓]\x1b[0m Linux launcher created: \x1b[36m${file}\x1b[0m`)
261
+ } catch {}
262
+ }
263
+ }
264
+ if (created) {
265
+ console.log(' 9Drive is now available in your application launcher!\n')
266
+ }
267
+ }
268
+ }
269
+
270
+ async function handleStart() {
271
+ ensureDirectories()
272
+ const config = loadOrCreateConfig()
273
+
274
+ console.log(BANNER)
275
+ console.log(` \x1b[32m[✓]\x1b[0m Data directory: \x1b[90m${dataDir}\x1b[0m`)
276
+
277
+ const shortcutCheck = process.platform === 'win32'
278
+ ? path.join(os.homedir(), 'Desktop/9Drive.lnk')
279
+ : path.join(os.homedir(), 'Desktop/9Drive.command')
280
+ if (!fs.existsSync(shortcutCheck)) {
281
+ console.log(` \x1b[33m[💡 Tip]\x1b[0m Ingin pasang icon di Desktop? Jalankan: \x1b[36m9drive shortcut\x1b[0m`)
282
+ }
283
+
284
+ const isSqlite = config.DATABASE_URL.startsWith('file:') || config.DATABASE_URL.startsWith('sqlite:')
285
+
286
+ // Auto-backup database on startup if existing SQLite database found
287
+ if (isSqlite && fs.existsSync(sqliteDbPath)) {
288
+ try {
289
+ const stats = fs.statSync(sqliteDbPath)
290
+ if (stats.size > 0) {
291
+ const timestamp = new Date().toISOString().slice(0, 10)
292
+ const autoBackup = path.join(backupsDir, `auto-backup-${timestamp}.db`)
293
+ fs.copyFileSync(sqliteDbPath, autoBackup)
294
+ }
295
+ } catch {}
296
+ }
297
+
298
+ // Resolve backend root and prisma
299
+ const backendDir = path.resolve(__dirname, '../backend')
300
+ const rootDir = path.resolve(__dirname, '..')
301
+
302
+ // Sync SQLite schema automatically if in SQLite mode
303
+ if (isSqlite) {
304
+ const sqliteSchemaPath = path.join(backendDir, 'prisma/schema.sqlite.prisma')
305
+ const sqliteClientIndex = path.join(backendDir, 'prisma/generated/sqlite-client/index.js')
306
+ const prismaCli = [
307
+ path.join(rootDir, 'node_modules/prisma/build/index.js'),
308
+ path.join(backendDir, 'node_modules/prisma/build/index.js'),
309
+ ].find((p) => fs.existsSync(p)) || null
310
+
311
+ if (fs.existsSync(sqliteSchemaPath) && prismaCli) {
312
+ if (!fs.existsSync(sqliteClientIndex)) {
313
+ process.stdout.write(' [⧗] Generating SQLite database client... ')
314
+ try {
315
+ execSync(`node "${prismaCli}" generate --schema="${sqliteSchemaPath}"`, {
316
+ stdio: 'ignore',
317
+ env: { ...process.env, DATABASE_URL: config.DATABASE_URL },
318
+ })
319
+ console.log('\x1b[32m[Ready]\x1b[0m')
320
+ } catch {
321
+ console.log('\x1b[33m[Notice: Generator skipped]\x1b[0m')
322
+ }
323
+ }
324
+
325
+ process.stdout.write(' [⧗] Checking database schema... ')
326
+ try {
327
+ execSync(
328
+ `node "${prismaCli}" db push --schema="${sqliteSchemaPath}" --skip-generate`,
329
+ {
330
+ stdio: 'ignore',
331
+ env: {
332
+ ...process.env,
333
+ DATABASE_URL: config.DATABASE_URL,
334
+ },
335
+ }
336
+ )
337
+ console.log('\x1b[32m[Ready]\x1b[0m')
338
+ } catch (err) {
339
+ console.log('\x1b[33m[Notice: Using existing schema]\x1b[0m')
340
+ }
341
+ }
342
+ }
343
+
344
+ // Locate server entry file (dist/server.js or src/server.ts)
345
+ let serverEntry = path.join(backendDir, 'dist/server.js')
346
+ let useTsx = false
347
+
348
+ if (!fs.existsSync(serverEntry)) {
349
+ serverEntry = path.join(backendDir, 'src/server.ts')
350
+ useTsx = true
351
+ }
352
+
353
+ const serverUrl = `http://localhost:${config.APP_PORT}`
354
+ console.log(` \x1b[32m[✓]\x1b[0m Web Dashboard: \x1b[36m\x1b[4m${serverUrl}\x1b[0m`)
355
+ console.log(` \x1b[90m[i] Press Ctrl+C to stop the server.\x1b[0m\n`)
356
+
357
+ const nodePath = [
358
+ path.join(backendDir, 'node_modules'),
359
+ path.join(rootDir, 'node_modules'),
360
+ process.env.NODE_PATH || '',
361
+ ].filter(Boolean).join(path.delimiter)
362
+
363
+ const envVars = {
364
+ ...process.env,
365
+ NODE_PATH: nodePath,
366
+ NINEDRIVE_CLI: 'true',
367
+ NINEDRIVE_DATA_DIR: dataDir,
368
+ APP_PORT: String(config.APP_PORT),
369
+ DATABASE_URL: config.DATABASE_URL,
370
+ FRONTEND_URL: config.FRONTEND_URL || serverUrl,
371
+ JWT_ACCESS_SECRET: config.JWT_ACCESS_SECRET,
372
+ TOKEN_ENCRYPTION_KEY: config.TOKEN_ENCRYPTION_KEY,
373
+ SERVE_FRONTEND: 'true',
374
+ }
375
+
376
+ const runner = useTsx
377
+ ? path.join(backendDir, 'node_modules/tsx/dist/cli.mjs')
378
+ : null
379
+
380
+ const child = runner
381
+ ? spawn('node', [runner, serverEntry], {
382
+ cwd: backendDir,
383
+ env: envVars,
384
+ stdio: 'inherit',
385
+ })
386
+ : spawn('node', [serverEntry], {
387
+ cwd: backendDir,
388
+ env: envVars,
389
+ stdio: 'inherit',
390
+ })
391
+
392
+ // Open browser after short delay
393
+ if (!noOpen) {
394
+ setTimeout(() => {
395
+ openBrowser(serverUrl)
396
+ }, 1500)
397
+ }
398
+
399
+ child.on('exit', (code) => {
400
+ process.exit(code || 0)
401
+ })
402
+ }
403
+
404
+ // Route commands
405
+ switch (command) {
406
+ case 'status':
407
+ handleStatus()
408
+ break
409
+ case 'shortcut':
410
+ handleShortcut()
411
+ break
412
+ case 'backup':
413
+ handleBackup()
414
+ break
415
+ case 'open':
416
+ const conf = loadOrCreateConfig()
417
+ openBrowser(`http://localhost:${conf.APP_PORT}`)
418
+ break
419
+ case 'start':
420
+ default:
421
+ handleStart()
422
+ break
423
+ }