nstantpage-agent 0.2.2 → 0.3.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
@@ -20,7 +20,7 @@ const program = new Command();
20
20
  program
21
21
  .name('nstantpage')
22
22
  .description('Local development agent for nstantpage.com — run projects on your machine, preview in the cloud')
23
- .version('0.2.0');
23
+ .version('0.3.0');
24
24
  program
25
25
  .command('login')
26
26
  .description('Authenticate with nstantpage.com')
@@ -28,12 +28,14 @@ program
28
28
  program
29
29
  .command('start')
30
30
  .description('Start the agent for a project (replaces cloud containers)')
31
- .argument('[directory]', 'Project directory (defaults to current directory)', '.')
31
+ .argument('[directory]', 'Project directory (defaults to ~/.nstantpage/projects/<id>)', '.')
32
32
  .option('-p, --port <port>', 'Local dev server port', '3000')
33
33
  .option('-a, --api-port <port>', 'Local API server port (internal)', '18924')
34
34
  .option('--project-id <id>', 'Link to a specific project ID')
35
35
  .option('--gateway <url>', 'Gateway URL', 'wss://webprev.live')
36
+ .option('--backend <url>', 'Backend API URL (auto-detected from gateway)')
36
37
  .option('--token <token>', 'Auth token (skip login flow)')
38
+ .option('--dir <path>', 'Project directory override')
37
39
  .option('--no-dev', 'Skip starting the dev server (start manually later)')
38
40
  .action(startCommand);
39
41
  program
@@ -2,9 +2,12 @@
2
2
  * Start command — launch the nstantpage agent
3
3
  *
4
4
  * Architecture:
