nivii 0.2.5 → 0.3.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 +224 -1
- package/dist/cli.js +1 -1
- package/package.json +3 -4
- package/src/cli.ts +118 -0
- package/src/commands/config.ts +33 -0
- package/src/commands/share.ts +159 -0
- package/src/commands/whoami.ts +16 -0
- package/src/core/analytics.ts +64 -0
- package/src/core/build.ts +46 -0
- package/src/core/detect.ts +159 -0
- package/src/core/qr.ts +30 -0
- package/src/core/tunnel.ts +70 -0
- package/src/core/upload.ts +95 -0
- package/src/ui/spinner.ts +36 -0
- package/src/ui/theme.ts +35 -0
- package/src/ui/welcome.ts +104 -0
- package/src/utils/config.ts +47 -0
- package/src/utils/hash.ts +11 -0
- package/src/utils/zip.ts +22 -0
package/README.md
CHANGED
|
@@ -153,10 +153,233 @@ nivii --help # Show all commands
|
|
|
153
153
|
|
|
154
154
|
---
|
|
155
155
|
|
|
156
|
+
## Architecture
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
User runs: nivii share
|
|
160
|
+
│
|
|
161
|
+
▼
|
|
162
|
+
┌─────────────────────────────────────────────────────────┐
|
|
163
|
+
│ Nivii CLI (Node.js) │
|
|
164
|
+
│ │
|
|
165
|
+
│ 1. detectFramework() → identifies build system │
|
|
166
|
+
│ 2. runBuild() → executes build command │
|
|
167
|
+
│ 3. zipDirectory() → zips output dir │
|
|
168
|
+
│ 4. uploadDeploy() → POST /deploy to API │
|
|
169
|
+
│ 5. showQR() → renders QR in terminal │
|
|
170
|
+
│ 6. startLiveSync() → WebSocket watcher (--live) │
|
|
171
|
+
└──────────────────────────────┬──────────────────────────┘
|
|
172
|
+
│ HTTPS multipart/form-data
|
|
173
|
+
▼
|
|
174
|
+
┌─────────────────────────────────────────────────────────┐
|
|
175
|
+
│ api.nivii.app (Cloudflare Worker) │
|
|
176
|
+
│ │
|
|
177
|
+
│ POST /deploy │
|
|
178
|
+
│ ├─ Stores metadata in KV (slug, password, expiry…) │
|
|
179
|
+
│ └─ Uploads deploy.zip to R2 bucket │
|
|
180
|
+
└──────────────┬────────────────────────────────┬─────────┘
|
|
181
|
+
│ │
|
|
182
|
+
▼ ▼
|
|
183
|
+
┌──────────────────────┐ ┌─────────────────────────────┐
|
|
184
|
+
│ Cloudflare R2 │ │ Cloudflare KV │
|
|
185
|
+
│ (Object Storage) │ │ (Metadata + Analytics) │
|
|
186
|
+
│ │ │ │
|
|
187
|
+
│ deploys/{slug}/ │ │ deploy:{slug} → JSON meta │
|
|
188
|
+
│ └─ deploy.zip │ │ analytics:{slug} → stats │
|
|
189
|
+
└──────────────────────┘ └─────────────────────────────┘
|
|
190
|
+
│
|
|
191
|
+
▼
|
|
192
|
+
┌─────────────────────────────────────────────────────────┐
|
|
193
|
+
│ *.nivii.app (Cloudflare Worker wildcard routing) │
|
|
194
|
+
│ │
|
|
195
|
+
│ GET xyz.nivii.app/ │
|
|
196
|
+
│ ├─ Checks KV for metadata (expiry, password, OTP) │
|
|
197
|
+
│ ├─ Serves files from R2 │
|
|
198
|
+
│ └─ SPA fallback to index.html │
|
|
199
|
+
│ │
|
|
200
|
+
│ live.nivii.app/live/{slug} (WebSocket) │
|
|
201
|
+
│ └─ LiveSyncDO (Durable Object) │
|
|
202
|
+
│ └─ Broadcasts reload events to connected viewers │
|
|
203
|
+
└─────────────────────────────────────────────────────────┘
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Self-Hosting on Cloudflare
|
|
209
|
+
|
|
210
|
+
Deploy your own Nivii instance in ~10 minutes:
|
|
211
|
+
|
|
212
|
+
### Prerequisites
|
|
213
|
+
|
|
214
|
+
- [Cloudflare account](https://cloudflare.com) (free tier works)
|
|
215
|
+
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/): `npm i -g wrangler`
|
|
216
|
+
- Node.js 18+
|
|
217
|
+
|
|
218
|
+
### Steps
|
|
219
|
+
|
|
220
|
+
**1. Clone and install**
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
git clone https://github.com/Aadigarg111/Nivi-sh.git
|
|
224
|
+
cd Nivi-sh
|
|
225
|
+
npm install
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**2. Create Cloudflare resources**
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Login to Cloudflare
|
|
232
|
+
wrangler login
|
|
233
|
+
|
|
234
|
+
# Create KV namespace
|
|
235
|
+
wrangler kv:namespace create "DB"
|
|
236
|
+
wrangler kv:namespace create "DB" --preview
|
|
237
|
+
|
|
238
|
+
# Create R2 bucket
|
|
239
|
+
wrangler r2 bucket create nivii-deployments
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**3. Update wrangler.toml**
|
|
243
|
+
|
|
244
|
+
```toml
|
|
245
|
+
# packages/worker/wrangler.toml
|
|
246
|
+
[[kv_namespaces]]
|
|
247
|
+
binding = "DB"
|
|
248
|
+
id = "YOUR_ACTUAL_KV_ID" # from step 2
|
|
249
|
+
preview_id = "YOUR_PREVIEW_KV_ID"
|
|
250
|
+
|
|
251
|
+
[[r2_buckets]]
|
|
252
|
+
binding = "BUCKET"
|
|
253
|
+
bucket_name = "nivii-deployments"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**4. Deploy the worker**
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
cd packages/worker
|
|
260
|
+
wrangler deploy
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**5. Configure DNS**
|
|
264
|
+
|
|
265
|
+
Add a wildcard DNS record:
|
|
266
|
+
```
|
|
267
|
+
Type: CNAME
|
|
268
|
+
Name: *
|
|
269
|
+
Target: your-worker.workers.dev
|
|
270
|
+
Proxy: Enabled (orange cloud)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**6. Point the CLI at your instance**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
export NIVII_API=https://api.your-domain.com
|
|
277
|
+
|
|
278
|
+
# Or in config
|
|
279
|
+
nivii config apiBase https://api.your-domain.com
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Configuration
|
|
285
|
+
|
|
286
|
+
Config is stored at `~/.nivii/config.json`:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"token": "your-pro-token",
|
|
291
|
+
"plan": "free",
|
|
292
|
+
"apiBase": "https://api.nivii.app",
|
|
293
|
+
"deployments": [...]
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Manage it:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
nivii config # show all
|
|
301
|
+
nivii config token <tok> # set token
|
|
302
|
+
nivii config apiBase <url> # point to self-hosted instance
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Roadmap v2
|
|
308
|
+
|
|
309
|
+
- [ ] **Team workspaces** — share deployments with your team
|
|
310
|
+
- [ ] **Custom domains** — deploy to `preview.your-company.com`
|
|
311
|
+
- [ ] **GitHub Actions integration** — auto-share on every PR
|
|
312
|
+
- [ ] **VS Code extension** — deploy with a keyboard shortcut
|
|
313
|
+
- [ ] **Mobile app** — scan QR codes from Nivii deployments
|
|
314
|
+
- [ ] **Zip-streaming upload** — faster uploads for large projects
|
|
315
|
+
- [ ] **Screenshot previews** — auto-thumbnail for each deployment
|
|
316
|
+
- [ ] **API keys + webhooks** — programmatic deployments
|
|
317
|
+
- [ ] **Deployment history UI** — web dashboard at `app.nivii.app`
|
|
318
|
+
- [ ] **SSR support** — run Express/Fastify apps in serverless mode
|
|
319
|
+
- [ ] **Monorepo support** — deploy multiple packages at once
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Contributing
|
|
324
|
+
|
|
325
|
+
Contributions are welcome! This is an AGPL-3.0 open-source project.
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Fork and clone
|
|
329
|
+
git clone https://github.com/YOUR_USERNAME/Nivi-sh.git
|
|
330
|
+
cd Nivi-sh
|
|
331
|
+
|
|
332
|
+
# Install dependencies
|
|
333
|
+
npm install
|
|
334
|
+
|
|
335
|
+
# Build CLI
|
|
336
|
+
cd packages/cli && npm run build
|
|
337
|
+
|
|
338
|
+
# Run locally
|
|
339
|
+
node dist/cli.js share --help
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Project structure
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
packages/cli/ - The NPM CLI tool (published as `nivii`)
|
|
346
|
+
packages/worker/ - Cloudflare Worker backend
|
|
347
|
+
landing/ - Landing page HTML
|
|
348
|
+
.github/ - GitHub Actions workflows
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Submitting PRs
|
|
352
|
+
|
|
353
|
+
1. Fork the repository
|
|
354
|
+
2. Create a feature branch: `git checkout -b feat/my-feature`
|
|
355
|
+
3. Commit your changes: `git commit -m 'feat: add something cool'`
|
|
356
|
+
4. Push to your fork: `git push origin feat/my-feature`
|
|
357
|
+
5. Open a Pull Request
|
|
358
|
+
|
|
359
|
+
Please follow conventional commits and keep PRs focused.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## License
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
Nivii — Futuristic Project Sharing CLI
|
|
367
|
+
Copyright (C) 2025 Aadigarg111
|
|
368
|
+
|
|
369
|
+
This program is free software: you can redistribute it and/or modify
|
|
370
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
371
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
372
|
+
(at your option) any later version.
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**[AGPL-3.0](./LICENSE)** — See the LICENSE file for full terms.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
156
379
|
<div align="center">
|
|
157
380
|
|
|
158
381
|
Made with ❤️ by [Aadigarg111](https://github.com/Aadigarg111)
|
|
159
382
|
|
|
160
|
-
⚡ [nivii.app](https://nivii.app) · [npm](https://www.npmjs.com/package/nivii)
|
|
383
|
+
⚡ [nivii.app](https://nivii.app) · [npm](https://www.npmjs.com/package/nivii) · [GitHub](https://github.com/Aadigarg111/Nivi-sh)
|
|
161
384
|
|
|
162
385
|
</div>
|
package/dist/cli.js
CHANGED
|
@@ -92,7 +92,7 @@ cli
|
|
|
92
92
|
.action(async (slug) => {
|
|
93
93
|
await runAnalytics(slug);
|
|
94
94
|
});
|
|
95
|
-
cli.version('0.
|
|
95
|
+
cli.version('0.2.0');
|
|
96
96
|
// ─── Show help when no command given ─────────────────────────────────────────
|
|
97
97
|
cli.parse(process.argv, { run: false });
|
|
98
98
|
if (!cli.matchedCommand) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nivii",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "⚡ The fastest, most beautiful way to share any local project via a live URL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,8 +45,7 @@
|
|
|
45
45
|
"url": "https://github.com/Aadigarg111/Nivi-sh.git"
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://nivii.app",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"email": "contact@aadigarg.in"
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "mailto:contact@nivii.app"
|
|
51
50
|
}
|
|
52
51
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cac } from 'cac';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { runShare } from './commands/share.js';
|
|
5
|
+
import { runConfig } from './commands/config.js';
|
|
6
|
+
import { runWhoami } from './commands/whoami.js';
|
|
7
|
+
import { runAnalytics } from './core/analytics.js';
|
|
8
|
+
import { loadConfig, updateConfig } from './utils/config.js';
|
|
9
|
+
import { showWelcome, showHelp } from './ui/welcome.js';
|
|
10
|
+
|
|
11
|
+
const cli = cac('nivii');
|
|
12
|
+
|
|
13
|
+
// ─── First run ────────────────────────────────────────────────────────────────
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
if (config.firstRun) {
|
|
16
|
+
showWelcome();
|
|
17
|
+
updateConfig({ firstRun: false });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── share ────────────────────────────────────────────────────────────────────
|
|
21
|
+
cli
|
|
22
|
+
.command('share', 'Deploy your project and get a live URL')
|
|
23
|
+
.option('--dir <dir>', 'Directory to deploy (default: cwd)')
|
|
24
|
+
.option('--slug <slug>', 'Custom slug (Pro) e.g. my-demo → my-demo.nivii.app')
|
|
25
|
+
.option('--pass <password>', 'Password-protect your deployment')
|
|
26
|
+
.option('--otp', 'Enable one-time password access')
|
|
27
|
+
.option('--expires <duration>', 'Expiry: 1h / 24h / 7d / 30d / never')
|
|
28
|
+
.option('--live', 'Enable live sync (watch & auto-redeploy on changes)')
|
|
29
|
+
.option('--collab', 'Enable real-time collaborative preview (Pro)')
|
|
30
|
+
.option('--no-build', 'Skip build, use existing output directory')
|
|
31
|
+
.option('--cmd <cmd>', 'Custom build command')
|
|
32
|
+
.option('--port <port>', 'Port for server-mode proxy', { default: 3000 })
|
|
33
|
+
.option('--max-views <n>', 'Self-destruct after N views')
|
|
34
|
+
.option('--self-destruct', 'Self-destruct after first view')
|
|
35
|
+
.option('--no-qr', 'Skip QR code')
|
|
36
|
+
.option('--no-open', 'Do not open browser')
|
|
37
|
+
.action(async (options) => {
|
|
38
|
+
try {
|
|
39
|
+
await runShare({
|
|
40
|
+
dir: options.dir,
|
|
41
|
+
slug: options.slug,
|
|
42
|
+
pass: options.pass,
|
|
43
|
+
otp: options.otp,
|
|
44
|
+
expires: options.expires,
|
|
45
|
+
live: options.live,
|
|
46
|
+
collab: options.collab,
|
|
47
|
+
noBuild: options['no-build'],
|
|
48
|
+
cmd: options.cmd,
|
|
49
|
+
port: options.port,
|
|
50
|
+
maxViews: options.maxViews,
|
|
51
|
+
selfDestruct: options.selfDestruct,
|
|
52
|
+
open: options.open,
|
|
53
|
+
qr: options.qr,
|
|
54
|
+
});
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
console.error(chalk.red('\n ✗ ') + err.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─── config ───────────────────────────────────────────────────────────────────
|
|
62
|
+
cli
|
|
63
|
+
.command('config [key] [value]', 'View or set config values')
|
|
64
|
+
.action((key, value) => {
|
|
65
|
+
runConfig(key, value);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ─── whoami ───────────────────────────────────────────────────────────────────
|
|
69
|
+
cli
|
|
70
|
+
.command('whoami', 'Show current account info')
|
|
71
|
+
.action(() => {
|
|
72
|
+
runWhoami();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── ls (list deployments) ────────────────────────────────────────────────────
|
|
76
|
+
cli
|
|
77
|
+
.command('ls', 'List your recent deployments')
|
|
78
|
+
.action(() => {
|
|
79
|
+
const cfg = loadConfig();
|
|
80
|
+
const deployments = cfg.deployments || [];
|
|
81
|
+
if (!deployments.length) {
|
|
82
|
+
console.log(chalk.hex('#6B7280')('\n No deployments yet. Run: nivii share\n'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(chalk.hex('#A78BFA')(' Recent Deployments'));
|
|
87
|
+
console.log('');
|
|
88
|
+
deployments.forEach((d) => {
|
|
89
|
+
const age = timeSince(new Date(d.createdAt));
|
|
90
|
+
console.log(` ${chalk.cyan(d.slug)} · ${chalk.underline(d.url)} · ${chalk.hex('#6B7280')(age)}`);
|
|
91
|
+
});
|
|
92
|
+
console.log('');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── analytics ────────────────────────────────────────────────────────────────
|
|
96
|
+
cli
|
|
97
|
+
.command('analytics [slug]', 'View analytics for a deployment')
|
|
98
|
+
.action(async (slug) => {
|
|
99
|
+
await runAnalytics(slug);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
cli.version('0.1.8');
|
|
103
|
+
|
|
104
|
+
// ─── Show help when no command given ─────────────────────────────────────────
|
|
105
|
+
cli.parse(process.argv, { run: false });
|
|
106
|
+
if (!cli.matchedCommand) {
|
|
107
|
+
showHelp();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
await cli.runMatchedCommand();
|
|
111
|
+
|
|
112
|
+
function timeSince(date: Date): string {
|
|
113
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
114
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
115
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
116
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
117
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, saveConfig } from '../utils/config.js';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
|
|
5
|
+
export function runConfig(key?: string, value?: string) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
|
|
8
|
+
if (!key) {
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log(theme.primary(' Nivii Config') + theme.muted(' (~/.nivii/config.json)'));
|
|
11
|
+
console.log('');
|
|
12
|
+
Object.entries(config).forEach(([k, v]) => {
|
|
13
|
+
if (k === 'token') {
|
|
14
|
+
console.log(` ${chalk.cyan(k)}: ${chalk.hex('#6B7280')('[hidden]')}`);
|
|
15
|
+
} else if (k === 'deployments') {
|
|
16
|
+
console.log(` ${chalk.cyan(k)}: ${chalk.hex('#6B7280')(JSON.stringify(v).slice(0, 60) + '…')}`);
|
|
17
|
+
} else {
|
|
18
|
+
console.log(` ${chalk.cyan(k)}: ${chalk.white(String(v))}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
console.log('');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
console.log(` ${chalk.cyan(key)}: ${chalk.white(String((config as any)[key] ?? 'not set'))}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
(config as any)[key] = value;
|
|
31
|
+
saveConfig(config);
|
|
32
|
+
console.log(theme.success(` ✓ Set ${key} = ${value}`));
|
|
33
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { detectFramework } from '../core/detect.js';
|
|
4
|
+
import { runBuild } from '../core/build.js';
|
|
5
|
+
import { uploadDeploy } from '../core/upload.js';
|
|
6
|
+
import { generateSlug } from '../utils/hash.js';
|
|
7
|
+
import { loadConfig, updateConfig } from '../utils/config.js';
|
|
8
|
+
import { showQR, copyToClipboard, openBrowser } from '../core/qr.js';
|
|
9
|
+
import { startLiveSync } from '../core/tunnel.js';
|
|
10
|
+
import { showBanner } from '../ui/welcome.js';
|
|
11
|
+
import { step, success, warn } from '../ui/spinner.js';
|
|
12
|
+
import { theme } from '../ui/theme.js';
|
|
13
|
+
|
|
14
|
+
export interface ShareOptions {
|
|
15
|
+
dir?: string;
|
|
16
|
+
slug?: string;
|
|
17
|
+
pass?: string;
|
|
18
|
+
otp?: boolean;
|
|
19
|
+
expires?: string;
|
|
20
|
+
live?: boolean;
|
|
21
|
+
collab?: boolean;
|
|
22
|
+
noBuild?: boolean;
|
|
23
|
+
cmd?: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
maxViews?: number;
|
|
26
|
+
selfDestruct?: boolean;
|
|
27
|
+
open?: boolean;
|
|
28
|
+
qr?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runShare(opts: ShareOptions) {
|
|
32
|
+
showBanner();
|
|
33
|
+
|
|
34
|
+
const cwd = opts.dir ? path.resolve(opts.dir) : process.cwd();
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
|
|
37
|
+
// ─── 1. Detect framework ───────────────────────────────────────────────────
|
|
38
|
+
step('🔍', 'Detecting project type…', cwd);
|
|
39
|
+
const detected = detectFramework(cwd);
|
|
40
|
+
|
|
41
|
+
const frameworkLabel: Record<string, string> = {
|
|
42
|
+
nextjs: 'Next.js', vite: 'Vite', 'react-cra': 'React (CRA)',
|
|
43
|
+
vue: 'Vue', svelte: 'Svelte', sveltekit: 'SvelteKit',
|
|
44
|
+
astro: 'Astro', remix: 'Remix', nuxt: 'Nuxt',
|
|
45
|
+
solid: 'Solid.js', express: 'Express', fastify: 'Fastify',
|
|
46
|
+
hono: 'Hono', static: 'Static HTML', unknown: 'Unknown',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
success(`Detected ${chalk.cyan(frameworkLabel[detected.framework] || detected.framework)} ` +
|
|
50
|
+
`${theme.muted(`(${detected.confidence}% confidence)`)}`);
|
|
51
|
+
|
|
52
|
+
if (detected.framework === 'unknown' && !opts.noBuild) {
|
|
53
|
+
warn('Could not detect framework. Using current directory as static site.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── 2. Build ──────────────────────────────────────────────────────────────
|
|
57
|
+
const { outputDir } = await runBuild({
|
|
58
|
+
cwd,
|
|
59
|
+
detect: detected,
|
|
60
|
+
customCmd: opts.cmd,
|
|
61
|
+
noBuild: opts.noBuild,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const fullOutputDir = path.resolve(cwd, outputDir);
|
|
65
|
+
|
|
66
|
+
// ─── 3. Slug ───────────────────────────────────────────────────────────────
|
|
67
|
+
let slug = opts.slug;
|
|
68
|
+
if (!slug) {
|
|
69
|
+
slug = generateSlug(8);
|
|
70
|
+
} else if (config.plan !== 'pro') {
|
|
71
|
+
warn('Custom slugs require Nivii Pro. Using random slug instead.');
|
|
72
|
+
slug = generateSlug(8);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── 4. Upload (files directly, no zip) ────────────────────────────────────
|
|
76
|
+
const deployResult = await uploadDeploy({
|
|
77
|
+
buildDir: fullOutputDir,
|
|
78
|
+
slug,
|
|
79
|
+
token: config.token,
|
|
80
|
+
password: opts.pass,
|
|
81
|
+
otp: opts.otp,
|
|
82
|
+
expires: opts.expires || (config.plan === 'pro' ? undefined : '48h'),
|
|
83
|
+
live: opts.live,
|
|
84
|
+
collab: opts.collab,
|
|
85
|
+
maxViews: opts.maxViews,
|
|
86
|
+
selfDestruct: opts.selfDestruct,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ─── 5. Save to config ─────────────────────────────────────────────────────
|
|
90
|
+
const deployments = config.deployments || [];
|
|
91
|
+
deployments.unshift({
|
|
92
|
+
slug: deployResult.slug,
|
|
93
|
+
url: deployResult.url,
|
|
94
|
+
createdAt: new Date().toISOString(),
|
|
95
|
+
expiresAt: deployResult.expiresAt,
|
|
96
|
+
});
|
|
97
|
+
updateConfig({ deployments: deployments.slice(0, 20) });
|
|
98
|
+
|
|
99
|
+
// ─── 6. Output ─────────────────────────────────────────────────────────────
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(theme.box([
|
|
102
|
+
theme.accent(' 🚀 Your project is live!'),
|
|
103
|
+
'',
|
|
104
|
+
' ' + theme.primary('URL:') + ' ' + chalk.underline.cyan(deployResult.url),
|
|
105
|
+
...(deployResult.expiresAt ? [' ' + theme.muted('Expires:') + ' ' + theme.muted(new Date(deployResult.expiresAt).toLocaleString())] : []),
|
|
106
|
+
...(opts.pass ? [' ' + theme.muted('Protected:') + ' ' + theme.success('✓ Password')] : []),
|
|
107
|
+
...(opts.otp && deployResult.otpCode ? [' ' + theme.muted('OTP:') + ' ' + theme.warning(deployResult.otpCode)] : []),
|
|
108
|
+
...(opts.live ? [' ' + theme.muted('Live sync:') + ' ' + theme.success('✓ Active')] : []),
|
|
109
|
+
...(opts.collab ? [' ' + theme.muted('Collab:') + ' ' + theme.success('✓ Active')] : []),
|
|
110
|
+
]));
|
|
111
|
+
console.log('');
|
|
112
|
+
|
|
113
|
+
// ─── 7. Copy URL to clipboard ──────────────────────────────────────────────
|
|
114
|
+
const copied = await copyToClipboard(deployResult.url);
|
|
115
|
+
if (copied) {
|
|
116
|
+
success('URL copied to clipboard');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── 8. QR code ────────────────────────────────────────────────────────────
|
|
120
|
+
if (opts.qr !== false) {
|
|
121
|
+
await showQR(deployResult.url);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── 9. Open browser ───────────────────────────────────────────────────────
|
|
125
|
+
if (opts.open !== false) {
|
|
126
|
+
await openBrowser(deployResult.url);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── 10. Live sync ─────────────────────────────────────────────────────────
|
|
130
|
+
if (opts.live && deployResult.liveToken) {
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(theme.accent(' ⚡ Live Sync Active') + theme.muted(' — watching for changes…'));
|
|
133
|
+
console.log(theme.muted(' Press Ctrl+C to stop'));
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
const stopLive = startLiveSync({
|
|
137
|
+
watchDir: fullOutputDir,
|
|
138
|
+
slug: deployResult.slug,
|
|
139
|
+
liveToken: deployResult.liveToken,
|
|
140
|
+
onUpdate: async () => {
|
|
141
|
+
await uploadDeploy({
|
|
142
|
+
buildDir: fullOutputDir,
|
|
143
|
+
slug: deployResult.slug,
|
|
144
|
+
token: config.token,
|
|
145
|
+
live: true,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
process.on('SIGINT', () => {
|
|
151
|
+
stopLive();
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(theme.muted(' ↳ Live sync stopped'));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await new Promise(() => {});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from '../utils/config.js';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
|
|
5
|
+
export function runWhoami() {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
console.log('');
|
|
8
|
+
if (!config.token) {
|
|
9
|
+
console.log(theme.muted(' Not logged in. Using anonymous mode.'));
|
|
10
|
+
console.log(' ' + theme.accent('nivii login') + theme.muted(' to unlock Pro features.'));
|
|
11
|
+
} else {
|
|
12
|
+
console.log(theme.success(' ✓ Logged in'));
|
|
13
|
+
console.log(' ' + chalk.cyan('Plan: ') + chalk.white(config.plan || 'free'));
|
|
14
|
+
}
|
|
15
|
+
console.log('');
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nivii Analytics — View deployment analytics
|
|
3
|
+
*/
|
|
4
|
+
import { loadConfig } from '../utils/config.js';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
export interface AnalyticsData {
|
|
9
|
+
views: number;
|
|
10
|
+
sessions: Array<{
|
|
11
|
+
timestamp: string;
|
|
12
|
+
country: string;
|
|
13
|
+
city: string;
|
|
14
|
+
ua: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runAnalytics(slug?: string) {
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
const apiBase = config.apiBase || 'https://api.nivii.app';
|
|
21
|
+
|
|
22
|
+
if (!slug) {
|
|
23
|
+
const deployments = config.deployments || [];
|
|
24
|
+
if (!deployments.length) {
|
|
25
|
+
console.log(theme.muted('\n No deployments found. Run: nivii share\n'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
slug = deployments[0].slug;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log(theme.primary(' Analytics') + theme.muted(` for ${slug}.nivii.app`));
|
|
33
|
+
console.log('');
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const { default: fetch } = await import('node-fetch');
|
|
37
|
+
const res = await fetch(`${apiBase}/analytics/${slug}`, {
|
|
38
|
+
headers: config.token ? { Authorization: `Bearer ${config.token}` } : {},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
console.log(theme.error(' Failed to fetch analytics: ' + res.status));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = await res.json() as AnalyticsData;
|
|
47
|
+
|
|
48
|
+
console.log(' ' + chalk.cyan('Total views:') + ' ' + chalk.white(String(data.views)));
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
if (data.sessions && data.sessions.length > 0) {
|
|
52
|
+
console.log(' ' + theme.muted('Recent visitors:'));
|
|
53
|
+
console.log('');
|
|
54
|
+
data.sessions.slice(-10).reverse().forEach(s => {
|
|
55
|
+
const time = new Date(s.timestamp).toLocaleString();
|
|
56
|
+
console.log(` ${chalk.hex('#6B7280')(time)} ${chalk.cyan(s.country)}/${chalk.white(s.city)}`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('');
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
console.log(theme.error(' ' + err.message));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import type { DetectResult } from './detect.js';
|
|
3
|
+
import { createSpinner, step } from '../ui/spinner.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export interface BuildOptions {
|
|
7
|
+
cwd: string;
|
|
8
|
+
detect: DetectResult;
|
|
9
|
+
customCmd?: string;
|
|
10
|
+
noBuild?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runBuild(opts: BuildOptions): Promise<{ outputDir: string }> {
|
|
14
|
+
const { cwd, detect, customCmd, noBuild } = opts;
|
|
15
|
+
|
|
16
|
+
if (noBuild || detect.buildCommand === null) {
|
|
17
|
+
step('📁', 'Skipping build', detect.outputDir);
|
|
18
|
+
return { outputDir: detect.outputDir };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cmd = customCmd || detect.buildCommand;
|
|
22
|
+
const spinner = createSpinner(`Building with ${chalk.cyan(detect.framework)}…`);
|
|
23
|
+
spinner.start();
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const proc = spawn(cmd, [], {
|
|
27
|
+
cwd,
|
|
28
|
+
shell: true,
|
|
29
|
+
env: { ...process.env, NODE_ENV: 'production', CI: '1' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let stderr = '';
|
|
33
|
+
proc.stderr.on('data', d => { stderr += d.toString(); });
|
|
34
|
+
|
|
35
|
+
proc.on('close', code => {
|
|
36
|
+
if (code === 0) {
|
|
37
|
+
spinner.succeed(chalk.hex('#10B981')(`Build complete `) + chalk.hex('#6B7280')(`(${detect.framework})`));
|
|
38
|
+
resolve({ outputDir: detect.outputDir });
|
|
39
|
+
} else {
|
|
40
|
+
spinner.fail(chalk.hex('#EF4444')('Build failed'));
|
|
41
|
+
console.log(chalk.hex('#6B7280')(stderr.slice(-800)));
|
|
42
|
+
reject(new Error(`Build exited with code ${code}`));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export type Framework =
|
|
5
|
+
| 'nextjs'
|
|
6
|
+
| 'vite'
|
|
7
|
+
| 'react-cra'
|
|
8
|
+
| 'vue'
|
|
9
|
+
| 'svelte'
|
|
10
|
+
| 'sveltekit'
|
|
11
|
+
| 'astro'
|
|
12
|
+
| 'remix'
|
|
13
|
+
| 'nuxt'
|
|
14
|
+
| 'solid'
|
|
15
|
+
| 'express'
|
|
16
|
+
| 'fastify'
|
|
17
|
+
| 'hono'
|
|
18
|
+
| 'static'
|
|
19
|
+
| 'unknown';
|
|
20
|
+
|
|
21
|
+
export interface DetectResult {
|
|
22
|
+
framework: Framework;
|
|
23
|
+
buildCommand: string | null;
|
|
24
|
+
outputDir: string;
|
|
25
|
+
isServer: boolean;
|
|
26
|
+
port?: number;
|
|
27
|
+
packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
28
|
+
confidence: number; // 0-100
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function detectFramework(cwd: string): DetectResult {
|
|
32
|
+
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
33
|
+
const deps = {
|
|
34
|
+
...(pkg?.dependencies || {}),
|
|
35
|
+
...(pkg?.devDependencies || {}),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const has = (name: string) => name in deps;
|
|
39
|
+
const hasFile = (...files: string[]) =>
|
|
40
|
+
files.some(f => fs.existsSync(path.join(cwd, f)));
|
|
41
|
+
|
|
42
|
+
const pm = detectPackageManager(cwd);
|
|
43
|
+
|
|
44
|
+
// Next.js
|
|
45
|
+
if (has('next')) {
|
|
46
|
+
const outputDir = fs.existsSync(path.join(cwd, 'out')) ? 'out' :
|
|
47
|
+
fs.existsSync(path.join(cwd, '.next')) ? '.next' : 'out';
|
|
48
|
+
return {
|
|
49
|
+
framework: 'nextjs',
|
|
50
|
+
buildCommand: `${pm} run build`,
|
|
51
|
+
outputDir,
|
|
52
|
+
isServer: false,
|
|
53
|
+
packageManager: pm,
|
|
54
|
+
confidence: 99,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// SvelteKit
|
|
59
|
+
if (has('@sveltejs/kit')) {
|
|
60
|
+
return {
|
|
61
|
+
framework: 'sveltekit',
|
|
62
|
+
buildCommand: `${pm} run build`,
|
|
63
|
+
outputDir: 'build',
|
|
64
|
+
isServer: false,
|
|
65
|
+
packageManager: pm,
|
|
66
|
+
confidence: 95,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Astro
|
|
71
|
+
if (has('astro')) {
|
|
72
|
+
return {
|
|
73
|
+
framework: 'astro',
|
|
74
|
+
buildCommand: `${pm} run build`,
|
|
75
|
+
outputDir: 'dist',
|
|
76
|
+
isServer: false,
|
|
77
|
+
packageManager: pm,
|
|
78
|
+
confidence: 95,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Remix
|
|
83
|
+
if (has('@remix-run/react') || has('@remix-run/node')) {
|
|
84
|
+
return {
|
|
85
|
+
framework: 'remix',
|
|
86
|
+
buildCommand: `${pm} run build`,
|
|
87
|
+
outputDir: 'public',
|
|
88
|
+
isServer: true,
|
|
89
|
+
packageManager: pm,
|
|
90
|
+
confidence: 90,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Nuxt
|
|
95
|
+
if (has('nuxt') || has('nuxt3') || has('nuxt-edge')) {
|
|
96
|
+
return {
|
|
97
|
+
framework: 'nuxt',
|
|
98
|
+
buildCommand: `${pm} run generate`,
|
|
99
|
+
outputDir: '.output/public',
|
|
100
|
+
isServer: false,
|
|
101
|
+
packageManager: pm,
|
|
102
|
+
confidence: 90,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Vite (React/Vue/Svelte/Solid)
|
|
107
|
+
if (has('vite')) {
|
|
108
|
+
const outputDir = 'dist';
|
|
109
|
+
if (has('react') || has('react-dom')) {
|
|
110
|
+
return { framework: 'react-cra', buildCommand: `${pm} run build`, outputDir, isServer: false, packageManager: pm, confidence: 88 };
|
|
111
|
+
}
|
|
112
|
+
if (has('vue')) {
|
|
113
|
+
return { framework: 'vue', buildCommand: `${pm} run build`, outputDir, isServer: false, packageManager: pm, confidence: 88 };
|
|
114
|
+
}
|
|
115
|
+
if (has('svelte')) {
|
|
116
|
+
return { framework: 'svelte', buildCommand: `${pm} run build`, outputDir, isServer: false, packageManager: pm, confidence: 88 };
|
|
117
|
+
}
|
|
118
|
+
if (has('solid-js')) {
|
|
119
|
+
return { framework: 'solid', buildCommand: `${pm} run build`, outputDir, isServer: false, packageManager: pm, confidence: 88 };
|
|
120
|
+
}
|
|
121
|
+
return { framework: 'vite', buildCommand: `${pm} run build`, outputDir, isServer: false, packageManager: pm, confidence: 85 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Express/Fastify/Hono server
|
|
125
|
+
if (has('express')) {
|
|
126
|
+
return { framework: 'express', buildCommand: null, outputDir: '.', isServer: true, port: 3000, packageManager: pm, confidence: 80 };
|
|
127
|
+
}
|
|
128
|
+
if (has('fastify')) {
|
|
129
|
+
return { framework: 'fastify', buildCommand: null, outputDir: '.', isServer: true, port: 3000, packageManager: pm, confidence: 80 };
|
|
130
|
+
}
|
|
131
|
+
if (has('hono')) {
|
|
132
|
+
return { framework: 'hono', buildCommand: null, outputDir: '.', isServer: true, port: 3000, packageManager: pm, confidence: 80 };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Static HTML/dist/build
|
|
136
|
+
if (hasFile('index.html') || hasFile('dist/index.html') || hasFile('build/index.html') || hasFile('out/index.html')) {
|
|
137
|
+
const outputDir = fs.existsSync(path.join(cwd, 'dist/index.html')) ? 'dist' :
|
|
138
|
+
fs.existsSync(path.join(cwd, 'build/index.html')) ? 'build' :
|
|
139
|
+
fs.existsSync(path.join(cwd, 'out/index.html')) ? 'out' : '.';
|
|
140
|
+
return { framework: 'static', buildCommand: null, outputDir, isServer: false, packageManager: pm, confidence: 70 };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { framework: 'unknown', buildCommand: null, outputDir: '.', isServer: false, packageManager: pm, confidence: 10 };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function detectPackageManager(cwd: string): 'npm' | 'pnpm' | 'yarn' | 'bun' {
|
|
147
|
+
if (fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun';
|
|
148
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
149
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
150
|
+
return 'npm';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readJson(filePath: string): any {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/core/qr.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import qrcode from 'qrcode-terminal';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
|
|
5
|
+
export async function showQR(url: string): Promise<void> {
|
|
6
|
+
console.log('');
|
|
7
|
+
console.log(theme.muted(' ┌─────────────────────────────────┐'));
|
|
8
|
+
console.log(theme.muted(' │') + ' ' + theme.accent('Scan to open on mobile') + ' ' + theme.muted('│'));
|
|
9
|
+
console.log(theme.muted(' └─────────────────────────────────┘'));
|
|
10
|
+
console.log('');
|
|
11
|
+
qrcode.generate(url, { small: true });
|
|
12
|
+
console.log('');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
const { default: clipboardy } = await import('clipboardy');
|
|
18
|
+
await clipboardy.write(text);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function openBrowser(url: string): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
const { default: open } = await import('open');
|
|
28
|
+
await open(url);
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import chokidar from 'chokidar';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
|
|
7
|
+
export interface LiveSyncOptions {
|
|
8
|
+
watchDir: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
liveToken: string;
|
|
11
|
+
wsBase?: string;
|
|
12
|
+
onUpdate?: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function startLiveSync(opts: LiveSyncOptions): () => void {
|
|
16
|
+
const wsUrl = opts.wsBase || 'wss://live.nivii.app';
|
|
17
|
+
let ws: WebSocket | null = null;
|
|
18
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
+
|
|
20
|
+
function connect() {
|
|
21
|
+
ws = new WebSocket(`${wsUrl}/live/${opts.slug}?token=${opts.liveToken}`);
|
|
22
|
+
|
|
23
|
+
ws.on('open', () => {
|
|
24
|
+
console.log(theme.success(' ⚡') + ' ' + chalk.white('Live sync connected'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
ws.on('close', () => {
|
|
28
|
+
console.log(theme.muted(' ↻ Live sync reconnecting…'));
|
|
29
|
+
reconnectTimer = setTimeout(connect, 3000);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
ws.on('error', () => {
|
|
33
|
+
// silent
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
connect();
|
|
38
|
+
|
|
39
|
+
// Watch for file changes
|
|
40
|
+
const watcher = chokidar.watch(opts.watchDir, {
|
|
41
|
+
ignored: /(^|[/\\])\..|(node_modules)/,
|
|
42
|
+
persistent: true,
|
|
43
|
+
ignoreInitial: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
const triggerUpdate = (filePath: string) => {
|
|
48
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
49
|
+
debounceTimer = setTimeout(async () => {
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(theme.accent(' ⚡') + ' ' + chalk.white('Change detected: ') + chalk.hex('#6B7280')(path.relative(opts.watchDir, filePath)));
|
|
52
|
+
if (opts.onUpdate) {
|
|
53
|
+
await opts.onUpdate();
|
|
54
|
+
}
|
|
55
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
56
|
+
ws.send(JSON.stringify({ type: 'reload', slug: opts.slug }));
|
|
57
|
+
}
|
|
58
|
+
}, 300);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
watcher.on('change', triggerUpdate);
|
|
62
|
+
watcher.on('add', triggerUpdate);
|
|
63
|
+
watcher.on('unlink', triggerUpdate);
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
watcher.close();
|
|
67
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
68
|
+
ws?.close();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import FormData from 'form-data';
|
|
6
|
+
import { createSpinner } from '../ui/spinner.js';
|
|
7
|
+
import { API_BASE } from '../utils/config.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
export interface UploadOptions {
|
|
11
|
+
buildDir: string; // path to the built output directory
|
|
12
|
+
slug: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
password?: string;
|
|
15
|
+
otp?: boolean;
|
|
16
|
+
expires?: string;
|
|
17
|
+
live?: boolean;
|
|
18
|
+
collab?: boolean;
|
|
19
|
+
maxViews?: number;
|
|
20
|
+
selfDestruct?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeployResult {
|
|
24
|
+
url: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
liveToken?: string;
|
|
27
|
+
collabToken?: string;
|
|
28
|
+
otpCode?: string;
|
|
29
|
+
expiresAt?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Recursively collect all files under a directory */
|
|
33
|
+
function collectFiles(dir: string, base: string = dir): { abs: string; rel: string }[] {
|
|
34
|
+
const results: { abs: string; rel: string }[] = [];
|
|
35
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const abs = path.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
results.push(...collectFiles(abs, base));
|
|
39
|
+
} else {
|
|
40
|
+
results.push({ abs, rel: path.relative(base, abs).replace(/\\/g, '/') });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function uploadDeploy(opts: UploadOptions): Promise<DeployResult> {
|
|
47
|
+
const spinner = createSpinner('Uploading to edge network…');
|
|
48
|
+
spinner.start();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const files = collectFiles(opts.buildDir);
|
|
52
|
+
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
throw new Error('No files found in build directory');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const form = new FormData();
|
|
58
|
+
form.append('slug', opts.slug);
|
|
59
|
+
form.append('fileCount', String(files.length));
|
|
60
|
+
if (opts.password) form.append('password', opts.password);
|
|
61
|
+
if (opts.otp) form.append('otp', 'true');
|
|
62
|
+
if (opts.expires) form.append('expires', opts.expires);
|
|
63
|
+
if (opts.live) form.append('live', 'true');
|
|
64
|
+
if (opts.collab) form.append('collab', 'true');
|
|
65
|
+
if (opts.maxViews) form.append('maxViews', String(opts.maxViews));
|
|
66
|
+
if (opts.selfDestruct) form.append('selfDestruct', 'true');
|
|
67
|
+
|
|
68
|
+
// Attach each file with its relative path as the field name
|
|
69
|
+
for (const { abs, rel } of files) {
|
|
70
|
+
form.append('file[]', fs.createReadStream(abs), { filename: rel });
|
|
71
|
+
form.append('path[]', rel);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const res = await fetch(`${API_BASE}/deploy`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}),
|
|
78
|
+
...form.getHeaders(),
|
|
79
|
+
},
|
|
80
|
+
body: form,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
const body = await res.text();
|
|
85
|
+
throw new Error(`Upload failed: ${res.status} ${body}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = await res.json() as DeployResult;
|
|
89
|
+
spinner.succeed(chalk.hex('#10B981')('Deployed to edge ') + chalk.hex('#6B7280')(`(${opts.slug})`));
|
|
90
|
+
return result;
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
spinner.fail(chalk.hex('#EF4444')('Upload failed'));
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import ora, { Ora } from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
const FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
5
|
+
|
|
6
|
+
export function createSpinner(text: string): Ora {
|
|
7
|
+
return ora({
|
|
8
|
+
text: chalk.hex('#A78BFA')(text),
|
|
9
|
+
spinner: {
|
|
10
|
+
interval: 80,
|
|
11
|
+
frames: FRAMES.map(f => chalk.hex('#7C3AED')(f)),
|
|
12
|
+
},
|
|
13
|
+
color: 'magenta',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function step(emoji: string, text: string, detail?: string) {
|
|
18
|
+
const msg = chalk.hex('#A78BFA')(emoji + ' ' + chalk.white(text));
|
|
19
|
+
if (detail) {
|
|
20
|
+
console.log(msg + ' ' + chalk.hex('#6B7280')(detail));
|
|
21
|
+
} else {
|
|
22
|
+
console.log(msg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function success(text: string) {
|
|
27
|
+
console.log(chalk.hex('#10B981')(' ✓ ') + chalk.white(text));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function warn(text: string) {
|
|
31
|
+
console.log(chalk.hex('#F59E0B')(' ⚠ ') + chalk.white(text));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function error(text: string) {
|
|
35
|
+
console.log(chalk.hex('#EF4444')(' ✗ ') + chalk.white(text));
|
|
36
|
+
}
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const theme = {
|
|
4
|
+
primary: chalk.hex('#7C3AED'), // violet
|
|
5
|
+
accent: chalk.hex('#06B6D4'), // cyan
|
|
6
|
+
success: chalk.hex('#10B981'), // emerald
|
|
7
|
+
warning: chalk.hex('#F59E0B'), // amber
|
|
8
|
+
error: chalk.hex('#EF4444'), // red
|
|
9
|
+
muted: chalk.hex('#6B7280'), // gray
|
|
10
|
+
white: chalk.white,
|
|
11
|
+
bold: chalk.bold,
|
|
12
|
+
dim: chalk.dim,
|
|
13
|
+
|
|
14
|
+
gradient: (text: string) => {
|
|
15
|
+
const chars = text.split('');
|
|
16
|
+
const colors = ['#7C3AED', '#8B5CF6', '#A78BFA', '#06B6D4', '#22D3EE'];
|
|
17
|
+
return chars.map((c, i) => chalk.hex(colors[i % colors.length])(c)).join('');
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
box: (lines: string[]) => {
|
|
21
|
+
const maxLen = Math.max(...lines.map(l => stripAnsi(l).length));
|
|
22
|
+
const border = chalk.hex('#7C3AED')('─'.repeat(maxLen + 4));
|
|
23
|
+
const top = chalk.hex('#7C3AED')('╭') + border + chalk.hex('#7C3AED')('╮');
|
|
24
|
+
const bottom = chalk.hex('#7C3AED')('╰') + border + chalk.hex('#7C3AED')('╯');
|
|
25
|
+
const mid = lines.map(l => {
|
|
26
|
+
const pad = maxLen - stripAnsi(l).length;
|
|
27
|
+
return chalk.hex('#7C3AED')('│') + ' ' + l + ' '.repeat(pad) + ' ' + chalk.hex('#7C3AED')('│');
|
|
28
|
+
});
|
|
29
|
+
return [top, ...mid, bottom].join('\n');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function stripAnsi(str: string): string {
|
|
34
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
35
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// Fixed NIVII ASCII art — V now properly narrows to a point
|
|
4
|
+
const NIVII_ASCII = [
|
|
5
|
+
'███╗ ██╗██╗██╗ ██╗██╗██╗',
|
|
6
|
+
'████╗ ██║██║██║ ██║██║██║',
|
|
7
|
+
'██╔██╗ ██║██║██║ ██║██║██║',
|
|
8
|
+
'██║╚██╗██║██║╚██╗ ██╔╝██║██║',
|
|
9
|
+
'██║ ╚████║██║ ╚████╔╝ ██║██║',
|
|
10
|
+
'╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚═╝╚═╝',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const GRADIENT = [
|
|
14
|
+
'#6B21A8',
|
|
15
|
+
'#7C3AED',
|
|
16
|
+
'#8B5CF6',
|
|
17
|
+
'#A78BFA',
|
|
18
|
+
'#C4B5FD',
|
|
19
|
+
'#DDD6FE',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const cyan = chalk.hex('#06B6D4');
|
|
23
|
+
const gray = chalk.hex('#6B7280');
|
|
24
|
+
const green = chalk.hex('#10B981');
|
|
25
|
+
const orange = chalk.hex('#F97316');
|
|
26
|
+
const white = chalk.white;
|
|
27
|
+
const purple = chalk.hex('#A78BFA');
|
|
28
|
+
|
|
29
|
+
// Snow/sparkle effect row
|
|
30
|
+
const SNOW_CHARS = ['✦', '✧', '⋆', '·', '❄', '❆', '✵', '˚', '*', ' ', ' ', ' ', ' ', ' '];
|
|
31
|
+
function snowRow(width: number, color: string): string {
|
|
32
|
+
return Array.from({ length: width }, () => {
|
|
33
|
+
const ch = SNOW_CHARS[Math.floor(Math.random() * SNOW_CHARS.length)];
|
|
34
|
+
return chalk.hex(color)(ch);
|
|
35
|
+
}).join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printSnow(): void {
|
|
39
|
+
// Two rows of snow — alternating purple shades
|
|
40
|
+
console.log(' ' + snowRow(30, '#7C3AED'));
|
|
41
|
+
console.log(' ' + snowRow(30, '#A78BFA'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function showWelcome(): void {
|
|
45
|
+
console.log('');
|
|
46
|
+
printSnow();
|
|
47
|
+
for (let i = 0; i < NIVII_ASCII.length; i++) {
|
|
48
|
+
console.log(' ' + chalk.hex(GRADIENT[i])(NIVII_ASCII[i]));
|
|
49
|
+
}
|
|
50
|
+
printSnow();
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(' ' + orange('⚡') + ' ' + cyan('Share any project in seconds. No accounts. No config.'));
|
|
53
|
+
console.log(' ' + gray('v0.1.8 · nivii.app'));
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(' ' + gray('─'.repeat(46)));
|
|
56
|
+
console.log(' ' + cyan('Quick start:') + ' ' + white('nivii share'));
|
|
57
|
+
console.log(' ' + cyan('Custom slug:') + ' ' + white('nivii share --slug my-demo'));
|
|
58
|
+
console.log(' ' + cyan('With password:') + white('nivii share --pass secret123'));
|
|
59
|
+
console.log(' ' + cyan('Live sync:') + ' ' + white('nivii share --live'));
|
|
60
|
+
console.log(' ' + gray('─'.repeat(46)));
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(' ' + green('✓') + ' ' + cyan('First-run setup complete!') + gray(' Config saved to ~/.nivii/config.json'));
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function showHelp(): void {
|
|
67
|
+
console.log('');
|
|
68
|
+
printSnow();
|
|
69
|
+
for (let i = 0; i < NIVII_ASCII.length; i++) {
|
|
70
|
+
console.log(' ' + chalk.hex(GRADIENT[i])(NIVII_ASCII[i]));
|
|
71
|
+
}
|
|
72
|
+
printSnow();
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log(' ' + orange('⚡') + ' ' + cyan('Share any project in seconds. No accounts. No config.'));
|
|
75
|
+
console.log(' ' + gray('v0.1.8 · nivii.app'));
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(' ' + gray('─'.repeat(50)));
|
|
78
|
+
console.log(' ' + purple.bold('Commands'));
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(' ' + cyan('nivii share') + ' ' + gray('Deploy current directory'));
|
|
81
|
+
console.log(' ' + cyan('nivii share --slug my-demo') + ' ' + gray('Custom slug (Pro)'));
|
|
82
|
+
console.log(' ' + cyan('nivii share --pass secret') + ' ' + gray('Password protect'));
|
|
83
|
+
console.log(' ' + cyan('nivii share --expires 24h') + ' ' + gray('Set expiry (1h/24h/7d/30d)'));
|
|
84
|
+
console.log(' ' + cyan('nivii share --live') + ' ' + gray('Live sync on file changes'));
|
|
85
|
+
console.log(' ' + cyan('nivii share --no-build') + ' ' + gray('Skip build step'));
|
|
86
|
+
console.log(' ' + cyan('nivii ls') + ' ' + gray('List recent deployments'));
|
|
87
|
+
console.log(' ' + cyan('nivii whoami') + ' ' + gray('Show account info'));
|
|
88
|
+
console.log(' ' + cyan('nivii analytics [slug]') + ' ' + gray('View deployment analytics'));
|
|
89
|
+
console.log(' ' + cyan('nivii config') + ' ' + gray('View/set config values'));
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(' ' + gray('─'.repeat(50)));
|
|
92
|
+
console.log(' ' + gray('Docs: ') + white('https://nivii.app') + ' ' + gray('·') + ' ' + gray('Pro: ') + white('https://nivii.app/#pricing'));
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function showBanner(): void {
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(
|
|
99
|
+
' ' + orange('⚡') + ' ' +
|
|
100
|
+
chalk.hex('#A78BFA').bold('NIVII') + ' ' +
|
|
101
|
+
gray('v0.1.8')
|
|
102
|
+
);
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface NiviiConfig {
|
|
6
|
+
token?: string;
|
|
7
|
+
plan?: 'free' | 'pro';
|
|
8
|
+
apiBase?: string;
|
|
9
|
+
firstRun?: boolean;
|
|
10
|
+
deployments?: Array<{
|
|
11
|
+
slug: string;
|
|
12
|
+
url: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
expiresAt?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CONFIG_DIR = path.join(os.homedir(), '.nivii');
|
|
19
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
20
|
+
|
|
21
|
+
export const API_BASE = process.env.NIVII_API || 'https://api.nivii.app';
|
|
22
|
+
|
|
23
|
+
export function loadConfig(): NiviiConfig {
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
27
|
+
const defaults: NiviiConfig = { firstRun: true, apiBase: API_BASE, deployments: [] };
|
|
28
|
+
saveConfig(defaults);
|
|
29
|
+
return defaults;
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return { firstRun: true, apiBase: API_BASE, deployments: [] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function saveConfig(config: NiviiConfig): void {
|
|
38
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function updateConfig(updates: Partial<NiviiConfig>): NiviiConfig {
|
|
43
|
+
const current = loadConfig();
|
|
44
|
+
const updated = { ...current, ...updates };
|
|
45
|
+
saveConfig(updated);
|
|
46
|
+
return updated;
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export function generateSlug(length = 8): string {
|
|
4
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
5
|
+
const bytes = crypto.randomBytes(length);
|
|
6
|
+
return Array.from(bytes).map(b => chars[b % chars.length]).join('');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hashFile(content: Buffer): string {
|
|
10
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
11
|
+
}
|
package/src/utils/zip.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createWriteStream } from 'fs';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
export async function zipDirectory(sourceDir: string): Promise<string> {
|
|
8
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nivii-'));
|
|
9
|
+
const zipPath = path.join(tmpDir, 'deploy.zip');
|
|
10
|
+
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const output = createWriteStream(zipPath);
|
|
13
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
14
|
+
|
|
15
|
+
output.on('close', () => resolve(zipPath));
|
|
16
|
+
archive.on('error', reject);
|
|
17
|
+
|
|
18
|
+
archive.pipe(output);
|
|
19
|
+
archive.directory(sourceDir, false);
|
|
20
|
+
archive.finalize();
|
|
21
|
+
});
|
|
22
|
+
}
|