nexus-sync-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +292 -2
- package/bin/nexus-sync.js +38 -0
- package/package.json +41 -4
- package/src/commands/collab.js +41 -0
- package/src/commands/db.js +78 -0
- package/src/commands/deploy.js +61 -0
- package/src/commands/init.js +75 -0
- package/src/commands/start.js +100 -0
- package/src/commands/status.js +44 -0
- package/src/commands/stop.js +26 -0
- package/src/commands/test.js +66 -0
- package/src/utils/config.js +51 -0
- package/src/utils/logger.js +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adityareal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,293 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# ✦ Nexus-Sync CLI
|
|
4
|
+
|
|
5
|
+
**Develop locally, run in the cloud.**
|
|
6
|
+
|
|
7
|
+
A Full-Stack-as-a-Service CLI that mirrors your local dev environment to a live cloud container — your "Shadow Environment" — in real time.
|
|
8
|
+
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://www.npmjs.com/package/nexus-sync-cli)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🚀 What is Nexus-Sync?
|
|
18
|
+
|
|
19
|
+
Nexus-Sync eliminates the gap between local development and production. When you save a file, it's instantly reflected in your **Shadow Environment** — a live cloud container that mirrors your full stack (frontend, backend, database, edge functions).
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Local Machine Shadow Environment (Cloud)
|
|
23
|
+
┌──────────────────┐ real-time ┌──────────────────────────┐
|
|
24
|
+
│ src/app/page.tsx │ ──── sync ────▶ │ https://my-app.nexus- │
|
|
25
|
+
│ api/users.ts │ (20-60ms) │ sync.example │
|
|
26
|
+
│ db/schema.sql │ │ │
|
|
27
|
+
└──────────────────┘ │ ✔ Live preview │
|
|
28
|
+
│ ✔ Shared with team │
|
|
29
|
+
│ ✔ Full-stack sandbox │
|
|
30
|
+
└──────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Why?
|
|
34
|
+
|
|
35
|
+
- **No more "works on my machine"** — your Shadow Environment IS the deployed version
|
|
36
|
+
- **Instant feedback** — every save is live in ~35ms
|
|
37
|
+
- **Team collaboration** — invite teammates to your live environment
|
|
38
|
+
- **Safe database testing** — spin up anonymized DB sandboxes in seconds
|
|
39
|
+
- **One-command deploy** — push to production when you're ready
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📦 Installation
|
|
44
|
+
|
|
45
|
+
### Global install (recommended)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g nexus-sync-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then use `nexus-sync` from any project directory.
|
|
52
|
+
|
|
53
|
+
### Local install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install nexus-sync-cli
|
|
57
|
+
npx nexus-sync --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### From source
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/Adityareal/nexus-sync-cli.git
|
|
64
|
+
cd nexus-sync-cli
|
|
65
|
+
npm install
|
|
66
|
+
node bin/nexus-sync.js --help
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> **Requirements:** Node.js 18 or higher
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ⚡ Quick Start
|
|
74
|
+
|
|
75
|
+
### 1. Initialize your project
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
nexus-sync init
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This walks you through an interactive setup:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
✦ Nexus-Sync
|
|
85
|
+
|
|
86
|
+
? Project name: my-app
|
|
87
|
+
? Framework: Next.js
|
|
88
|
+
? Preferred region: us-east-1
|
|
89
|
+
|
|
90
|
+
✔ Project initialized!
|
|
91
|
+
Config written to .nexus/config.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A `.nexus/config.json` file is created with your preferences:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"projectName": "my-app",
|
|
99
|
+
"framework": "Next.js",
|
|
100
|
+
"region": "us-east-1",
|
|
101
|
+
"watchPaths": ["src/", "app/", "pages/", "api/"],
|
|
102
|
+
"createdAt": "2026-05-30T12:00:00.000Z"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Start syncing
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
nexus-sync start
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
✦ Nexus-Sync
|
|
114
|
+
|
|
115
|
+
Watching: src/, app/, pages/, api/
|
|
116
|
+
✦ Shadow live at: https://my-app.nexus-sync.example
|
|
117
|
+
|
|
118
|
+
[12:04:31] ✔ Synced src/app/page.tsx → shadow (38ms)
|
|
119
|
+
[12:04:45] ✔ Synced api/users.ts → shadow (24ms)
|
|
120
|
+
[12:05:02] ✔ Added src/lib/utils.ts → shadow (31ms)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The watcher monitors your source files in real time. Press `Ctrl+C` to stop.
|
|
124
|
+
|
|
125
|
+
### 3. Check status
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
nexus-sync status
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
✦ Nexus-Sync
|
|
133
|
+
|
|
134
|
+
Shadow Environment
|
|
135
|
+
─────────────────────────────────
|
|
136
|
+
Sync state ● active
|
|
137
|
+
Last synced src/app/page.tsx (2s ago)
|
|
138
|
+
Uptime 00:14:32
|
|
139
|
+
Live URL https://my-app.nexus-sync.example
|
|
140
|
+
Latency ⚡ 34ms
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 📖 All Commands
|
|
146
|
+
|
|
147
|
+
| Command | Description |
|
|
148
|
+
|---------|-------------|
|
|
149
|
+
| `nexus-sync init` | Interactive project setup → creates `.nexus/config.json` |
|
|
150
|
+
| `nexus-sync start` | Start real-time file sync to your Shadow Environment |
|
|
151
|
+
| `nexus-sync stop` | Stop the file watcher gracefully |
|
|
152
|
+
| `nexus-sync status` | Show sync state, uptime, latency, and live URL |
|
|
153
|
+
| `nexus-sync db sandbox` | Create an isolated database sandbox with anonymized data |
|
|
154
|
+
| `nexus-sync collab invite <email>` | Invite a teammate to your live environment |
|
|
155
|
+
| `nexus-sync test` | Run the test suite against your Shadow Environment |
|
|
156
|
+
| `nexus-sync deploy` | Deploy to production |
|
|
157
|
+
| `nexus-sync --help` | Show all available commands |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `nexus-sync db sandbox`
|
|
162
|
+
|
|
163
|
+
Spin up an isolated database sandbox cloned from production with PII automatically anonymized:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
✦ Nexus-Sync
|
|
167
|
+
|
|
168
|
+
→ Cloning production schema... ████████████████ 100%
|
|
169
|
+
→ Anonymizing PII fields... ████████████████ 100%
|
|
170
|
+
|
|
171
|
+
✔ Sandbox ready at: postgres://sandbox.nexus-sync.example:5432/my-app_sandbox
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### `nexus-sync collab invite <email>`
|
|
177
|
+
|
|
178
|
+
Share your Shadow Environment with teammates:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
nexus-sync collab invite teammate@company.com
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
✦ Nexus-Sync
|
|
186
|
+
|
|
187
|
+
✔ Invite sent to teammate@company.com
|
|
188
|
+
They can join at: https://my-app.nexus-sync.example?collab=a1b2c3d4e5f6
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### `nexus-sync test`
|
|
194
|
+
|
|
195
|
+
Run your test suite against the Shadow Environment:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
✦ Nexus-Sync
|
|
199
|
+
|
|
200
|
+
✔ API route /api/users returns 200 (12ms)
|
|
201
|
+
✔ DB connection healthy (8ms)
|
|
202
|
+
✔ Auth middleware blocks unauthenticated (5ms)
|
|
203
|
+
✔ Edge function /api/geo responds correctly (19ms)
|
|
204
|
+
✗ Email service timeout (3001ms)
|
|
205
|
+
|
|
206
|
+
─────────────────────────────────
|
|
207
|
+
4 passed, 1 failed
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `nexus-sync deploy`
|
|
213
|
+
|
|
214
|
+
Ship to production with a single command:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
✦ Nexus-Sync
|
|
218
|
+
|
|
219
|
+
✔ ⚡ Building project...
|
|
220
|
+
✔ ⚡ Running pre-deploy tests...
|
|
221
|
+
✔ ⚡ Pushing to Shadow Environment...
|
|
222
|
+
✔ ⚡ Warming edge nodes...
|
|
223
|
+
|
|
224
|
+
✦ Production deploy complete: https://my-app.nexus-sync.example
|
|
225
|
+
✦ Deploy time: 2.4s
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 🗂️ Project Structure
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
nexus-sync-cli/
|
|
234
|
+
├── bin/
|
|
235
|
+
│ └── nexus-sync.js # CLI entry point
|
|
236
|
+
├── src/
|
|
237
|
+
│ ├── commands/
|
|
238
|
+
│ │ ├── init.js # Interactive project setup
|
|
239
|
+
│ │ ├── start.js # File watcher + sync
|
|
240
|
+
│ │ ├── stop.js # Graceful shutdown
|
|
241
|
+
│ │ ├── status.js # Status dashboard
|
|
242
|
+
│ │ ├── db.js # Database sandbox
|
|
243
|
+
│ │ ├── collab.js # Collaboration invites
|
|
244
|
+
│ │ ├── test.js # Test runner
|
|
245
|
+
│ │ └── deploy.js # Production deployment
|
|
246
|
+
│ └── utils/
|
|
247
|
+
│ ├── logger.js # Shared chalk helpers
|
|
248
|
+
│ └── config.js # .nexus/config.json I/O
|
|
249
|
+
├── package.json
|
|
250
|
+
├── README.md
|
|
251
|
+
└── .gitignore
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 🛠️ Tech Stack
|
|
257
|
+
|
|
258
|
+
| Package | Purpose |
|
|
259
|
+
|---------|---------|
|
|
260
|
+
| [commander](https://www.npmjs.com/package/commander) | Command parsing and help generation |
|
|
261
|
+
| [chalk](https://www.npmjs.com/package/chalk) | Terminal colors and styling |
|
|
262
|
+
| [ora](https://www.npmjs.com/package/ora) | Elegant terminal spinners |
|
|
263
|
+
| [cli-progress](https://www.npmjs.com/package/cli-progress) | Progress bars |
|
|
264
|
+
| [chokidar](https://www.npmjs.com/package/chokidar) | Cross-platform file watching |
|
|
265
|
+
| [inquirer](https://www.npmjs.com/package/inquirer) | Interactive command-line prompts |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 🔧 Configuration
|
|
270
|
+
|
|
271
|
+
The `.nexus/config.json` file controls the CLI behavior:
|
|
272
|
+
|
|
273
|
+
| Field | Type | Description |
|
|
274
|
+
|-------|------|-------------|
|
|
275
|
+
| `projectName` | `string` | Your project's slug (used in URLs) |
|
|
276
|
+
| `framework` | `string` | `Next.js` \| `Remix` \| `SvelteKit` \| `Other` |
|
|
277
|
+
| `region` | `string` | Cloud region: `us-east-1` \| `eu-west-1` \| `ap-south-1` |
|
|
278
|
+
| `watchPaths` | `string[]` | Directories to watch for file changes |
|
|
279
|
+
| `createdAt` | `string` | ISO timestamp of initialization |
|
|
280
|
+
|
|
281
|
+
You can manually edit this file or re-run `nexus-sync init`.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 📝 Note
|
|
286
|
+
|
|
287
|
+
This CLI is currently a **simulation/prototype**. All sync, deploy, and database operations produce realistic terminal output but don't connect to actual cloud infrastructure. It's designed to demonstrate the developer experience of a full-stack shadow environment platform.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 📄 License
|
|
292
|
+
|
|
293
|
+
MIT © [Adityareal](https://github.com/Adityareal)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { registerInit } from '../src/commands/init.js';
|
|
7
|
+
import { registerStart } from '../src/commands/start.js';
|
|
8
|
+
import { registerStop } from '../src/commands/stop.js';
|
|
9
|
+
import { registerStatus } from '../src/commands/status.js';
|
|
10
|
+
import { registerDb } from '../src/commands/db.js';
|
|
11
|
+
import { registerCollab } from '../src/commands/collab.js';
|
|
12
|
+
import { registerTest } from '../src/commands/test.js';
|
|
13
|
+
import { registerDeploy } from '../src/commands/deploy.js';
|
|
14
|
+
|
|
15
|
+
const pkg = JSON.parse(
|
|
16
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf-8')
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('nexus-sync')
|
|
23
|
+
.version(pkg.version)
|
|
24
|
+
.description(
|
|
25
|
+
chalk.bold.cyan('✦ Nexus-Sync') +
|
|
26
|
+
' — develop locally, run in the cloud.'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
registerInit(program);
|
|
30
|
+
registerStart(program);
|
|
31
|
+
registerStop(program);
|
|
32
|
+
registerStatus(program);
|
|
33
|
+
registerDb(program);
|
|
34
|
+
registerCollab(program);
|
|
35
|
+
registerTest(program);
|
|
36
|
+
registerDeploy(program);
|
|
37
|
+
|
|
38
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-sync-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Nexus-Sync CLI: develop locally, run in the cloud. A simulation/prototype that demos the shadow-environment workflow; it does not connect to real cloud infrastructure.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nexus-sync": "./bin/nexus-sync.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Adityareal/nexus-sync-cli.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Adityareal/nexus-sync-cli/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/Adityareal/nexus-sync-cli#readme",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cli",
|
|
28
|
+
"sync",
|
|
29
|
+
"cloud",
|
|
30
|
+
"shadow-environment",
|
|
31
|
+
"full-stack"
|
|
32
|
+
],
|
|
33
|
+
"author": "Adityareal",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"chokidar": "^3.6.0",
|
|
38
|
+
"cli-progress": "^3.12.0",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"inquirer": "^9.3.7",
|
|
41
|
+
"ora": "^8.1.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readConfig, configExists } from '../utils/config.js';
|
|
4
|
+
import { header, error, success } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the `collab` command with subcommand `invite`.
|
|
8
|
+
* @param {import('commander').Command} program
|
|
9
|
+
*/
|
|
10
|
+
export function registerCollab(program) {
|
|
11
|
+
const collab = program
|
|
12
|
+
.command('collab')
|
|
13
|
+
.description('Collaboration commands');
|
|
14
|
+
|
|
15
|
+
collab
|
|
16
|
+
.command('invite <email>')
|
|
17
|
+
.description('Invite a collaborator by email')
|
|
18
|
+
.action((email) => {
|
|
19
|
+
header();
|
|
20
|
+
|
|
21
|
+
if (!configExists()) {
|
|
22
|
+
error('No config found. Run `nexus-sync init` first.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = readConfig();
|
|
27
|
+
const projectName = config.projectName || 'my-app';
|
|
28
|
+
const token = crypto.randomBytes(8).toString('hex');
|
|
29
|
+
const collabUrl =
|
|
30
|
+
`https://${projectName}.nexus-sync.example` +
|
|
31
|
+
`?collab=${token}`;
|
|
32
|
+
|
|
33
|
+
console.log();
|
|
34
|
+
success(`Invite sent to ${chalk.bold(email)}`);
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.gray(` They can join at: `) +
|
|
37
|
+
chalk.cyan.underline(collabUrl)
|
|
38
|
+
);
|
|
39
|
+
console.log();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import cliProgress from 'cli-progress';
|
|
3
|
+
import { readConfig, configExists } from '../utils/config.js';
|
|
4
|
+
import { header, error, success, sleep } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the `db` command with subcommand `sandbox`.
|
|
8
|
+
* @param {import('commander').Command} program
|
|
9
|
+
*/
|
|
10
|
+
export function registerDb(program) {
|
|
11
|
+
const db = program
|
|
12
|
+
.command('db')
|
|
13
|
+
.description('Database management commands');
|
|
14
|
+
|
|
15
|
+
db.command('sandbox')
|
|
16
|
+
.description('Create an isolated database sandbox')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
header();
|
|
19
|
+
|
|
20
|
+
if (!configExists()) {
|
|
21
|
+
error('No config found. Run `nexus-sync init` first.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = readConfig();
|
|
26
|
+
const projectName = config.projectName || 'my-app';
|
|
27
|
+
|
|
28
|
+
console.log();
|
|
29
|
+
|
|
30
|
+
// Step 1: Clone schema
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
chalk.white(' → Cloning production schema... ')
|
|
33
|
+
);
|
|
34
|
+
const bar1 = new cliProgress.SingleBar(
|
|
35
|
+
{
|
|
36
|
+
format: `${chalk.cyan('{bar}')} {percentage}%`,
|
|
37
|
+
barCompleteChar: '█',
|
|
38
|
+
barIncompleteChar: '░',
|
|
39
|
+
hideCursor: true,
|
|
40
|
+
},
|
|
41
|
+
cliProgress.Presets.shades_classic
|
|
42
|
+
);
|
|
43
|
+
bar1.start(100, 0);
|
|
44
|
+
for (let i = 0; i <= 100; i += 5) {
|
|
45
|
+
bar1.update(i);
|
|
46
|
+
await sleep(60);
|
|
47
|
+
}
|
|
48
|
+
bar1.stop();
|
|
49
|
+
|
|
50
|
+
// Step 2: Anonymize PII
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
chalk.white(' → Anonymizing PII fields... ')
|
|
53
|
+
);
|
|
54
|
+
const bar2 = new cliProgress.SingleBar(
|
|
55
|
+
{
|
|
56
|
+
format: `${chalk.cyan('{bar}')} {percentage}%`,
|
|
57
|
+
barCompleteChar: '█',
|
|
58
|
+
barIncompleteChar: '░',
|
|
59
|
+
hideCursor: true,
|
|
60
|
+
},
|
|
61
|
+
cliProgress.Presets.shades_classic
|
|
62
|
+
);
|
|
63
|
+
bar2.start(100, 0);
|
|
64
|
+
for (let i = 0; i <= 100; i += 4) {
|
|
65
|
+
bar2.update(Math.min(i, 100));
|
|
66
|
+
await sleep(50);
|
|
67
|
+
}
|
|
68
|
+
bar2.update(100);
|
|
69
|
+
bar2.stop();
|
|
70
|
+
|
|
71
|
+
console.log();
|
|
72
|
+
const sandboxUrl =
|
|
73
|
+
`postgres://sandbox.nexus-sync.example:5432` +
|
|
74
|
+
`/${projectName}_sandbox`;
|
|
75
|
+
success(`Sandbox ready at: ${chalk.underline(sandboxUrl)}`);
|
|
76
|
+
console.log();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { readConfig, configExists } from '../utils/config.js';
|
|
4
|
+
import { header, error, sleep } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const DEPLOY_STEPS = [
|
|
7
|
+
{ text: 'Building project...', duration: 600 },
|
|
8
|
+
{ text: 'Running pre-deploy tests...', duration: 600 },
|
|
9
|
+
{ text: 'Pushing to Shadow Environment...', duration: 600 },
|
|
10
|
+
{ text: 'Warming edge nodes...', duration: 600 },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register the `deploy` command.
|
|
15
|
+
* @param {import('commander').Command} program
|
|
16
|
+
*/
|
|
17
|
+
export function registerDeploy(program) {
|
|
18
|
+
program
|
|
19
|
+
.command('deploy')
|
|
20
|
+
.description('Deploy to production')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
header();
|
|
23
|
+
|
|
24
|
+
if (!configExists()) {
|
|
25
|
+
error('No config found. Run `nexus-sync init` first.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const config = readConfig();
|
|
30
|
+
const projectName = config.projectName || 'my-app';
|
|
31
|
+
const liveUrl = `https://${projectName}.nexus-sync.example`;
|
|
32
|
+
|
|
33
|
+
console.log();
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
|
|
36
|
+
for (const step of DEPLOY_STEPS) {
|
|
37
|
+
const spinner = ora({
|
|
38
|
+
text: `⚡ ${step.text}`,
|
|
39
|
+
indent: 2,
|
|
40
|
+
}).start();
|
|
41
|
+
await sleep(step.duration);
|
|
42
|
+
spinner.succeed(`⚡ ${step.text}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
46
|
+
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.bold.cyan(
|
|
50
|
+
` ✦ Production deploy complete: ` +
|
|
51
|
+
chalk.underline(liveUrl)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.bold.cyan(
|
|
56
|
+
` ✦ Deploy time: ${elapsed}s`
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
console.log();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { writeConfig, configExists } from '../utils/config.js';
|
|
4
|
+
import { header, success, warn, info } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the `init` command.
|
|
8
|
+
* @param {import('commander').Command} program
|
|
9
|
+
*/
|
|
10
|
+
export function registerInit(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('init')
|
|
13
|
+
.description('Scaffold a .nexus/ config directory')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
header();
|
|
16
|
+
|
|
17
|
+
if (configExists()) {
|
|
18
|
+
warn('A .nexus/config.json already exists.');
|
|
19
|
+
const { overwrite } = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'confirm',
|
|
22
|
+
name: 'overwrite',
|
|
23
|
+
message: 'Overwrite existing config?',
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
if (!overwrite) {
|
|
28
|
+
info('Aborted.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const answers = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'input',
|
|
36
|
+
name: 'projectName',
|
|
37
|
+
message: 'Project name:',
|
|
38
|
+
default: 'my-app',
|
|
39
|
+
validate: (v) =>
|
|
40
|
+
/^[a-z0-9-]+$/.test(v)
|
|
41
|
+
? true
|
|
42
|
+
: 'Lowercase alphanumeric + hyphens only',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'list',
|
|
46
|
+
name: 'framework',
|
|
47
|
+
message: 'Framework:',
|
|
48
|
+
choices: ['Next.js', 'Remix', 'SvelteKit', 'Other'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'list',
|
|
52
|
+
name: 'region',
|
|
53
|
+
message: 'Preferred region:',
|
|
54
|
+
choices: ['us-east-1', 'eu-west-1', 'ap-south-1'],
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const config = {
|
|
59
|
+
projectName: answers.projectName,
|
|
60
|
+
framework: answers.framework,
|
|
61
|
+
region: answers.region,
|
|
62
|
+
watchPaths: ['src/', 'app/', 'pages/', 'api/'],
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
writeConfig(config);
|
|
67
|
+
|
|
68
|
+
console.log();
|
|
69
|
+
success('Project initialized!');
|
|
70
|
+
info(
|
|
71
|
+
`Config written to ${chalk.underline('.nexus/config.json')}`
|
|
72
|
+
);
|
|
73
|
+
console.log();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readConfig, configExists } from '../utils/config.js';
|
|
4
|
+
import {
|
|
5
|
+
header,
|
|
6
|
+
success,
|
|
7
|
+
error,
|
|
8
|
+
info,
|
|
9
|
+
timestamp,
|
|
10
|
+
} from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register the `start` command.
|
|
14
|
+
* @param {import('commander').Command} program
|
|
15
|
+
*/
|
|
16
|
+
export function registerStart(program) {
|
|
17
|
+
program
|
|
18
|
+
.command('start')
|
|
19
|
+
.description('Start real-time file sync to shadow env')
|
|
20
|
+
.action(() => {
|
|
21
|
+
header();
|
|
22
|
+
|
|
23
|
+
if (!configExists()) {
|
|
24
|
+
error('No config found. Run `nexus-sync init` first.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = readConfig();
|
|
29
|
+
const projectName = config.projectName || 'my-app';
|
|
30
|
+
const watchPaths = config.watchPaths || [
|
|
31
|
+
'src/',
|
|
32
|
+
'app/',
|
|
33
|
+
'pages/',
|
|
34
|
+
'api/',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const liveUrl = `https://${projectName}.nexus-sync.example`;
|
|
38
|
+
|
|
39
|
+
console.log();
|
|
40
|
+
info(`Watching: ${chalk.cyan(watchPaths.join(', '))}`);
|
|
41
|
+
console.log(
|
|
42
|
+
chalk.bold.cyan(` ✦ Shadow live at: ${chalk.underline(liveUrl)}`)
|
|
43
|
+
);
|
|
44
|
+
console.log();
|
|
45
|
+
|
|
46
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
47
|
+
ignoreInitial: true,
|
|
48
|
+
ignored: [
|
|
49
|
+
'**/node_modules/**',
|
|
50
|
+
'**/.git/**',
|
|
51
|
+
'**/.nexus/**',
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
watcher.on('change', (filePath) => {
|
|
56
|
+
const latency = Math.floor(Math.random() * 41) + 20; // 20-60ms
|
|
57
|
+
console.log(
|
|
58
|
+
`${timestamp()} ${chalk.green('✔')} Synced ` +
|
|
59
|
+
`${chalk.white(filePath)} → ` +
|
|
60
|
+
`${chalk.cyan('shadow')} ` +
|
|
61
|
+
`${chalk.gray(`(${latency}ms)`)}`
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
watcher.on('add', (filePath) => {
|
|
66
|
+
const latency = Math.floor(Math.random() * 41) + 20;
|
|
67
|
+
console.log(
|
|
68
|
+
`${timestamp()} ${chalk.green('✔')} Added ` +
|
|
69
|
+
`${chalk.white(filePath)} → ` +
|
|
70
|
+
`${chalk.cyan('shadow')} ` +
|
|
71
|
+
`${chalk.gray(`(${latency}ms)`)}`
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
watcher.on('unlink', (filePath) => {
|
|
76
|
+
console.log(
|
|
77
|
+
`${timestamp()} ${chalk.yellow('⚠')} Removed ` +
|
|
78
|
+
`${chalk.white(filePath)}`
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
watcher.on('error', (err) => {
|
|
83
|
+
error(`Watcher error: ${err.message}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Graceful shutdown
|
|
87
|
+
const shutdown = () => {
|
|
88
|
+
console.log();
|
|
89
|
+
info('Stopping file watcher...');
|
|
90
|
+
watcher.close().then(() => {
|
|
91
|
+
success('Shadow sync stopped. Goodbye! 👋');
|
|
92
|
+
console.log();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
process.on('SIGINT', shutdown);
|
|
98
|
+
process.on('SIGTERM', shutdown);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readConfig, configExists } from '../utils/config.js';
|
|
3
|
+
import { header, error, keyValue } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register the `status` command.
|
|
7
|
+
* @param {import('commander').Command} program
|
|
8
|
+
*/
|
|
9
|
+
export function registerStatus(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('status')
|
|
12
|
+
.description('Show current sync status')
|
|
13
|
+
.action(() => {
|
|
14
|
+
header();
|
|
15
|
+
|
|
16
|
+
if (!configExists()) {
|
|
17
|
+
error('No config found. Run `nexus-sync init` first.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = readConfig();
|
|
22
|
+
const projectName = config.projectName || 'my-app';
|
|
23
|
+
const liveUrl = `https://${projectName}.nexus-sync.example`;
|
|
24
|
+
const latency = Math.floor(Math.random() * 41) + 20;
|
|
25
|
+
|
|
26
|
+
// Simulated uptime
|
|
27
|
+
const uptimeH = Math.floor(Math.random() * 2);
|
|
28
|
+
const uptimeM = Math.floor(Math.random() * 60);
|
|
29
|
+
const uptimeS = Math.floor(Math.random() * 60);
|
|
30
|
+
const hh = String(uptimeH).padStart(2, '0');
|
|
31
|
+
const mm = String(uptimeM).padStart(2, '0');
|
|
32
|
+
const ss = String(uptimeS).padStart(2, '0');
|
|
33
|
+
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.bold.white(' Shadow Environment'));
|
|
36
|
+
console.log(chalk.gray(' ─────────────────────────────────'));
|
|
37
|
+
keyValue('Sync state ', chalk.green('● active'));
|
|
38
|
+
keyValue('Last synced', `src/app/page.tsx ${chalk.gray('(2s ago)')}`);
|
|
39
|
+
keyValue('Uptime ', `${hh}:${mm}:${ss}`);
|
|
40
|
+
keyValue('Live URL ', chalk.cyan.underline(liveUrl));
|
|
41
|
+
keyValue('Latency ', `⚡ ${chalk.green(`${latency}ms`)}`);
|
|
42
|
+
console.log();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { header, success, info } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register the `stop` command.
|
|
6
|
+
* @param {import('commander').Command} program
|
|
7
|
+
*/
|
|
8
|
+
export function registerStop(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('stop')
|
|
11
|
+
.description('Stop the shadow sync watcher')
|
|
12
|
+
.action(() => {
|
|
13
|
+
header();
|
|
14
|
+
console.log();
|
|
15
|
+
info('Shutting down shadow environment...');
|
|
16
|
+
success('File watcher stopped.');
|
|
17
|
+
success('Shadow environment suspended.');
|
|
18
|
+
console.log();
|
|
19
|
+
console.log(
|
|
20
|
+
chalk.gray(
|
|
21
|
+
' Run `nexus-sync start` to resume syncing.'
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
console.log();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { header, sleep } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const TEST_CASES = [
|
|
6
|
+
{ name: 'API route /api/users returns 200', pass: true, ms: 12 },
|
|
7
|
+
{ name: 'DB connection healthy', pass: true, ms: 8 },
|
|
8
|
+
{ name: 'Auth middleware blocks unauthenticated', pass: true, ms: 5 },
|
|
9
|
+
{ name: 'Edge function /api/geo responds correctly', pass: true, ms: 19 },
|
|
10
|
+
{ name: 'Email service timeout', pass: false, ms: 3001 },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register the `test` command.
|
|
15
|
+
* @param {import('commander').Command} program
|
|
16
|
+
*/
|
|
17
|
+
export function registerTest(program) {
|
|
18
|
+
program
|
|
19
|
+
.command('test')
|
|
20
|
+
.description('Run simulated test suite')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
header();
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
const spinner = ora({
|
|
26
|
+
text: 'Running test suite...',
|
|
27
|
+
indent: 2,
|
|
28
|
+
}).start();
|
|
29
|
+
|
|
30
|
+
await sleep(1500);
|
|
31
|
+
spinner.stop();
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
|
|
36
|
+
for (const tc of TEST_CASES) {
|
|
37
|
+
await sleep(180);
|
|
38
|
+
const timeStr = chalk.gray(`(${tc.ms}ms)`);
|
|
39
|
+
if (tc.pass) {
|
|
40
|
+
passed++;
|
|
41
|
+
console.log(
|
|
42
|
+
` ${chalk.green('✔')} ${chalk.white(tc.name)}` +
|
|
43
|
+
` ${timeStr}`
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
failed++;
|
|
47
|
+
console.log(
|
|
48
|
+
` ${chalk.red('✗')} ${chalk.white(tc.name)}` +
|
|
49
|
+
` ${timeStr}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.gray(' ─────────────────────────────────')
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const summary = [];
|
|
60
|
+
if (passed > 0) summary.push(chalk.green(`${passed} passed`));
|
|
61
|
+
if (failed > 0) summary.push(chalk.red(`${failed} failed`));
|
|
62
|
+
|
|
63
|
+
console.log(` ${summary.join(chalk.gray(', '))}`);
|
|
64
|
+
console.log();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const NEXUS_DIR = '.nexus';
|
|
5
|
+
const CONFIG_FILE = 'config.json';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the path to the .nexus directory in the cwd.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function nexusDir() {
|
|
12
|
+
return join(process.cwd(), NEXUS_DIR);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the path to the config file.
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function configPath() {
|
|
20
|
+
return join(nexusDir(), CONFIG_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check whether a .nexus/config.json exists.
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function configExists() {
|
|
28
|
+
return existsSync(configPath());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read and parse the config file.
|
|
33
|
+
* @returns {object}
|
|
34
|
+
*/
|
|
35
|
+
export function readConfig() {
|
|
36
|
+
const raw = readFileSync(configPath(), 'utf-8');
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write a config object to .nexus/config.json.
|
|
42
|
+
* Creates the .nexus/ directory if it doesn't exist.
|
|
43
|
+
* @param {object} config
|
|
44
|
+
*/
|
|
45
|
+
export function writeConfig(config) {
|
|
46
|
+
const dir = nexusDir();
|
|
47
|
+
if (!existsSync(dir)) {
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2) + '\n');
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Print the branded header line.
|
|
5
|
+
*/
|
|
6
|
+
export function header() {
|
|
7
|
+
console.log(chalk.bold.cyan('\n✦ Nexus-Sync'));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Print a success line.
|
|
12
|
+
* @param {string} msg
|
|
13
|
+
*/
|
|
14
|
+
export function success(msg) {
|
|
15
|
+
console.log(chalk.green(` ✔ ${msg}`));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Print a warning line.
|
|
20
|
+
* @param {string} msg
|
|
21
|
+
*/
|
|
22
|
+
export function warn(msg) {
|
|
23
|
+
console.log(chalk.yellow(` ⚠ ${msg}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Print an error line.
|
|
28
|
+
* @param {string} msg
|
|
29
|
+
*/
|
|
30
|
+
export function error(msg) {
|
|
31
|
+
console.log(chalk.red(` ✗ ${msg}`));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print an info line.
|
|
36
|
+
* @param {string} msg
|
|
37
|
+
*/
|
|
38
|
+
export function info(msg) {
|
|
39
|
+
console.log(chalk.white(` ${msg}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Print a highlighted URL or key value.
|
|
44
|
+
* @param {string} label
|
|
45
|
+
* @param {string} value
|
|
46
|
+
*/
|
|
47
|
+
export function keyValue(label, value) {
|
|
48
|
+
console.log(` ${chalk.gray(label)} ${chalk.bold.white(value)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return a formatted timestamp string [HH:MM:SS].
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function timestamp() {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
58
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
59
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
60
|
+
return chalk.gray(`[${hh}:${mm}:${ss}]`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Utility: sleep for ms milliseconds.
|
|
65
|
+
* @param {number} ms
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
*/
|
|
68
|
+
export function sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|