visual-ai-staging 0.0.4 → 0.0.6

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.
Files changed (2) hide show
  1. package/cli.js +131 -6
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -4,6 +4,41 @@ const http = require('http');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const url = require('url');
7
+ const os = require('os');
8
+ const { exec } = require('child_process');
9
+
10
+ function openBrowser(targetUrl) {
11
+ const startCommand = process.platform === 'darwin' ? 'open' :
12
+ process.platform === 'win32' ? 'start ""' :
13
+ 'xdg-open';
14
+ exec(`${startCommand} "${targetUrl}"`, (err) => {
15
+ // Silently ignore if default browser cannot be launched
16
+ });
17
+ }
18
+
19
+ function resolveStoragePath(originHeader, folderType) {
20
+ const resolvedBase = path.resolve(__dirname);
21
+ let hostname = '';
22
+ if (originHeader) {
23
+ try {
24
+ const parsed = new URL(originHeader);
25
+ hostname = parsed.hostname;
26
+ } catch (e) {
27
+ if (originHeader.includes('localhost')) {
28
+ hostname = 'localhost';
29
+ } else if (originHeader.includes('127.0.0.1')) {
30
+ hostname = '127.0.0.1';
31
+ }
32
+ }
33
+ }
34
+
35
+ const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
36
+ if (isLocal || !originHeader) {
37
+ return path.join(resolvedBase, '.ai-staging', folderType);
38
+ } else {
39
+ return path.join(resolvedBase, '.ai-staging', 'external', folderType);
40
+ }
41
+ }
7
42
 
8
43
  const args = process.argv.slice(2);
9
44
  const command = args[0];
