nstantpage-agent 0.6.1 → 0.7.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
@@ -23,6 +23,7 @@ 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
25
  import { runCommand } from './commands/run.js';
26
+ import { syncCommand } from './commands/sync.js';
26
27
  import { getPackageVersion } from './version.js';
27
28
  const program = new Command();
28
29
  program
@@ -57,6 +58,17 @@ program
57
58
  .description('Open the nstantpage desktop app')
58
59
  .option('--local [port]', 'Connect to local backend (default: 5001)')
59
60
  .action(runCommand);
61
+ program
62
+ .command('sync')
63
+ .description('Sync local directory to nstantpage and start agent')
64
+ .argument('[directory]', 'Project directory to sync (defaults to current directory)', '.')
65
+ .option('-p, --port <port>', 'Local dev server port', '3000')
66
+ .option('-a, --api-port <port>', 'Local API server port', '18924')
67
+ .option('--gateway <url>', 'Gateway URL (default: from login)')
68
+ .option('--backend <url>', 'Backend API URL (auto-detected from gateway)')
69
+ .option('--token <token>', 'Auth token (skip login flow)')
70
+ .option('--no-start', 'Only sync files, do not start the agent')
71
+ .action(syncCommand);
60
72
  program
61
73
  .command('stop')
62
74
  .description('Stop the running agent')
