nstantpage-agent 0.6.1 → 0.7.1

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')
@@ -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.1",
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": {