nstantpage-agent 0.7.1 → 0.8.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/dist/cli.js CHANGED
@@ -22,7 +22,6 @@ import { startCommand } from './commands/start.js';
22
22
  import { statusCommand } from './commands/status.js';
23
23
  import { serviceInstallCommand, serviceUninstallCommand, serviceStatusCommand, serviceStartCommand, serviceStopCommand } from './commands/service.js';
24
24
  import { updateCommand } from './commands/update.js';
25
- import { runCommand } from './commands/run.js';
26
25
  import { syncCommand } from './commands/sync.js';
27
26
  import { getPackageVersion } from './version.js';
28
27
  const program = new Command();
@@ -53,17 +52,13 @@ program
53
52
  .option('--dir <path>', 'Project directory override')
54
53
  .option('--no-dev', 'Skip starting the dev server (start manually later)')
55
54
  .action(startCommand);
56
- program
57
- .command('run')
58
- .description('Open the nstantpage desktop app')
59
- .option('--local [port]', 'Connect to local backend (default: 5001)')
60
- .action(runCommand);
61
55
  program
62
56
  .command('sync')
63
57
  .description('Sync local directory to nstantpage and start agent')
64
58
  .argument('[directory]', 'Project directory to sync (defaults to current directory)', '.')
65
59
  .option('-p, --port <port>', 'Local dev server port', '3000')
66
60
  .option('-a, --api-port <port>', 'Local API server port', '18924')
61
+ .option('--local [port]', 'Sync to local backend (default: localhost:5001)')
67
62
  .option('--gateway <url>', 'Gateway URL (default: from login)')
68
63
  .option('--backend <url>', 'Backend API URL (auto-detected from gateway)')
69
64
  .option('--token <token>', 'Auth token (skip login flow)')
@@ -124,9 +119,7 @@ service
124
119
  .action(serviceStatusCommand);
125
120
  program
126
121
  .command('update')
127
- .description('Update CLI and desktop app to the latest version')
128
- .option('--cli', 'Update only the CLI package')
129
- .option('--desktop', 'Update only the desktop app')
122
+ .description('Update CLI to the latest version')
130
123
  .action(updateCommand);
131
124
  program.parse();
132
125
  //# sourceMappingURL=cli.js.map
@@ -20,11 +20,8 @@ import fs from 'fs';
20
20
  import path from 'path';
21
21
  import os from 'os';
22
22
  import { execSync } from 'child_process';
23
- import { fileURLToPath } from 'url';
24
23
  import { getConfig } from '../config.js';
25
24
  import { startCommand } from './start.js';
26
- const __filename_esm = fileURLToPath(import.meta.url);
27
- const __dirname_esm = path.dirname(__filename_esm);
28
25
  const PLIST_LABEL = 'com.nstantpage.agent';
29
26
  const SYSTEMD_SERVICE = 'nstantpage-agent';
30
27
  const WIN_TASK_NAME = 'NstantpageAgent';
@@ -115,23 +112,6 @@ export async function serviceInstallCommand(options = {}) {
115
112
  }
116
113
  const gateway = options.gateway || 'wss://webprev.live';
117
114
  const platform = os.platform();
118
- // On desktop platforms, prefer the Electron tray app (agent + tray icon in one)
119
- const electronAppPath = findElectronApp();
120
- if (electronAppPath && (platform === 'darwin' || platform === 'win32')) {
121
- console.log(chalk.blue(`\n Found nstantpage desktop app: ${electronAppPath}`));
122
- console.log(chalk.blue(' Installing with system tray icon...\n'));
123
- if (platform === 'darwin') {
124
- await installElectronLaunchd(electronAppPath);
125
- }
126
- else if (platform === 'win32') {
127
- await installElectronWindowsTask(electronAppPath);
128
- }
129
- return;
130
- }
131
- // Fallback: headless service (no tray icon)
132
- if (electronAppPath === null) {
133
- console.log(chalk.gray(' Tip: Run "nstantpage update --desktop" to download the desktop app with tray icon.'));
134
- }
135
115
  if (platform === 'darwin') {
136
116
  await installLaunchd(gateway, token);
137
117
  }