@@ -38,19 +73,45 @@ if (command === 'init') {
38
73
  const resolvedBase = path.resolve(__dirname);
39
74
  const audioDir = path.join(resolvedBase, '.ai-staging', 'audio');
40
75
  const feedbackDir = path.join(resolvedBase, '.ai-staging', 'feedback');
76
+ const extAudioDir = path.join(resolvedBase, '.ai-staging', 'external', 'audio');
77
+ const extFeedbackDir = path.join(resolvedBase, '.ai-staging', 'external', 'feedback');
41
78
 
42
79
  try {
43
- let alreadyExists = fs.existsSync(audioDir) && fs.existsSync(feedbackDir);
80
+ let alreadyExists = fs.existsSync(audioDir) && fs.existsSync(feedbackDir) && fs.existsSync(extAudioDir) && fs.existsSync(extFeedbackDir);
44
81
 
45
82
  fs.mkdirSync(audioDir, { recursive: true });
46
83
  fs.mkdirSync(feedbackDir, { recursive: true });
84
+ fs.mkdirSync(extAudioDir, { recursive: true });
85
+ fs.mkdirSync(extFeedbackDir, { recursive: true });
47
86
 
48
87
  // Create .gitkeep to ensure empty directories are tracked by Git
49
88
  const gitkeepAudio = path.join(audioDir, '.gitkeep');
50
89
  const gitkeepFeedback = path.join(feedbackDir, '.gitkeep');
90
+ const gitkeepExtAudio = path.join(extAudioDir, '.gitkeep');
91
+ const gitkeepExtFeedback = path.join(extFeedbackDir, '.gitkeep');
51
92
 
52
93
  if (!fs.existsSync(gitkeepAudio)) fs.writeFileSync(gitkeepAudio, '');
53
94
  if (!fs.existsSync(gitkeepFeedback)) fs.writeFileSync(gitkeepFeedback, '');
95
+ if (!fs.existsSync(gitkeepExtAudio)) fs.writeFileSync(gitkeepExtAudio, '');
96
+ if (!fs.existsSync(gitkeepExtFeedback)) fs.writeFileSync(gitkeepExtFeedback, '');
97
+
98
+ console.log(`
99
+ ┌───────────────────────────────────────────────────────────┐
100
+ │ │
101
+ │ Welcome to Visual AI Staging! │
102
+ │ │
103
+ │ To start staging your active layouts in hot-memory: │
104
+ │ │
105
+ │ 1. Install the official companion browser extension: │
106
+ │ https://chrome.google.com/webstore │
107
+ │ │
108
+ │ 2. Start your local staging server by running: │
109
+ │ vais dev │
110
+ │ │
111
+ │ 3. Click the extension icon on any page to begin. │
112
+ │ │
113
+ └───────────────────────────────────────────────────────────┘
114
+ `);
54
115
 
55
116
  if (alreadyExists) {
56
117
  console.log('Visual AI Staging environment already initialized in this workspace.');
@@ -59,7 +120,12 @@ if (command === 'init') {
59
120
  console.log('Created directories:');
60
121
  console.log(' - .ai-staging/audio/');
61
122
  console.log(' - .ai-staging/feedback/');
123
+ console.log(' - .ai-staging/external/audio/');
124
+ console.log(' - .ai-staging/external/feedback/');
62
125
  }
126
+
127
+ console.log('\nOpening Chrome Web Store to download the companion extension...');
128
+ openBrowser('https://chrome.google.com/webstore');
63
129
  } catch (err) {
64
130
  console.error('Error initializing workspace:', err.message);
65
131
  process.exit(1);
@@ -68,28 +134,72 @@ if (command === 'init') {
68
134
  }
69
135
 
70
136
  if (command === 'dev') {
71
- const port = 3000;
137
+ let activePort = 3000;
72
138
  const resolvedBase = path.resolve(__dirname);
73
139
 
74
140
  // Auto-initialize directories silently on server startup if they do not exist
75
141
  const audioDir = path.join(resolvedBase, '.ai-staging', 'audio');
76
142
  const feedbackDir = path.join(resolvedBase, '.ai-staging', 'feedback');
143
+ const extAudioDir = path.join(resolvedBase, '.ai-staging', 'external', 'audio');
144
+ const extFeedbackDir = path.join(resolvedBase, '.ai-staging', 'external', 'feedback');
77
145
  try {
78
146
  fs.mkdirSync(audioDir, { recursive: true });
79
147
  fs.mkdirSync(feedbackDir, { recursive: true });
148
+ fs.mkdirSync(extAudioDir, { recursive: true });
149
+ fs.mkdirSync(extFeedbackDir, { recursive: true });
80
150
 
81
151
  const gitkeepAudio = path.join(audioDir, '.gitkeep');
82
152
  const gitkeepFeedback = path.join(feedbackDir, '.gitkeep');
153
+ const gitkeepExtAudio = path.join(extAudioDir, '.gitkeep');
154
+ const gitkeepExtFeedback = path.join(extFeedbackDir, '.gitkeep');
83
155
  if (!fs.existsSync(gitkeepAudio)) fs.writeFileSync(gitkeepAudio, '');
84
156
  if (!fs.existsSync(gitkeepFeedback)) fs.writeFileSync(gitkeepFeedback, '');
157
+ if (!fs.existsSync(gitkeepExtAudio)) fs.writeFileSync(gitkeepExtAudio, '');
158
+ if (!fs.existsSync(gitkeepExtFeedback)) fs.writeFileSync(gitkeepExtFeedback, '');
85
159
  } catch (err) {
86
160
  console.warn('Warning: Failed to auto-initialize staging directories on startup:', err.message);
87
161
  }
88
162
 
89
163
  const server = http.createServer((req, res) => {
164
+ const origin = req.headers.origin || '*';
165
+
166
+ // CORS preflight OPTIONS request
167
+ if (req.method === 'OPTIONS') {
168
+ res.writeHead(204, {
169
+ 'Access-Control-Allow-Origin': origin,
170
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
171
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Project-Origin',
172
+ 'Access-Control-Allow-Credentials': 'true'
173
+ });
174
+ res.end();
175
+ return;
176
+ }
177
+
90
178
  const parsedUrl = url.parse(req.url, true);
91
179
  const pathname = parsedUrl.pathname;
92
180
 
181
+ // Apply CORS headers for save and session info API responses
182
+ if (pathname === '/api/save-audio' || pathname === '/api/save-feedback' || pathname === '/api/session-info') {
183
+ res.setHeader('Access-Control-Allow-Origin', origin);
184
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
185
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Project-Origin');
186
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
187
+ }
188
+
189
+ // Handle GET /api/session-info
190
+ if (req.method === 'GET' && pathname === '/api/session-info') {
191
+ res.statusCode = 200;
192
+ res.setHeader('Content-Type', 'application/json');
193
+ const projectName = path.basename(resolvedBase);
194
+ res.end(JSON.stringify({
195
+ success: true,
196
+ port: activePort,
197
+ projectName: projectName,
198
+ projectPath: resolvedBase
199
+ }));
200
+ return;
201
+ }
202
+
93
203
  // Handle POST /api/save-audio?filename=<encoded_filename>
94
204
  if (req.method === 'POST' && pathname === '/api/save-audio') {
95
205
  const rawFilename = parsedUrl.query.filename || '';
@@ -103,7 +213,7 @@ if (command === 'dev') {
103
213
  return;
104
214
  }
105
215
 
106
- const targetDir = path.join(resolvedBase, '.ai-staging', 'audio');
216
+ const targetDir = resolveStoragePath(req.headers.origin, 'audio');
107
217
  try {
108
218
  fs.mkdirSync(targetDir, { recursive: true });
109
219
  } catch (err) {
@@ -153,7 +263,7 @@ if (command === 'dev') {
153
263
  return;
154
264
  }
155
265
 
156
- const targetDir = path.join(resolvedBase, '.ai-staging', 'feedback');
266
+ const targetDir = resolveStoragePath(req.headers.origin, 'feedback');
157
267
  try {
158
268
  fs.mkdirSync(targetDir, { recursive: true });
159
269
  } catch (err) {
@@ -232,9 +342,24 @@ if (command === 'dev') {
232
342
  res.end('405 Method Not Allowed');
233
343
  });
234
344
 
235
- server.listen(port, () => {
236
- console.log(`Visual AI Staging Dev Server running at http://localhost:${port}/`);
345
+ function startServer(p) {
346
+ server.listen(p, () => {
347
+ console.log(`Visual AI Staging Dev Server running at http://localhost:${p}/`);
348
+ });
349
+ }
350
+
351
+ server.on('error', (err) => {
352
+ if (err.code === 'EADDRINUSE') {
353
+ console.log(`Port ${activePort} is occupied. Retrying on next port...`);
354
+ activePort++;
355
+ startServer(activePort);
356
+ } else {
357
+ console.error('Server error:', err.message);
358
+ process.exit(1);
359
+ }
237
360
  });
361
+
362
+ startServer(activePort);
238
363
  } else {
239
364
  console.error(`Unknown command: ${command}`);
240
365
  console.error('Run "vais --help" for usage.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visual-ai-staging",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Visual AI Staging Companion — Bridge the gap between UI design staging and AI coding assistants.",
5
5
  "license": "MIT",
6
6
  "main": "app.js",