5
- * 1. Start local API server (handles /live/* requests for file sync, checks, etc.)
6
- * 2. Start local dev server (Vite/Next.js runs project on user's machine)
7
- * 3. Connect tunnel to gateway (relays requests from cloud to local machine)
5
+ * 1. Resolve or create project directory (~/.nstantpage/projects/{id}/)
6
+ * 2. Fetch project files from the backend API
7
+ * 3. Install dependencies (npm/pnpm)
8
+ * 4. Start local API server (handles /live/* requests for file sync, checks, etc.)
9
+ * 5. Start local dev server (Vite/Next.js — runs project on user's machine)
10
+ * 6. Connect tunnel to gateway (relays requests from cloud to local machine)
8
11
  *
9
12
  * The agent fully replaces Docker containers:
10
13
  * - Files are on the user's disk (no docker cp needed)
@@ -17,7 +20,9 @@ interface StartOptions {
17
20
  apiPort: string;
18
21
  projectId?: string;
19
22
  gateway: string;
23
+ backend?: string;
20
24
  token?: string;
25
+ dir?: string;
21
26
  noDev?: boolean;
22
27
  }
23
28
  export declare function startCommand(directory: string, options: StartOptions): Promise<void>;
@@ -2,9 +2,12 @@
2
2
  * Start command — launch the nstantpage agent
3
3
  *
4
4
  * Architecture:
5
- * 1. Start local API server (handles /live/* requests for file sync, checks, etc.)
6
- * 2. Start local dev server (Vite/Next.js runs project on user's machine)
7
- * 3. Connect tunnel to gateway (relays requests from cloud to local machine)
5
+ * 1. Resolve or create project directory (~/.nstantpage/projects/{id}/)
6
+ * 2. Fetch project files from the backend API
7
+ * 3. Install dependencies (npm/pnpm)
8
+ * 4. Start local API server (handles /live/* requests for file sync, checks, etc.)
9
+ * 5. Start local dev server (Vite/Next.js — runs project on user's machine)
10
+ * 6. Connect tunnel to gateway (relays requests from cloud to local machine)
8
11
  *
9
12
  * The agent fully replaces Docker containers:
10
13
  * - Files are on the user's disk (no docker cp needed)
@@ -15,9 +18,85 @@
15
18
  import chalk from 'chalk';
16
19
  import path from 'path';
17
20
  import fs from 'fs';
21
+ import os from 'os';
18
22
  import { getConfig } from '../config.js';
19
23
  import { TunnelClient } from '../tunnel.js';
20
24
  import { LocalServer } from '../localServer.js';
25
+ import { PackageInstaller } from '../packageInstaller.js';
26
+ const VERSION = '0.3.0';
27
+ /**
28
+ * Resolve the backend API base URL.
29
+ * - If --backend is passed, use it
30
+ * - If gateway is local (ws://localhost), assume backend is localhost:5001
31
+ * - Otherwise, default to https://ntstantpage.com
32
+ */
33
+ function resolveBackendUrl(options) {
34
+ if (options.backend)
35
+ return options.backend.replace(/\/$/, '');
36
+ const isLocal = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(options.gateway);
37
+ return isLocal ? 'http://localhost:5001' : 'https://ntstantpage.com';
38
+ }
39
+ /**
40
+ * Resolve project directory:
41
+ * - If --dir or [directory] arg given → use that
42
+ * - Otherwise → ~/.nstantpage/projects/{projectId}/
43
+ */
44
+ function resolveProjectDir(directory, projectId, customDir) {
45
+ // Explicit --dir flag takes priority
46
+ if (customDir)
47
+ return path.resolve(customDir);
48
+ // If user passed a non-default directory argument (not '.')
49
+ if (directory !== '.')
50
+ return path.resolve(directory);
51
+ // Default: create in ~/.nstantpage/projects/{projectId}/
52
+ const defaultBase = path.join(os.homedir(), '.nstantpage', 'projects', projectId);
53
+ return defaultBase;
54
+ }
55
+ /**
56
+ * Fetch project files from the .NET backend and write them to disk.
57
+ */
58
+ async function fetchProjectFiles(backendUrl, projectId, projectDir, token) {
59
+ const url = `${backendUrl}/api/sandbox/files?projectId=${projectId}`;
60
+ console.log(chalk.gray(` Fetching project files...`));
61
+ const response = await fetch(url, {
62
+ headers: {
63
+ 'Authorization': `Bearer ${token}`,
64
+ 'Accept': 'application/json',
65
+ },
66
+ });
67
+ if (!response.ok) {
68
+ const text = await response.text().catch(() => '');
69
+ throw new Error(`Failed to fetch project files (${response.status}): ${text}`);
70
+ }
71
+ const data = await response.json();
72
+ if (!data.files || data.files.length === 0) {
73
+ throw new Error('No files returned from backend. Is the project ID correct?');
74
+ }
75
+ // Check if we already have this version (skip re-downloading)
76
+ const versionFile = path.join(projectDir, '.nstantpage-version');
77
+ const existingVersion = fs.existsSync(versionFile)
78
+ ? fs.readFileSync(versionFile, 'utf-8').trim()
79
+ : null;
80
+ if (existingVersion === String(data.versionId)) {
81
+ console.log(chalk.gray(` Files up-to-date (version ${data.version})`));
82
+ return { fileCount: data.files.length, isNew: false };
83
+ }
84
+ // Write all files to disk
85
+ let written = 0;
86
+ for (const file of data.files) {
87
+ const filePath = path.join(projectDir, file.path);
88
+ const dir = path.dirname(filePath);
89
+ if (!fs.existsSync(dir)) {
90
+ fs.mkdirSync(dir, { recursive: true });
91
+ }
92
+ fs.writeFileSync(filePath, file.content, 'utf-8');
93
+ written++;
94
+ }
95
+ // Write version marker
96
+ fs.writeFileSync(versionFile, String(data.versionId), 'utf-8');
97
+ console.log(chalk.green(` ✓ ${written} files downloaded (version ${data.version})`));
98
+ return { fileCount: written, isNew: true };
99
+ }
21
100
  export async function startCommand(directory, options) {
22
101
  const conf = getConfig();
23
102
  // If --token was passed, persist it
@@ -36,53 +115,77 @@ export async function startCommand(directory, options) {
36
115
  if (!token && isLocalGateway) {
37
116
  token = 'local-dev';
38
117
  }
39
- // Resolve project directory
40
- const projectDir = path.resolve(directory);
41
- if (!fs.existsSync(projectDir)) {
42
- console.log(chalk.red(`✗ Directory not found: ${projectDir}`));
43
- process.exit(1);
44
- }
45
- // Verify it's a project directory
46
- if (!fs.existsSync(path.join(projectDir, 'package.json'))) {
47
- console.log(chalk.red(`✗ No package.json found in ${projectDir}`));
48
- console.log(chalk.gray(' Make sure you\'re in a project directory'));
49
- process.exit(1);
50
- }
51
118
  const devPort = parseInt(options.port, 10);
52
119
  const apiPort = parseInt(options.apiPort, 10);
53
120
  // Determine project ID
54
121
  let projectId = options.projectId || conf.get('projectId');
55
- // Try .nstantpage.json in project dir
56
- const localConfig = path.join(projectDir, '.nstantpage.json');
57
- if (!projectId && fs.existsSync(localConfig)) {
58
- try {
59
- const data = JSON.parse(fs.readFileSync(localConfig, 'utf-8'));
60
- projectId = data.projectId;
61
- }
62
- catch { }
63
- }
64
122
  if (!projectId) {
65
123
  console.log(chalk.yellow('⚠ No project ID specified.'));
66
- console.log(chalk.gray(' Use --project-id <id> or create a .nstantpage.json file'));
67
- console.log(chalk.gray(' with { "projectId": "<your-project-id>" }'));
124
+ console.log(chalk.gray(' Use --project-id <id>'));
125
+ console.log(chalk.gray(' Example: npx nstantpage-agent start --project-id 1234'));
68
126
  process.exit(1);
69
127
  }
128
+ // Resolve project directory
129
+ const projectDir = resolveProjectDir(directory, projectId, options.dir);
130
+ // Create directory if it doesn't exist
131
+ if (!fs.existsSync(projectDir)) {
132
+ fs.mkdirSync(projectDir, { recursive: true });
133
+ }
70
134
  // Save project ID
71
135
  conf.set('projectId', projectId);
72
- console.log(chalk.blue(`\n🚀 nstantpage agent v0.2.0\n`));
73
- console.log(chalk.gray(` Directory: ${projectDir}`));
136
+ const backendUrl = resolveBackendUrl(options);
137
+ console.log(chalk.blue(`\n🚀 nstantpage agent v${VERSION}\n`));
74
138
  console.log(chalk.gray(` Project ID: ${projectId}`));
139
+ console.log(chalk.gray(` Directory: ${projectDir}`));
75
140
  console.log(chalk.gray(` Dev server: port ${devPort}`));
76
141
  console.log(chalk.gray(` API server: port ${apiPort}`));
77
- console.log(chalk.gray(` Gateway: ${options.gateway}\n`));
78
- // 1. Start local server (API + dev server)
142
+ console.log(chalk.gray(` Gateway: ${options.gateway}`));
143
+ console.log(chalk.gray(` Backend: ${backendUrl}\n`));
144
+ // 1. Fetch project files from the backend
145
+ try {
146
+ const { fileCount, isNew } = await fetchProjectFiles(backendUrl, projectId, projectDir, token);
147
+ // 2. Install dependencies if this is a new download or node_modules is missing
148
+ const hasNodeModules = fs.existsSync(path.join(projectDir, 'node_modules'));
149
+ if (isNew || !hasNodeModules) {
150
+ if (fs.existsSync(path.join(projectDir, 'package.json'))) {
151
+ console.log(chalk.gray(' Installing dependencies...'));
152
+ const installer = new PackageInstaller({ projectDir });
153
+ const result = await installer.install([], false);
154
+ if (result.success) {
155
+ console.log(chalk.green(` ✓ Dependencies installed`));
156
+ }
157
+ else {
158
+ console.log(chalk.yellow(` ⚠ Dependency installation had issues: ${result.output.slice(0, 200)}`));
159
+ }
160
+ }
161
+ }
162
+ else {
163
+ console.log(chalk.gray(` Dependencies already installed`));
164
+ }
165
+ }
166
+ catch (err) {
167
+ console.log(chalk.yellow(` ⚠ Could not fetch project files: ${err.message}`));
168
+ console.log(chalk.gray(' Continuing with existing local files (if any)...'));
169
+ // Don't exit — maybe local files exist from a previous run
170
+ if (!fs.existsSync(path.join(projectDir, 'package.json'))) {
171
+ console.log(chalk.red(`\n✗ No package.json found and cannot fetch files from backend.`));
172
+ console.log(chalk.gray(' Check your project ID and authentication.'));
173
+ process.exit(1);
174
+ }
175
+ }
176
+ // Save .nstantpage.json for future runs
177
+ const localConfigPath = path.join(projectDir, '.nstantpage.json');
178
+ if (!fs.existsSync(localConfigPath)) {
179
+ fs.writeFileSync(localConfigPath, JSON.stringify({ projectId }, null, 2), 'utf-8');
180
+ }
181
+ // 3. Start local server (API + dev server)
79
182
  const localServer = new LocalServer({
80
183
  projectDir,
81
184
  projectId,
82
185
  apiPort,
83
186
  devPort,
84
187
  });
85
- // 2. Create tunnel client
188
+ // 4. Create tunnel client
86
189
  const tunnel = new TunnelClient({
87
190
  gatewayUrl: options.gateway,
88
191
  token,
@@ -133,6 +236,7 @@ export async function startCommand(directory, options) {
133
236
  console.log(chalk.blue.bold(` ├──────────────────────────────────────────────┤`));
134
237
  console.log(chalk.white(` │ Cloud: https://${projectId}.webprev.live`));
135
238
  console.log(chalk.white(` │ Local: http://localhost:${devPort}`));
239
+ console.log(chalk.white(` │ Files: ${projectDir}`));
136
240
  console.log(chalk.blue.bold(` └──────────────────────────────────────────────┘\n`));
137
241
  console.log(chalk.gray(` Mode: ${chalk.green('Agent')} (no containers needed)`));
138
242
  console.log(chalk.gray(` All builds, checks, and previews run on this machine.`));
@@ -18,7 +18,8 @@ export class PackageInstaller {
18
18
  const args = this.buildInstallArgs(pm, packages, dev);
19
19
  console.log(` [Installer] ${pm} ${args.join(' ')}`);
20
20
  try {
21
- const output = await this.runCommand(pm, args, 60_000);
21
+ const timeout = packages.length === 0 ? 120_000 : 60_000;
22
+ const output = await this.runCommand(pm, args, timeout);
22
23
  return {
23
24
  success: true,
24
25
  output,
@@ -57,6 +58,10 @@ export class PackageInstaller {
57
58
  return 'npm';
58
59
  }
59
60
  buildInstallArgs(pm, packages, dev) {
61
+ // If no packages specified, just run install (restore from lockfile)
62
+ if (packages.length === 0) {
63
+ return ['install'];
64
+ }
60
65
  const devFlag = dev
61
66
  ? (pm === 'npm' ? '--save-dev' : '-D')
62
67
  : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.2.2",
3
+ "version": "0.3.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": {