blinkhost 0.0.0-stage → 1.0.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 +132 -2
- package/dist/cli.js +565 -0
- package/package.json +55 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blink Authors
|
|
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,133 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ⚡ Blink CLI (`blinkhost`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Instant, zero-signup ephemeral frontend hosting CLI. Deploy static websites in seconds with automatic teardown after 15, 35, or 45 minutes.
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.org/package/blinkhost)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- 🚀 **Zero Signup Required**: No account, no API keys, no credentials needed.
|
|
13
|
+
- ⏳ **Ephemeral by Design**: Choose 15, 35, or 45-minute TTL. Sites automatically self-destruct.
|
|
14
|
+
- 📁 **Auto-Detection**: Automatically finds `dist/`, `build/`, `out/`, `public/`, or current directory.
|
|
15
|
+
- 🔒 **Machine Verified**: Uses hardware machine GUID to enforce 1 active site per machine (preventing abuse while allowing effortless redeployment).
|
|
16
|
+
- 📋 **Auto-Clipboard**: Instantly copies the live URL to your clipboard.
|
|
17
|
+
- 📱 **Mobile QR Code**: Optional `--qr` flag generates a terminal QR code for phone testing.
|
|
18
|
+
- 🛡️ **Safe & Fast**: Only uploads static web assets (rejects server executables).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
You can run Blink without installing it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx blinkhost
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install it globally:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g blinkhost
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### 1. Basic Deployment
|
|
41
|
+
Run `blink` inside any frontend project (React, Vue, Svelte, Vite, Astro, Next.js static export, or plain HTML):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
blink
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Blink will:
|
|
48
|
+
1. Auto-detect your build folder (`dist`, `build`, `out`, `public`, etc.).
|
|
49
|
+
2. Ask you to pick a TTL (15, 35, or 45 minutes) if run interactively.
|
|
50
|
+
3. Upload your site, display your live URL, and copy it to your clipboard.
|
|
51
|
+
|
|
52
|
+
### 2. Specify Directory
|
|
53
|
+
```bash
|
|
54
|
+
blink ./my-app/dist
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Set Duration Flag
|
|
58
|
+
Skip the interactive prompt by setting the duration:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
blink -t 35 # 35 minutes
|
|
62
|
+
blink -t 45 # 45 minutes
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 4. Custom Subdomain Alias
|
|
66
|
+
```bash
|
|
67
|
+
blink --name my-portfolio
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5. Display Mobile QR Code
|
|
71
|
+
```bash
|
|
72
|
+
blink --qr
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 6. Auto-Replace Active Site
|
|
76
|
+
If you already have an active deployment and want to overwrite it without the interactive confirmation:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
blink --replace
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Other Commands
|
|
85
|
+
|
|
86
|
+
### Check Deployment Status
|
|
87
|
+
View your active site URL, expiration timestamp, and time remaining:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
blink status
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Delete Active Deployment Early
|
|
94
|
+
Permanently tear down your active deployment before the timer runs out:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
blink delete
|
|
98
|
+
# or
|
|
99
|
+
blink destroy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Environment Variables
|
|
105
|
+
|
|
106
|
+
| Variable | Description | Default |
|
|
107
|
+
| :--- | :--- | :--- |
|
|
108
|
+
| `BLINK_SERVER_URL` | Custom Blink server instance URL | `http://localhost:3000` |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Clone the repository
|
|
116
|
+
git clone https://github.com/Jyothirao2685/blink.git
|
|
117
|
+
cd blink
|
|
118
|
+
|
|
119
|
+
# Install dependencies
|
|
120
|
+
npm install
|
|
121
|
+
|
|
122
|
+
# Build CLI
|
|
123
|
+
npm run build
|
|
124
|
+
|
|
125
|
+
# Link locally for testing
|
|
126
|
+
npm link
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk2 from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import os from "os";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var CONFIG = {
|
|
13
|
+
// Production server URL or local dev override via BLINK_SERVER_URL
|
|
14
|
+
DEFAULT_SERVER_URL: process.env.BLINK_SERVER_URL || "https://api.restrodash.in",
|
|
15
|
+
// Maximum uncompressed upload size: 50MB
|
|
16
|
+
MAX_UPLOAD_SIZE_BYTES: 50 * 1024 * 1024,
|
|
17
|
+
// Allowed TTL durations in minutes
|
|
18
|
+
ALLOWED_DURATIONS: [15, 35, 45],
|
|
19
|
+
DEFAULT_DURATION: 15,
|
|
20
|
+
// Default directory candidates to look for
|
|
21
|
+
DEFAULT_BUILD_DIRS: ["dist", "build", "out", "public"],
|
|
22
|
+
// Forbidden file extensions on frontend sites
|
|
23
|
+
FORBIDDEN_EXTENSIONS: /* @__PURE__ */ new Set([
|
|
24
|
+
".exe",
|
|
25
|
+
".bat",
|
|
26
|
+
".cmd",
|
|
27
|
+
".sh",
|
|
28
|
+
".bin",
|
|
29
|
+
".elf",
|
|
30
|
+
".msi",
|
|
31
|
+
".php",
|
|
32
|
+
".phtml",
|
|
33
|
+
".py",
|
|
34
|
+
".rb",
|
|
35
|
+
".cgi",
|
|
36
|
+
".pl"
|
|
37
|
+
]),
|
|
38
|
+
// Local Blink cache/config directory
|
|
39
|
+
CONFIG_DIR: path.join(os.homedir(), ".blink")
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/machine.ts
|
|
43
|
+
import nodeMachineId from "node-machine-id";
|
|
44
|
+
import crypto from "crypto";
|
|
45
|
+
import os2 from "os";
|
|
46
|
+
import fs from "fs";
|
|
47
|
+
import path2 from "path";
|
|
48
|
+
var { machineId } = nodeMachineId;
|
|
49
|
+
var cachedMachineId = null;
|
|
50
|
+
async function getMachineId() {
|
|
51
|
+
if (cachedMachineId) {
|
|
52
|
+
return cachedMachineId;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const id = await machineId();
|
|
56
|
+
cachedMachineId = crypto.createHash("sha256").update(id).digest("hex").substring(0, 32);
|
|
57
|
+
return cachedMachineId;
|
|
58
|
+
} catch {
|
|
59
|
+
const fallbackId = generateFallbackId();
|
|
60
|
+
cachedMachineId = fallbackId;
|
|
61
|
+
return cachedMachineId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function generateFallbackId() {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(CONFIG.CONFIG_DIR)) {
|
|
67
|
+
fs.mkdirSync(CONFIG.CONFIG_DIR, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
const idFile = path2.join(CONFIG.CONFIG_DIR, "machine.id");
|
|
70
|
+
if (fs.existsSync(idFile)) {
|
|
71
|
+
const stored = fs.readFileSync(idFile, "utf8").trim();
|
|
72
|
+
if (stored.length >= 16) return stored;
|
|
73
|
+
}
|
|
74
|
+
const cpus = os2.cpus();
|
|
75
|
+
const cpuModel = cpus.length > 0 ? cpus[0].model : "";
|
|
76
|
+
const totalMem = os2.totalmem().toString();
|
|
77
|
+
const platform = os2.platform();
|
|
78
|
+
const hostname = os2.hostname();
|
|
79
|
+
const hash = crypto.createHash("sha256").update(`${hostname}-${platform}-${cpuModel}-${totalMem}`).digest("hex").substring(0, 32);
|
|
80
|
+
fs.writeFileSync(idFile, hash, "utf8");
|
|
81
|
+
return hash;
|
|
82
|
+
} catch {
|
|
83
|
+
return crypto.randomBytes(16).toString("hex");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/detector.ts
|
|
88
|
+
import fs2 from "fs";
|
|
89
|
+
import path3 from "path";
|
|
90
|
+
function resolveDeployDirectory(userSpecifiedPath) {
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
let resolvedPath = "";
|
|
93
|
+
if (userSpecifiedPath) {
|
|
94
|
+
resolvedPath = path3.resolve(cwd, userSpecifiedPath);
|
|
95
|
+
if (!fs2.existsSync(resolvedPath)) {
|
|
96
|
+
throw new Error(`Specified path does not exist: ${userSpecifiedPath}`);
|
|
97
|
+
}
|
|
98
|
+
const stat = fs2.statSync(resolvedPath);
|
|
99
|
+
if (!stat.isDirectory()) {
|
|
100
|
+
throw new Error(`Specified path is not a directory: ${userSpecifiedPath}`);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
for (const candidate of CONFIG.DEFAULT_BUILD_DIRS) {
|
|
104
|
+
const candidatePath = path3.join(cwd, candidate);
|
|
105
|
+
if (fs2.existsSync(candidatePath) && fs2.statSync(candidatePath).isDirectory()) {
|
|
106
|
+
resolvedPath = candidatePath;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!resolvedPath) {
|
|
111
|
+
resolvedPath = cwd;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const { totalFiles, totalSizeBytes, hasIndexHtml } = scanDirectory(resolvedPath);
|
|
115
|
+
if (totalFiles === 0) {
|
|
116
|
+
throw new Error(`Directory is empty: ${resolvedPath}`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
targetPath: resolvedPath,
|
|
120
|
+
hasIndexHtml,
|
|
121
|
+
totalFiles,
|
|
122
|
+
totalSizeBytes
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function scanDirectory(dir) {
|
|
126
|
+
let totalFiles = 0;
|
|
127
|
+
let totalSizeBytes = 0;
|
|
128
|
+
let hasIndexHtml = false;
|
|
129
|
+
function walk(currentDir) {
|
|
130
|
+
const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name.startsWith(".")) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const fullPath = path3.join(currentDir, entry.name);
|
|
136
|
+
if (entry.isDirectory()) {
|
|
137
|
+
walk(fullPath);
|
|
138
|
+
} else if (entry.isFile()) {
|
|
139
|
+
totalFiles++;
|
|
140
|
+
const stat = fs2.statSync(fullPath);
|
|
141
|
+
totalSizeBytes += stat.size;
|
|
142
|
+
if (entry.name.toLowerCase() === "index.html" && path3.relative(dir, fullPath) === "index.html") {
|
|
143
|
+
hasIndexHtml = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
walk(dir);
|
|
149
|
+
return { totalFiles, totalSizeBytes, hasIndexHtml };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/packager.ts
|
|
153
|
+
import { createRequire } from "module";
|
|
154
|
+
import fs3 from "fs";
|
|
155
|
+
import path4 from "path";
|
|
156
|
+
var require2 = createRequire(import.meta.url);
|
|
157
|
+
var archiver = require2("archiver");
|
|
158
|
+
async function packageDirectory(directoryPath) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const chunks = [];
|
|
161
|
+
let totalUncompressedBytes = 0;
|
|
162
|
+
let fileCount = 0;
|
|
163
|
+
const archive = archiver("zip", {
|
|
164
|
+
zlib: { level: 9 }
|
|
165
|
+
// Maximum compression for fast network upload
|
|
166
|
+
});
|
|
167
|
+
archive.on("data", (chunk) => {
|
|
168
|
+
chunks.push(chunk);
|
|
169
|
+
});
|
|
170
|
+
archive.on("error", (err) => {
|
|
171
|
+
reject(err);
|
|
172
|
+
});
|
|
173
|
+
archive.on("end", () => {
|
|
174
|
+
const buffer = Buffer.concat(chunks);
|
|
175
|
+
resolve({
|
|
176
|
+
buffer,
|
|
177
|
+
sizeBytes: buffer.length,
|
|
178
|
+
fileCount
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
function addFiles(currentDir, relativeDir = "") {
|
|
182
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const fullPath = path4.join(currentDir, entry.name);
|
|
185
|
+
const relPath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
186
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name.startsWith(".env") || entry.name === ".DS_Store" || entry.name === "Thumbs.db") {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
addFiles(fullPath, relPath);
|
|
191
|
+
} else if (entry.isFile()) {
|
|
192
|
+
const ext = path4.extname(entry.name).toLowerCase();
|
|
193
|
+
if (CONFIG.FORBIDDEN_EXTENSIONS.has(ext)) {
|
|
194
|
+
archive.abort();
|
|
195
|
+
return reject(
|
|
196
|
+
new Error(
|
|
197
|
+
`Security violation: Executable/server-side file detected: "${relPath}". Blink only allows static frontend assets.`
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const stat = fs3.statSync(fullPath);
|
|
202
|
+
totalUncompressedBytes += stat.size;
|
|
203
|
+
if (totalUncompressedBytes > CONFIG.MAX_UPLOAD_SIZE_BYTES) {
|
|
204
|
+
archive.abort();
|
|
205
|
+
return reject(
|
|
206
|
+
new Error(
|
|
207
|
+
`Upload size exceeded 50MB limit (Current: ${(totalUncompressedBytes / (1024 * 1024)).toFixed(1)}MB). Please remove large assets.`
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
fileCount++;
|
|
212
|
+
archive.file(fullPath, { name: relPath });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
addFiles(directoryPath);
|
|
218
|
+
archive.finalize();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
reject(err);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/api.ts
|
|
226
|
+
var BlinkApi = class {
|
|
227
|
+
constructor(baseUrl) {
|
|
228
|
+
this.baseUrl = baseUrl;
|
|
229
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
230
|
+
}
|
|
231
|
+
baseUrl;
|
|
232
|
+
/**
|
|
233
|
+
* Deploys a packed website archive to the Blink server.
|
|
234
|
+
*/
|
|
235
|
+
async deploy(params) {
|
|
236
|
+
const formData = new FormData();
|
|
237
|
+
const blob = new Blob([params.buffer], { type: "application/zip" });
|
|
238
|
+
formData.append("archive", blob, "site.zip");
|
|
239
|
+
formData.append("machineId", params.machineId);
|
|
240
|
+
formData.append("duration", params.duration.toString());
|
|
241
|
+
if (params.alias) {
|
|
242
|
+
formData.append("alias", params.alias);
|
|
243
|
+
}
|
|
244
|
+
if (params.replace) {
|
|
245
|
+
formData.append("replace", "true");
|
|
246
|
+
}
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await fetch(`${this.baseUrl}/api/deploy`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
body: formData,
|
|
252
|
+
headers: {
|
|
253
|
+
"X-Blink-Machine-Id": params.machineId
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new Error(`Failed to connect to Blink server at ${this.baseUrl}: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
const data = await response.json();
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const err = new Error(data.error || "Deployment failed");
|
|
262
|
+
err.code = data.code;
|
|
263
|
+
err.activeSite = data.activeSite;
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
return data;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Queries the status of the current active site for this machine.
|
|
270
|
+
*/
|
|
271
|
+
async getStatus(machineId2) {
|
|
272
|
+
let response;
|
|
273
|
+
try {
|
|
274
|
+
response = await fetch(`${this.baseUrl}/api/status?machineId=${encodeURIComponent(machineId2)}`, {
|
|
275
|
+
method: "GET",
|
|
276
|
+
headers: {
|
|
277
|
+
"X-Blink-Machine-Id": machineId2
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
} catch (err) {
|
|
281
|
+
throw new Error(`Failed to connect to Blink server at ${this.baseUrl}: ${err.message}`);
|
|
282
|
+
}
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
throw new Error(data.error || "Failed to fetch status");
|
|
286
|
+
}
|
|
287
|
+
return data;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Deletes the currently active deployment for this machine.
|
|
291
|
+
*/
|
|
292
|
+
async deleteCurrentSite(machineId2) {
|
|
293
|
+
let response;
|
|
294
|
+
try {
|
|
295
|
+
response = await fetch(`${this.baseUrl}/api/deploy`, {
|
|
296
|
+
method: "DELETE",
|
|
297
|
+
headers: {
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
"X-Blink-Machine-Id": machineId2
|
|
300
|
+
},
|
|
301
|
+
body: JSON.stringify({ machineId: machineId2 })
|
|
302
|
+
});
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new Error(`Failed to connect to Blink server at ${this.baseUrl}: ${err.message}`);
|
|
305
|
+
}
|
|
306
|
+
const data = await response.json();
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new Error(data.error || "Failed to delete site");
|
|
309
|
+
}
|
|
310
|
+
return data;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/ui.ts
|
|
315
|
+
import chalk from "chalk";
|
|
316
|
+
import boxen from "boxen";
|
|
317
|
+
import clipboardy from "clipboardy";
|
|
318
|
+
import qrcode from "qrcode-terminal";
|
|
319
|
+
function renderBanner() {
|
|
320
|
+
console.log(
|
|
321
|
+
chalk.bold.hex("#FF6B6B")("\n \u26A1 BLINK") + chalk.gray(" - Ephemeral Frontend Hosting (Zero-Signup)\n")
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
async function copyToClipboard(url) {
|
|
325
|
+
try {
|
|
326
|
+
await clipboardy.write(url);
|
|
327
|
+
return true;
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function renderSuccess(data) {
|
|
333
|
+
const expiresDate = new Date(data.expiresAt);
|
|
334
|
+
const formattedTime = expiresDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
335
|
+
const sizeMb = (data.sizeBytes / (1024 * 1024)).toFixed(2);
|
|
336
|
+
const copiedBadge = data.copied ? chalk.greenBright(" \u2714 Copied to clipboard!") : chalk.gray(" (Use Ctrl+C to copy)");
|
|
337
|
+
const content = [
|
|
338
|
+
`${chalk.bold.green("\u2714 Successfully Deployed!")}`,
|
|
339
|
+
"",
|
|
340
|
+
` ${chalk.bold("URL:")} ${chalk.cyan.underline.bold(data.url)}${copiedBadge}`,
|
|
341
|
+
` ${chalk.bold("Expires In:")} ${chalk.yellow.bold(`${data.durationMinutes} minutes`)} ${chalk.gray(`(at ${formattedTime})`)}`,
|
|
342
|
+
` ${chalk.bold("Files:")} ${chalk.white(data.fileCount)} files ${chalk.gray(`(${sizeMb} MB compressed)`)}`,
|
|
343
|
+
"",
|
|
344
|
+
chalk.dim(" Site will automatically self-destruct when time expires.")
|
|
345
|
+
].join("\n");
|
|
346
|
+
console.log(
|
|
347
|
+
boxen(content, {
|
|
348
|
+
padding: 1,
|
|
349
|
+
margin: 1,
|
|
350
|
+
borderColor: "green",
|
|
351
|
+
borderStyle: "round"
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
function renderQrCode(url) {
|
|
356
|
+
console.log(chalk.bold.gray("\n Scan with phone to preview:"));
|
|
357
|
+
qrcode.generate(url, { small: true });
|
|
358
|
+
}
|
|
359
|
+
function renderActiveStatus(data) {
|
|
360
|
+
const expiresDate = new Date(data.expiresAt);
|
|
361
|
+
const formattedTime = expiresDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
362
|
+
const content = [
|
|
363
|
+
`${chalk.bold.cyan("\u25CF Active Blink Deployment")}`,
|
|
364
|
+
"",
|
|
365
|
+
` ${chalk.bold("URL:")} ${chalk.cyan.underline.bold(data.url)}`,
|
|
366
|
+
` ${chalk.bold("Site ID:")} ${chalk.gray(data.siteId)}`,
|
|
367
|
+
` ${chalk.bold("Time Left:")} ${chalk.yellow.bold(`${data.remainingMinutes}m remaining`)} ${chalk.gray(`(until ${formattedTime})`)}`,
|
|
368
|
+
"",
|
|
369
|
+
chalk.dim(" To delete early: ") + chalk.bold("blink delete")
|
|
370
|
+
].join("\n");
|
|
371
|
+
console.log(
|
|
372
|
+
boxen(content, {
|
|
373
|
+
padding: 1,
|
|
374
|
+
margin: 1,
|
|
375
|
+
borderColor: "cyan",
|
|
376
|
+
borderStyle: "round"
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/cli.ts
|
|
382
|
+
var program = new Command();
|
|
383
|
+
program.name("blink").description("Deploy static frontend websites temporarily without signing up").version("1.0.0").argument("[directory]", "Frontend build directory to deploy (auto-detected if omitted)").option("-t, --time <minutes>", "Expiration time: 15, 35, or 45 mins", (val) => parseInt(val, 10)).option("-n, --name <alias>", 'Custom subdomain alias (e.g. "my-project")').option("--qr", "Display a terminal QR code for phone scanning").option("--replace", "Automatically replace any active deployment without prompting").option("--server <url>", "Custom Blink server URL", CONFIG.DEFAULT_SERVER_URL).action(async (directoryArg, options) => {
|
|
384
|
+
try {
|
|
385
|
+
renderBanner();
|
|
386
|
+
const serverUrl = options.server || CONFIG.DEFAULT_SERVER_URL;
|
|
387
|
+
const api = new BlinkApi(serverUrl);
|
|
388
|
+
const spinner = ora("Detecting frontend directory...").start();
|
|
389
|
+
let detection;
|
|
390
|
+
try {
|
|
391
|
+
detection = resolveDeployDirectory(directoryArg);
|
|
392
|
+
spinner.succeed(
|
|
393
|
+
`Found directory: ${chalk2.cyan(detection.targetPath)} ${chalk2.gray(`(${detection.totalFiles} files)`)}`
|
|
394
|
+
);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
spinner.fail(err.message);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
if (!detection.hasIndexHtml) {
|
|
400
|
+
console.log(
|
|
401
|
+
chalk2.yellow(' \u26A0 Warning: No "index.html" found in the root of the target directory.')
|
|
402
|
+
);
|
|
403
|
+
console.log(chalk2.gray(" Visitors might see a 404 unless your frontend has an index.html.\n"));
|
|
404
|
+
}
|
|
405
|
+
let duration = options.time;
|
|
406
|
+
if (!duration || !CONFIG.ALLOWED_DURATIONS.includes(duration)) {
|
|
407
|
+
if (process.stdout.isTTY) {
|
|
408
|
+
const response = await prompts({
|
|
409
|
+
type: "select",
|
|
410
|
+
name: "selectedDuration",
|
|
411
|
+
message: "Select deployment duration (auto-deleted when expired):",
|
|
412
|
+
choices: [
|
|
413
|
+
{ title: "15 minutes (Default)", value: 15 },
|
|
414
|
+
{ title: "35 minutes", value: 35 },
|
|
415
|
+
{ title: "45 minutes", value: 45 }
|
|
416
|
+
],
|
|
417
|
+
initial: 0
|
|
418
|
+
});
|
|
419
|
+
if (!response.selectedDuration) {
|
|
420
|
+
console.log(chalk2.gray("\nDeployment cancelled."));
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
duration = response.selectedDuration;
|
|
424
|
+
} else {
|
|
425
|
+
duration = CONFIG.DEFAULT_DURATION;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const machineSpinner = ora("Authenticating machine...").start();
|
|
429
|
+
const machineId2 = await getMachineId();
|
|
430
|
+
machineSpinner.succeed(`Machine verified: ${chalk2.gray(machineId2.substring(0, 12) + "...")}`);
|
|
431
|
+
const packSpinner = ora("Packaging website assets...").start();
|
|
432
|
+
let packageResult;
|
|
433
|
+
try {
|
|
434
|
+
packageResult = await packageDirectory(detection.targetPath);
|
|
435
|
+
packSpinner.succeed(
|
|
436
|
+
`Packaged ${packageResult.fileCount} files (${(packageResult.sizeBytes / (1024 * 1024)).toFixed(2)} MB)`
|
|
437
|
+
);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
packSpinner.fail(err.message);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
const deploySpinner = ora("Deploying to Blink cloud...").start();
|
|
443
|
+
let deployResult;
|
|
444
|
+
let shouldReplace = options.replace || false;
|
|
445
|
+
try {
|
|
446
|
+
deployResult = await api.deploy({
|
|
447
|
+
buffer: packageResult.buffer,
|
|
448
|
+
machineId: machineId2,
|
|
449
|
+
duration,
|
|
450
|
+
alias: options.name,
|
|
451
|
+
replace: shouldReplace
|
|
452
|
+
});
|
|
453
|
+
} catch (err) {
|
|
454
|
+
deploySpinner.stop();
|
|
455
|
+
if (err.code === "ACTIVE_SITE_EXISTS" && err.activeSite) {
|
|
456
|
+
console.log(
|
|
457
|
+
chalk2.yellow(
|
|
458
|
+
`
|
|
459
|
+
\u26A0 You already have an active deployment: ${chalk2.bold.cyan(err.activeSite.url)}`
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
if (process.stdout.isTTY) {
|
|
463
|
+
const confirm = await prompts({
|
|
464
|
+
type: "confirm",
|
|
465
|
+
name: "replace",
|
|
466
|
+
message: "Would you like to replace your active deployment?",
|
|
467
|
+
initial: true
|
|
468
|
+
});
|
|
469
|
+
if (confirm.replace) {
|
|
470
|
+
const retrySpinner = ora("Replacing active deployment...").start();
|
|
471
|
+
try {
|
|
472
|
+
deployResult = await api.deploy({
|
|
473
|
+
buffer: packageResult.buffer,
|
|
474
|
+
machineId: machineId2,
|
|
475
|
+
duration,
|
|
476
|
+
alias: options.name,
|
|
477
|
+
replace: true
|
|
478
|
+
});
|
|
479
|
+
retrySpinner.succeed("Replacement complete!");
|
|
480
|
+
} catch (retryErr) {
|
|
481
|
+
retrySpinner.fail(retryErr.message);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
console.log(
|
|
486
|
+
chalk2.gray("\nDeployment kept as-is. Run ") + chalk2.bold("blink delete") + chalk2.gray(" to remove it.\n")
|
|
487
|
+
);
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
console.error(
|
|
492
|
+
chalk2.red(
|
|
493
|
+
"Deployment rejected: active site already running. Use --replace to overwrite.\n"
|
|
494
|
+
)
|
|
495
|
+
);
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
deploySpinner.fail(err.message);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (deployResult) {
|
|
504
|
+
deploySpinner.succeed("Uploaded & configured!");
|
|
505
|
+
const copied = await copyToClipboard(deployResult.url);
|
|
506
|
+
renderSuccess({
|
|
507
|
+
url: deployResult.url,
|
|
508
|
+
expiresAt: deployResult.expiresAt,
|
|
509
|
+
durationMinutes: deployResult.durationMinutes,
|
|
510
|
+
copied,
|
|
511
|
+
fileCount: packageResult.fileCount,
|
|
512
|
+
sizeBytes: packageResult.sizeBytes
|
|
513
|
+
});
|
|
514
|
+
if (options.qr) {
|
|
515
|
+
renderQrCode(deployResult.url);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} catch (err) {
|
|
519
|
+
console.error(chalk2.red(`
|
|
520
|
+
Error: ${err.message}`));
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
program.command("status").description("Check details of your current active Blink deployment").option("--server <url>", "Custom Blink server URL", CONFIG.DEFAULT_SERVER_URL).action(async (options) => {
|
|
525
|
+
try {
|
|
526
|
+
renderBanner();
|
|
527
|
+
const spinner = ora("Checking deployment status...").start();
|
|
528
|
+
const machineId2 = await getMachineId();
|
|
529
|
+
const api = new BlinkApi(options.server || CONFIG.DEFAULT_SERVER_URL);
|
|
530
|
+
const status = await api.getStatus(machineId2);
|
|
531
|
+
spinner.stop();
|
|
532
|
+
if (status.hasActiveSite && status.url && status.siteId && status.expiresAt) {
|
|
533
|
+
renderActiveStatus({
|
|
534
|
+
url: status.url,
|
|
535
|
+
siteId: status.siteId,
|
|
536
|
+
createdAt: status.createdAt || "",
|
|
537
|
+
expiresAt: status.expiresAt,
|
|
538
|
+
remainingMinutes: status.remainingMinutes || 0
|
|
539
|
+
});
|
|
540
|
+
} else {
|
|
541
|
+
console.log(chalk2.gray(" No active deployment found for this machine."));
|
|
542
|
+
console.log(chalk2.gray(" Run ") + chalk2.bold.cyan("blink") + chalk2.gray(" in any project folder to deploy.\n"));
|
|
543
|
+
}
|
|
544
|
+
} catch (err) {
|
|
545
|
+
console.error(chalk2.red(`
|
|
546
|
+
Error: ${err.message}`));
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
program.command("delete").alias("destroy").description("Immediately delete your current active Blink deployment").option("--server <url>", "Custom Blink server URL", CONFIG.DEFAULT_SERVER_URL).action(async (options) => {
|
|
551
|
+
try {
|
|
552
|
+
renderBanner();
|
|
553
|
+
const machineId2 = await getMachineId();
|
|
554
|
+
const api = new BlinkApi(options.server || CONFIG.DEFAULT_SERVER_URL);
|
|
555
|
+
const spinner = ora("Deleting active deployment...").start();
|
|
556
|
+
const result = await api.deleteCurrentSite(machineId2);
|
|
557
|
+
spinner.succeed(result.message || "Active deployment deleted successfully.");
|
|
558
|
+
console.log("");
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.error(chalk2.red(`
|
|
561
|
+
Error: ${err.message}`));
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blinkhost",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deploy static frontend websites temporarily without signing up.",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": "./dist/cli.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsup --watch",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"blink",
|
|
18
|
+
"deploy",
|
|
19
|
+
"frontend",
|
|
20
|
+
"static",
|
|
21
|
+
"temporary",
|
|
22
|
+
"ephemeral",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"author": "Jyothirao",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Jyothirao2685/blink.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/Jyothirao2685/blink/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://restrodash.in",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"archiver": "^7.0.1",
|
|
37
|
+
"boxen": "^8.0.1",
|
|
38
|
+
"chalk": "^5.4.1",
|
|
39
|
+
"clipboardy": "^4.0.0",
|
|
40
|
+
"commander": "^13.1.0",
|
|
41
|
+
"node-machine-id": "^1.1.12",
|
|
42
|
+
"ora": "^8.2.0",
|
|
43
|
+
"prompts": "^2.4.2",
|
|
44
|
+
"qrcode-terminal": "^0.12.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/archiver": "^6.0.3",
|
|
48
|
+
"@types/node": "^22.13.10",
|
|
49
|
+
"@types/prompts": "^2.4.9",
|
|
50
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
51
|
+
"tsup": "^9.0.0",
|
|
52
|
+
"typescript": "^5.8.2"
|
|
53
|
+
},
|
|
54
|
+
"overrides": {
|
|
55
|
+
"minimatch": "^11.0.8"
|
|
56
|
+
}
|
|
57
|
+
}
|