@@ -228,118 +208,6 @@ export async function serviceStatusCommand() {
228
208
  }
229
209
  }
230
210
  // ─── Helper Functions ─────────────────────────────────
231
- // ─── Electron App Detection ──────────────────────────────
232
- function findElectronApp() {
233
- const platform = os.platform();
234
- if (platform === 'darwin') {
235
- // Check common macOS install paths
236
- const candidates = [
237
- // Downloaded by postinstall (npm i -g nstantpage-agent)
238
- path.join(os.homedir(), '.nstantpage', 'desktop', 'nstantpage.app'),
239
- '/Applications/nstantpage.app',
240
- path.join(os.homedir(), 'Applications', 'nstantpage.app'),
241
- // Dev build location (from electron-builder --dir)
242
- path.join(__dirname_esm, '..', '..', 'tray', 'dist', 'mac-arm64', 'nstantpage.app'),
243
- path.join(__dirname_esm, '..', '..', 'tray', 'dist', 'mac', 'nstantpage.app'),
244
- ];
245
- for (const p of candidates) {
246
- if (fs.existsSync(p))
247
- return p;
248
- }
249
- }
250
- else if (platform === 'win32') {
251
- const candidates = [
252
- // Downloaded by postinstall (npm i -g nstantpage-agent)
253
- path.join(os.homedir(), '.nstantpage', 'desktop', 'nstantpage.exe'),
254
- path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'nstantpage', 'nstantpage.exe'),
255
- path.join('C:\\Program Files', 'nstantpage', 'nstantpage.exe'),
256
- ];
257
- for (const p of candidates) {
258
- if (fs.existsSync(p))
259
- return p;
260
- }
261
- }
262
- return null;
263
- }
264
- async function installElectronLaunchd(appPath) {
265
- const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
266
- const plistPath = path.join(plistDir, `${PLIST_LABEL}.plist`);
267
- const logPath = path.join(os.homedir(), '.nstantpage', 'agent.log');
268
- const errPath = path.join(os.homedir(), '.nstantpage', 'agent.err.log');
269
- fs.mkdirSync(plistDir, { recursive: true });
270
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
271
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
272
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
273
- <plist version="1.0">
274
- <dict>
275
- <key>Label</key>
276
- <string>${PLIST_LABEL}</string>
277
- <key>ProgramArguments</key>
278
- <array>
279
- <string>/usr/bin/open</string>
280
- <string>-a</string>
281
- <string>${appPath}</string>
282
- </array>
283
- <key>RunAtLoad</key>
284
- <true/>
285
- <key>KeepAlive</key>
286
- <dict>
287
- <key>Crashed</key>
288
- <true/>
289
- </dict>
290
- <key>ProcessType</key>
291
- <string>Interactive</string>
292
- <key>StandardOutPath</key>
293
- <string>${logPath}</string>
294
- <key>StandardErrorPath</key>
295
- <string>${errPath}</string>
296
- <key>EnvironmentVariables</key>
297
- <dict>
298
- <key>NSAppSleepDisabled</key>
299
- <string>YES</string>
300
- </dict>
301
- <key>ThrottleInterval</key>
302
- <integer>10</integer>
303
- </dict>
304
- </plist>`;
305
- fs.writeFileSync(plistPath, plist, 'utf-8');
306
- try {
307
- execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
308
- }
309
- catch { }
310
- execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8' });
311
- console.log(chalk.green('\n ✓ nstantpage installed as desktop app with tray icon\n'));
312
- console.log(chalk.gray(` App: ${appPath}`));
313
- console.log(chalk.gray(` Plist: ${plistPath}`));
314
- console.log(chalk.gray(` Log: ${logPath}`));
315
- console.log(chalk.gray(` Status: nstantpage service status`));
316
- console.log(chalk.gray(` Remove: nstantpage service uninstall\n`));
317
- console.log(chalk.blue(' The app will start on login with a tray icon showing connection status.'));
318
- console.log(chalk.blue(' Right-click the tray icon for options. Open nstantpage.com to connect projects.\n'));
319
- }
320
- async function installElectronWindowsTask(appPath) {
321
- const logPath = path.join(os.homedir(), '.nstantpage', 'agent.log');
322
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
323
- try {
324
- execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f 2>nul`, { encoding: 'utf-8' });
325
- }
326
- catch { }
327
- try {
328
- execSync(`schtasks /create /tn "${WIN_TASK_NAME}" /tr "\\"${appPath}\\"" /sc onlogon /rl highest /delay 0000:10 /f`, { encoding: 'utf-8' });
329
- // Start immediately
330
- execSync(`start "" "${appPath}"`, { encoding: 'utf-8' });
331
- }
332
- catch (err) {
333
- console.log(chalk.yellow(` ⚠ Task Scheduler error: ${err.message}`));
334
- return;
335
- }
336
- console.log(chalk.green('\n ✓ nstantpage installed as desktop app with tray icon\n'));
337
- console.log(chalk.gray(` App: ${appPath}`));
338
- console.log(chalk.gray(` Task: ${WIN_TASK_NAME}`));
339
- console.log(chalk.gray(` Status: nstantpage service status`));
340
- console.log(chalk.gray(` Remove: nstantpage service uninstall\n`));
341
- console.log(chalk.blue(' The app will start on login with a tray icon.\n'));
342
- }
343
211
  function getAgentBinPath() {
344
212
  try {
345
213
  const resolved = execSync('which nstantpage 2>/dev/null || which nstantpage-agent 2>/dev/null', { encoding: 'utf-8' }).trim();
@@ -623,6 +623,7 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
623
623
  devPort: allocatePortsForProject(pid).devPort,
624
624
  apiPort: allocatePortsForProject(pid).apiPort,
625
625
  tunnelStatus: proj.tunnel.status,
626
+ projectDir: resolveProjectDir('.', pid),
626
627
  })),