@@ -24,6 +24,7 @@ import { getConfig, getProjectConfig, setProjectConfig, clearProjectConfig, getD
24
24
  import { TunnelClient } from '../tunnel.js';
25
25
  import { LocalServer } from '../localServer.js';
26
26
  import { PackageInstaller } from '../packageInstaller.js';
27
+ import { ensureGit } from '../gitSetup.js';
27
28
  import { probeLocalPostgres, ensureLocalProjectDb, closeAdminPool, writeDatabaseUrlToEnv } from '../projectDb.js';
28
29
  import { StatusServer } from '../statusServer.js';
29
30
  import { getPackageVersion } from '../version.js';
@@ -254,6 +255,14 @@ export async function startCommand(directory, options) {
254
255
  cleanupPreviousAgent(projectId, apiPort, devPort);
255
256
  // Brief pause for OS to release ports (50ms is sufficient on macOS/Linux)
256
257
  await new Promise(r => setTimeout(r, 50));
258
+ // Ensure git is installed (needed for undo/version control)
259
+ const gitAvailable = await ensureGit();
260
+ if (gitAvailable) {
261
+ console.log(chalk.green(` ✓ Git available`));
262
+ }
263
+ else {
264
+ console.log(chalk.yellow(` ⚠ Git not available — undo feature will be limited`));
265
+ }
257
266
  console.log(chalk.blue(`\n🚀 nstantpage agent v${VERSION}\n`));
258
267
  console.log(chalk.gray(` Project ID: ${projectId}`));
259
268
  console.log(chalk.gray(` Device ID: ${deviceId.slice(0, 12)}...`));
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Sync command — push local files to nstantpage DB and start the agent.
3
+ *
4
+ * Usage:
5
+ * nstantpage sync — Sync current directory to nstantpage, then start agent
6
+ * nstantpage sync /path/to/dir — Sync a specific directory
7
+ *
8
+ * What it does:
9
+ * 1. Reads all source files in the directory (skips node_modules, .git, dist, etc.)
10
+ * 2. Creates a LocalRepo project if one doesn't exist (name = folder name)
11
+ * 3. Pushes all files to the DB (FullFiles table)
12
+ * 4. Starts the agent (equivalent to `nstantpage start`)
13
+ */
14
+ interface SyncOptions {
15
+ port: string;
16
+ apiPort: string;
17
+ gateway?: string;
18
+ backend?: string;
19
+ token?: string;
20
+ noStart?: boolean;
21
+ }
22
+ export declare function syncCommand(directory: string, options: SyncOptions): Promise<void>;
23
+ export {};
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Sync command — push local files to nstantpage DB and start the agent.
3
+ *
4
+ * Usage:
5
+ * nstantpage sync — Sync current directory to nstantpage, then start agent
6
+ * nstantpage sync /path/to/dir — Sync a specific directory
7
+ *
8
+ * What it does:
9
+ * 1. Reads all source files in the directory (skips node_modules, .git, dist, etc.)
10
+ * 2. Creates a LocalRepo project if one doesn't exist (name = folder name)
11
+ * 3. Pushes all files to the DB (FullFiles table)
12
+ * 4. Starts the agent (equivalent to `nstantpage start`)
13
+ */
14
+ import chalk from 'chalk';
15
+ import path from 'path';
16
+ import fs from 'fs';
17
+ import { getConfig, getDeviceId } from '../config.js';
18
+ import { startCommand } from './start.js';
19
+ const SKIP_DIRS = new Set([
20
+ 'node_modules', 'dist', '.git', '.vite-cache', '.next',
21
+ '__pycache__', '.turbo', '.cache', 'build', 'out',
22
+ '.svelte-kit', '.nuxt', '.output', '.vercel',
23
+ ]);
24
+ const SKIP_FILES = new Set([
25
+ '.DS_Store', 'Thumbs.db', '.env.local',
26
+ ]);
27
+ /** Max file size to sync (1MB — skip large binaries) */
28
+ const MAX_FILE_SIZE = 1_048_576;
29
+ /** Binary file extensions to skip */
30
+ const BINARY_EXTENSIONS = new Set([
31
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.tiff',
32
+ '.mp4', '.mp3', '.wav', '.ogg', '.webm', '.avi',
33
+ '.zip', '.tar', '.gz', '.rar', '.7z',
34
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
35
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
36
+ '.exe', '.dll', '.so', '.dylib',
37
+ '.sqlite', '.db',
38
+ ]);
39
+ /**
40
+ * Resolve the backend API base URL (same logic as start.ts).
41
+ */
42
+ function resolveBackendUrl(gateway, backend) {
43
+ if (backend)
44
+ return backend.replace(/\/$/, '');
45
+ const isLocal = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(gateway);
46
+ return isLocal ? 'http://localhost:5001' : 'https://nstantpage.com';
47
+ }
48
+ /**
49
+ * Recursively collect all source files in a directory.
50
+ * Returns array of { relativePath, content }.
51
+ */
52
+ async function collectFiles(baseDir, currentDir) {
53
+ const files = [];
54
+ let entries;
55
+ try {
56
+ entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ return files;
60
+ }
61
+ for (const entry of entries) {
62
+ const fullPath = path.join(currentDir, entry.name);
63
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
64
+ if (entry.isDirectory()) {
65
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.nstantpage'))
66
+ continue;
67
+ const subFiles = await collectFiles(baseDir, fullPath);
68
+ files.push(...subFiles);
69
+ }
70
+ else if (entry.isFile()) {
71
+ if (SKIP_FILES.has(entry.name))
72
+ continue;
73
+ const ext = path.extname(entry.name).toLowerCase();
74
+ if (BINARY_EXTENSIONS.has(ext))
75
+ continue;
76
+ try {
77
+ const stat = await fs.promises.stat(fullPath);
78
+ if (stat.size > MAX_FILE_SIZE)
79
+ continue;
80
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
81
+ // Skip files with null bytes (binary)
82
+ if (content.includes('\0'))
83
+ continue;
84
+ files.push({ relativePath, content });
85
+ }
86
+ catch {
87
+ // Can't read — skip
88
+ }
89
+ }
90
+ }
91
+ return files;
92
+ }
93
+ export async function syncCommand(directory, options) {
94
+ const conf = getConfig();
95
+ // Resolve directory
96
+ const projectDir = path.resolve(directory);
97
+ if (!fs.existsSync(projectDir)) {
98
+ console.log(chalk.red(` ✗ Directory not found: ${projectDir}`));
99
+ process.exit(1);
100
+ }
101
+ // Check authentication
102
+ const gateway = options.gateway || conf.get('gatewayUrl') || 'wss://webprev.live';
103
+ const isLocalGateway = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(gateway);
104
+ let token = options.token || conf.get('token');
105
+ if (!token && !isLocalGateway) {
106
+ console.log(chalk.red(' ✗ Not authenticated. Run "nstantpage login" first.'));
107
+ process.exit(1);
108
+ }
109
+ if (!token && isLocalGateway) {
110
+ token = 'local-dev';
111
+ }
112
+ const backendUrl = resolveBackendUrl(gateway, options.backend);
113
+ const deviceId = getDeviceId();
114
+ const folderName = path.basename(projectDir);
115
+ console.log(chalk.blue(`\n 📂 nstantpage sync\n`));
116
+ console.log(chalk.gray(` Directory: ${projectDir}`));
117
+ console.log(chalk.gray(` Backend: ${backendUrl}\n`));
118
+ // 1. Collect all files
119
+ console.log(chalk.gray(' Scanning files...'));
120
+ const files = await collectFiles(projectDir, projectDir);
121
+ if (files.length === 0) {
122
+ console.log(chalk.yellow(' ⚠ No source files found in directory'));
123
+ process.exit(1);
124
+ }
125
+ console.log(chalk.green(` ✓ Found ${files.length} files`));
126
+ // 2. Get user info from token
127
+ let userId;
128
+ if (token && token !== 'local-dev') {
129
+ try {
130
+ const parts = token.split('.');
131
+ if (parts.length === 3) {
132
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
133
+ userId = payload.sub;
134
+ }
135
+ }
136
+ catch { }
137
+ }
138
+ // 3. Create or find LocalRepo project
139
+ console.log(chalk.gray(' Creating project...'));
140
+ let projectId;
141
+ try {
142
+ const createRes = await fetch(`${backendUrl}/api/projects`, {
143
+ method: 'POST',
144
+ headers: {
145
+ 'Authorization': `Bearer ${token}`,
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ body: JSON.stringify({
149
+ name: folderName,
150
+ type: 'LocalRepo',
151
+ localFolderPath: projectDir,
152
+ userId,
153
+ deviceId,
154
+ }),
155
+ });
156
+ if (!createRes.ok) {
157
+ const text = await createRes.text().catch(() => '');
158
+ throw new Error(`Failed to create project (${createRes.status}): ${text}`);
159
+ }
160
+ const project = await createRes.json();
161
+ projectId = String(project.id);
162
+ console.log(chalk.green(` ✓ Project: ${project.name} (ID: ${projectId})`));
163
+ }
164
+ catch (err) {
165
+ console.log(chalk.red(` ✗ ${err.message}`));
166
+ process.exit(1);
167
+ }
168
+ // 4. Push all files to DB
169
+ console.log(chalk.gray(` Pushing ${files.length} files to database...`));
170
+ // Push in batches of 100 to avoid huge payloads
171
+ const BATCH_SIZE = 100;
172
+ let totalPushed = 0;
173
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
174
+ const batch = files.slice(i, i + BATCH_SIZE);
175
+ const filesMap = {};
176
+ for (const f of batch) {
177
+ filesMap[f.relativePath] = f.content;
178
+ }
179
+ try {
180
+ const pushRes = await fetch(`${backendUrl}/api/sandbox/push-files`, {
181
+ method: 'POST',
182
+ headers: {
183
+ 'Authorization': `Bearer ${token}`,
184
+ 'Content-Type': 'application/json',
185
+ },
186
+ body: JSON.stringify({
187
+ projectId: parseInt(projectId, 10),
188
+ files: filesMap,
189
+ }),
190
+ });
191
+ if (!pushRes.ok) {
192
+ const text = await pushRes.text().catch(() => '');
193
+ throw new Error(`Push failed (${pushRes.status}): ${text}`);
194
+ }
195
+ totalPushed += batch.length;
196
+ if (files.length > BATCH_SIZE) {
197
+ process.stdout.write(`\r Pushed ${totalPushed}/${files.length} files...`);
198
+ }
199
+ }
200
+ catch (err) {
201
+ console.log(chalk.red(`\n ✗ ${err.message}`));
202
+ process.exit(1);
203
+ }
204
+ }
205
+ if (files.length > BATCH_SIZE)
206
+ process.stdout.write('\n');
207
+ console.log(chalk.green(` ✓ ${totalPushed} files synced to database`));
208
+ // 5. Start the agent unless --no-start
209
+ if (options.noStart) {
210
+ console.log(chalk.blue(`\n ✓ Sync complete! Project ID: ${projectId}\n`));
211
+ console.log(chalk.gray(` To start the agent: nstantpage start --project-id ${projectId} --dir "${projectDir}"`));
212
+ return;
213
+ }
214
+ console.log(chalk.gray('\n Starting agent...\n'));
215
+ await startCommand(projectDir, {
216
+ port: options.port,
217
+ apiPort: options.apiPort,
218
+ projectId,
219
+ gateway,
220
+ backend: options.backend,
221
+ token,
222
+ dir: projectDir,
223
+ });
224
+ }
225
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1,10 @@
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>;
@@ -0,0 +1,162 @@
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.6.1",
3
+ "version": "0.7.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": {
@@ -111,7 +111,66 @@ function getAssetPattern() {
111
111
  return null;
112
112
  }
113
113
 
114
+ /**
115
+ * Ensure git is installed. Try to auto-install if missing.
116
+ */
117
+ function ensureGit() {
118
+ try {
119
+ execSync('git --version', { stdio: 'pipe' });
120
+ return; // already installed
121
+ } catch {
122
+ // git not found — try to install
123
+ }
124
+
125
+ console.log(' Installing git (required for undo/version control)...');
126
+ const platform = os.platform();
127
+
128
+ try {
129
+ if (platform === 'darwin') {
130
+ // Try Homebrew first (non-interactive)
131
+ try {
132
+ execSync('brew --version', { stdio: 'pipe' });
133
+ execSync('brew install git', { stdio: 'inherit' });
134
+ console.log(' ✓ Git installed via Homebrew');
135
+ return;
136
+ } catch {}
137
+ // Trigger Xcode CLT install
138
+ console.log(' Run this to install git: xcode-select --install');
139
+ } else if (platform === 'win32') {
140
+ try {
141
+ execSync('winget --version', { stdio: 'pipe' });
142
+ execSync('winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements', { stdio: 'inherit' });
143
+ console.log(' ✓ Git installed via winget');
144
+ return;
145
+ } catch {}
146
+ console.log(' Install Git for Windows: https://git-scm.com/download/win');
147
+ } else {
148
+ // Linux: try apt, dnf, etc.
149
+ const managers = [
150
+ { check: 'apt-get --version', install: 'sudo apt-get install -y git' },
151
+ { check: 'dnf --version', install: 'sudo dnf install -y git' },
152
+ { check: 'yum --version', install: 'sudo yum install -y git' },
153
+ { check: 'pacman --version', install: 'sudo pacman -S --noconfirm git' },
154
+ ];
155
+ for (const { check, install } of managers) {
156
+ try {
157
+ execSync(check, { stdio: 'pipe' });
158
+ execSync(install, { stdio: 'inherit' });
159
+ console.log(' ✓ Git installed');
160
+ return;
161
+ } catch { continue; }
162
+ }
163
+ console.log(' Please install git: sudo apt-get install git');
164
+ }
165
+ } catch (err) {
166
+ console.log(` Note: Could not auto-install git (${err.message}). Install it manually for undo support.`);
167
+ }
168
+ }
169
+
114
170
  async function main() {
171
+ // Ensure git is available (non-fatal)
172
+ try { ensureGit(); } catch {}
173
+
115
174
  try {
116
175
  const assetPattern = getAssetPattern();
117
176
  if (!assetPattern) {