627
628
  requestsForwarded: standbyTunnel.stats.requestsForwarded +
628
629
  Array.from(activeProjects.values()).reduce((sum, p) => sum + p.tunnel.stats.requestsForwarded, 0),
@@ -14,6 +14,7 @@
14
14
  interface SyncOptions {
15
15
  port: string;
16
16
  apiPort: string;
17
+ local?: boolean | string;
17
18
  gateway?: string;
18
19
  backend?: string;
19
20
  token?: string;
@@ -98,6 +98,12 @@ export async function syncCommand(directory, options) {
98
98
  console.log(chalk.red(` ✗ Directory not found: ${projectDir}`));
99
99
  process.exit(1);
100
100
  }
101
+ // Handle --local flag: override backend & gateway to point at localhost
102
+ if (options.local !== undefined && options.local !== false) {
103
+ const localPort = (typeof options.local === 'string') ? options.local : '5001';
104
+ options.backend = `http://localhost:${localPort}`;
105
+ options.gateway = `ws://localhost:4000`;
106
+ }
101
107
  // Check authentication
102
108
  const gateway = options.gateway || conf.get('gatewayUrl') || 'wss://webprev.live';
103
109
  const isLocalGateway = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(gateway);
@@ -1,14 +1,7 @@
1
1
  /**
2
- * Update command — update both CLI (npm) and desktop app (GitHub releases).
2
+ * Update command — update the CLI via npm.
3
3
  *
4
4
  * Usage:
5
- * nstantpage update — Update both CLI and desktop
6
- * nstantpage update --cli — Update CLI only (npm)
7
- * nstantpage update --desktop — Update desktop only (GitHub releases)
5
+ * nstantpage update — Update CLI to latest version
8
6
  */
9
- interface UpdateOptions {
10
- cli?: boolean;
11
- desktop?: boolean;
12
- }
13
- export declare function updateCommand(options?: UpdateOptions): Promise<void>;
14
- export {};
7
+ export declare function updateCommand(): Promise<void>;
@@ -1,29 +1,16 @@
1
1
  /**
2
- * Update command — update both CLI (npm) and desktop app (GitHub releases).
2
+ * Update command — update the CLI via npm.
3
3
  *
4
4
  * Usage:
5
- * nstantpage update — Update both CLI and desktop
6
- * nstantpage update --cli — Update CLI only (npm)
7
- * nstantpage update --desktop — Update desktop only (GitHub releases)
5
+ * nstantpage update — Update CLI to latest version
8
6
  */
9
7
  import chalk from 'chalk';
10
- import fs from 'fs';
11
- import path from 'path';
12
- import os from 'os';
13
8
  import { execSync } from 'child_process';
14
9
  import { getPackageVersion } from '../version.js';
15
- const DESKTOP_DIR = path.join(os.homedir(), '.nstantpage', 'desktop');
16
- const VERSION_FILE = path.join(DESKTOP_DIR, '.version');
17
- export async function updateCommand(options = {}) {
18
- const updateBoth = !options.cli && !options.desktop;
10
+ export async function updateCommand() {
19
11
  const currentVersion = getPackageVersion();
20
12
  console.log(chalk.blue(`\n nstantpage v${currentVersion}\n`));
21
- if (updateBoth || options.cli) {
22
- await updateCli(currentVersion);
23
- }
24
- if (updateBoth || options.desktop) {
25
- await updateDesktop();
26
- }
13
+ await updateCli(currentVersion);
27
14
  console.log('');
28
15
  }
29
16
  async function updateCli(currentVersion) {
@@ -47,27 +34,4 @@ async function updateCli(currentVersion) {
47
34
  console.log(chalk.gray(' Try manually: npm install -g nstantpage-agent@latest'));
48
35
  }
49
36
  }
50
- async function updateDesktop() {
51
- console.log(chalk.gray(' Checking GitHub for desktop updates...'));
52
- try {
53
- // Re-run the postinstall script which handles download + version check
54
- const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', 'scripts', 'postinstall.mjs');
55
- if (fs.existsSync(scriptPath)) {
56
- execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
57
- }
58
- else {
59
- // If installed globally, the script is in the package root
60
- const altPath = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'scripts', 'postinstall.mjs');
61
- if (fs.existsSync(altPath)) {
62
- execSync(`node "${altPath}"`, { stdio: 'inherit' });
63
- }
64
- else {
65
- console.log(chalk.yellow(' ⚠ Desktop updater script not found. Reinstall to fix: npm i -g nstantpage-agent'));
66
- }
67
- }
68
- }
69
- catch (err) {
70
- console.log(chalk.red(` ✗ Desktop update failed: ${err.message}`));
71
- }
72
- }
73
37
  //# sourceMappingURL=update.js.map
@@ -657,7 +657,9 @@ export class LocalServer {
657
657
  let shell = null;
658
658
  let ptyProcess = null;
659
659
  // Ensure project directory exists (posix_spawnp fails if cwd is missing)
660
- const spawnCwd = fs.existsSync(this.options.projectDir) ? this.options.projectDir : process.cwd();
660
+ // Use cwd from request if provided (e.g. LocalRepo projects use the actual disk folder path)
661
+ const requestedCwd = parsed.cwd && fs.existsSync(parsed.cwd) ? parsed.cwd : null;
662
+ const spawnCwd = requestedCwd || (fs.existsSync(this.options.projectDir) ? this.options.projectDir : process.cwd());
661
663
  // Prefer node-pty for real PTY (interactive shell, echo, prompt, resize)
662
664
  if (ptyModule) {
663
665
  try {
@@ -37,6 +37,7 @@ export interface ActiveProjectInfo {
37
37
  apiPort: number;
38
38
  tunnelStatus: string;
39
39
  startedAt?: string;
40
+ projectDir?: string;
40
41
  }
41
42
  type StatusProvider = () => AgentStatusInfo;
42
43
  type ShutdownHandler = () => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Local development agent for nstantpage.com — run your projects locally, preview in the cloud. Replaces cloud containers for faster builds.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,14 +11,12 @@
11
11
  "files": [
12
12
  "dist/**/*.js",
13
13
  "dist/**/*.d.ts",
14
- "scripts/postinstall.mjs",
15
14
  "README.md"
16
15
  ],
17
16
  "scripts": {
18
17
  "build": "tsc",
19
18
  "dev": "tsx src/cli.ts",
20
19
  "start": "node dist/cli.js",
21
- "postinstall": "node scripts/postinstall.mjs",
22
20
  "prepublishOnly": "npm run build"
23
21
  },
24
22
  "engines": {
@@ -1,10 +0,0 @@
1
- /**
2
- * Git Setup — ensures git is installed on the user's machine.
3
- * Called during agent startup so GitSnapshotService (backend) can use it for undo.
4
- */
5
- /**
6
- * Check if git is available, and try to install it if not.
7
- * Returns true if git is available after this call.
8
- * Non-blocking: agent continues even if git can't be installed (undo just won't work).
9
- */
10
- export declare function ensureGit(): Promise<boolean>;
package/dist/gitSetup.js DELETED
@@ -1,162 +0,0 @@
1
- /**
2
- * Git Setup — ensures git is installed on the user's machine.
3
- * Called during agent startup so GitSnapshotService (backend) can use it for undo.
4
- */
5
- import { execSync } from 'child_process';
6
- import os from 'os';
7
- import chalk from 'chalk';
8
- /**
9
- * Check if git is available, and try to install it if not.
10
- * Returns true if git is available after this call.
11
- * Non-blocking: agent continues even if git can't be installed (undo just won't work).
12
- */
13
- export async function ensureGit() {
14
- if (isGitAvailable()) {
15
- return true;
16
- }
17
- console.log(chalk.yellow(' ⚠ Git not found — installing (needed for undo/version control)'));
18
- const platform = os.platform();
19
- try {
20
- if (platform === 'darwin') {
21
- return await installGitMacOS();
22
- }
23
- else if (platform === 'win32') {
24
- return await installGitWindows();
25
- }
26
- else {
27
- return installGitLinux();
28
- }
29
- }
30
- catch (err) {
31
- console.log(chalk.red(` ✗ Could not install git: ${err.message}`));
32
- printManualInstructions();
33
- return false;
34
- }
35
- }
36
- function isGitAvailable() {
37
- try {
38
- execSync('git --version', { stdio: 'pipe', encoding: 'utf-8' });
39
- return true;
40
- }
41
- catch {
42
- return false;
43
- }
44
- }
45
- async function installGitMacOS() {
46
- // Check if Xcode CLT is installed (it includes git)
47
- try {
48
- execSync('xcode-select -p', { stdio: 'pipe' });
49
- // CLT installed but git not in PATH — unusual, may need PATH fix
50
- console.log(chalk.yellow(' Xcode CLT installed but git not in PATH'));
51
- printManualInstructions();
52
- return false;
53
- }
54
- catch {
55
- // CLT not installed — trigger installation
56
- }
57
- // Try Homebrew first (non-interactive, faster)
58
- try {
59
- execSync('brew --version', { stdio: 'pipe' });
60
- console.log(chalk.gray(' Installing git via Homebrew...'));
61
- execSync('brew install git', { stdio: 'inherit' });
62
- if (isGitAvailable()) {
63
- const ver = execSync('git --version', { encoding: 'utf-8' }).trim();
64
- console.log(chalk.green(` ✓ ${ver}`));
65
- return true;
66
- }
67
- }
68
- catch {
69
- // Homebrew not available — fall through to xcode-select
70
- }
71
- // Trigger Xcode CLT install dialog
72
- console.log(chalk.gray(' Installing Xcode Command Line Tools (includes git)...'));
73
- console.log(chalk.gray(' Please click "Install" in the dialog if prompted.'));
74
- try {
75
- execSync('xcode-select --install 2>/dev/null', { stdio: 'inherit' });
76
- }
77
- catch {
78
- // xcode-select --install returns non-zero if dialog was shown
79
- }
80
- // Poll for git availability (max 5 minutes — the Xcode dialog is async)
81
- const maxWaitMs = 300_000;
82
- const pollMs = 5_000;
83
- const start = Date.now();
84
- while (Date.now() - start < maxWaitMs) {
85
- await new Promise(r => setTimeout(r, pollMs));
86
- if (isGitAvailable()) {
87
- const ver = execSync('git --version', { encoding: 'utf-8' }).trim();
88
- console.log(chalk.green(` ✓ ${ver}`));
89
- return true;
90
- }
91
- }
92
- console.log(chalk.yellow(' ⚠ Timed out waiting for install. Restart after Xcode Tools finishes.'));
93
- return false;
94
- }
95
- async function installGitWindows() {
96
- // Try winget (available on Windows 10 1709+)
97
- try {
98
- execSync('winget --version', { stdio: 'pipe' });
99
- console.log(chalk.gray(' Installing git via winget...'));
100
- execSync('winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements', { stdio: 'inherit' });
101
- if (isGitAvailable()) {
102
- const ver = execSync('git --version', { encoding: 'utf-8' }).trim();
103
- console.log(chalk.green(` ✓ ${ver}`));
104
- return true;
105
- }
106
- // winget installs may need a new shell for PATH update
107
- console.log(chalk.yellow(' Git installed — restart your terminal for PATH to update.'));
108
- return false;
109
- }
110
- catch {
111
- // winget not available
112
- }
113
- console.log(chalk.yellow(' Install Git for Windows:'));
114
- console.log(chalk.gray(' https://git-scm.com/download/win'));
115
- console.log(chalk.gray(' or: winget install --id Git.Git'));
116
- return false;
117
- }
118
- function installGitLinux() {
119
- // Try common package managers (non-interactive)
120
- const managers = [
121
- { check: 'apt-get --version', install: 'sudo apt-get install -y git' },
122
- { check: 'dnf --version', install: 'sudo dnf install -y git' },
123
- { check: 'yum --version', install: 'sudo yum install -y git' },
124
- { check: 'pacman --version', install: 'sudo pacman -S --noconfirm git' },
125
- { check: 'apk --version', install: 'sudo apk add git' },
126
- ];
127
- for (const { check, install } of managers) {
128
- try {
129
- execSync(check, { stdio: 'pipe' });
130
- console.log(chalk.gray(` Installing git: ${install}`));
131
- execSync(install, { stdio: 'inherit' });
132
- if (isGitAvailable()) {
133
- const ver = execSync('git --version', { encoding: 'utf-8' }).trim();
134
- console.log(chalk.green(` ✓ ${ver}`));
135
- return true;
136
- }
137
- }
138
- catch {
139
- continue;
140
- }
141
- }
142
- console.log(chalk.yellow(' Please install git:'));
143
- console.log(chalk.gray(' Ubuntu/Debian: sudo apt-get install git'));
144
- console.log(chalk.gray(' Fedora: sudo dnf install git'));
145
- console.log(chalk.gray(' Arch: sudo pacman -S git'));
146
- return false;
147
- }
148
- function printManualInstructions() {
149
- const platform = os.platform();
150
- console.log(chalk.yellow(' Install git manually:'));
151
- if (platform === 'darwin') {
152
- console.log(chalk.gray(' xcode-select --install'));
153
- console.log(chalk.gray(' or: brew install git'));
154
- }
155
- else if (platform === 'win32') {
156
- console.log(chalk.gray(' https://git-scm.com/download/win'));
157
- }
158
- else {
159
- console.log(chalk.gray(' sudo apt-get install git'));
160
- }
161
- }
162
- //# sourceMappingURL=gitSetup.js.map
@@ -1,190 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * postinstall script for nstantpage-agent npm package.
4
- * Downloads the platform-specific Electron desktop app from GitHub releases
5
- * to ~/.nstantpage/desktop/ so that `nstantpage service install` can use it.
6
- *
7
- * Runs automatically after `npm i -g nstantpage-agent`.
8
- * Skips silently if:
9
- * - Already downloaded and up-to-date
10
- * - Network is unavailable
11
- * - Running in CI (CI=true)
12
- * - Platform has no desktop build (e.g., Linux currently)
13
- */
14
-
15
- import https from 'https';
16
- import http from 'http';
17
- import fs from 'fs';
18
- import path from 'path';
19
- import os from 'os';
20
- import { execSync } from 'child_process';
21
- import { createRequire } from 'module';
22
-
23
- const require_ = createRequire(import.meta.url);
24
-
25
- // Skip in CI environments
26
- if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
27
- process.exit(0);
28
- }
29
-
30
- const GITHUB_OWNER = 'nightwishh';
31
- const GITHUB_REPO = 'nstantpage-desktop';
32
- const DESKTOP_DIR = path.join(os.homedir(), '.nstantpage', 'desktop');
33
- const VERSION_FILE = path.join(DESKTOP_DIR, '.version');
34
-
35
- /**
36
- * Fetch JSON from a URL, following redirects.
37
- */
38
- function fetchJson(url) {
39
- return new Promise((resolve, reject) => {
40
- const get = url.startsWith('https') ? https.get : http.get;
41
- get(url, { headers: { 'User-Agent': 'nstantpage-agent-postinstall' } }, (res) => {
42
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
43
- return fetchJson(res.headers.location).then(resolve, reject);
44
- }
45
- if (res.statusCode !== 200) {
46
- return reject(new Error(`HTTP ${res.statusCode}`));
47
- }
48
- let data = '';
49
- res.on('data', (chunk) => (data += chunk));
50
- res.on('end', () => {
51
- try { resolve(JSON.parse(data)); }
52
- catch (e) { reject(e); }
53
- });
54
- }).on('error', reject);
55
- });
56
- }
57
-
58
- /**
59
- * Download a file from URL (following redirects) to dest path.
60
- * Shows progress on stdout.
61
- */
62
- function downloadFile(url, dest) {
63
- return new Promise((resolve, reject) => {
64
- const get = url.startsWith('https') ? https.get : http.get;
65
- get(url, { headers: { 'User-Agent': 'nstantpage-agent-postinstall' } }, (res) => {
66
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
67
- return downloadFile(res.headers.location, dest).then(resolve, reject);
68
- }
69
- if (res.statusCode !== 200) {
70
- return reject(new Error(`HTTP ${res.statusCode}`));
71
- }
72
- const total = parseInt(res.headers['content-length'] || '0', 10);
73
- let downloaded = 0;
74
- const file = fs.createWriteStream(dest);
75
- res.on('data', (chunk) => {
76
- downloaded += chunk.length;
77
- file.write(chunk);
78
- if (total > 0) {
79
- const pct = Math.round((downloaded / total) * 100);
80
- process.stdout.write(`\r Downloading nstantpage desktop... ${pct}%`);
81
- }
82
- });
83
- res.on('end', () => {
84
- file.end();
85
- if (total > 0) process.stdout.write('\n');
86
- resolve(undefined);
87
- });
88
- res.on('error', (err) => { file.close(); reject(err); });
89
- }).on('error', reject);
90
- });
91
- }
92
-
93
- /**
94
- * Determine the release asset name for this platform/arch.
95
- * Returns null if no desktop build is available.
96
- */
97
- function getAssetPattern() {
98
- const platform = os.platform();
99
- const arch = os.arch();
100
-
101
- if (platform === 'darwin') {
102
- // macOS: zip contains the .app
103
- if (arch === 'arm64') return /-arm64-mac\.zip$/;
104
- return /-mac\.zip$/; // x64
105
- }
106
- if (platform === 'win32') {
107
- // Windows: portable exe or nsis
108
- return /\.exe$/;
109
- }
110
- // Linux: skip for now (AppImage support could be added later)
111
- return null;
112
- }
113
-
114
- async function main() {
115
- try {
116
- const assetPattern = getAssetPattern();
117
- if (!assetPattern) {
118
- // No desktop build for this platform — skip silently
119
- process.exit(0);
120
- }
121
-
122
- // Check latest release
123
- const release = await fetchJson(
124
- `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`
125
- );
126
- const version = release.tag_name; // e.g., "v0.5.44"
127
-
128
- // Check if already downloaded and up-to-date
129
- if (fs.existsSync(VERSION_FILE)) {
130
- const installed = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
131
- if (installed === version) {
132
- console.log(` nstantpage desktop ${version} already installed.`);
133
- process.exit(0);
134
- }
135
- }
136
-
137
- // Find the matching asset
138
- const asset = release.assets.find((a) => assetPattern.test(a.name));
139
- if (!asset) {
140
- // No matching asset — skip silently
141
- process.exit(0);
142
- }
143
-
144
- console.log(` Installing nstantpage desktop ${version}...`);
145
-
146
- // Prepare directories
147
- fs.mkdirSync(DESKTOP_DIR, { recursive: true });
148
-
149
- const platform = os.platform();
150
-
151
- if (platform === 'darwin') {
152
- // Download zip, extract .app
153
- const zipPath = path.join(DESKTOP_DIR, 'download.zip');
154
- await downloadFile(asset.browser_download_url, zipPath);
155
-
156
- // Remove old app if exists
157
- const appPath = path.join(DESKTOP_DIR, 'nstantpage.app');
158
- if (fs.existsSync(appPath)) {
159
- fs.rmSync(appPath, { recursive: true, force: true });
160
- }
161
-
162
- // Extract using ditto (macOS native, preserves permissions and code signing)
163
- execSync(`ditto -xk "${zipPath}" "${DESKTOP_DIR}"`, { stdio: 'pipe' });
164
- fs.unlinkSync(zipPath);
165
-
166
- if (fs.existsSync(appPath)) {
167
- // Remove quarantine attribute so macOS doesn't block it
168
- try { execSync(`xattr -rd com.apple.quarantine "${appPath}" 2>/dev/null`); } catch {}
169
- console.log(` ✓ nstantpage desktop ${version} installed to ${appPath}`);
170
- } else {
171
- console.log(' ⚠ Extraction succeeded but app not found at expected path.');
172
- }
173
- } else if (platform === 'win32') {
174
- // Download exe
175
- const exePath = path.join(DESKTOP_DIR, 'nstantpage.exe');
176
- await downloadFile(asset.browser_download_url, exePath);
177
- console.log(` ✓ nstantpage desktop ${version} installed to ${exePath}`);
178
- }
179
-
180
- // Record version
181
- fs.writeFileSync(VERSION_FILE, version, 'utf-8');
182
- } catch (err) {
183
- // Non-fatal: network errors, rate limits, etc. User can still use CLI-only mode.
184
- // Only show a brief note, don't fail the npm install.
185
- console.log(` Note: Could not download desktop app (${err.message}). CLI mode will work fine.`);
186
- console.log(' Run "nstantpage update" later to retry.');
187
- }
188
- }
189
-
190
- main();